Skip to main content

...

....

C# WPF Tutorial - Using The ListView, Part 2 - Sorting [Intermediate]


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 inWPF, 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 atit 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 situation.

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).
Source Files:

Comments

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >();

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as

C# WPF Tutorial - Implementing IScrollInfo [Advanced]

The ScrollViewer in WPF is pretty handy (and quite flexible) - especially when compared to what you had to work with in WinForms ( ScrollableControl ). 98% of the time, I can make the ScrollViewer do what I need it to for the given situation. Those other 2 percent, though, can get kind of hairy. Fortunately, WPF provides the IScrollInfo interface - which is what we will be talking about today. So what is IScrollInfo ? Well, it is a way to take over the logic behind scrolling, while still maintaining the look and feel of the standard ScrollViewer . Now, first off, why in the world would we want to do that? To answer that question, I'm going to take a an example from a tutorial that is over a year old now - Creating a Custom Panel Control . In that tutorial, we created our own custom WPF panel (that animated!). One of the issues with that panel though (and the WPF WrapPanel in general) is that you have to disable the horizontal scrollbar if you put the panel in a ScrollV