It's not obvious how to listen to changes to the
Items of an ItemsControl.
This post outlines various problems and identifies a simple way to listen to this collection regardless of how the ItemsControl was populated.
In general, there are two ways to add items to an ItemsControl:
- Manually add to the ItemCollection of the control. This is accessed using the Items property of the control.
- Assign a notifying source to the ItemsSource of the control. This will update the Items of the
control automatically as the collection changes.
If you are adding items manually to your ItemsControl, then listening to changes isn't such a big
deal since you can just call a method after you have added an item. If you are using an ItemsSource, however, then
listening to changes is often required for UI tasks such as bringing a new item into view.
The standard way of listening to a notifying source is by using a
CollectionViewSource. If you
were to attempt to use it to listen to the items of a ListBox you would do something like this:
<Window.Resources>
<CollectionViewSource x:Key="cvs" x:Name="cvs" Source="{Binding}"/>
</Window.Resources>
<ListBox x:Name="mListBox" ItemsSource="{Binding}"/>
public MyWindow()
{
InitializeComponent();
CollectionViewSource cvs = (CollectionViewSource)FindResource("cvs");
cvs.View.CollectionChanged += View_CollectionChanged;
}
private void View_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
// scroll the new item into view
mListBox.ScrollIntoView(e.NewItems[0]);
}
}
To me, this is quite an ugly solution since it isn't resistant to a change
of ItemsSource on the ListBox. At first, it seems this could be fixed by binding
the Source of the CollectionViewSource directly to the ListBox:
<Window.Resources>
<CollectionViewSource x:Key="cvs" x:Name="cvs"
Source="{Binding ElementName=mListBox, Path=ItemsSource}"/>
</Window.Resources>
<ListBox x:Name="mListBox" ItemsSource="{Binding}"/>
But this doesn't help. Since the
View property of the CollectionViewSource
has no update mechanism (it isn't a dependency property), there's no way
of knowing when it changes.
A different approach is required...
The first thing most developers look for when attacking this problem is an event handler on Items.
But, when you look at the members generated by Intellisense you find...
...that there's no
CollectionChanged event handler here, so how do you listen to changes in the collection?
The problem is that the
INotifyCollectionChanged interface which contains the event handler
is
explicitly implemented, which means you have to first cast the ItemCollection before
the event handler can be used:
public MyWindow()
{
InitializeComponent();
((INotifyCollectionChanged)mListBox.Items).CollectionChanged +=
mListBox_CollectionChanged;
}
private void mListBox_CollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
// scroll the new item into view
mListBox.ScrollIntoView(e.NewItems[0]);
}
}
Ta da! This is much better - it's simple and robust.
So, in conclusion, this is the best way to listen to changes to the Items of an ItemsControl. There's no messing about with CollectionViewSource and worrying about
updating when a new ItemsSource is assigned...it works regardless of whether ItemsSource is being used and
is resistant to changes in ItemsSource.