Sunday, November 22, 2009

Building a smarter WPF CollectionView: One that actively tracks group changes!

Recently, I needed to group items by category into a ListView.  So of course I set up a CollectionViewSource and described my grouping criteria.  Everything was hunky-dory except this particular application doesn’t make all of its changes to the objects through the gui. Sometimes in the object layer, the value being wrapped in my view models might change.  This raises the INotifyPropertyChanged event.  WPF sees the new value (it’s also bound to a Label), but does not move the item from one category to another.

Looking online for solutions I found two – neither of which met my needs.

One was to call CollectionViewSource.View.Refresh() when I get a property change notification.  This solution did not meet my needs because it was expensive (performance wise) and it was too drastic – rebuilding DataTemplates and consequently resetting unbound values of dependency properties (an expander is used in the ListView’s GroupStyle where its IsExpanded is not bound for example). 

The second was even more unpalatable, it required me to implement IEditableObject and again the fact that not all changes came through the gui (some were from WCF callbacks for example), this solution didn’t work.  Either way it also seemed tedious to me to implement this interface.

The notable Dr. WPF describes the problem and his proposed workarounds here: http://www.drwpf.com/blog/Home/tabid/36/EntryID/42/Default.aspx

There was only one solution for me, someone would need to build a view that attached Bindings for each PropertyGroupDescription to each item and therefore would know when group values changed.  Since I could not find such a thing anywhere on the internet, and believe me I looked, it became apparent that I would have to write one myself.

This involved many steps and a good amount of time inside Reflector to figure out all the points I absolutely needed to override to prevent the CollectionView class from processing changes in a way I don’t want.  That being said, it was probably only about an hour or so of work, so it’s rewarding to say the least.

Choosing a base class:
I wanted to extend ListCollectionView as I knew I wanted to support both SortDescriptionCollection and IComparer sorting, however seeing how much of it was not virtual, it became necessary for me to extend CollectionView (and then add a CustomSort property).

SortDescriptionCollectionComparer:
A class that implements IComparer, and given a set of SortDescriptions sets up a Binding for each item, caching the result per item in an ArrayList, such that two comparisons using the same item will only need to evaluate the Binding for that item once.  Then it returns a comparison of two items by comparing the result of their Binding values.  If sorting by multiple keys but it can create a comparison using only one, the comparer will not even attempt to evaluate the second Binding.

ActiveGroupingCollectionViewItem:
A class that extends DependencyObject and contains the source collection index of the item as well as a reference to the source collection item.  If grouping is being used, it will also have a reference to the group object.  As well as all that, it has a dependency property for group values (a simple object[]).  Using a dependency property allows notification of a change of any of the grouping criteria.

ActiveGroupingCollectionViewItemCollection:
A KeyedCollection keyed by source collection index to ActiveGroupingCollectionViewItem.  This makes handling INotifyCollectionChanged events from the source collection much easier and faster.  The CollectionView class will internally keep one instance of this collection as its sortable copy of the items collection.

Using the attached code:
All you have to do is specify to the CollectionViewSource the CollectionViewType property.

<CollectionViewSource x:Key="ActiveGroupingCollectionView" Source="{x:Static local:PersonViewModel.People}" 
CollectionViewType="{x:Type e:ActiveGroupingCollectionView}">
<
CollectionViewSource.GroupDescriptions>
<
PropertyGroupDescription PropertyName="Category" />
<
PropertyGroupDescription PropertyName="Gender" />
</
CollectionViewSource.GroupDescriptions>
<
CollectionViewSource.SortDescriptions>
<
scm:SortDescription PropertyName="Age" />
</
CollectionViewSource.SortDescriptions>
</
CollectionViewSource>




Sample application:

The sample application shows two views on the same source collection.  One uses the standard ListCollectionView whereas the other uses the ActiveGroupingCollectionView – go ahead and play with the app.  Select a person and then modify its attributes, you’ll see it move from group to group in the view on the right but it will not do so in the view on the left.



Get the bits:

http://activecollectionview.codeplex.com/