Joel Rumerman's Blog

Thinking I'm better looking and wealthier than I really am.
posts - 15, comments - 29, trackbacks - 2

Tuesday, April 03, 2007

A Sortable GridView (I mean ListView) Control in WPF

I’ve been tinkering with WPF over the last couple of weeks or so and now that I’m getting a bit comfortable in it, I decided to make a ListView I was working with (that contains a GridView) sortable based upon the column header being clicked. After doing some research, I came across the MSDN how-to article that described a basic way of accomplishing this task. This blog post also has the same example explained. Unfortunately, though, it relied upon using the Column Header text as the property name on the underlying colllection on which to sort. After implementing the example, I decided this stank. Actually, I realized this stank beforehand, but wanted to implement the example anyways.

It stinks because Column Headers can contain anything. They don’t necessarily contain text. They could contain a button, a list, a drawing, a 3D animation, etc. Furthermore, even if they did just contain plain text, who’s to say that the text in the column header is actually a property name on my underlying object? What if I used “Birth Date” as the column header text and the property value was “DOB?” While the example was simple and made to be that way, I wanted a bit more control of how my GridView sorted.

I’m not going to dive into all the problems I see with the GridView and the ViewBase aspects of WPF in this blog, but that isn’t to say that I don’t think there are a ton of problems with this half-assed implementation. I’m just going to talk about the simple solution that I came up with.

I decided that in order to make my solution semi-intuitive, I needed to describe on the column level, what property on the underlying object it should sort on.

Something that looks like this written in XAML:

<GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" SortPropertyName="ID" Width="50" />

That being said, if I wanted to add properties I needed to extend the current GridViewColumn object to contain them so I was really talking about creating a new Grid View Column, extending the existing one.

So my XAML would really look like:

<local:SortableGridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" SortPropertyName="ID" IsDefaultSortColumn=”True” Width="50" />

where local is defined as the namespace that points back to the namespace in which my new control is defined. Something like:

xmlns:local="clr-namespace:ModelingTool"

That being said I needed to define my new SortableGridViewColumn. This was actually quite easy. I created a new CS file named SortableGridViewColumn.cs and then inherited from GridViewColumn and added a new dependency property called SortPropertyName

Here’s the code.

using System;

using System.Collections.Generic;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Controls.Primitives;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Shapes;

 

namespace ThreatModel

{

    public class SortableGridViewColumn : GridViewColumn

    {

 

        public string SortPropertyName

        {

            get { return (string)GetValue(SortPropertyNameProperty); }

            set { SetValue(SortPropertyNameProperty, value); }

        }

 

        // Using a DependencyProperty as the backing store for SortPropertyName.  This enables animation, styling, binding, etc...

        public static readonly DependencyProperty SortPropertyNameProperty =

            DependencyProperty.Register("SortPropertyName", typeof(string), typeof(SortableGridViewColumn), new UIPropertyMetadata(""));

 

 

        public bool IsDefaultSortColumn

        {

            get { return (bool)GetValue(IsDefaultSortColumnProperty); }

            set { SetValue(IsDefaultSortColumnProperty, value); }

        }

 

        public static readonly DependencyProperty IsDefaultSortColumnProperty =

            DependencyProperty.Register("IsDefaultSortColumn", typeof(bool), typeof(SortableGridViewColumn), new UIPropertyMetadata(false));

 

    }

}

 

As  you can see, I’m not doing anything real fancy here, just adding two dependency properties. One that provides the string name of the property that column should sort on when applied to the object. And one that sets a Boolean property to indicate if this column is the default sort column.

Now, whenever I declare my GridView’s columns, I use the

 

        <local:SortableGridViewColumn Header="Element Name" DisplayMemberBinding="{Binding ElementName}" SortPropertyName="ElementName" />

 syntax.

Defining the new SortableGridViewColumn control was the first step. For true encapsulation, the ListView should actually handle all of the sorting internally. (In fact, it should be the GridView that handles the sorting internally. Unfortunately, the GridView doesn’t provide any mechanism to accomplish this as it isn’t a real control, but is actually a View; a very important difference.)

To complete the ListView’s sorting capabilities, we have to create a new ListView that encapsulates the sorting functionality. We’ll call it the SortableListView and it’ll inherit from ListView.

Here’s the code in all it’s glory.

using System;

using System.Collections.Generic;

using System.Text;

using System.Windows.Controls;

using System.Windows;

using System.Collections;

using System.ComponentModel;

using System.Windows.Data;

using System.Windows.Media;

 

namespace SortableWPFGridView

{

 

    // if the GridView exposed any methods at all that allowed for overriding at a control level, I would be

    // able to do all of this work inside it rather than the ListView. However, b/c it doesn't, I have to do the

    // work inside the ListView.

 

