WPF Tutorial - Using The ListView, Part 2 - Sorting

Skill

WPF Tutorial - Using The ListView, Part 2 - Sorting

Posted in:

Well, it has been quite a while, but I was reminded by a reader that when I say I'm going to write a part 2 to a tutorial, I probably should actually write a part 2 (and not wait 8 months to do it). Back in February, I wrote a tutorial on using the ListView in WPF, and after publishing it I got bored/distracted and so never ended up continuing the series. Well, guess what - it's back!

Today we are going to take a look at how to sort the content of a ListView. The actual sorting is actually really easy to do in code (it only takes a couple lines to tell the ListView to sort on a particular field). Where the fun comes in (and most of the work) is triggering the sort from the user interface and displaying the current sort direction in the column header. We are going to start from the exact same code as the end of the previous tutorial, so (since it has been a while) I recommend that you go take a look at it before you continue.

You've looked at the old code now? Good. Then we can move on to a screenshot of the app we will be creating today:

List View Sorting Screenshot

As you might have guessed, the data in the grid is sorted in Ascending by Creator - shown by that black triangle pointing downward in that column header. We will be writing the code to make that triangle appear in the correct header (and pointing in the right direction) - and that code is probably the most interesting in the tutorial. Without further ado, the XAML:

<Window x:Class="ListViewTest3.ListViewTest" Name="This"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="Some Game Data" Height="216" Width="435">
  <StackPanel>
    <ListView x:Name="gameListView" ItemsSource=
       "{Binding ElementName=This, Path=GameCollection}">
      <ListView.View>
        <GridView>
          <GridViewColumn Width="140"
               DisplayMemberBinding="{Binding GameName}">
            <GridViewColumnHeader Click="SortClick"
                                 Tag="GameName"
                                 Content="Game Name" />
          </GridViewColumn>
          <GridViewColumn Width="140"
               DisplayMemberBinding="{Binding Creator}">
            <GridViewColumnHeader Click="SortClick"
                                 Tag="Creator"
                                 Content="Creator" />
          </GridViewColumn>
          <GridViewColumn Width="140"
               DisplayMemberBinding="{Binding Publisher}">
            <GridViewColumnHeader Click="SortClick"
                                 Tag="Publisher"
                                 Content="Publisher" />
          </GridViewColumn>
        </GridView>
      </ListView.View>
    </ListView>
    <Button HorizontalAlignment="Right" Margin="5"
           Content="Add Row" Click="AddRowClick" />
  </StackPanel>
</Window>

First off, you might notice that I'm no longer setting the DataContext on the Window like I was in the previous ListView tutorial. This isn't because of anything specific to this tutorial, it is just that I've grown older and wiser. Setting the DataContext on a control can lead to some very frustrating binding debugging, especially when nesting user controls. Granted, every single time, it comes down to a "Duh" moment, but I've learned it is just safer to avoid setting the DataContext. There are still situations where it is warranted, but trying to save a few keystrokes when typing a binding expression is not one of those situations :P

Now that I've got that off my chest, lets dive into this code. There are actually only two significant things that changed between this XAML and the XAML of the previous tutorial - the removal of the Header attribute on the GridViewColumn tags, and the addition of GridViewColumnHeader tags inside of the GridViewColumn tags. This is because we want to do more complicated stuff with the column headers this time.

You can put anything you want inside of a GridViewColumnHeader (images, fancy other controls, etc..), but by this point you probably guessed that since everything else in WPF acts like that too. In this case we aren't doing anything special, we are just setting the Content attribute to the text we want to appear. The other two attributes, however, are more interesting. The Click event is hooked up to the function SortClick - the code for which we will be exploring in a moment. The Tag attribute holds some data that we will use in the SortClick function - in this case, the name of the backing field that we want to sort on (if we were going to sort on that column).

Just for reference purposes, here is the small class for the data displayed in the list view:

public class GameData
{
  public string GameName { get; set; }
  public string Creator { get; set; }
  public string Publisher { get; set; }
}

I said it was small, didn't I? While it is possible to get these field name out of the DisplayMemberBinding for the particular grid column, it is just a lot easier to store the info a second time in the easy to access Tag attribute. But by now you are probably wondering what we do with that info - so it is time for the SortClick function code:

