WPF DataGrid CustomSort for each Column
Here is one way:
using System;
using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
public static class DataGridSort
{
public static readonly DependencyProperty ComparerProperty = DependencyProperty.RegisterAttached(
"Comparer",
typeof(IComparer),
typeof(DataGridSort),
new PropertyMetadata(
default(IComparer),
OnComparerChanged));
private static readonly DependencyProperty ColumnComparerProperty = DependencyProperty.RegisterAttached(
"ColumnComparer",
typeof(ColumnComparer),
typeof(DataGridSort),
new PropertyMetadata(default(ColumnComparer)));
private static readonly DependencyProperty PreviousComparerProperty = DependencyProperty.RegisterAttached(
"PreviousComparer",
typeof(IComparer),
typeof(DataGridSort),
new PropertyMetadata(default(IComparer)));
public static readonly DependencyProperty UseCustomSortProperty = DependencyProperty.RegisterAttached(
"UseCustomSort",
typeof(bool),
typeof(DataGridSort),
new PropertyMetadata(default(bool), OnUseCustomSortChanged));
public static void SetComparer(DataGridColumn element, IComparer value)
{
element.SetValue(ComparerProperty, value);
}
public static IComparer GetComparer(DataGridColumn element)
{
return (IComparer)element.GetValue(ComparerProperty);
}
public static void SetUseCustomSort(DependencyObject element, bool value)
{
element.SetValue(UseCustomSortProperty, value);
}
public static bool GetUseCustomSort(DependencyObject element)
{
return (bool)element.GetValue(UseCustomSortProperty);
}
private static void OnComparerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var column = (DataGridColumn)d;
var columnComparer = new ColumnComparer((IComparer)e.NewValue, column);
column.SetValue(ColumnComparerProperty, columnComparer);
}
private static void OnUseCustomSortChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dataGrid = (DataGrid)d;
if ((bool)e.NewValue)
{
WeakEventManager<DataGrid, DataGridSortingEventArgs>.AddHandler(dataGrid, nameof(dataGrid.Sorting), OnDataGridSorting);
}
else
{
WeakEventManager<DataGrid, DataGridSortingEventArgs>.RemoveHandler(dataGrid, nameof(dataGrid.Sorting), OnDataGridSorting);
}
}
private static void OnDataGridSorting(object sender, DataGridSortingEventArgs e)
{
var column = e.Column;
var columnComparer = (ColumnComparer)column.GetValue(ColumnComparerProperty);
var dataGrid = (DataGrid)sender;
var view = CollectionViewSource.GetDefaultView(dataGrid.ItemsSource) as ListCollectionView;
if (view == null)
{
return;
}
if (columnComparer == null)
{
view.CustomSort = (IComparer)dataGrid.GetValue(PreviousComparerProperty);
}
else
{
if (!(view.CustomSort is ColumnComparer))
{
dataGrid.SetValue(PreviousComparerProperty, view.CustomSort);
}
switch (column.SortDirection)
{
case ListSortDirection.Ascending:
column.SortDirection = ListSortDirection.Descending;
view.CustomSort = columnComparer.Descending;
break;
case null:
case ListSortDirection.Descending:
column.SortDirection = ListSortDirection.Ascending;
view.CustomSort = columnComparer.Ascending;
break;
default:
throw new ArgumentOutOfRangeException();
}
e.Handled = true;
}
}
private class ColumnComparer : IComparer
{
private readonly IComparer valueComparer;
private readonly DataGridColumn column;
private readonly InvertedComparer inverted;
public ColumnComparer(IComparer valueComparer, DataGridColumn column)
{
this.valueComparer = valueComparer;
this.column = column;
inverted = new InvertedComparer(this);
}
public IComparer Ascending => this;
public IComparer Descending => inverted;
int IComparer.Compare(object x, object y)
{
if (x == y)
{
return 0;
}
if (x == null)
{
return -1;
}
if (y == null)
{
return 1;
}
// this can perhaps be a bit slow
// Not adding caching yet.
var xProp = x.GetType().GetProperty(column.SortMemberPath);
var xValue = xProp.GetValue(x);
var yProp = x.GetType().GetProperty(column.SortMemberPath);
var yValue = yProp.GetValue(y);
return valueComparer.Compare(xValue, yValue);
}
private class InvertedComparer : IComparer
{
private readonly IComparer comparer;
public InvertedComparer(IComparer comparer)
{
this.comparer = comparer;
}
public int Compare(object x, object y)
{
return comparer.Compare(y, x);
}
}
}
}
Usage:
<DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding DataItems}"
local:DataGridSort.UseCustomSort="True">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Key}"
Header="Key"
local:DataGridSort.Comparer="{x:Static local:StringLengthComparer.Default}" />
<DataGridTextColumn Binding="{Binding Value}" Header="Value" />
</DataGrid.Columns>
</DataGrid>
This answer is very similar to trilson86's solution -- it was based on it -- but it accounts for SortMemberPath
in a manner such that the values passed to your comparer are the actual values of the column, rather than the rows. This facilitates far greater re-use on your sorters. Furthermore, it eliminates the need for a custom sort interface altogether.
DataGridSortBehavior.cs
using System;
using System.Collections;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace YourNamespace
{
public class DataGridSortBehavior
{
public static IComparer GetSorter(DataGridColumn column)
{
return (IComparer)column.GetValue(SorterProperty);
}
public static void SetSorter(DataGridColumn column, IComparer value)
{
column.SetValue(SorterProperty, value);
}
public static bool GetAllowCustomSort(DataGrid grid)
{
return (bool)grid.GetValue(AllowCustomSortProperty);
}
public static void SetAllowCustomSort(DataGrid grid, bool value)
{
grid.SetValue(AllowCustomSortProperty, value);
}
public static readonly DependencyProperty SorterProperty = DependencyProperty.RegisterAttached("Sorter", typeof(IComparer),
typeof(DataGridSortBehavior));
public static readonly DependencyProperty AllowCustomSortProperty = DependencyProperty.RegisterAttached("AllowCustomSort", typeof(bool),
typeof(DataGridSortBehavior), new UIPropertyMetadata(false, OnAllowCustomSortChanged));
private static void OnAllowCustomSortChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var grid = (DataGrid)obj;
bool oldAllow = (bool)e.OldValue;
bool newAllow = (bool)e.NewValue;
if (!oldAllow && newAllow)
{
grid.Sorting += HandleCustomSorting;
}
else
{
grid.Sorting -= HandleCustomSorting;
}
}
public static bool ApplySort(DataGrid grid, DataGridColumn column)
{
IComparer sorter = GetSorter(column);
if (sorter == null)
{
return false;
}
var listCollectionView = CollectionViewSource.GetDefaultView(grid.ItemsSource) as ListCollectionView;
if (listCollectionView == null)
{
throw new Exception("The ICollectionView associated with the DataGrid must be of type, ListCollectionView");
}
listCollectionView.CustomSort = new DataGridSortComparer(sorter, column.SortDirection ?? ListSortDirection.Ascending, column.SortMemberPath);
return true;
}
private static void HandleCustomSorting(object sender, DataGridSortingEventArgs e)
{
IComparer sorter = GetSorter(e.Column);
if (sorter == null)
{
return;
}
var grid = (DataGrid)sender;
e.Column.SortDirection = e.Column.SortDirection == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
if (ApplySort(grid, e.Column))
{
e.Handled = true;
}
}
private class DataGridSortComparer : IComparer
{
private IComparer comparer;
private ListSortDirection sortDirection;
private string propertyName;
private PropertyInfo property;
public DataGridSortComparer(IComparer comparer, ListSortDirection sortDirection, string propertyName)
{
this.comparer = comparer;
this.sortDirection = sortDirection;
this.propertyName = propertyName;
}
public int Compare(object x, object y)
{
PropertyInfo property = this.property ?? (this.property = x.GetType().GetProperty(propertyName));
object value1 = property.GetValue(x);
object value2 = property.GetValue(y);
int result = comparer.Compare(value1, value2);
if (sortDirection == ListSortDirection.Descending)
{
result = -result;
}
return result;
}
}
}
}
Your Xaml
This should look similar to trilson86's solution as well:
<UserControl.Resources>
<converters:MyComparer x:Key="MyComparer"/>
</UserControl.Resources>
<DataGrid behaviours:DataGridSortBehavior.AllowCustomSort="True" ItemsSource="{Binding MyListCollectionView}">
<DataGrid.Columns>
<DataGridTextColumn Header="Test" Binding="{Binding MyValue}" behaviours:DataGridSortBehavior.Sorter="{StaticResource MyComparer}" />
</DataGrid.Columns>
</DataGrid>
The answer given by trilson86 is excellent. However, the third parameter in the two DependencyProperty declarations is incorrect. Instead of DataGrid and DataGridColumn, they should be CustomSortBehaviour, as such:
public static readonly DependencyProperty AllowCustomSortProperty =
DependencyProperty.RegisterAttached("AllowCustomSort",
typeof(bool),
typeof(CustomSortBehaviour), // <- Here
new UIPropertyMetadata(false, OnAllowCustomSortChanged));
public static readonly DependencyProperty CustomSorterProperty =
DependencyProperty.RegisterAttached("CustomSorter",
typeof(ICustomSorter),
typeof(CustomSortBehaviour)); // <- Here
I kept getting a warning that the AllowCustomSort property was already registered. A little research led me to the answer here.
At any rate, it's an excellent answer, so thank you.
I created a couple of attached properties which handle this issue. I hope this comes in handy for someone!
First - a simple interface for your directionalised comparer. This extends IComparer but gives us one more property (SortDirection). Your implementation should use this to determine the correct ordering of elements (which would otherwise have been lost).
public interface ICustomSorter : IComparer
{
ListSortDirection SortDirection { get; set; }
}
Next is the attached behavior - this does two things: 1) tells the grid to use custom sort logic (AllowCustomSort=true) and b) gives us the ability to set this logic at a per-column level.
public class CustomSortBehaviour
{
public static readonly DependencyProperty CustomSorterProperty =
DependencyProperty.RegisterAttached("CustomSorter", typeof(ICustomSorter), typeof(CustomSortBehaviour));
public static ICustomSorter GetCustomSorter(DataGridColumn gridColumn)
{
return (ICustomSorter)gridColumn.GetValue(CustomSorterProperty);
}
public static void SetCustomSorter(DataGridColumn gridColumn, ICustomSorter value)
{
gridColumn.SetValue(CustomSorterProperty, value);
}
public static readonly DependencyProperty AllowCustomSortProperty =
DependencyProperty.RegisterAttached("AllowCustomSort", typeof(bool),
typeof(CustomSortBehaviour), new UIPropertyMetadata(false, OnAllowCustomSortChanged));
public static bool GetAllowCustomSort(DataGrid grid)
{
return (bool)grid.GetValue(AllowCustomSortProperty);
}
public static void SetAllowCustomSort(DataGrid grid, bool value)
{
grid.SetValue(AllowCustomSortProperty, value);
}
private static void OnAllowCustomSortChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var existing = d as DataGrid;
if (existing == null) return;
var oldAllow = (bool)e.OldValue;
var newAllow = (bool)e.NewValue;
if (!oldAllow && newAllow)
{
existing.Sorting += HandleCustomSorting;
}
else
{
existing.Sorting -= HandleCustomSorting;
}
}
private static void HandleCustomSorting(object sender, DataGridSortingEventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null || !GetAllowCustomSort(dataGrid)) return;
var listColView = dataGrid.ItemsSource as ListCollectionView;
if (listColView == null)
throw new Exception("The DataGrid's ItemsSource property must be of type, ListCollectionView");
// Sanity check
var sorter = GetCustomSorter(e.Column);
if (sorter == null) return;
// The guts.
e.Handled = true;
var direction = (e.Column.SortDirection != ListSortDirection.Ascending)
? ListSortDirection.Ascending
: ListSortDirection.Descending;
e.Column.SortDirection = sorter.SortDirection = direction;
listColView.CustomSort = sorter;
}
}
To use it, implement an ICustomComparer (with a parameterless constructor) and in your XAML:
<UserControl.Resources>
<converters:MyComparer x:Key="MyComparer"/>
<!-- add more if you need them -->
</UserControl.Resources>
<DataGrid behaviours:CustomSortBehaviour.AllowCustomSort="True" ItemsSource="{Binding MyListCollectionView}">
<DataGrid.Columns>
<DataGridTextColumn Header="Test" Binding="{Binding MyValue}" behaviours:CustomSortBehaviour.CustomSorter="{StaticResource MyComparer}" />
</DataGrid.Columns>
</DataGrid>