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.