public partial class ListViewTest : Window
{
  private GridViewColumnHeader _CurSortCol = null;
  private SortAdorner _CurAdorner = null;

  /* Other class code here .... */

  private void SortClick(object sender, RoutedEventArgs e)
  {
    GridViewColumnHeader column = sender as GridViewColumnHeader;
    String field = column.Tag as String;

    if (_CurSortCol != null)
    {
      AdornerLayer.GetAdornerLayer(_CurSortCol).Remove(_CurAdorner);
      gameListView.Items.SortDescriptions.Clear();
    }

    ListSortDirection newDir = ListSortDirection.Ascending;
    if (_CurSortCol == column && _CurAdorner.Direction == newDir)
      newDir = ListSortDirection.Descending;

    _CurSortCol = column;
    _CurAdorner = new SortAdorner(_CurSortCol, newDir);
    AdornerLayer.GetAdornerLayer(_CurSortCol).Add(_CurAdorner);
    gameListView.Items.SortDescriptions.Add(new SortDescription(field, newDir));
  }
}

So when this function is called, the very first thing we do is figure out is what column header the user clicked on - pretty easy to do, we just cast the sender as a GridViewColumnHeader. Then we pull that field name out of the Tag property. The next block of code might look a little unfamiliar, but what we are doing is if we were sorting on a previous column, we remove that black triangle and tell the list to stop sorting.

"Huh? What is all this stuff about Adorners?" is what you are probably saying right now. Well, Adorners are a handy thing in WPF for decoration type things. We don't really want to have to build a special GridViewColumnHeader to support the little triangle that we want to display when sorting. It would probably be pretty annoying and really clutter up the XAML (an extra canvas layer, etc..). Temporary UI decorations like this work really well as Adorners, which sit in their own layer on top of everything else. That is what the call to AdornerLayer.GetAdornerLayer is doing - we are getting the adorner layer for the GridViewColumnHeader. Adorner layers are often shared by many elements, but you can always add your own by adding an AdornerDecorator element.

Ok, that is enough about adorners for the moment. The next couple lines in the function determine the sort direction for the data. We default to ascending, but if the user has clicked on the same column that we were already sorting on (and we were previously in the ascending direction) we switch to descending.

Next we make the new sort adorner and add it to the adorner layer for the new sort column. We will take a look in a moment at the code behind SortAdorner. And finally, we create a new SortDescription and add it to the SortDescriptions Collection on the ItemCollection behind the ListView (which we just cleared out a few lines above). That's right, all we need the the string representing the property name, and a direction, and WPF sorts the collection for us!

Ok now for the code behind that SortAdorner:

public class SortAdorner : Adorner
{
  private readonly static Geometry _AscGeometry =
      Geometry.Parse("M 0,0 L 10,0 L 5,5 Z");

  private readonly static Geometry _DescGeometry =
      Geometry.Parse("M 0,5 L 10,5 L 5,0 Z");

  public ListSortDirection Direction { get; private set; }

  public SortAdorner(UIElement element, ListSortDirection dir)
    : base(element)
  { Direction = dir; }

  protected override void OnRender(DrawingContext drawingContext)
  {
    base.OnRender(drawingContext);

    if (AdornedElement.RenderSize.Width < 20)
      return;

    drawingContext.PushTransform(
        new TranslateTransform(
          AdornedElement.RenderSize.Width - 15,
          (AdornedElement.RenderSize.Height - 5) / 2));

    drawingContext.DrawGeometry(Brushes.Black, null,
        Direction == ListSortDirection.Ascending ?
          _AscGeometry : _DescGeometry);

    drawingContext.Pop();
  }
}

So all adorners derive from from Adorner, and require a UIElement to be passed in to the constructor. The UIElement will be the element that the adorner will adorn (all positions and rendering in the adorner will be relative to that element). In our case we also want a sort direction, so that we know which way to draw the arrow. At the top, we have two static Geometries, one for the up arrow and one for the down arrow. If you want some help interpreting that geometry string, you should take a look at our tutorial on Drawing Custom Shapes With XAML.