    // The GridView has access to the ItemSource on the ListView through the dependency property mechanism.

 

    public class SortableListView : ListView

    {

        SortableGridViewColumn lastSortedOnColumn = null;

        ListSortDirection lastDirection = ListSortDirection.Ascending;

 

 

        #region New Dependency Properties

 

        public string ColumnHeaderSortedAscendingTemplate

        {

            get { return (string)GetValue(ColumnHeaderSortedAscendingTemplateProperty); }

            set { SetValue(ColumnHeaderSortedAscendingTemplateProperty, value); }

        }

 

        // Using a DependencyProperty as the backing store for ColumnHeaderSortedAscendingTemplate.  This enables animation, styling, binding, etc...

        public static readonly DependencyProperty ColumnHeaderSortedAscendingTemplateProperty =

            DependencyProperty.Register("ColumnHeaderSortedAscendingTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));

 

 

        public string ColumnHeaderSortedDescendingTemplate

        {

            get { return (string)GetValue(ColumnHeaderSortedDescendingTemplateProperty); }

            set { SetValue(ColumnHeaderSortedDescendingTemplateProperty, value); }

        }

 

        // Using a DependencyProperty as the backing store for ColumnHeaderSortedDescendingTemplate.  This enables animation, styling, binding, etc...

        public static readonly DependencyProperty ColumnHeaderSortedDescendingTemplateProperty =

            DependencyProperty.Register("ColumnHeaderSortedDescendingTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));

 

 

        public string ColumnHeaderNotSortedTemplate

        {

            get { return (string)GetValue(ColumnHeaderNotSortedTemplateProperty); }

            set { SetValue(ColumnHeaderNotSortedTemplateProperty, value); }

        }

 

        // Using a DependencyProperty as the backing store for ColumnHeaderNotSortedTemplate.  This enables animation, styling, binding, etc...

        public static readonly DependencyProperty ColumnHeaderNotSortedTemplateProperty =

            DependencyProperty.Register("ColumnHeaderNotSortedTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));

 

 

        #endregion

 

        ///

        /// Executes when the control is initialized completely the first time through. Runs only once.

        ///

        ///

        protected override void OnInitialized(EventArgs e)

        {

            // add the event handler to the GridViewColumnHeader. This strongly ties this ListView to a GridView.

            this.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(GridViewColumnHeaderClickedHandler));

 

            // cast the ListView's View to a GridView

            GridView gridView = this.View as GridView;

            if (gridView != null)

            {

                // determine which column is marked as IsDefaultSortColumn. Stops on the first column marked this way.

                SortableGridViewColumn sortableGridViewColumn = null;

                foreach (GridViewColumn gridViewColumn in gridView.Columns)

                {

                    sortableGridViewColumn = gridViewColumn as SortableGridViewColumn;

                    if (sortableGridViewColumn != null)

                    {

                        if (sortableGridViewColumn.IsDefaultSortColumn)

                        {

                            break;

                        }

                        sortableGridViewColumn = null;

                    }

                }

 

                // if the default sort column is defined, sort the data and then update the templates as necessary.

                if (sortableGridViewColumn != null)

                {

                    lastSortedOnColumn = sortableGridViewColumn;

                    Sort(sortableGridViewColumn.SortPropertyName, ListSortDirection.Ascending);

 

                    if (!String.IsNullOrEmpty(this.ColumnHeaderSortedAscendingTemplate))

                    {

                        sortableGridViewColumn.HeaderTemplate = this.TryFindResource(ColumnHeaderSortedAscendingTemplate) as DataTemplate;

                    }

 

                    this.SelectedIndex = 0;

                }

            }

 

            base.OnInitialized(e);

        }

 

        ///

        /// Event Handler for the ColumnHeader Click Event.

        ///

        ///

        ///

        private void GridViewColumnHeaderClickedHandler(object sender, RoutedEventArgs e)

        {

            GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;

 

            // ensure that we clicked on the column header and not the padding that's added to fill the space.

            if (headerClicked != null && headerClicked.Role != GridViewColumnHeaderRole.Padding)

            {

                // attempt to cast to the sortableGridViewColumn object.

                SortableGridViewColumn sortableGridViewColumn = (headerClicked.Column) as SortableGridViewColumn;

 

                // ensure that the column header is the correct type and a sort property has been set.

                if (sortableGridViewColumn != null && !String.IsNullOrEmpty(sortableGridViewColumn.SortPropertyName))

                {

 

                    ListSortDirection direction;

                    bool newSortColumn = false;

 

                    // determine if this is a new sort, or a switch in sort direction.

                    if (lastSortedOnColumn == null

                        || String.IsNullOrEmpty(lastSortedOnColumn.SortPropertyName)

                        || !String.Equals(sortableGridViewColumn.SortPropertyName, lastSortedOnColumn.SortPropertyName, StringComparison.InvariantCultureIgnoreCase))

                    {

                        newSortColumn = true;

                        direction = ListSortDirection.Ascending;

                    }

                    else

                    {

                        if (lastDirection == ListSortDirection.Ascending)

                        {

                            direction = ListSortDirection.Descending;

                        }

                        else

                        {

                            direction = ListSortDirection.Ascending;

                        }

                    }

 

                    // get the sort property name from the column's information.

                    string sortPropertyName = sortableGridViewColumn.SortPropertyName;

 

                    // Sort the data.

                    Sort(sortPropertyName, direction);

 

                    if (direction == ListSortDirection.Ascending)

                    {

                        if (!String.IsNullOrEmpty(this.ColumnHeaderSortedAscendingTemplate))

                        {

                            sortableGridViewColumn.HeaderTemplate = this.TryFindResource(ColumnHeaderSortedAscendingTemplate) as DataTemplate;

                        }

                        else

                        {

                            sortableGridViewColumn.HeaderTemplate = null;

                        }

                    }

                    else

                    {

                        if (!String.IsNullOrEmpty(this.ColumnHeaderSortedDescendingTemplate))

                        {

                            sortableGridViewColumn.HeaderTemplate = this.TryFindResource(ColumnHeaderSortedDescendingTemplate) as DataTemplate;

                        }

                        else

                        {

                            sortableGridViewColumn.HeaderTemplate = null;

                        }

                    }

 

                    // Remove arrow from previously sorted header

                    if (newSortColumn && lastSortedOnColumn != null)

                    {

                        if (!String.IsNullOrEmpty(this.ColumnHeaderNotSortedTemplate))

                        {

                            lastSortedOnColumn.HeaderTemplate = this.TryFindResource(ColumnHeaderNotSortedTemplate) as DataTemplate;

                        }

                        else

                        {

                            lastSortedOnColumn.HeaderTemplate = null;

                        }

                    }

                    lastSortedOnColumn = sortableGridViewColumn;

                }

            }

        }

 

        ///

        /// Helper method that sorts the data.

        ///

        ///

        ///

        private void Sort(string sortBy, ListSortDirection direction)

        {

            lastDirection = direction;

            ICollectionView dataView = CollectionViewSource.GetDefaultView(this.ItemsSource);

 

            dataView.SortDescriptions.Clear();

            SortDescription sd = new SortDescription(sortBy, direction);

            dataView.SortDescriptions.Add(sd);

            dataView.Refresh();

        }

    }

}

 

It might seem like a lot, but it really isn’t.

The dependency properties at the top,

