Jul 15, 2015

How-to: ListView with column sorting

In the last chapter we saw how we could easily sort a ListView from Code-behind, and while this will suffice for some cases, it doesn't allow the end-user to decide on the sorting. Besides that, there was no indication on which column the ListView was sorted by. In Windows, and in many user interfaces in general, it's common to illustrate sort directions in a list by drawing a triangle next to the column name currently used to sort by.
In this how-to article, I'll give you a practical solution that gives us all of the above, but please bear in mind that some of the code here goes a bit beyond what we have learned so far - that's why it has the "how-to" label.
This article builds upon the previous one, but I'll still explain each part as we go along. Here's our goal - a ListView with column sorting, including visual indication of sort field and direction. The user simply clicks a column to sort by and if the same column is clicked again, the sort direction is reversed. Here's how it looks: 


<Window x:Class="WpfTutorialSamples.ListView_control.ListViewColumnSortingSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListViewColumnSortingSample" Height="200" Width="350">
    <Grid Margin="10">
        <ListView Name="lvUsers">
            <ListView.View>
                <GridView>
                    <GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Tag="Name" Click="lvUsersColumnHeader_Click">Name</GridViewColumnHeader>
                        </GridViewColumn.Header>
                    </GridViewColumn>
                    <GridViewColumn Width="80" DisplayMemberBinding="{Binding Age}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Tag="Age" Click="lvUsersColumnHeader_Click">Age</GridViewColumnHeader>
                        </GridViewColumn.Header>
                    </GridViewColumn>
                    <GridViewColumn Width="80" DisplayMemberBinding="{Binding Sex}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Tag="Sex" Click="lvUsersColumnHeader_Click">Sex</GridViewColumnHeader>
                        </GridViewColumn.Header>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>
 
 
 
Notice how I have specified headers for each of the columns using an actual GridViewColumnHeader element instead of just specifying a string. This is done so that I may set additional properties, in this case the Tag property as well as the Click event.
The Tag property is used to hold the field name that will be used to sort by, if this particular column is clicked. This is done in the lvUsersColumnHeader_Click event that each of the columns subscribes to.
That was the key concepts of the XAML. Besides that, we bind to our Code-behind properties Name, Age and Sex, which we'll discuss now.

The Code-behind

In Code-behind, there are quite a few things happening. I use a total of three classes, which you would normally divide up into individual files, but for convenience, I have kept them in the same file, giving us a total of ~100 lines. First the code and then I'll explain how it works: 


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
namespace WpfTutorialSamples.ListView_control
{
        public partial class ListViewColumnSortingSample : Window
        {
                private GridViewColumnHeader listViewSortCol = null;
                private SortAdorner listViewSortAdorner = null;

                public ListViewColumnSortingSample()
                {
                        InitializeComponent();
                        List<User> items = new List<User>();
                        items.Add(new User() { Name = "John Doe", Age = 42, Sex = SexType.Male });
                        items.Add(new User() { Name = "Jane Doe", Age = 39, Sex = SexType.Female });
                        items.Add(new User() { Name = "Sammy Doe", Age = 13, Sex = SexType.Male });
                        items.Add(new User() { Name = "Donna Doe", Age = 13, Sex = SexType.Female });
                        lvUsers.ItemsSource = items;
                }

                private void lvUsersColumnHeader_Click(object sender, RoutedEventArgs e)
                {
                        GridViewColumnHeader column = (sender as GridViewColumnHeader);
                        string sortBy = column.Tag.ToString();
                        if(listViewSortCol != null)
                        {
                                AdornerLayer.GetAdornerLayer(listViewSortCol).Remove(listViewSortAdorner);
                                lvUsers.Items.SortDescriptions.Clear();
                        }

                        ListSortDirection newDir = ListSortDirection.Ascending;
                        if(listViewSortCol == column && listViewSortAdorner.Direction == newDir)
                                newDir = ListSortDirection.Descending;

                        listViewSortCol = column;
                        listViewSortAdorner = new SortAdorner(listViewSortCol, newDir);
                        AdornerLayer.GetAdornerLayer(listViewSortCol).Add(listViewSortAdorner);
                        lvUsers.Items.SortDescriptions.Add(new SortDescription(sortBy, newDir));
                }
        }

        public enum SexType { Male, Female };

        public class User
        {
                public string Name { get; set; }

                public int Age { get; set; }

                public string Mail { get; set; }

                public SexType Sex { get; set; }
        }

        public class SortAdorner : Adorner
        {
                private static Geometry ascGeometry =
                        Geometry.Parse("M 0 4 L 3.5 0 L 7 4 Z");

                private static Geometry descGeometry =
                        Geometry.Parse("M 0 0 L 3.5 4 L 7 0 Z");

                public ListSortDirection Direction { get; private set; }

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

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

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

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

                        Geometry geometry = ascGeometry;
                        if(this.Direction == ListSortDirection.Descending)
                                geometry = descGeometry;
                        drawingContext.DrawGeometry(Brushes.Black, null, geometry);

                        drawingContext.Pop();
                }
        }
}
 
 
Allow me to start from the bottom and then work my way up while explaining what happens. The last class in the file is an Adorner class called SortAdorner. All this little class does is to draw a triangle, either pointing up or down, depending on the sort direction. WPF uses the concept of adorners to allow you to paint stuff over other controls, and this is exactly what we want here: The ability to draw a sorting triangle on top of our ListView column header.
The SortAdorner works by defining two Geometry objects, which are basically used to describe 2D shapes - in this case a triangle with the tip pointing up and one with the tip pointing down. The Geometry.Parse() method uses the list of points to draw the triangles, which will be explained more thoroughly in a later article.
The SortAdorner is aware of the sort direction, because it needs to draw the proper triangle, but is not aware of the field that we order by - this is handled in the UI layer.
The User class is just a basic information class, used to contain information about a user. Some of this information is used in the UI layer, where we bind to the Name, Age and Sex properties.
In the Window class, we have two methods: The constructor where we build a list of users and assign it to the ItemsSource of our ListView, and then the more interesting click event handler that will be hit when the user clicks a column. In the top of the class, we have defined two private variables: listViewSortCol and listViewSortAdorner. These will help us keep track of which column we're currently sorting by and the adorner we placed to indicate it.
In the lvUsersColumnHeader_Click event handler, we start off by getting a reference to the column that the user clicked. With this, we can decide which property on the User class to sort by, simply by looking at the Tag property that we defined in XAML. We then check if we're already sorting by a column - if that is the case, we remove the adorner and clear the current sort descriptions.
After that, we're ready to decide the direction. The default is ascending, but we do a check to see if we're already sorting by the column that the user clicked - if that is the case, we change the direction to descending.
In the end, we create a new SortAdorner, passing in the column that it should be rendered on, as well as the direction. We add this to the AdornerLayer of the column header, and at the very end, we add a SortDescription to the ListView, to let it know which property to sort by and in which direction.

 

No comments:

Post a Comment