The actual drawing is done in the OnRender method. First, if the column header is less than 20 pixels wide, we don't bother drawing the arrow (it looks kind of silly). Then we push a TranslateTransform onto the drawing context transform stack. This makes sure that the arrow will be drawn at the right edge of the column and vertically in the center. In the next line, we draw the geometry (looking at the Direction to figure out which one), and then finally we pop the translate transform off of the transform stack (just to keep the transform stack clean).

And that is about it. Below is the entire C# code behind in one block:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Windows.Media;
using System.Windows.Documents;

namespace ListViewTest3
{
  public partial class ListViewTest : Window
  {
    private ObservableCollection<GameData> _GameCollection =
        new ObservableCollection<GameData>();

    private GridViewColumnHeader _CurSortCol = null;
    private SortAdorner _CurAdorner = null;

    public ListViewTest()
    {
      _GameCollection.Add(new GameData {
          GameName = "World Of Warcraft",
          Creator = "Blizzard",
          Publisher = "Blizzard" });
      _GameCollection.Add(new GameData {
          GameName = "Halo",
          Creator = "Bungie",
          Publisher = "Microsoft" });
      _GameCollection.Add(new GameData {
          GameName = "Gears Of War",
          Creator = "Epic",
          Publisher = "Microsoft" });

      InitializeComponent();
    }

    public ObservableCollection<GameData> GameCollection
    { get { return _GameCollection; } }

    private void AddRowClick(object sender, RoutedEventArgs e)
    {
      _GameCollection.Add(new GameData {
          GameName = "A New Game",
          Creator = "A New Creator",
          Publisher = "A New Publisher" });
    }

    private void SortClick(object sender, RoutedEventArgs e)
    {
      GridViewColumnHeader column = sender as GridViewColumnHeader;
      String field = column.Tag as String;

      if (_CurSortCol != null)
      {
        AdornerLayer.GetAdornerLayer(_CurSortCol).Remove(_CurAdorner);
        gameListView.Items.SortDescriptions.Clear();
      }

      ListSortDirection newDir = ListSortDirection.Ascending;
      if (_CurSortCol == column && _CurAdorner.Direction == newDir)
        newDir = ListSortDirection.Descending;

      _CurSortCol = column;
      _CurAdorner = new SortAdorner(_CurSortCol, newDir);
      AdornerLayer.GetAdornerLayer(_CurSortCol).Add(_CurAdorner);
      gameListView.Items.SortDescriptions.Add(
          new SortDescription(field, newDir));
    }
  }

  public class GameData
  {
    public string GameName { get; set; }
    public string Creator { get; set; }
    public string Publisher { get; set; }
  }

  public class SortAdorner : Adorner
  {
    private readonly static Geometry _AscGeometry =
        Geometry.Parse("M 0,0 L 10,0 L 5,5 Z");
    private readonly static Geometry _DescGeometry =
        Geometry.Parse("M 0,5 L 10,5 L 5,0 Z");

    public ListSortDirection Direction { get; private set; }

    public SortAdorner(UIElement element, ListSortDirection dir)
      : base(element)
    { Direction = dir; }

    protected override void OnRender(DrawingContext drawingContext)
    {
      base.OnRender(drawingContext);

      if (AdornedElement.RenderSize.Width < 20)
        return;

      drawingContext.PushTransform(
          new TranslateTransform(
            AdornedElement.RenderSize.Width - 15,
            (AdornedElement.RenderSize.Height - 5) / 2));

      drawingContext.DrawGeometry(Brushes.Black, null,
          Direction == ListSortDirection.Ascending ?
            _AscGeometry : _DescGeometry);

      drawingContext.Pop();
    }
  }
}

So that pretty much covers sorting a ListView, and a whole bunch of other stuff along the way. You can download the Visual Studio solution here, which includes all the code for all the ListView examples (both this and the other tutorial). So I have yet to decide what to cover in the next ListView tutorial - filtering or editing rows, but rest assured, the next one won't take nearly as long before it gets written. As always, please leave any questions or comments below (and if you have an opinion on what the next ListView tutorial should be about let me know).

Rick Pingry
10/30/2008 - 10:26

Thanks for the great article, it was just what I needed.

reply