        #region New Dependency Properties

 

        public string ColumnHeaderSortedAscendingTemplate

        {

            get { return (string)GetValue(ColumnHeaderSortedAscendingTemplateProperty); }

            set { SetValue(ColumnHeaderSortedAscendingTemplateProperty, value); }

        }

 

        // Using a DependencyProperty as the backing store for ColumnHeaderSortedAscendingTemplate.  This enables animation, styling, binding, etc...

        public static readonly DependencyProperty ColumnHeaderSortedAscendingTemplateProperty =

            DependencyProperty.Register("ColumnHeaderSortedAscendingTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));

 

 

        public string ColumnHeaderSortedDescendingTemplate

        {

            get { return (string)GetValue(ColumnHeaderSortedDescendingTemplateProperty); }

            set { SetValue(ColumnHeaderSortedDescendingTemplateProperty, value); }

        }

 

        // Using a DependencyProperty as the backing store for ColumnHeaderSortedDescendingTemplate.  This enables animation, styling, binding, etc...

        public static readonly DependencyProperty ColumnHeaderSortedDescendingTemplateProperty =

            DependencyProperty.Register("ColumnHeaderSortedDescendingTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));

 

 

        public string ColumnHeaderNotSortedTemplate

        {

            get { return (string)GetValue(ColumnHeaderNotSortedTemplateProperty); }

            set { SetValue(ColumnHeaderNotSortedTemplateProperty, value); }

        }

 

        // Using a DependencyProperty as the backing store for ColumnHeaderNotSortedTemplate.  This enables animation, styling, binding, etc...

        public static readonly DependencyProperty ColumnHeaderNotSortedTemplateProperty =

            DependencyProperty.Register("ColumnHeaderNotSortedTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));

 

        #endregion

provide a mechanism to dynamically style the column header’s based upon if that column is sorted ascending, descending, or not sorted at all.

Once you get past that, the rest of the code is similar to the code listed in the above MSDN example.

A major differences is the inclusion of the overridden OnInitialized method.

        ///

        /// Executes when the control is initialized completely the first time through. Runs only once.