Skip Navigation LinksHome > View Post

Sortable ListView in WPF

Before we get to our little exercise in Xaml fun I promised in my last post, I want to solve a problem that's already been solved. Sounds like fun eh?

The ListView in WPF is a great control and long-term readers will know I'm a big fan of Winfows.Form's ListView. The one thing everybody wants from their ListView control is sorting.

Fortunately, the guys at MSDN have this covered and have a nice concise article covering just how to do this: How to: Sort a GridView Column When a Header Is Clicked. Nice.

However, if you read the article I expect, like me, your object oriented spider sense* will start to tingle. All the code for the sorting finds its way into your Window or Pages's code-behind and needs member variables. What if I want multiple ListViews? It's all going to get very untidy!

However, there is a very simple and much more elegant solution within easy grasp.

Extending an existing control

The header above should give you a clue to what we're going to do here.

public class SortableListView : ListView
{

Easy start eh? We create a new 'SortableListView' by deriving from the existing ListView to get all that juicy functionality without any effort - we just want to extend it.

In order to support sorting, we need our new implementation to be notified whenever a nested Column Header is clicked. Let's wire up a RoutedEventHandler in the constructor.

public SortableListView()
{
    // register a handler for the GridViewColumnHeader's Click Event
    this.AddHandler(
        GridViewColumnHeader.ClickEvent,
        new RoutedEventHandler(GridViewColumnHeaderClickedHandler));
}

The actual delegate 'GridViewColumnHeaderClickedHandler' is pretty much taken exactly from that MSDN article with one main difference which we'll come to later. We also steal their 'Sort' method and make two little changes...

private void Sort(string sortBy, ListSortDirection direction)
{
    // refer to our *OWN* ItemsSource
    ICollectionView dataView = CollectionViewSource.GetDefaultView(this.ItemsSource);

    // check the dataView isn't null
    if (dataView != null)
    {
        dataView.SortDescriptions.Clear();
        SortDescription sd = new SortDescription(sortBy, direction);
        dataView.SortDescriptions.Add(sd);
        dataView.Refresh();
    }
}

First, we reference our own ItemsSource rather than that of the specified ListView control, called 'lv' in the example. Secondly, we check that the dataView isn't null to avoid an error when the ListView is empty.

And we're done...

But it won't work!

....well, not quite. There is another problem with the example code (and it is just an example, so fair enough!). If the Header of your GridViewColumn does not exactly match the member name then sorting won't work.

This is likely to be a common requirement as we don't want to show headers like 'MemberName' in the UI, much better that we can specify this independently.

Step up, attached properties

What we want to do here, is add an expando attribute to GridViewColumn to allow us to specify the SortPropertyName. Mike Hillberg has a great post on creating attached properties so I'll just show you the code to create one.

public static readonly DependencyProperty SortPropertyNameProperty =
DependencyProperty.RegisterAttached("SortPropertyName", typeof(string), typeof(SortableListView));

public static string GetSortPropertyName(GridViewColumn obj)
{
return (string)obj.GetValue(SortPropertyNameProperty);
}

public static void SetSortPropertyName(GridViewColumn obj, string value)
{
obj.SetValue(SortPropertyNameProperty, value);
}

Finally, let's take a look at that GridViewColumnHeaderClickedHandler method we mostly stole from the MSDN article:

private void GridViewColumnHeaderClickedHandler(object sender, RoutedEventArgs e)
{
    GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
    ListSortDirection direction;

    if (headerClicked != null &&
        headerClicked.Role != GridViewColumnHeaderRole.Padding)
    {
        if (headerClicked != _lastHeaderClicked)
        {
            direction = ListSortDirection.Ascending;
        }
        else
        {
            if (_lastDirection == ListSortDirection.Ascending)
            {
                direction = ListSortDirection.Descending;
            }
            else
            {
                direction = ListSortDirection.Ascending;
            }
        }

        // see if we have an attached SortPropertyName value
        string sortBy = GetSortPropertyName(headerClicked.Column);
        if (string.IsNullOrEmpty(sortBy))
        {
            // otherwise use the column header name
            sortBy = headerClicked.Column.Header as string;
        }
        Sort(sortBy, direction);

        _lastHeaderClicked = headerClicked;
        _lastDirection = direction;
    }
}

We've used the GetSortPropertyName to see if the clicked column has a SortPropertyName, if not we just fallback to using the Header just like the example.

Now, the only Xaml we need in our Window is below.

<controls:SortableListView ItemsSource="{Binding Employees}">
<controls:SortableListView.View>
    <GridView>
     <GridViewColumn Header="First Name" DisplayMemberBinding="{Binding FirstName}"
        controls:SortableListView.SortPropertyName="FirstName"/>
     <GridViewColumn Header="Surname" DisplayMemberBinding="{Binding AverageCallTime}" />
    </GridView>
</controls:SortableListView.View>
</controls:SortableListView>

Best of all, we don't need any codebehind at all from now on! Download the full class listing.

Good lord. I love WPF. And Xaml.

* thanks to Chris Anderson for this superb phrase!

Note that I've left the up and down arrow as an exercise for the reader :)

PS - This example was put together pretty quickly so your own mileage may vary! Be careful to test it meets your needs before using it!

Tags: WPF

 
Josh Post By Josh Twist
2:52 AM
04 Jul 2007

» Next Post: Infinite Resolution and SnapsToDevicePixels
« Previous Post: System.Diagnostics hidden SourceLevels

Comments are closed for this post.

Posted by Mike Brown @ 10 Jul 2007 8:23 AM
Hey Josh, I think another Josh (Smith) went the same route of using inheritance to implement sorting in the ListView.

I went another route. I used attach properties to decorate a standard ListView with sorting functionality. Check it out and tell me what you think.

http://mbrownchicago.spaces.live.com/blog/cns!2221DC39E0C749A4!331.entry

Posted by Mike Brown @ 10 Jul 2007 8:39 AM
Also you don't need an attached property to get the actual property name to sort by. You can get that off of the column's DisplayMemberBinding property like so

string headerProperty = ((Binding)headerClicked.Column.DisplayMemberBinding).Path.Path;

Posted by Josh @ 10 Jul 2007 11:44 PM
Hey Mike, thanks for dropping by. I did think about using the DisplayMemberBinding but I like'd the idea that the SortPropertyName is entirely independant.

The DisplayMemberBinding would probably be a better 'fallback' though.

I did point out that 'I want to solve a problem that's already been solved' - there are hundreds of posts out there :)

I like your implementation too - isn't Xaml awesome.

Posted by Dean White @ 22 Apr 2008 9:58 PM
Hi Josh

I am trying your Implimentation on a listview bound to a Linq to SQL datasource but without much sucess.

Should I maybe be sorting in Linq and rebinding

Regards,
Dean

Posted by Joe @ 15 May 2008 7:23 AM
Hi Mike, I am fairly new to WPF and XAML...

I am getting an error 'SortableListView is not supported in a WPF project'...

Is there something I need to include in the header of the XAML?

Posted by Joe @ 15 May 2008 7:24 AM
Follow up:

Also getting 'The namespace prefix "controls" is not defined...

Posted by Josh @ 17 May 2008 3:02 AM
You need to include that xlm namespace and have it reference the clr namespace. e.g.

<Window xmlns:controls="clr-namespace:Namespace.With.Your.Control.In.It; assembly=NameOfAssembly">

Posted by omkar dande @ 21 Jun 2009 11:58 PM
I have listview which contains numbers (double). I am using the same code but the sort is not working. Althought it works perfectly for the columns containing String ( ex, firstname or lastname) datatype.

Thanks in advance.

Posted by josh @ 22 Jun 2009 8:50 AM
Hi Omkar,

Is the datatype of the numbers actually a string or a double?

We use the ListCollectionView to do the sorting here and it deals with numeric types just fine - but if the datatype of the property is string, it will sort in alphanumeric order.

Josh

Posted by Tuby @ 02 Aug 2009 6:12 AM
Thanks for an excellent article...just what I was looking for.

Posted by Shin @ 29 Aug 2009 4:02 AM
Is there any way to stop sorting once its started???

Iam spawing other thread to handle sorting but it doesnt work out correctly somehow :-(

Posted by Josh @ 06 Sep 2009 11:43 PM
Hi Shin,

You could programmatically clear the SortDescriptions. But this implies that the items are sorted in some way originally (even if randomly). If you want it to return to that state you'll have to somehow remember the order.

Josh

© 2005 - 2014 Josh Twist - All Rights Reserved.