I want to be able to maintain a list in the background that puts new items at the end of the list (to avoid Insert() pushing the items around on updates) but to be able to display it in the reverse order without "sorting".
I just want it to show up in the list view in the reverse order that it is in the list. Can I do this with a template or something similar?
You can change the ListView's ItemsPanel to be a DockPanel with LastChildFill set to false. Then in the ItemContainerStyle, set the DockPanel.Dock property to bottom. This will start filling at the bottom and work its way up to the top. I put the ListView in a grid with 2 rows, first's Height="Auto" and second's Height="*" and it acted just like a normal ListView, but with the items reversed.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="DockPanel.Dock"
Value="Bottom" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel LastChildFill="False" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
Idea taken from:
https://stackoverflow.com/a/493059/2812277
Update
Here is an Attached Behavior which will reverse any ItemsControl. Use it like this
<ListBox behaviors:ReverseItemsControlBehavior.ReverseItemsControl="True"
...>
ReverseItemsControlBehavior
public class ReverseItemsControlBehavior
{
public static DependencyProperty ReverseItemsControlProperty =
DependencyProperty.RegisterAttached("ReverseItemsControl",
typeof(bool),
typeof(ReverseItemsControlBehavior),
new FrameworkPropertyMetadata(false, OnReverseItemsControlChanged));
public static bool GetReverseItemsControl(DependencyObject obj)
{
return (bool)obj.GetValue(ReverseItemsControlProperty);
}
public static void SetReverseItemsControl(DependencyObject obj, object value)
{
obj.SetValue(ReverseItemsControlProperty, value);
}
private static void OnReverseItemsControlChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue == true)
{
ItemsControl itemsControl = sender as ItemsControl;
if (itemsControl.IsLoaded == true)
{
DoReverseItemsControl(itemsControl);
}
else
{
RoutedEventHandler loadedEventHandler = null;
loadedEventHandler = (object sender2, RoutedEventArgs e2) =>
{
itemsControl.Loaded -= loadedEventHandler;
DoReverseItemsControl(itemsControl);
};
itemsControl.Loaded += loadedEventHandler;
}
}
}
private static void DoReverseItemsControl(ItemsControl itemsControl)
{
Panel itemPanel = GetItemsPanel(itemsControl);
itemPanel.LayoutTransform = new ScaleTransform(1, -1);
Style itemContainerStyle;
if (itemsControl.ItemContainerStyle == null)
{
itemContainerStyle = new Style();
}
else
{
itemContainerStyle = CopyStyle(itemsControl.ItemContainerStyle);
}
Setter setter = new Setter();
setter.Property = ItemsControl.LayoutTransformProperty;
setter.Value = new ScaleTransform(1, -1);
itemContainerStyle.Setters.Add(setter);
itemsControl.ItemContainerStyle = itemContainerStyle;
}
private static Panel GetItemsPanel(ItemsControl itemsControl)
{
ItemsPresenter itemsPresenter = GetVisualChild<ItemsPresenter>(itemsControl);
if (itemsPresenter == null)
return null;
return GetVisualChild<Panel>(itemsControl);
}
private static Style CopyStyle(Style style)
{
Style styleCopy = new Style();
foreach (SetterBase currentSetter in style.Setters)
{
styleCopy.Setters.Add(currentSetter);
}
foreach (TriggerBase currentTrigger in style.Triggers)
{
styleCopy.Triggers.Add(currentTrigger);
}
return styleCopy;
}
private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
}
Otherwise, you can follow what's outlined in the following link: WPF reverse ListView
<ListBox ...>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel VerticalAlignment="Top" Orientation="Vertical">
<VirtualizingStackPanel.LayoutTransform>
<ScaleTransform ScaleX="1" ScaleY="-1" />
</VirtualizingStackPanel.LayoutTransform>
</VirtualizingStackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="LayoutTransform">
<Setter.Value>
<ScaleTransform ScaleX="1" ScaleY="-1" />
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
After googling and trying things for half of a day I came across #ryan-west answer, and slightly modified it to fit my needs. I was able to get exactly what I wanted a listbox in a scrollviewer that showed a list exactly as normally seen, but in reverse order.
<ScrollViewer>
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled"
VerticalAlignment="Top"
ItemsSource="{Binding MyList, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel>
<Image DockPanel.Dock="Left"
Source="MyIcon.png"
Width="16" />
<Label DockPanel.Dock="Left"
Content="{Binding MyName, Mode=TwoWay}"/>
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="DockPanel.Dock"
Value="Bottom" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel LastChildFill="False" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</ScrollViewer>
Assuming that the ItemsSource is an ObservableCollection, my solution was to implement a ReverseObservableCollection:
public class ReverseObservableCollection<T> : IReadOnlyList<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
#region Private fields
private readonly ObservableCollection<T> _observableCollection;
#endregion Private fields
#region Constructor
public ReverseObservableCollection(ObservableCollection<T> observableCollection)
{
_observableCollection = observableCollection;
observableCollection.CollectionChanged += ObservableCollection_CollectionChanged;
}
#endregion
#region Event handlers
private void ObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (new[] { NotifyCollectionChangedAction.Add, NotifyCollectionChangedAction.Remove, NotifyCollectionChangedAction.Reset }.Contains(e.Action))
{
OnPropertyChanged(nameof(Count));
}
OnPropertyChanged(Binding.IndexerName); // ObservableCollection does this to improve WPF performance.
var newItems = Reverse(e.NewItems);
var oldItems = Reverse(e.OldItems);
int newStartingIndex = e.NewItems != null ? _observableCollection.Count - e.NewStartingIndex - e.NewItems.Count : -1;
//int oldCount = _observableCollection.Count - (e.NewItems?.Count ?? 0) + (e.OldItems?.Count ?? 0);
//int oldStartingIndex = e.OldItems != null ? oldCount - e.OldStartingIndex - e.OldItems.Count : -1;
int oldStartingIndex = e.OldItems != null ? _observableCollection.Count - e.OldStartingIndex - (e.NewItems?.Count ?? 0) : -1;
var eventArgs = e.Action switch
{
NotifyCollectionChangedAction.Add => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, newStartingIndex),
NotifyCollectionChangedAction.Remove => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems, oldStartingIndex),
NotifyCollectionChangedAction.Replace => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItems, oldItems, oldStartingIndex),
NotifyCollectionChangedAction.Move => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, oldItems, newStartingIndex, oldStartingIndex),
NotifyCollectionChangedAction.Reset => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset),
_ => throw new ArgumentException("Unexpected Action", nameof(e)),
};
OnCollectionChanged(eventArgs);
}
#endregion
#region IReadOnlyList<T> implementation
public T this[int index] => _observableCollection[_observableCollection.Count - index - 1];
public int Count => _observableCollection.Count;
public IEnumerator<T> GetEnumerator()
{
for (int i = _observableCollection.Count - 1; i >= 0; --i)
{
yield return _observableCollection[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
#region INotifyCollectionChanged implementation
public event NotifyCollectionChangedEventHandler? CollectionChanged;
private void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
}
#endregion
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string? propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region Private methods
private IList? Reverse(IList? list)
{
if (list == null) return null;
object[] result = new object[list.Count];
for (int i = 0; i < list.Count; ++i)
{
result[i] = list[list.Count - i - 1];
}
return result;
}
#endregion
}
Then, you just add a new property to the ViewModel and bind to it:
public class ViewModel
{
// Your old ItemsSource:
public ObservableCollection<string> Collection { get; } = new ObservableCollection<string>();
// New ItemsSource:
private ReverseObservableCollection<string>? _reverseCollection = null;
public ReverseObservableCollection<string> ReverseCollection => _reverseCollection ??= new ReverseObservableCollection<string>(Collection);
}
Related
I have a List of Lists and display it with nested ListBoxes:
MainWindow.xaml.cs
using System.Collections.Generic;
namespace WPF_Sandbox
{
public partial class MainWindow
{
public IEnumerable<IEnumerable<string>> ListOfStringLists { get; set; } = new[] { new[] { "a", "b" }, new[] { "c", "d" } };
public MainWindow()
{
InitializeComponent();
DoSomethingButton.Click += (sender, e) =>
{
// do something with all selected items
};
}
}
}
MainWindow.xaml
<Window x:Class="WPF_Sandbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
x:Name="ThisControl">
<StackPanel>
<ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<TextBlock Text="{Binding}" />
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Name="DoSomethingButton" Content="DoSomething" />
</StackPanel>
</Window>
How can I get all selected items across all ListBoxes?
I found a few solutions getting one selected item but could not figure out how to do applie those in my scenario.
I have an idea on how to do this by wrapping the string arrays but I would prefer not doing this.
I would just add an event handler to the inner ListBox like so if not doing things the MVVM way:
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple" SelectionChanged="ListBox_SelectionChanged">
Then in your code behind implement the ListBox_SelectionChanged like so:
public List<string> FlatStringList = new List<string>();
private void ListBox_SelectionChanged(object sender,System.Windows.Controls.SelectionChangedEventArgs e)
{
FlatStringList.AddRange(e.AddedItems.Cast<string>());
foreach(string s in e.RemovedItems)
{
FlatStringList.Remove(s);
}
}
This is assuming you don't mind storing the selected strings in a flat list. Then you could implement your DoSomething button click event handler to do something with the FlatStringList.
Hope that helps.
The easiest way would be to iterate through the items in the ListBoxes:
private void DoSomethingButton_Click(object sender, RoutedEventArgs e)
{
List<string> selectedStrings = new List<string>();
foreach (IEnumerable<string> array in outerListBox.Items.OfType<IEnumerable<string>>())
{
ListBoxItem lbi = outerListBox.ItemContainerGenerator.ContainerFromItem(array) as ListBoxItem;
if (lbi != null)
{
ListBox innerListBox = GetChildOfType<ListBox>(lbi);
if (innerListBox != null)
{
foreach (string selectedString in innerListBox.SelectedItems.OfType<string>())
selectedStrings.Add(selectedString);
}
}
}
}
private static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null)
return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null)
return result;
}
return null;
}
Note that the ListBoxItem may be virtualized away if you have a lot of inner IEnumerable<string>. You will then have to force the generation of the containers or disable UI virtualization:
WPF ListView virtualization. How to disable ListView virtualization?
This may affect the performance negatively so if this is an issue you should probably consider binding to an IEnumerable<YourType> and bind the SelectedItems property of the inner ListBox to a property of a YourType using a behaviour.
Since the SelectedItems property of a ListBox is read-only you can't bind to it directly: https://blog.magnusmontin.net/2014/01/30/wpf-using-behaviours-to-bind-to-readonly-properties-in-mvvm/.
Why don't you create a wrapper (as you said):
public class MyString : INotifyPropertyChanged
{
public MyString(string value) { Value = value; }
string _value;
public string Value { get { return _value; } set { _value = value; RaisePropertyChanged("Value"); } }
bool _isSelected;
public bool IsSelected { get { return _isSelected; } set { _isSelected = value; RaisePropertyChanged("IsSelected"); } }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
Bind the IsSelected property of the ListBoxItems:
<StackPanel>
<ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<TextBlock Text="{Binding Value}" />
</ItemContainerTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Name="DoSomethingButton" Content="DoSomething" />
</StackPanel>
and you are already done:
public IEnumerable<IEnumerable<MyString>> ListOfStringLists { get; set; } = new[] { new[] { new MyString("a"), new MyString("b") { IsSelected = true } }, new[] { new MyString("c"), new MyString("d") } };
public MainWindow()
{
this.InitializeComponent();
DoSomethingButton.Click += (sender, e) =>
{
foreach (var i in ListOfStringLists)
foreach (var j in i)
{
if (j.IsSelected)
{
// ....
}
}
};
}
Given an arbitrary ItemsControl, like a ListView, I want to set a Binding from inside the ItemsTemplate to the hosting Container. How can I do that easily? For example, in WPF we can do it using this inside the ItemTemplate
<ListView.ItemTemplate>
<DataTemplate>
<SomeControl Property="{Binding Path=TargetProperty, RelativeSouce={RelativeSource FindAncestor, AncestorType={x:Type MyContainer}}}" />
</DataTemplate>
<ListView.ItemTemplate>
In this example (for WPF) the Binding will be set between Property in SomeControl and TargetProperty of the ListViewItem (implicit, because it will be generated dynamically by the ListView to host the each of its items).
How can we do achieve the same in UWP?
I want something that is MVVM-friendly. Maybe with attached properties or an Interaction Behavior.
When the selection changes, search the visual tree for the radio button with the DataContext corresponding to selected/deselected items. Once it's found, you can check/uncheck at your leisure.
I have a toy model object looking like this:
public class Data
{
public string Name { get; set; }
}
My Page is named self and contains this collection property:
public Data[] Data { get; set; } =
{
new Data { Name = "One" },
new Data { Name = "Two" },
new Data { Name = "Three" },
};
The list view, binding to the above collection:
<ListView
ItemsSource="{Binding Data, ElementName=self}"
SelectionChanged="OnSelectionChanged">
<ListView.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The SelectionChanged event handler:
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListView lv = sender as ListView;
var removed = FindRadioButtonWithDataContext(lv, e.RemovedItems.FirstOrDefault());
if (removed != null)
{
removed.IsChecked = false;
}
var added = FindRadioButtonWithDataContext(lv, e.AddedItems.FirstOrDefault());
if (added != null)
{
added.IsChecked = true;
}
}
Finding the radio button with a DataContext matching our Data instance:
public static RadioButton FindRadioButtonWithDataContext(
DependencyObject parent,
object data)
{
if (parent != null)
{
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
ListViewItem lv = child as ListViewItem;
if (lv != null)
{
RadioButton rb = FindVisualChild<RadioButton>(child);
if (rb?.DataContext == data)
{
return rb;
}
}
RadioButton childOfChild = FindRadioButtonWithDataContext(child, data);
if (childOfChild != null)
{
return childOfChild;
}
}
}
return null;
}
And finally, a helper method to find a child of a specific type:
public static T FindVisualChild<T>(
DependencyObject parent)
where T : DependencyObject
{
if (parent != null)
{
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
T candidate = child as T;
if (candidate != null)
{
return candidate;
}
T childOfChild = FindVisualChild<T>(child);
if (childOfChild != null)
{
return childOfChild;
}
}
}
return default(T);
}
The result:
This will break if a given model instance shows up more than once in the list.
Note: this answer is based on WPF, there might be some changes necessary for UWP.
There are basically two cases to consider:
You have a data driven aspect that needs to be bound to the item container
You have a view-only property
Lets assume a customized listview for both cases:
public class MyListView : ListView
{
protected override DependencyObject GetContainerForItemOverride()
{
return new DesignerItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is DesignerItem;
}
}
public class DesignerItem : ListViewItem
{
public bool IsEditing
{
get { return (bool)GetValue(IsEditingProperty); }
set { SetValue(IsEditingProperty, value); }
}
public static readonly DependencyProperty IsEditingProperty =
DependencyProperty.Register("IsEditing", typeof(bool), typeof(DesignerItem));
}
In case 1, you can use the ItemContainerStyle to link your viewmodel property with a binding and then bind the same property inside the datatemplate
class MyData
{
public bool IsEditing { get; set; } // also need to implement INotifyPropertyChanged here!
}
XAML:
<local:MyListView ItemsSource="{Binding Source={StaticResource items}}">
<local:MyListView.ItemContainerStyle>
<Style TargetType="{x:Type local:DesignerItem}">
<Setter Property="IsEditing" Value="{Binding IsEditing,Mode=TwoWay}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</local:MyListView.ItemContainerStyle>
<local:MyListView.ItemTemplate>
<DataTemplate>
<Border Background="Red" Margin="5" Padding="5">
<CheckBox IsChecked="{Binding IsEditing}"/>
</Border>
</DataTemplate>
</local:MyListView.ItemTemplate>
</local:MyListView>
In case 2, it appears that you don't really have a data driven property and consequently, the effects of your property should be reflected within the control (ControlTemplate).
In the following example a toolbar is made visible based on the IsEditing property. A togglebutton can be used to control the property, the ItemTemplate is used as an inner element next to the toolbar and button, it knows nothing of the IsEditing state:
<local:MyListView ItemsSource="{Binding Source={StaticResource items}}">
<local:MyListView.ItemContainerStyle>
<Style TargetType="{x:Type local:DesignerItem}">
<Setter Property="IsEditing" Value="{Binding IsEditing,Mode=TwoWay}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DesignerItem}">
<DockPanel>
<ToggleButton DockPanel.Dock="Right" Margin="5" VerticalAlignment="Top" IsChecked="{Binding IsEditing,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}" Content="Edit"/>
<!--Toolbar is something control related, rather than data related-->
<ToolBar x:Name="MyToolBar" DockPanel.Dock="Top" Visibility="Collapsed">
<Button Content="Tool"/>
</ToolBar>
<ContentPresenter ContentSource="Content"/>
</DockPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsEditing" Value="True">
<Setter TargetName="MyToolBar" Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</local:MyListView.ItemContainerStyle>
<local:MyListView.ItemTemplate>
<DataTemplate>
<Border Background="Red" Margin="5" Padding="5">
<TextBlock Text="Hello World"/>
</Border>
</DataTemplate>
</local:MyListView.ItemTemplate>
</local:MyListView>
For a better control template, you may chose to use Blend and create the control template starting at the full ListViewItem template and just editing your changes into it.
If your DesignerItem generally has a specific enhanced appearance, consider designing it in the Themes/Generic.xaml with the appropriate default style.
As commented, you could provide a separate data template for the editing mode. To do this, add a property to MyListView and to DesignerItem and use MyListView.PrepareContainerForItemOverride(...) to transfer the template.
In order to apply the template without the need for Setter.Value bindings, you can use value coercion on DesignerItem.ContentTemplate based on IsEditing.
public class MyListView : ListView
{
protected override DependencyObject GetContainerForItemOverride()
{
return new DesignerItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is DesignerItem;
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var elem = element as DesignerItem;
elem.ContentEditTemplate = ItemEditTemplate;
}
public DataTemplate ItemEditTemplate
{
get { return (DataTemplate)GetValue(ItemEditTemplateProperty); }
set { SetValue(ItemEditTemplateProperty, value); }
}
public static readonly DependencyProperty ItemEditTemplateProperty =
DependencyProperty.Register("ItemEditTemplate", typeof(DataTemplate), typeof(MyListView));
}
public class DesignerItem : ListViewItem
{
static DesignerItem()
{
ContentTemplateProperty.OverrideMetadata(typeof(DesignerItem), new FrameworkPropertyMetadata(
null, new CoerceValueCallback(CoerceContentTemplate)));
}
private static object CoerceContentTemplate(DependencyObject d, object baseValue)
{
var self = d as DesignerItem;
if (self != null && self.IsEditing)
{
return self.ContentEditTemplate;
}
return baseValue;
}
private static void OnIsEditingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.CoerceValue(ContentTemplateProperty);
}
public bool IsEditing
{
get { return (bool)GetValue(IsEditingProperty); }
set { SetValue(IsEditingProperty, value); }
}
public static readonly DependencyProperty IsEditingProperty =
DependencyProperty.Register("IsEditing", typeof(bool), typeof(DesignerItem), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsEditingChanged)));
public DataTemplate ContentEditTemplate
{
get { return (DataTemplate)GetValue(ContentEditTemplateProperty); }
set { SetValue(ContentEditTemplateProperty, value); }
}
// Using a DependencyProperty as the backing store for ContentEditTemplate. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContentEditTemplateProperty =
DependencyProperty.Register("ContentEditTemplate", typeof(DataTemplate), typeof(DesignerItem));
}
Note, for an easier example I will activate the "edit" mode by ListViewItem.IsSelected with some trigger:
<local:MyListView ItemsSource="{Binding Source={StaticResource items}}">
<local:MyListView.ItemContainerStyle>
<Style TargetType="{x:Type local:DesignerItem}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="IsEditing" Value="True"/>
</Trigger>
</Style.Triggers>
</Style>
</local:MyListView.ItemContainerStyle>
<local:MyListView.ItemTemplate>
<DataTemplate>
<Border Background="Red" Margin="5" Padding="5">
<TextBlock Text="Hello World"/>
</Border>
</DataTemplate>
</local:MyListView.ItemTemplate>
<local:MyListView.ItemEditTemplate>
<DataTemplate>
<Border Background="Green" Margin="5" Padding="5">
<TextBlock Text="Hello World"/>
</Border>
</DataTemplate>
</local:MyListView.ItemEditTemplate>
</local:MyListView>
Intended behavior: the selected item becomes edit-enabled, getting the local:MyListView.ItemEditTemplate (green) instead of the default template (red)
Just in case you might want to have an IsSelected property in your view model item class, you may create a derived ListView that establishes a Binding of its ListViewItems to the view model property:
public class MyListView : ListView
{
public string ItemIsSelectedPropertyName { get; set; } = "IsSelected";
protected override void PrepareContainerForItemOverride(
DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
BindingOperations.SetBinding(element,
ListViewItem.IsSelectedProperty,
new Binding
{
Path = new PropertyPath(ItemIsSelectedPropertyName),
Source = item,
Mode = BindingMode.TwoWay
});
}
}
You might now simply bind the RadioButton's IsChecked property in the ListView's ItemTemplate to the same view model property:
<local:MyListView ItemsSource="{Binding DataItems}">
<ListView.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Content}"
IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
</DataTemplate>
</ListView.ItemTemplate>
</local:MyListView>
In the above example the data item class also has Content property. Obviously, the IsSelected property of the data item class must fire a PropertyChanged event.
I'm having a Custom Control - Implemented for AutoComplete TextBox.
I got all idea's from the following question Create a Custom Control with the combination of multiple controls in WPF C#. In that Custom Control, they suggest the following code for adding item, its perfectly working and the Two-Way Binding too.
(this.ItemsSource as IList<string>).Add(this._textBox.Text);
But, I changed the following code to Unknown Object, so I changed IList<string> to IList<object>
(this.ItemsSource as IList<object>).Add(item);
XAML:
<local:BTextBox
ItemsSource="{Binding Collection}"
ProviderCommand="{Binding AutoBTextCommand}"
AutoItemsSource="{Binding SuggCollection}" />
But it's not updating the ViewModel Property Collection. I too tried the following changes in the xaml
ItemsSource="{Binding Collection, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
I don't know where I did the mistake.
Functionality: The TextBox inside the CustomControl takes the input from the User and it triggers the ProviderCommand, that Command filters the Remote data based on the User Input and sends the Filtered Collection via AutoItemsSource, this Property is binded as a ItemsSource of the ListBox inside that CustomControl to select the Item. We can select the Item from the ListBox Item, by clicking the Item, it triggers the Command AddCommand which is in the CustomControl Class, it add the selected item in ItemSource Property of the CustomControl. I'm having the Two-Way Binding issue in this Property ItemsSource. From this property only we may get the Selected item as a Collection.
Here is my Complete Source Code
The Custom Control C# Code:
public class BTextBox : ItemsControl
{
static BTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(BTextBox), new FrameworkPropertyMetadata(typeof(BTextBox)));
}
#region Private Members
private TextBox _textBox;
private ItemsControl _itemsView;
#endregion
#region Dependency Property Private Members
public static readonly DependencyProperty ProviderCommandProperty = DependencyProperty.Register("ProviderCommand", typeof(ICommand), typeof(BTextBox), new PropertyMetadata(null));
public static readonly DependencyProperty AutoItemsSourceProperty = DependencyProperty.Register("AutoItemsSource", typeof(IEnumerable<dynamic>), typeof(BTextBox), new PropertyMetadata(null, OnItemsSourceChanged));
#endregion
#region Dependency Property Public members
public IEnumerable<dynamic> AutoItemsSource
{
get { return (IEnumerable<dynamic>)GetValue(AutoItemsSourceProperty); }
set { SetValue(AutoItemsSourceProperty, value); }
}
#endregion
#region Listener Methods
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var tb = d as BTextBox;
if ((e.NewValue != null) && ((tb.ItemsSource as IList<object>) != null))
{
(tb.AutoItemsSource as IList<object>).Add(e.NewValue);
}
}
#endregion
#region Override Methods
public override void OnApplyTemplate()
{
this._textBox = this.GetTemplateChild("PART_TextBox") as TextBox;
this._itemsView = this.GetTemplateChild("PART_ListBox") as ItemsControl;
this._textBox.TextChanged += (sender, args) =>
{
if (this.ProviderCommand != null)
{
this.ProviderCommand.Execute(this._textBox.Text);
}
};
base.OnApplyTemplate();
}
#endregion
#region Command
public ICommand ProviderCommand
{
get { return (ICommand)GetValue(ProviderCommandProperty); }
set { SetValue(ProviderCommandProperty, value); }
}
public ICommand AddCommand
{
get
{
return new DelegatingCommand((obj) =>
{
(this.ItemsSource as IList<object>).Add(obj);
});
}
}
#endregion
}
The Generic.xaml Code is
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SampleControl">
<Style TargetType="{x:Type local:BTextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:BTextBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox x:Name="PART_TextBox" Grid.Row="0" Width="*" VerticalAlignment="Center" />
<ListBox ItemsSource="{TemplateBinding AutoItemsSource}" Grid.Row="1" x:Name="PART_ListBox_Sugg" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Value.IsChecked}" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:BTextBox}}, Path=AddCommand}" CommandParameter="{Binding }" Foreground="#404040">
<CheckBox.Content>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding }" Visibility="Visible" TextWrapping="Wrap" MaxWidth="270"/>
</StackPanel>
</CheckBox.Content>
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The MainWindow.xaml Code is
<Window x:Class="SampleControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SampleControl"
Title="MainWindow" Height="400" Width="525">
<Grid>
<local:BTextBox
ItemsSource="{Binding Collection}"
ProviderCommand="{Binding AutoBTextCommand}"
AutoItemsSource="{Binding SuggCollection}" />
</Grid>
</Window>
The MainWindow.xaml's Code Behind C# Code
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new StringModel();
}
}
I'm having TWO ViewModels
ViewModel #1 StringModel
class StringModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<string> _collection = new ObservableCollection<string>();
private ObservableCollection<string> _suggCollection = new ObservableCollection<string>();
private ObservableCollection<string> _primaryCollection = new ObservableCollection<string>();
public ObservableCollection<string> Collection
{
get { return _collection; }
set
{
_collection = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Collection"));
}
}
public ObservableCollection<string> SuggCollection
{
get { return _suggCollection; }
set
{
_suggCollection = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("SuggCollection"));
}
}
public StringModel()
{
_primaryCollection = new ObservableCollection<string> {
"John", "Jack", "James", "Emma", "Peter"
};
}
public ICommand AutoBTextCommand
{
get
{
return new DelegatingCommand((obj) =>
{
Search(obj as string);
});
}
}
private void Search(string str)
{
SuggCollection = new ObservableCollection<string>(_primaryCollection.Where(m => m.ToLowerInvariant().Contains(str.ToLowerInvariant())).Select(m => m));
}
}
ViewModel #2 IntModel
class IntModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<int> _collection = new ObservableCollection<int>();
private ObservableCollection<int> _suggCollection = new ObservableCollection<int>();
private ObservableCollection<int> _primaryCollection = new ObservableCollection<int>();
public ObservableCollection<int> Collection
{
get { return _collection; }
set
{
_collection = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Collection"));
}
}
public ObservableCollection<int> SuggCollection
{
get { return _suggCollection; }
set
{
_suggCollection = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("SuggCollection"));
}
}
public IntModel()
{
_primaryCollection = new ObservableCollection<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 16, 17, 18, 19, 20 };
}
public ICommand AutoBTextCommand
{
get
{
return new DelegatingCommand((obj) =>
{
Search(obj as string);
});
}
}
private void Search(string str)
{
int item = 0;
int.TryParse(str, out item);
SuggCollection = new ObservableCollection<int>(_primaryCollection.Where(m => m == item).Select(m => m));
}
}
First of all, this post would have fitted better in CodeReview.
Second, i can imagine, what you did want to do.
To shorten things, i recommend you to not use generic collections in your case.
I've modified the Control a bit:
public class BTextBox : ItemsControl {
static BTextBox() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(BTextBox), new FrameworkPropertyMetadata(typeof(BTextBox)));
}
private TextBox _textBox;
private ItemsControl _itemsView;
public static readonly DependencyProperty ProviderCommandProperty = DependencyProperty.Register("ProviderCommand", typeof(ICommand), typeof(BTextBox), new PropertyMetadata(null));
public static readonly DependencyProperty AutoItemsSourceProperty = DependencyProperty.Register("AutoItemsSource", typeof(IEnumerable), typeof(BTextBox), new PropertyMetadata(null, OnItemsSourceChanged));
public IEnumerable AutoItemsSource {
get {
return (IEnumerable)GetValue(AutoItemsSourceProperty);
}
set {
SetValue(AutoItemsSourceProperty, value);
}
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var tb = d as BTextBox;
if ((e.NewValue != null) && ((tb.ItemsSource as IList) != null)) {
foreach (var item in e.NewValue as IEnumerable) {
(tb.AutoItemsSource as IList).Add(item);
}
}
}
public override void OnApplyTemplate() {
this._textBox = this.GetTemplateChild("PART_TextBox") as TextBox;
this._itemsView = this.GetTemplateChild("PART_ListBox_Sugg") as ItemsControl;
this._itemsView.ItemsSource = this.AutoItemsSource;
this._textBox.TextChanged += (sender, args) => {
this.ProviderCommand?.Execute(this._textBox.Text);
};
base.OnApplyTemplate();
}
public ICommand ProviderCommand {
get {
return (ICommand) this.GetValue(ProviderCommandProperty);
}
set {
this.SetValue(ProviderCommandProperty, value);
}
}
public ICommand AddCommand {
get {
return new RelayCommand(obj => {
(this.ItemsSource as IList)?.Add(obj);
});
}
}
}
Then i've fixed your XAML to get thing to even compile and run:
<Style TargetType="{x:Type local:BTextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:BTextBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox x:Name="PART_TextBox" Grid.Row="0" VerticalAlignment="Center" />
<ListBox ItemsSource="{TemplateBinding AutoItemsSource}" Grid.Row="1" x:Name="PART_ListBox_Sugg" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:BTextBox}}, Path=AddCommand}" CommandParameter="{Binding}" Foreground="#404040">
<CheckBox.Content>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding }" Visibility="Visible" TextWrapping="Wrap" MaxWidth="270"/>
</StackPanel>
</CheckBox.Content>
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
At last a valuable remark:
Never ever allow setters on your ItemsSources. If you override them, the binding will break. Use .Clear() and .Add() instead as you see below:
public class StringModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private readonly ObservableCollection<string> _collection = new ObservableCollection<string>();
private readonly ObservableCollection<string> _suggCollection = new ObservableCollection<string>();
private readonly ObservableCollection<string> _primaryCollection = new ObservableCollection<string>();
public ObservableCollection<string> Collection => this._collection;
public ObservableCollection<string> SuggCollection => this._suggCollection;
public StringModel() {
this._primaryCollection.Add("John");
this._primaryCollection.Add("Jack");
this._primaryCollection.Add("James");
this._primaryCollection.Add("Emma");
this._primaryCollection.Add("Peter");
}
public ICommand AutoBTextCommand {
get {
return new RelayCommand(obj => {
this.Search(obj as string);
});
}
}
private void Search(string str) {
this.SuggCollection.Clear();
foreach (var result in this._primaryCollection.Where(m => m.ToLowerInvariant().Contains(str.ToLowerInvariant())).Select(m => m)) {
this.SuggCollection.Add(result);
}
}
}
Note
Sice i didnt have your DelegateCommand-implementation, i've used my RelayCommand instead. You can change it withour any issues. I think its the same thing but a different name for it.
You also might consider to display your suggestions right from the start. This might provide a better user-expierience, but thats just my opinion
I have a board with clickable labels (Grass and Unit), When I click a Grass label I it should move the Unit Label to the Grass's x and y position. It works, but kinda wrong. When I click on a label, nothing happens until I move the cursor out of the clicked label, then the wanted behaviour executes.
XAML
<local:Grass Grid.Row="9" Grid.Column="16" />
<local:Unit Grid.Row="{Binding Path=xPos, UpdateSourceTrigger=PropertyChanged}" Grid.Column="{Binding Path=yPos, UpdateSourceTrigger=PropertyChanged}" >
<local:Unit.Background>
<ImageBrush ImageSource="Images/tjej.png"/>
</local:Unit.Background>
</local:Unit>
ObjectInspector
public class ObjectInspector : INotifyPropertyChanged
{
private int _xPos = 1, _yPos = 2;
public int xPos
{
get { return _xPos; }
set
{
_xPos = value;
NotifyPropertyChanged("xPos");
}
}
public int yPos
{
get { return _yPos; }
set {
_yPos = value;
NotifyPropertyChanged("yPos");
}
}
private string _type = "none";
public string type
{
get { return _type; }
set {
_type = value;
NotifyPropertyChanged("type");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
System.Diagnostics.Debug.WriteLine("property changed");
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Grass
public class Grass : Button
{
protected override void OnClick()
{
base.OnClick();
int x = (int)this.GetValue(Grid.RowProperty);
int y = (int)this.GetValue(Grid.ColumnProperty);
string type = this.GetType().Name;
MainWindow.objectInspector.xPos = x;
MainWindow.objectInspector.yPos = y;
MainWindow.objectInspector.type = type;
}
}
MainWindow
public partial class MainWindow : Window
{
public static ObjectInspector objectInspector= new ObjectInspector();
public MainWindow()
{
InitializeComponent();
this.DataContext = objectInspector;
}
}
Any ideas?
Edit
Added MainWindow and Grass
EDIT
Try to register to the common event handler Click of buttons:
<local:Grass Grid.Row="9" Grid.Column="16" Click="ClickEventHandler" />
...
And take the grass element from the sender, in the event handler method.
Anyway, I think a better way for doing this is usin MVVM patter. You may set a GrassViewModel and UnitViewModel. Then create a DataTemplate for each one. For example:
<DataTemplate DataType="{x:Type ViewModel:UnitViewModel}">
...Visual Elements Here...
</DataTemplate>
The for showing the elements in a grid you may use a ListBox with a Grid as items panel, some like this:
<ListBox ItemsSource={Binding AllItemsCollection}>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
...rows and columns definitions here...
</Grid>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<!--HERE THE ITEMS STYLE, HERE YOU SET THE COLUMN, ROW BINDINGS-->
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Grid.Row" Value="{Binding yPos}"/>
<Setter Property="Grid.Column" Value="{Binding xPos}"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Then you only need to create the AllItemsCollection in your view model with all the elements that you want. You can handler the click event using behaviors, or creating a UserControl for the grass (and controlling the click event inside):
<DataTemplate DataType="{x:Type ViewModel:UnitViewModel}">
<GrassUserControl ...Inside the grass user control you can handler the click event.../>
</DataTemplate>
Hope helps...
If your following MVVM then you can attach a property to the label as below. You can attache this behavior any control that derives from UIElement
Create a Attached property for MouseClick
public class MouseClick
{
public static readonly DependencyProperty MouseLeftClickProperty =
DependencyProperty.RegisterAttached("MouseLeftClick", typeof(ICommand), typeof(MouseClick),
new FrameworkPropertyMetadata(CallBack));
public static void SetMouseLeftClick(DependencyObject sender, ICommand value)
{
sender.SetValue(MouseLeftClickProperty, value);
}
public static ICommand GetMouseLeftClick(DependencyObject sender)
{
return sender.GetValue(MouseLeftClickProperty) as ICommand;
}
public static readonly DependencyProperty MouseEventParameterProperty =
DependencyProperty.RegisterAttached(
"MouseEventParameter",
typeof(object),
typeof(MouseClick),
new FrameworkPropertyMetadata((object)null, null));
public static object GetMouseEventParameter(DependencyObject d)
{
return d.GetValue(MouseEventParameterProperty);
}
public static void SetMouseEventParameter(DependencyObject d, object value)
{
d.SetValue(MouseEventParameterProperty, value);
}
private static void CallBack(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender != null)
{
UIElement element = sender as UIElement;
if (element != null)
{
if (e.OldValue != null)
{
element.RemoveHandler(UIElement.MouseDownEvent, new MouseButtonEventHandler(Handler));
}
if (e.NewValue != null)
{
element.AddHandler(UIElement.MouseDownEvent, new MouseButtonEventHandler(Handler), true);
}
}
}
}
private static void Handler(object sender, EventArgs e)
{
UIElement element = sender as UIElement;
if (sender != null)
{
ICommand cmd = element.GetValue(MouseLeftClickProperty) as ICommand;
if (cmd != null)
{
RoutedCommand routedCmd =cmd as RoutedCommand;
object paramenter = element.GetValue(MouseEventParameterProperty);
if (paramenter == null)
{
paramenter = element;
}
if (routedCmd != null)
{
if (routedCmd.CanExecute(paramenter, element))
{
routedCmd.Execute(paramenter, element);
}
}
else
{
if (cmd.CanExecute(paramenter))
{
cmd.Execute(paramenter);
}
}
}
}
}
}
In you Xaml attache the Command of your viewModel as below
<Label Height="30" Width="200" Margin="10" Content="Click" local:MouseClick.MouseLeftClick="{Binding Click}" />
How can i know if a ListBoxItem is the last item of the collection (in the ItemContainerStyle or in the ItemContainer's template) inside a Wpf's ListBox?
That question is because I need to know if an item is the last item to show it in other way. For example: suppose i want to show items separated by semi-colons but the last one: a;b;c
This is easy to do in html and ccs, using ccs selector. But, how can i do this in Wpf?
As it seems to be rather difficult to implement an "Index" attached property to ListBoxItem to do the job right, I believe the easier way to accomplish that would be in MVVM.
You can add the logic necessary (a "IsLast" property, etc) to the entity type of the list and let the ViewModel deal with this, updating it when the collection is modified or replaced.
EDIT
After some attempts, I managed to implement indexing of ListBoxItems (and consequently checking for last) using a mix of attached properties and inheriting ListBox. Check it out:
public class IndexedListBox : System.Windows.Controls.ListBox
{
public static int GetIndex(DependencyObject obj)
{
return (int)obj.GetValue(IndexProperty);
}
public static void SetIndex(DependencyObject obj, int value)
{
obj.SetValue(IndexProperty, value);
}
/// <summary>
/// Keeps track of the index of a ListBoxItem
/// </summary>
public static readonly DependencyProperty IndexProperty =
DependencyProperty.RegisterAttached("Index", typeof(int), typeof(IndexedListBox), new UIPropertyMetadata(0));
public static bool GetIsLast(DependencyObject obj)
{
return (bool)obj.GetValue(IsLastProperty);
}
public static void SetIsLast(DependencyObject obj, bool value)
{
obj.SetValue(IsLastProperty, value);
}
/// <summary>
/// Informs if a ListBoxItem is the last in the collection.
/// </summary>
public static readonly DependencyProperty IsLastProperty =
DependencyProperty.RegisterAttached("IsLast", typeof(bool), typeof(IndexedListBox), new UIPropertyMetadata(false));
protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
{
// We capture the ItemsSourceChanged to check if the new one is modifiable, so we can react to its changes.
var oldSource = oldValue as INotifyCollectionChanged;
if(oldSource != null)
oldSource.CollectionChanged -= ItemsSource_CollectionChanged;
var newSource = newValue as INotifyCollectionChanged;
if (newSource != null)
newSource.CollectionChanged += ItemsSource_CollectionChanged;
base.OnItemsSourceChanged(oldValue, newValue);
}
void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.ReindexItems();
}
protected override void PrepareContainerForItemOverride(System.Windows.DependencyObject element, object item)
{
// We set the index and other related properties when generating a ItemContainer
var index = this.Items.IndexOf(item);
SetIsLast(element, index == this.Items.Count - 1);
SetIndex(element, index);
base.PrepareContainerForItemOverride(element, item);
}
private void ReindexItems()
{
// If the collection is modified, it may be necessary to reindex all ListBoxItems.
foreach (var item in this.Items)
{
var itemContainer = this.ItemContainerGenerator.ContainerFromItem(item);
if (itemContainer == null) continue;
int index = this.Items.IndexOf(item);
SetIsLast(itemContainer, index == this.Items.Count - 1);
SetIndex(itemContainer, index);
}
}
}
To test it, we setup a simple ViewModel and an Item class:
public class ViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
private ObservableCollection<Item> items;
public ObservableCollection<Item> Items
{
get { return this.items; }
set
{
if (this.items != value)
{
this.items = value;
this.OnPropertyChanged("Items");
}
}
}
public ViewModel()
{
this.InitItems(20);
}
public void InitItems(int count)
{
this.Items = new ObservableCollection<Item>();
for (int i = 0; i < count; i++)
this.Items.Add(new Item() { MyProperty = "Element" + i });
}
}
public class Item
{
public string MyProperty { get; set; }
public override string ToString()
{
return this.MyProperty;
}
}
The view:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication3"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="WpfApplication3.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="DataTemplate">
<Border x:Name="border">
<StackPanel Orientation="Horizontal">
<TextBlock TextWrapping="Wrap" Text="{Binding (local:IndexedListBox.Index), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Margin="0,0,8,0"/>
<TextBlock TextWrapping="Wrap" Text="{Binding (local:IndexedListBox.IsLast), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Margin="0,0,8,0"/>
<ContentPresenter Content="{Binding}"/>
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding (local:IndexedListBox.IsLast), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="True">
<Setter Property="Background" TargetName="border" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="0.949*"/>
</Grid.RowDefinitions>
<local:IndexedListBox ItemsSource="{Binding Items}" Grid.Row="1" ItemTemplate="{DynamicResource DataTemplate}"/>
<Button Content="Button" HorizontalAlignment="Left" Width="75" d:LayoutOverrides="Height" Margin="8" Click="Button_Click"/>
<Button Content="Button" HorizontalAlignment="Left" Width="75" Margin="110,8,0,8" Click="Button_Click_1" d:LayoutOverrides="Height"/>
<Button Content="Button" Margin="242,8,192,8" Click="Button_Click_2" d:LayoutOverrides="Height"/>
</Grid>
</Window>
In the view's code behind I put some logic to test the behavior of the solution when updating the collection:
public partial class MainWindow : Window
{
public ViewModel ViewModel { get { return this.DataContext as ViewModel; } }
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.ViewModel.Items.Insert( 5, new Item() { MyProperty= "NewElement" });
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
this.ViewModel.Items.RemoveAt(5);
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
this.ViewModel.InitItems(new Random().Next(10,30));
}
}
This solution can handle static lists and also ObservableCollections and adding, removing, inserting items to it. Hope you find it useful.
EDIT
Tested it with CollectionViews and it works just fine.
In the first test, I changed Sort/GroupDescriptions in the ListBox.Items. When one of them was changed, the ListBox recreates the containeirs, and then PrepareContainerForItemOverride hits. As it looks for the right index in the ListBox.Items itself, the order is updated correctly.
In the second I made the Items property in the ViewModel a ListCollectionView. In this case, when the descriptions were changed, the CollectionChanged was raised and the ListBox reacted as expected.