A few months ago, I wrote a post where I explained how to automatically sort a GridView when a column header is clicked. I had mentioned a possible improvement : add a sort glyph in the column header to show which column is sorted. In today’s post, I present a new version of the GridViewSort class, which displays the sort glyph.

GridViewSort sample with sort glyph
To achieve this result, I used an Adorner : this is a component which allows to draw over existing UI elements, on an independant rendering layer.
The new version of the GridViewSort class can be used as before, in that case the grid displays default sort glyphs. These default glyphs are not particularly good-looking, so if you have some artistic skills you can provide you own images, as shown in the code below :
<ListView ItemsSource="{Binding Persons}"
IsSynchronizedWithCurrentItem="True"
util:GridViewSort.AutoSort="True"
util:GridViewSort.SortGlyphAscending="/Images/up.png"
util:GridViewSort.SortGlyphDescending="/Images/down.png">
It is also possible to disable the sort glyphs, by setting the ShowSortGlyph attached property to false :
<ListView ItemsSource="{Binding Persons}"
IsSynchronizedWithCurrentItem="True"
util:GridViewSort.AutoSort="True"
util:GridViewSort.ShowSortGlyph="False">
Note that in the current version, the sort glyph is only displayed when using the automatic sort mode (AutoSort = true). The case of a custom sort using the Command property is not handled yet.
Here is the complete code of the new version of the class :
using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Documents; namespace Wpf.Util { public class GridViewSort { #region Public attached properties public static ICommand GetCommand(DependencyObject obj) { return (ICommand)obj.GetValue(CommandProperty); } public static void SetCommand(DependencyObject obj, ICommand value) { obj.SetValue(CommandProperty, value); } // Using a DependencyProperty as the backing store for Command. This enables animation, styling, binding, etc... public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached( "Command", typeof(ICommand), typeof(GridViewSort), new UIPropertyMetadata( null, (o, e) => { ItemsControl listView = o as ItemsControl; if (listView != null) { if (!GetAutoSort(listView)) // Don't change click handler if AutoSort enabled { if (e.OldValue != null && e.NewValue == null) { listView.RemoveHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click)); } if (e.OldValue == null && e.NewValue != null) { listView.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click)); } } } } ) ); public static bool GetAutoSort(DependencyObject obj) { return (bool)obj.GetValue(AutoSortProperty); } public static void SetAutoSort(DependencyObject obj, bool value) { obj.SetValue(AutoSortProperty, value); } // Using a DependencyProperty as the backing store for AutoSort. This enables animation, styling, binding, etc... public static readonly DependencyProperty AutoSortProperty = DependencyProperty.RegisterAttached( "AutoSort", typeof(bool), typeof(GridViewSort), new UIPropertyMetadata( false, (o, e) => { ListView listView = o as ListView; if (listView != null) { if (GetCommand(listView) == null) // Don't change click handler if a command is set { bool oldValue = (bool)e.OldValue; bool newValue = (bool)e.NewValue; if (oldValue && !newValue) { listView.RemoveHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click)); } if (!oldValue && newValue) { listView.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click)); } } } } ) ); public static string GetPropertyName(DependencyObject obj) { return (string)obj.GetValue(PropertyNameProperty); } public static void SetPropertyName(DependencyObject obj, string value) { obj.SetValue(PropertyNameProperty, value); } // Using a DependencyProperty as the backing store for PropertyName. This enables animation, styling, binding, etc... public static readonly DependencyProperty PropertyNameProperty = DependencyProperty.RegisterAttached( "PropertyName", typeof(string), typeof(GridViewSort), new UIPropertyMetadata(null) ); public static bool GetShowSortGlyph(DependencyObject obj) { return (bool)obj.GetValue(ShowSortGlyphProperty); } public static void SetShowSortGlyph(DependencyObject obj, bool value) { obj.SetValue(ShowSortGlyphProperty, value); } // Using a DependencyProperty as the backing store for ShowSortGlyph. This enables animation, styling, binding, etc... public static readonly DependencyProperty ShowSortGlyphProperty = DependencyProperty.RegisterAttached("ShowSortGlyph", typeof(bool), typeof(GridViewSort), new UIPropertyMetadata(true)); public static ImageSource GetSortGlyphAscending(DependencyObject obj) { return (ImageSource)obj.GetValue(SortGlyphAscendingProperty); } public static void SetSortGlyphAscending(DependencyObject obj, ImageSource value) { obj.SetValue(SortGlyphAscendingProperty, value); } // Using a DependencyProperty as the backing store for SortGlyphAscending. This enables animation, styling, binding, etc... public static readonly DependencyProperty SortGlyphAscendingProperty = DependencyProperty.RegisterAttached("SortGlyphAscending", typeof(ImageSource), typeof(GridViewSort), new UIPropertyMetadata(null)); public static ImageSource GetSortGlyphDescending(DependencyObject obj) { return (ImageSource)obj.GetValue(SortGlyphDescendingProperty); } public static void SetSortGlyphDescending(DependencyObject obj, ImageSource value) { obj.SetValue(SortGlyphDescendingProperty, value); } // Using a DependencyProperty as the backing store for SortGlyphDescending. This enables animation, styling, binding, etc... public static readonly DependencyProperty SortGlyphDescendingProperty = DependencyProperty.RegisterAttached("SortGlyphDescending", typeof(ImageSource), typeof(GridViewSort), new UIPropertyMetadata(null)); #endregion #region Private attached properties private static GridViewColumnHeader GetSortedColumnHeader(DependencyObject obj) { return (GridViewColumnHeader)obj.GetValue(SortedColumnHeaderProperty); } private static void SetSortedColumnHeader(DependencyObject obj, GridViewColumnHeader value) { obj.SetValue(SortedColumnHeaderProperty, value); } // Using a DependencyProperty as the backing store for SortedColumn. This enables animation, styling, binding, etc... private static readonly DependencyProperty SortedColumnHeaderProperty = DependencyProperty.RegisterAttached("SortedColumnHeader", typeof(GridViewColumnHeader), typeof(GridViewSort), new UIPropertyMetadata(null)); #endregion #region Column header click event handler private static void ColumnHeader_Click(object sender, RoutedEventArgs e) { GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader; if (headerClicked != null && headerClicked.Column != null) { string propertyName = GetPropertyName(headerClicked.Column); if (!string.IsNullOrEmpty(propertyName)) { ListView listView = GetAncestor<ListView>(headerClicked); if (listView != null) { ICommand command = GetCommand(listView); if (command != null) { if (command.CanExecute(propertyName)) { command.Execute(propertyName); } } else if (GetAutoSort(listView)) { ApplySort(listView.Items, propertyName, listView, headerClicked); } } } } } #endregion #region Helper methods public static T GetAncestor<T>(DependencyObject reference) where T : DependencyObject { DependencyObject parent = VisualTreeHelper.GetParent(reference); while (!(parent is T)) { parent = VisualTreeHelper.GetParent(parent); } if (parent != null) return (T)parent; else return null; } public static void ApplySort(ICollectionView view, string propertyName, ListView listView, GridViewColumnHeader sortedColumnHeader) { ListSortDirection direction = ListSortDirection.Ascending; if (view.SortDescriptions.Count > 0) { SortDescription currentSort = view.SortDescriptions[0]; if (currentSort.PropertyName == propertyName) { if (currentSort.Direction == ListSortDirection.Ascending) direction = ListSortDirection.Descending; else direction = ListSortDirection.Ascending; } view.SortDescriptions.Clear(); GridViewColumnHeader currentSortedColumnHeader = GetSortedColumnHeader(listView); if (currentSortedColumnHeader != null) { RemoveSortGlyph(currentSortedColumnHeader); } } if (!string.IsNullOrEmpty(propertyName)) { view.SortDescriptions.Add(new SortDescription(propertyName, direction)); if (GetShowSortGlyph(listView)) AddSortGlyph( sortedColumnHeader, direction, direction == ListSortDirection.Ascending ? GetSortGlyphAscending(listView) : GetSortGlyphDescending(listView)); SetSortedColumnHeader(listView, sortedColumnHeader); } } private static void AddSortGlyph(GridViewColumnHeader columnHeader, ListSortDirection direction, ImageSource sortGlyph) { AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(columnHeader); adornerLayer.Add( new SortGlyphAdorner( columnHeader, direction, sortGlyph )); } private static void RemoveSortGlyph(GridViewColumnHeader columnHeader) { AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(columnHeader); Adorner[] adorners = adornerLayer.GetAdorners(columnHeader); if (adorners != null) { foreach (Adorner adorner in adorners) { if (adorner is SortGlyphAdorner) adornerLayer.Remove(adorner); } } } #endregion #region SortGlyphAdorner nested class private class SortGlyphAdorner : Adorner { private GridViewColumnHeader _columnHeader; private ListSortDirection _direction; private ImageSource _sortGlyph; public SortGlyphAdorner(GridViewColumnHeader columnHeader, ListSortDirection direction, ImageSource sortGlyph) : base(columnHeader) { _columnHeader = columnHeader; _direction = direction; _sortGlyph = sortGlyph; } private Geometry GetDefaultGlyph() { double x1 = _columnHeader.ActualWidth - 13; double x2 = x1 + 10; double x3 = x1 + 5; double y1 = _columnHeader.ActualHeight / 2 - 3; double y2 = y1 + 5; if (_direction == ListSortDirection.Ascending) { double tmp = y1; y1 = y2; y2 = tmp; } PathSegmentCollection pathSegmentCollection = new PathSegmentCollection(); pathSegmentCollection.Add(new LineSegment(new Point(x2, y1), true)); pathSegmentCollection.Add(new LineSegment(new Point(x3, y2), true)); PathFigure pathFigure = new PathFigure( new Point(x1, y1), pathSegmentCollection, true); PathFigureCollection pathFigureCollection = new PathFigureCollection(); pathFigureCollection.Add(pathFigure); PathGeometry pathGeometry = new PathGeometry(pathFigureCollection); return pathGeometry; } protected override void OnRender(DrawingContext drawingContext) { base.OnRender(drawingContext); if (_sortGlyph != null) { double x = _columnHeader.ActualWidth - 13; double y = _columnHeader.ActualHeight / 2 - 5; Rect rect = new Rect(x, y, 10, 10); drawingContext.DrawImage(_sortGlyph, rect); } else { drawingContext.DrawGeometry(Brushes.LightGray, new Pen(Brushes.Gray, 1.0), GetDefaultGlyph()); } } } #endregion } }
I hope you’ll find that useful



August 30, 2009 at 20:11
C’est superb!
Merci bien for this article. I love the way you can do things like this with WPF. Just a note to others: if you find this article first, check out the earlier article to find out how to tell your GridView which columns should be sortable. The above samples are missing those bits of XAML, e.g.
util:GridViewSort.PropertyName=”DateOfBirth”
September 11, 2009 at 22:14
[...] idea can be found from this post, but the attached source code are missing, this post uses same technology with full version of source [...]
September 17, 2009 at 17:48
For anyone interested, I changed the default sort glyph to be more subtle by replacing:
drawingContext.DrawGeometry(Brushes.LightGray, new Pen(Brushes.Gray, 1.0), GetDefaultGlyph());
with:
drawingContext.DrawGeometry(new SolidColorBrush(Colors.LightGray) { Opacity = 0.5 },
new Pen(Brushes.Gray, 0.5), GetDefaultGlyph());
September 17, 2009 at 17:55
I guess it can only be better, my design skills are terrible
Anyway, the best option is probably to use the SortGlyphAscending and SortGlyphDescending properties…
October 16, 2009 at 19:22
Hi,
Is there any way to use this feature by creating the GridView from c# code instead of XAML?
Regards
October 17, 2009 at 01:21
Yes, of course ! as all attached properties, you can set them from code, using GridViewSort.SetAutoSort and associated methods : GridViewSort.SetAutoSort(myGrid, true)
November 10, 2009 at 22:56
I’m trying to sort an integer column and it is sorting as if it where a string. Will I need to alter the code or is there something I’m missing?
thanks,
Colin
November 10, 2009 at 23:50
Hi Colin,
It works fine for me… are you sure that the property you specified for
GridViewSort.PropertyNameis of type int ?December 3, 2009 at 15:34
Sorry for the late reply Thomas. I’ve been too busy to get back to the code where I was use the int column. When I finally get back to it i’ll post an update. I’m sure it’s something on my side. Thanks for the awesome piece of code. It works great for the rest of the columns I used it on.