Apr 09, 2006

How to add custom sorting logic

21CustomSorting

I showed in an earlier post how we can use CollectionViewSource to sort items in markup. This way of sorting allows us to pick Ascending or Descending sorting, but it doesn’t provide any flexibility beyond that. Today I will show you how you can write your own sorting logic that enables full control over the order of items in a view.

When I write a new blog sample, I create a directory named with a number followed by a short description of the scenario. For example, for today’s post, the name of the directory is 21CustomSorting. Windows Explorer knows how to do the right thing to order all those directories: the first one is 1DataContext, and all the others are displayed in increasing numeric order until 21CustomSorting. This is not a simple ascending string sort, though. A traditional string sort would display 10MasterDetail - 19ObjectDataProviderSample, 1DataContext, 20InsertingSeparators, 21CustomSorting, 2EmptyBinding - 9CollectionViewSourceSample, which is obviously not what I want. I will show you today how you can order strings that start with numbers the same way as Explorer. I will also give you a quick overview of Avalon’s view classes.

A view is a layer on top of a collection that allows us to sort, filter and group the collection. Views also remember the current item of a collection, which is useful for the master-detail scenario. The base class for all views is CollectionView, which is where we keep the API for sorting, filtering, grouping and current item. There are currently two classes that derive from CollectionView: BindingListCollectionView and ListCollectionView. The view type we create internally depends on the type of your source collection:

  • A CollectionView is created if your source implements IEnumerable. If the source implements IEnumerable *only*, you will not be able to sort or group the collection (you can only filter it). Also, perf will not be the best if the source has a large number of items or if you perform dynamic operations such as insertions and deletions. If this is your scenario, you should consider having your source implement a stronger interface. ICollection is slightly better because it provides a Count property.
  • ListCollectionView is the view type created when the source implements IList. Compared to IEnumerable and ICollection, IList performs much better for large or dynamic lists because it provides an indexer, allowing us quick random access. In addition, IList allows sorting, grouping and filtering. But ideally your source collection derives from ObservableCollection<T>, the mother of all collections in the eyes of data binding, since it provides several extra goodies such as property and collection change notifications.
  • BindingListCollectionView is the type of view created by Avalon when the source collection implements IBindingList. This is the view we deal with in the ADO.NET scenario. It supports sorting and grouping, but not traditional filtering. Instead, it has an additional CustomFilter property that delegates filtering to the DataView (see my post on ADO.NET for more details).

Note that if your source collection implements both IBindingList and IList, IBindingList has priority, and a BindingListCollectionView is created.

ListCollectionView has an extra CustomSort property that allows us to provide our own sorting logic, which is what I will show you next. The source collection for this sample is an ObservableCollection<string> of the directory names that I use for my blog posts. The UI of this sample contains a ListBox of blog post directory names, initially in random order, and a “Sort” Button:

    <Window.Resources>
        <local:BlogPosts x:Key="posts"/>
        (…)
    </Window.Resources>

    <ListBox ItemsSource="{StaticResource posts}" (…) Name="lb"/>
    <Button Click="Sort" Content="Sort" Margin="0,10,0,0"/>

When the user clicks the “Sort” button, we get the default view for the source collection, cast it to ListCollectionView, and give it some custom sort logic. The GetDefaultView method returns a CollectionView, but we can cast it to ListCollectionView because we know that the BlogPosts source collection implements IList.

    private void Sort(object sender, RoutedEventArgs args)
    {
        BlogPosts posts = (BlogPosts)(this.Resources["posts"]);
        ListCollectionView lcv = (ListCollectionView)(CollectionViewSource.GetDefaultView(posts));
        lcv.CustomSort = new SortPosts();
    }

The CustomSort property is of type IComparer. We need to create a new class that implements IComparer, and add our sorting logic to its Compare method. This method should return a negative number if x comes before y, a positive number if x comes after y, and zero if they are equivalent.

    public class SortPosts : IComparer
    {
        public int Compare(object x, object y)
        {
            (…)
        }
    }

Here is a screenshot of the completed sample:

Here you can find the VS project with this sample code. This works with February CTP WPF bits.

