In one of my recent experiments, I was required to add sortingability to a custom control. The control itself displays a bunch of items just like the SlideDeckPanel. Additionally for each item Add / Remove the control also sorts the items automatically. Before I describe how I did it, let me talk a little bit about the control, just to set the context.
A little bit about the control
The SlideDeckControl (haven’t blogged about it yet!) is quite a control in itself. It behaves just like an ItemsControl but gives enormous flexibility to plugin your own ItemContainerGenerator (for virtualizing purposes), LayoutModes (similar to ItemsPanel but lot more flexible), Hover behaviors, Selection behaviors. Initially when I started out, the SlideDeckControl derived from ItemsControl. It had only one kind of virtualizing layout, so a single ItemContainerGenerator worked well. However, after some talks with our Design team, the requirements for the control changed drastically. The control was now supposed to do multiple layouts and dynamically switch between them (based on some criteria). Additionally the UI generation logic for each LayoutMode was slightly different and hence required multiple ItemContainerGenerators. Also as you would imagine the virtualizing logic was also different. On top of that the control had dynamic Hover and Selection behaviors and had to maintain these behaviors across the different layout possibilities.
Surely the ItemsControl was not going to work for me. Thus I began writing a new control that had a few features of ItemsControl and some extra stuff for my needs. In the process I had to build some of the logic for ItemsSource, ItemTemplate, etc.
We need Sorting, Filtering, Grouping
Soon enough I had to add one more feature related to data-handling. I had to do automatic SORT whenever an item got added / removed / changed. This was little tricky given the fact that I had already built up some code for data-manipulation. Initially I thought of plugging in some LINQ query and detect collection changes. That required more effort to keep the UI items in sync with the data items. After some research, the CollectionView turned out to be a savior. The following quote in the Remarks section of the MSDN docs caught my eye:
In WPF applications, all collections have an associated default collection view. Rather than working with the collection directly, the binding engine always accesses the collection through the associated view. To get the default view, use the CollectionViewSource.GetDefaultView method.
So by default when I set up a {Binding} for my ItemsSource, I get an instance of CollectionView (more precisely an ICollectionView). Why is this interesting and exciting ? CollectionView provides out-of-the-box support for Sorting, Filtering, Grouping. If I setup the SortDescriptions, Filter predicate and the GroupDescriptions, the CollectionView takes care of determining the right indexes for the items. I only have to listen to the CollectionChanged event on the CollectionView and make the necessary changes on my UI. Cool !
In Code…
The following snippet shows the property-changed-handler for the ItemsSourcedependency property:
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SlideDeckControl control = d as SlideDeckControl;
if (e.OldValue != null)
{
ICollectionView view = CollectionViewSource.GetDefaultView(e.OldValue);
view.CollectionChanged -= control.ItemsSource_CollectionChanged;
control.Items = null;
}
if (e.NewValue != null)
{
control.Items = new List<object>();
ICollectionView view = CollectionViewSource.GetDefaultView(e.NewValue);
//FIX Adding a SortDescription just for test
view.SortDescriptions.Add(new SortDescription("SpineValue", ListSortDirection.Ascending));
view.CollectionChanged += control.ItemsSource_CollectionChanged;
}
}
Note how the collection changed handler is setup on the CollectionView. In the Collection changed handler I look at the type of change and process accordingly:
private void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
OnItemsAdded(args.NewStartingIndex, args.NewItems);
break;
case NotifyCollectionChangedAction.Move:
break;
case NotifyCollectionChangedAction.Remove:
OnItemsRemoved(args.OldStartingIndex, args.OldItems);
break;
case NotifyCollectionChangedAction.Replace:
break;
case NotifyCollectionChangedAction.Reset:
OnItemsReset();
break;
}
}
private void OnItemsReset()
{
Items.Clear();
NotifyUI(-1, NotifyCollectionChangedAction.Reset);
}
private void OnItemsRemoved(int index, IList items)
{
int itemCount = items.Count;
for (int i = 0; i < itemCount; i++)
{
Items.RemoveAt(index);
NotifyUI(index, NotifyCollectionChangedAction.Remove);
}
}
private void OnItemsAdded(int index, IList items)
{
int itemIndex = index;
foreach (object item in items)
{
// Add to Items collection
Items.Insert(itemIndex, item);
// Prepare UI
NotifyUI(itemIndex, NotifyCollectionChangedAction.Add);
itemIndex++;
}
}
NotifyUI() is a private function that looks at the current LayoutMode and passes control to it. The thing to note here is that the CollectionChanged event carries all the information regarding the final indexes of the items. So all one has to do is insert / remove at the given indexes. Can’t get any simpler !