Shtreber
10/30/2008 - 13:48

Thanks for the article.

One small note: you have reversed Ascending and Descending in your logic.
Ascending (growing) should have triangle that has base down (meaning it gets more narrow as you go down so its growing as you go down the list)

Descending should grow from wider to the narrow, so it should be represented as triangle with base up and peak down.

reply

The Tallest
10/30/2008 - 14:04

Whoops - and I even made sure at one point that I was doing it right by looking at Windows Explorer. I guess I flipped them at some point after that. Thanks for pointing that out.

reply

Sergey
04/08/2009 - 08:29

Currently the SortClick method works only when you attach it to a header of gameListView. You could greatly improve the method by looking up the list view through the parent chain, and storing _CurSortCol and _CurAdorner in a ListView-keyed dictionary.

Here is the code:

private readonly IDictionary<ListView,GridViewColumnHeader> sortColumns =
    new Dictionary<ListView,GridViewColumnHeader>();

private readonly IDictionary<ListView,SortAdorner> sortAdorners =
    new Dictionary<ListView,SortAdorner>();

...
private void ListViewSort_Click(object sender, RoutedEventArgs e) {
    var column = (GridViewColumnHeader)sender;
    var parent = column.Parent;
    while (parent != null) {
        if (parent is ListView) {
            break;
        }
        parent = VisualTreeHelper.GetParent(parent);
    }
    if (parent == null) {
        // Safety catch - this should not happen
        return;
    }
    var listView = (ListView) parent;

    var field = column.Tag as String;

    if (!sortColumns.ContainsKey(listView)) {
        sortColumns.Add(listView, null);
    }
    if (!sortAdorners.ContainsKey(listView)) {
        sortAdorners.Add(listView, null);
    }

    if (sortColumns[listView] != null) {
        AdornerLayer.GetAdornerLayer(sortColumns[listView]).Remove(sortAdorners[listView]);
        listView.Items.SortDescriptions.Clear();
    }

    var newDir = ListSortDirection.Ascending;
    if (sortColumns[listView] == column && sortAdorners[listView].Direction == newDir) {
        newDir = ListSortDirection.Descending;
    }

    sortColumns[listView] = column;
    sortAdorners[listView] = new SortAdorner(sortColumns[listView], newDir);
    AdornerLayer.GetAdornerLayer(sortColumns[listView]).Add(sortAdorners[listView]);
    listView.Items.SortDescriptions.Add(new SortDescription(field, newDir));
}

Thanks for the article!

reply

Anonymous
06/11/2009 - 12:43

Following the previous example , i did some changes to make it even easier to add sorting on your listview

no need anymore to keep tag information about the field and no need to declare GridViewCoumnHeader tag.

<ListView GridViewColumnHeader.Click="ListViewSort_Click">

        private readonly IDictionary<ListView, GridViewColumnHeader> sortColumns = new Dictionary<ListView, GridViewColumnHeader>();
        private readonly IDictionary<ListView, SortAdorner> sortAdorners = new Dictionary<ListView, SortAdorner>();


private void ListViewSort_Click(object sender, RoutedEventArgs e)
        {
            var listView = (ListView)sender;

            var column = (GridViewColumnHeader)e.OriginalSource;
            var field = (string)((Binding)((GridViewColumnHeader)e.OriginalSource).Column.DisplayMemberBinding).Path.Path;

            if (!sortColumns.ContainsKey(listView))
            {
                sortColumns.Add(listView, null);
            }
            if (!sortAdorners.ContainsKey(listView))
            {
                sortAdorners.Add(listView, null);
            }

            if (sortColumns[listView] != null)
            {
                AdornerLayer.GetAdornerLayer(sortColumns[listView]).Remove(sortAdorners[listView]);
                listView.Items.SortDescriptions.Clear();
            }

            var newDir = ListSortDirection.Ascending;
            if (sortColumns[listView] == column && sortAdorners[listView].Direction == newDir)
            {
                newDir = ListSortDirection.Descending;
            }

            sortColumns[listView] = column;
            sortAdorners[listView] = new SortAdorner(sortColumns[listView], newDir);
            AdornerLayer.GetAdornerLayer(sortColumns[listView]).Add(sortAdorners[listView]);
            listView.Items.SortDescriptions.Add(new SortDescription(field, newDir));
        }