17 Comments
  1. andy tarpey

    Hi Beatriz,

    I am finding that ListCollectionView is not being created as the view type when the list item source implements the generics IList(T), although it is when the source implements IList.

    Can you confirm if this is what you would expect?

    Thanks,
    Andy.

    • Bea

      Hi Andy,

      Yes, that is expected, and a very good point that I should have made clear in the blog post.

      IList(T) does not implement IList, it only implements IEnumerable. This causes a CollectionView to be created, and NOT a ListCollectionView. So if your collection implements IList(T), make sure it also implements IList to improve the performance of your app.

      Bea

  2. William

    Hi Beatriz,

    I have been going nuts trying to get binding to an IBindingList working with sorting. I have implemented all of my sorting properties and methods on my custom class, but, other than SupportsSortingCore, none of them ever get called…I can see by setting breakpoints on all of them. What do you have to do to get data binding work with sorting when you have a BindingListCollectionView? Here is my collection class:

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.ComponentModel;

    namespace CollectionViewSourceStuff
    {
    public class Persons : BindingList(lt)Person(gt)
    {
    private bool _isSorted = false;
    private ListSortDirection _sortDirection;
    private PropertyDescriptor _sortProperty;

    protected override bool SupportsChangeNotificationCore
    {
    get { return true; }
    }

    protected override bool SupportsSortingCore
    {
    get { return true; }
    }

    protected override bool IsSortedCore
    {
    get { return _isSorted; }
    }

    protected override ListSortDirection SortDirectionCore
    {
    get
    {
    return _sortDirection;
    }
    }

    protected override PropertyDescriptor SortPropertyCore
    {
    get { return _sortProperty; }
    }

    public Persons()
    {
    this.Add(new Person(“Fred”));
    this.Add(new Person(“Adam”));
    this.Add(new Person(“Jane”));
    this.Add(new Person(“Beth”));

    //Type type = typeof(Person);
    //PropertyDescriptor firstNameProp = TypeDescriptor.GetProperties(type).Find(“FirstName”, false);
    //this.ApplySortCore(firstNameProp, ListSortDirection.Ascending);
    }

    protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
    {
    _sortProperty = prop;
    _sortDirection = direction;

    List(lt)Person(gt) items = this.Items as List(lt)Person(gt);

    if (items != null)
    {
    PropertyComparer(lt)Person(gt) pc = new PropertyComparer(lt)Person(gt)(prop, direction);
    items.Sort(pc);
    _isSorted = true;
    }
    else
    {
    _isSorted = false;
    }

    OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
    }

    protected override void RemoveSortCore()
    {
    _isSorted = false;
    _sortProperty = null;
    }
    }
    }

    Thanks! I am about to go mad over this one…I just watched your Mix presentation to review data binding, but still no go. Argh. (Had to switch the generic tag syntax to (lt) and (gt) to post this…)

    William

    • Bea

      Hi William,

      Unfortunately you’re hitting a bug in WPF data binding. We don’t support sorting on a custom implementation of IBindingList unless you also implement ITypedList. The fact that this was found by a customer bumped up the priority of this bug, so hopefully we’ll get a chance to fix it for our next version.

      As a workaround, try implementing ITypedList. That should work, but if it doesn’t, let me know.

      Bea

      • Brette

        So does the ListView support sorting on collections derived from IBindingList ? I tried your suggestion of further implementing ITypedList and did not have any luck. Any idea if it is possible without wrapping it is some other collection that coerce the Source type to ListCollectionView?

        Brette

        • Bea

          Brette,

          If you wrap your source with a ListCollectionView, since IBindingList support IList, you should be able to sort it. In this case, sorting will not be delegated to the IBindingList - it will be performed the same way it would if the collection didn’t implement IBindingList.

          If the bug hasn’t been fixed and implementing ITypedList didn’t help, you may want to open a WPF bug on this issue. This will bump up the priority of the internal bug.

          Bea

  3. Anonymous

    Is it possible to use this method for multicolumn sorting?

    • Bea

      Can you explain better what you mean by multicolumn sorting?

      If you’re talking about a ListView scenario, where you first sort by one column, and have a secondary sort by another column, there is a simpler syntax to do that. You can simply add several SortDescription instances to the SortDescriptions collection. The first one is the primary sort, the second one is the secondary sort, and so on.

      But as a general rule, you should be able to use custom sorting to do any type of sorting. For some more common scenarios we have a simpler syntax, but custom sorting would still work.

      Thanks,
      Bea

  4. steve

    How about a sample project for download on multisorting on a listview

    • Bea

      Hi Steve,

      You can do that with custom sorting, as I show in this post. An even easier way of doing that would be to simply add several sort descriptions to the view. You could even do that all in XAML, by using a CollectionViewSource.

      Thanks,
      Bea

  5. Kenneth Haugland

    Hi Bea

    I know this is an old thread but, im facing a problem with the CollectionView…. It goes as follows:

    I have an ObservableCollection(of MyClass) and I use the example from the MSDN Library to do the column sorting….

    The problem Im having is that after I sort the Collection View the original index is lost at the CollectionView to the ObservableCollection. It messes up the whole code… Is there a way of binding the ObservableCollection and the CollectionView?

    Kenneth

    • Bea

      Hi Kenneth,

      I’m not sure that I understand your question. When you bind to an ObservableCollection, a collection view is created, and your UI ends up really binding to the view instead. Sorting happens at the view level only.

      Not sure if this is what you mean, but if you select an item and then sort the collection, the item you selected earlier remains selected. To be clear, if you select item A in index 0, and then sort the collection so that item A is now in index 3, item A (in index 3) is selected. This is by design - WPF tracks items based on their actual identity and not based on index.

      Bea

  6. Domingos

    Bea,

    This is my first comment on your blog! First, I would like to thank you for the very nice posts.

    Now to views… I was really frustrated that the BindingList support in WPF was incomplete even in the latest beta (.NET 4). I have the following scenario: suppose I have business objects Mother and Child. There is no default constructor for Child, since each instance must be initialized with a Mother. Mother has a BindingList collection of its children.

    Eventually I have Mother as the DataContext of some window and the child collection will be used as source in a CollectionViewSource which in turn is used as the ItemsSource of a DataGrid in that window.

    The default behavior of WPF is to create a BindingListCollectionView for the children. However, this does not allow me to sort the items (and I don’t want to write a tedious solution for sorting…). Forcing the view to be of type ListCollectionView does allow sorting, but now the “AddNew” functionality of the DataGrid, which relies on the view, does not work (because ListCollectionView does not know how to create a new instance of Child—it does not have a default constructor).

    I came with a prototype solution as follows. The collection of children must inherit from BindingList and override the “RemoveItem” method so that it can raise the ListChanged event with a parameter of type DeleteListChangedEventArgs.
    public class DeleteListChangedEventArgs : ListChangedEventArgs
    {
    public DeleteListChangedEventArgs(object item) : base(ListChangedType.ItemDeleted, -1)
    {
    Item = item;
    }

    public object Item { get; set; }
    }

    With this we have the actual deleted item on the notification of list changes of our BindingList.

    Here is the code for my CollectionView implementation:

    class MyBindingListCollectionView : ListCollectionView, IEditableCollectionView
    {
    public IBindingList MySourceCollection { get; private set; }

    private bool SuspendNotification;

    public Type ItemType { get; private set; }

    public MyBindingListCollectionView(IBindingList list)
    : base(CopyBindingList(list))
    {
    ItemType = ItemTypeFromList(list);
    MySourceCollection = list;
    MySourceCollection.ListChanged += new ListChangedEventHandler(MySourceCollection_ListChanged);
    }

    protected static Type ItemTypeFromList(object source)
    {
    Type srcT = source.GetType().GetInterfaces().First(i => i.Name.StartsWith(“IList”));
    return srcT.GetGenericArguments().First();
    }

    ///
    ///
    /// Given a source that implements IList, construct an
    /// ObservableCollection with a copy of the source.
    ///
    ///
    protected static IList CopyBindingList(IBindingList source)
    {
    Type desT = typeof(ObservableCollection).MakeGenericType(ItemTypeFromList(source));
    IList lst = Activator.CreateInstance(desT) as IList;
    foreach (object o in source)
    {
    lst.Add(o);
    }
    return lst;
    }

    protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs args)
    {
    base.OnCollectionChanged(args);
    if (SuspendNotification) return; // ignore changes
    SuspendNotification = true;
    try
    {
    switch (args.Action)
    {
    case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
    foreach (object o in args.NewItems)
    {
    if (o.GetType().Equals(ItemType))
    {
    MySourceCollection.Add(o);
    }
    }
    break;
    case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
    foreach (object o in args.OldItems)
    {
    MySourceCollection.Remove(o);
    }
    break;
    case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
    // what do we do in this case?
    break;
    }
    }
    finally
    {
    SuspendNotification = false;
    }
    }

    void MySourceCollection_ListChanged(object sender, ListChangedEventArgs e)
    {
    if (SuspendNotification) return; // ignore changes
    SuspendNotification = true;
    IList src = SourceCollection as IList;
    try
    {
    switch (e.ListChangedType)
    {
    case ListChangedType.ItemAdded:
    src.Add(MySourceCollection[e.NewIndex]);
    break;
    case ListChangedType.ItemDeleted:
    DeleteListChangedEventArgs ex = e as DeleteListChangedEventArgs;
    if (ex != null)
    {
    src.Remove(ex.Item);
    }
    break;
    case ListChangedType.Reset:
    src.Clear();
    foreach (object o in MySourceCollection)
    {
    src.Add(o);
    }
    break;
    }
    }
    finally
    {
    SuspendNotification = false;
    }
    }

    object m_New = null;
    object IEditableCollectionView.AddNew()
    {
    m_New = MySourceCollection.AddNew();
    return m_New;
    }

    bool IEditableCollectionView.CanAddNew
    {
    get { return CurrentAddItem == null && CurrentEditItem == null; }
    }

    object IEditableCollectionView.CurrentAddItem
    {
    get { return m_New; }
    }

    bool IEditableCollectionView.IsAddingNew
    {
    get { return m_New != null; }
    }

    void IEditableCollectionView.CancelNew()
    {
    MySourceCollection.Remove(m_New);
    m_New = null;
    }

    void IEditableCollectionView.CommitNew()
    {
    m_New = null;
    }
    }

    As you may have noted, I tried to implement only the methods of IEditableCollectionView that are strictly necessary for the AddNew functionality. Since I am forwarding the “AddNew” to the BindingList, objects of type Child are created when necessary. I tested (very briefly) this code with a DataGrid and I didn’t have any problems.

    It surely isn’t a very elegant solution, but it is a lot less work than to build a CollectionView from nothing. My question is: why didn’t the designers of ListCollectionView used the IBindingList contract to provide AddNew functionality when the SourceCollection implements it?

    Thanks,

    Domingos.

    • Bea

      Hi Domingos,

      I have several thoughts related to the problem you describe:

      - You mention that BindingListCollectionView doesn’t support sorting, but that is not correct. IBindingList supports sorting by specifying a property name and a direction, and BindingListCollectionView delegates sorting to the underlying collection. However, it doesn’t support custom sorting, which I’m assuming is your scenario.

      - You ask why ListCollectionView doesn’t delegate AddNew to the underlying collection. ListCollectionView is optimized to work with collections that implement IList, not collections that implement IBindingList. BindingListCollectionView, as expected, delegates AddNew to the underlying IBindingList collection.

      - The limitation of not being able to add a new item when the source has no default constructor is very well understood by the team. WPF 4.0 Beta 2 has a new feature that brings us a step closer to having a solution: the introduction of IEditableCollectionViewAddNewItem containing the AddNewItem method. You can read the MSDN documentation about this feature. The sample in MSDN shows how to use it when creating your own custom UI to add a new item (using a ListBox to display the data and a dialog box to enter the new item). From what I can tell, DataGrid doesn’t yet use this method though (although it’s a bit hard to be 100% sure because Reflector doesn’t decompile 4.0 Beta 2 bits).

      - Your solution looks great.

      Bea

  7. dan

    Hi Bea,
    Is there a simple way to force this custom sorting to happen on another thread and allow a sort to be cancelled mid way through?
    Thanks
    Daniel

    • Bea

      Hi Dan,

      There is nothing in WPF that makes that especially easy - you will need to write your own custom logic.
      If you want to allow it to be cancelled, you may need to create a copy of the view and sort that instead. This way, if the user cancels, you still have the original order.

      Bea

  8. Duncan Bayne

    Bea,

    Thanks for this & other handy Silverlight posts - I have another addition to my morning tech-related-RSS addiction :-)

Comments are closed.