Thursday, November 27, 2008

Notification when items are added or removed from an ItemsControl
(ListBox, TabControl, TreeView etc.)

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.



13 comments:

Adam Cataldo said...

This was very helpful. Thanks for posting this.

Anonymous said...

Good finding! However, in reality, I would prefer to create a viewmodel which contains an observable collection that are populated with the domain data. This will be a cleaner solution.

Dan Lamping said...

That is exactly the case that I'm catering for here!

An observable collection in the view-model is bound to the items in the listView using ItemsSource.

When the view-model's collection changes or is changed for another collection, the listview's Items collection will be updated by the binding.

You can then use the technique in the post to do such tasks as bring a newly added listviewitem into view.

Hope this helps
Dan

guy said...

Thanks, this is great -- I appreciate the post.

Ilija Injac said...

Hi there ! That's cool stuff, that saved me so much time ! Thx, Buddy !

Jayant D. Kulkarni said...

Hi, I tried with this but it seems that the CollectionChanged event is not firing when an item is added. I have assigned ItemsSource to a treeview. But it seems that CollectionChaged event is not working.

Sergei said...

thank you! I have spent lots of time before finding your excelent solution!

Brian Friesen said...

Beautiful! I hadn't even noticed that ItemCollection inherited from INotifyCollectionChanged. Sneaky explicit implementation...

Josant said...
This comment has been removed by the author.
Josant said...

Good Sample..!

But..Please help me..
How to create this WPF sample using Powerbuilder 12 .NET ?

schollii said...

Well described, thanks!

schollii said...

A very high-level comparison of this with another method is at stackoverflow. Your feedback would be welcome.

Anonymous said...

Thanks! Ive been looking everywhere for this. Simple, elegant ad it work!