reply

Anonymous
08/16/2009 - 19:39

Hey great job. You are the only article which had a demo the work and code easy to follow for sorting a listview. I must of looked at 30 demos and yours is the only one the made sense and workded, if you have any other demos or articles could you email them to the weblinks so I can read your articles.
Email me at steve_44@inbox.com

reply

Mohammad
12/27/2009 - 11:58

Thanks a lot, It works like a charm :)

reply

Anonymous
07/05/2009 - 02:57

I am also a WPF fan, You can also directly contact with me by MSN: zhoujiguo1985@live.cn. so we can instantly

communicate with each other. thanks.

reply

Anonymous
08/18/2009 - 08:00

Any ideas why the ling

var field = (string)((Binding)((GridViewColumnHeader)e.OriginalSource).Column.DisplayMemberBinding).Path.Path;
from the comment-Example throws a NullReferenceException?

Thanks for any hints!

Cheers

reply

GeoffT
10/15/2009 - 19:40

I had the same, but it because my listview didn't use "DisplayMemberBinding", it used a cell template instead. I resorted to using the "Tag" approach to avoid having complicated code to try and find the appropriate binding path name within the cell template.

reply

suren
08/25/2009 - 02:23

Thank you, i have save lot of time because of your code

reply

Ketan
11/07/2009 - 11:29

What library is the "SortAdorner" in? Can't seem to find it.

reply

Ketan
11/07/2009 - 11:33

Woops, just saw the class you made, my mistake

reply

grkowalski
11/26/2009 - 04:55

Another great article! Thanks!

reply

z0nd0r
11/26/2009 - 18:20

very very useful
Thanks a lot!

reply

prolingua.geo
02/21/2010 - 02:40

thanks, great article and code.
I have suggestion that would make the article code easier understand. I think the author should concentrate first on the sorting without involving the drawing of the ascending or descending graphics on the column header.
So I would suggest the authour start with something like:

private GridViewColumnHeader _CurSortCol = null;
        private ListSortDirection _CurDirCol;
        private SortAdorner _CurAdorner = null;
        public Window1()
        {
            _GameCollection.Add(new GameData
            {
                GameName = "World Of Warcraft",
                Creator = "Blizzard",
                Publisher = "Blizzard"
            });
            _GameCollection.Add(new GameData
            {
                GameName = "Halo",
                Creator = "Bungie",
                Publisher = "Microsoft"
            });
            _GameCollection.Add(new GameData
            {
                GameName = "Gears Of War",
                Creator = "Epic",
                Publisher = "Microsoft"
            });
            InitializeComponent();
        }

        public ObservableCollection<GameData> GameCollection
        { get { return _GameCollection; } }

        private void AddRowClick(object sender, RoutedEventArgs e)
        {
            _GameCollection.Add(new GameData
            {
                GameName = "A New Game",
                Creator = "A New Creator",
                Publisher = "A New Publisher"
            });
        }

        private void SortClick(object sender, RoutedEventArgs e)
        {
            GridViewColumnHeader column = sender as GridViewColumnHeader;
            String field = column.Tag as String;

            //if (_CurSortCol != null)
            //{
                //AdornerLayer.GetAdornerLayer(_CurSortCol).Remove(_CurAdorner);
                gameListView.Items.SortDescriptions.Clear();
            //}

            ListSortDirection newDir = ListSortDirection.Ascending;
            //if (_CurSortCol == column && _CurAdorner.Direction == newDir)
            if (_CurSortCol == column && _CurDirCol == newDir)
                newDir = ListSortDirection.Descending;

            _CurSortCol = column;
            _CurDirCol = newDir;

            //_CurAdorner = new SortAdorner(_CurSortCol, newDir);
            //AdornerLayer.GetAdornerLayer(_CurSortCol).Add(_CurAdorner);
            gameListView.Items.SortDescriptions.Add(
                new SortDescription(field, newDir));


        }

    }

See that I added a class level variable _CurDirCol to determine the current direction.

But it's really a great article. I predict Microsoft will make this functionality standard.

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.

Sponsors