It seems to be quite difficult to implement the attached property “Index” to the ListBoxItem to work correctly, I believe that an easier way to achieve this would be in MVVM. You can add the necessary logic (IsLast property, etc.) to the list entity type and allow the ViewModel to handle this by updating it when the collection is changed or replaced.
EDIT
After some attempts, I was able to implement ListBoxItems indexing (and therefore checking for the latter) using a combination of attached properties and inheriting from the ListBox. Check this:
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 this, we set up a simple ViewModel and 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; } }
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 code that I set, there is some logic for checking 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 process static lists as well as ObservableCollections and add, delete, insert elements into it. I hope you find this helpful.
EDIT
Tested it with CollectionViews and it works great.
In the first test, I changed Sort / GroupDescriptions to ListBox.Items. When one of them has been changed, the ListBox recreates the containers, and then runs PrepareContainerForItemOverride. Since it searches for the desired index in the ListBox.Items themselves, the order is updated correctly.
In the second, I created the Items property in ViewModel ListCollectionView. In this case, when the descriptions were changed, CollectionChanged was raised and the ListBox reacted as expected.