Browse Source

Merge pull request #4687 from AvaloniaUI/feature/data-grid-custom-sorting

Feature/data grid custom sorting
pull/4694/head
Max Katz 6 years ago
committed by GitHub
parent
commit
ca498f8f9b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  2. 54
      src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
  3. 5
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  4. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  5. 97
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  6. 5
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  7. 4
      src/Avalonia.Controls.DataGrid/EventArgs.cs
  8. 9
      tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs

35
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -1,8 +1,12 @@
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using ControlCatalog.Models; using ControlCatalog.Models;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
@ -11,12 +15,22 @@ namespace ControlCatalog.Pages
public DataGridPage() public DataGridPage()
{ {
this.InitializeComponent(); this.InitializeComponent();
var dataGridSortDescription = DataGridSortDescription.FromPath(nameof(Country.Region), ListSortDirection.Ascending, new ReversedStringComparer());
var collectionView1 = new DataGridCollectionView(Countries.All);
collectionView1.SortDescriptions.Add(dataGridSortDescription);
var dg1 = this.FindControl<DataGrid>("dataGrid1"); var dg1 = this.FindControl<DataGrid>("dataGrid1");
dg1.IsReadOnly = true; dg1.IsReadOnly = true;
dg1.LoadingRow += Dg1_LoadingRow; dg1.LoadingRow += Dg1_LoadingRow;
var collectionView1 = new DataGridCollectionView(Countries.All); dg1.Sorting += (s, a) =>
//collectionView.GroupDescriptions.Add(new PathGroupDescription("Region")); {
var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path;
if (property == dataGridSortDescription.PropertyPath
&& !collectionView1.SortDescriptions.Contains(dataGridSortDescription))
{
collectionView1.SortDescriptions.Add(dataGridSortDescription);
}
};
dg1.Items = collectionView1; dg1.Items = collectionView1;
var dg2 = this.FindControl<DataGrid>("dataGridGrouping"); var dg2 = this.FindControl<DataGrid>("dataGridGrouping");
@ -53,5 +67,20 @@ namespace ControlCatalog.Pages
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private class ReversedStringComparer : IComparer<object>, IComparer
{
public int Compare(object x, object y)
{
if (x is string left && y is string right)
{
var reversedLeft = new string(left.Reverse().ToArray());
var reversedRight = new string(right.Reverse().ToArray());
return reversedLeft.CompareTo(reversedRight);
}
return Comparer.Default.Compare(x, y);
}
}
} }
} }

54
src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs

@ -1,19 +1,21 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text;
using Avalonia.Controls;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Utilities;
namespace Avalonia.Collections namespace Avalonia.Collections
{ {
public abstract class DataGridSortDescription public abstract class DataGridSortDescription
{ {
public virtual string PropertyPath => null; public virtual string PropertyPath => null;
public virtual bool Descending => false;
[Obsolete("Use Direction property to read or override sorting direction.")]
public virtual bool Descending => Direction == ListSortDirection.Descending;
public virtual ListSortDirection Direction => ListSortDirection.Ascending;
public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath); public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath);
public abstract IComparer<object> Comparer { get; } public abstract IComparer<object> Comparer { get; }
@ -26,7 +28,7 @@ namespace Avalonia.Collections
return seq.ThenBy(o => o, Comparer); return seq.ThenBy(o => o, Comparer);
} }
internal virtual DataGridSortDescription SwitchSortDirection() public virtual DataGridSortDescription SwitchSortDirection()
{ {
return this; return this;
} }
@ -105,7 +107,7 @@ namespace Avalonia.Collections
private class DataGridPathSortDescription : DataGridSortDescription private class DataGridPathSortDescription : DataGridSortDescription
{ {
private readonly bool _descending; private readonly ListSortDirection _direction;
private readonly string _propertyPath; private readonly string _propertyPath;
private readonly Lazy<CultureSensitiveComparer> _cultureSensitiveComparer; private readonly Lazy<CultureSensitiveComparer> _cultureSensitiveComparer;
private readonly Lazy<IComparer<object>> _comparer; private readonly Lazy<IComparer<object>> _comparer;
@ -118,7 +120,7 @@ namespace Avalonia.Collections
{ {
if (_internalComparerTyped == null && _internalComparer != null) if (_internalComparerTyped == null && _internalComparer != null)
{ {
if (_internalComparerTyped is IComparer<object> c) if (_internalComparer is IComparer<object> c)
_internalComparerTyped = c; _internalComparerTyped = c;
else else
_internalComparerTyped = Comparer<object>.Create((x, y) => _internalComparer.Compare(x, y)); _internalComparerTyped = Comparer<object>.Create((x, y) => _internalComparer.Compare(x, y));
@ -130,19 +132,20 @@ namespace Avalonia.Collections
public override string PropertyPath => _propertyPath; public override string PropertyPath => _propertyPath;
public override IComparer<object> Comparer => _comparer.Value; public override IComparer<object> Comparer => _comparer.Value;
public override bool Descending => _descending; public override ListSortDirection Direction => _direction;
public DataGridPathSortDescription(string propertyPath, bool descending, CultureInfo culture) public DataGridPathSortDescription(string propertyPath, ListSortDirection direction, IComparer internalComparer, CultureInfo culture)
{ {
_propertyPath = propertyPath; _propertyPath = propertyPath;
_descending = descending; _direction = direction;
_cultureSensitiveComparer = new Lazy<CultureSensitiveComparer>(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture)); _cultureSensitiveComparer = new Lazy<CultureSensitiveComparer>(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture));
_internalComparer = internalComparer;
_comparer = new Lazy<IComparer<object>>(() => Comparer<object>.Create((x, y) => Compare(x, y))); _comparer = new Lazy<IComparer<object>>(() => Comparer<object>.Create((x, y) => Compare(x, y)));
} }
private DataGridPathSortDescription(DataGridPathSortDescription inner, bool descending) private DataGridPathSortDescription(DataGridPathSortDescription inner, ListSortDirection direction)
{ {
_propertyPath = inner._propertyPath; _propertyPath = inner._propertyPath;
_descending = descending; _direction = direction;
_propertyType = inner._propertyType; _propertyType = inner._propertyType;
_cultureSensitiveComparer = inner._cultureSensitiveComparer; _cultureSensitiveComparer = inner._cultureSensitiveComparer;
_internalComparer = inner._internalComparer; _internalComparer = inner._internalComparer;
@ -201,7 +204,7 @@ namespace Avalonia.Collections
result = _internalComparer?.Compare(v1, v2) ?? 0; result = _internalComparer?.Compare(v1, v2) ?? 0;
if (_descending) if (Direction == ListSortDirection.Descending)
return -result; return -result;
else else
return result; return result;
@ -218,7 +221,7 @@ namespace Avalonia.Collections
} }
public override IOrderedEnumerable<object> OrderBy(IEnumerable<object> seq) public override IOrderedEnumerable<object> OrderBy(IEnumerable<object> seq)
{ {
if(_descending) if (Direction == ListSortDirection.Descending)
{ {
return seq.OrderByDescending(o => GetValue(o), InternalComparer); return seq.OrderByDescending(o => GetValue(o), InternalComparer);
} }
@ -229,7 +232,7 @@ namespace Avalonia.Collections
} }
public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq) public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq)
{ {
if (_descending) if (Direction == ListSortDirection.Descending)
{ {
return seq.ThenByDescending(o => GetValue(o), InternalComparer); return seq.ThenByDescending(o => GetValue(o), InternalComparer);
} }
@ -239,15 +242,28 @@ namespace Avalonia.Collections
} }
} }
internal override DataGridSortDescription SwitchSortDirection() public override DataGridSortDescription SwitchSortDirection()
{ {
return new DataGridPathSortDescription(this, !_descending); var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
return new DataGridPathSortDescription(this, newDirection);
} }
} }
public static DataGridSortDescription FromPath(string propertyPath, bool descending = false, CultureInfo culture = null) public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction = ListSortDirection.Ascending, CultureInfo culture = null)
{
return new DataGridPathSortDescription(propertyPath, direction, null, culture);
}
[Obsolete("Use overload taking a ListSortDirection.")]
public static DataGridSortDescription FromPath(string propertyPath, bool descending, CultureInfo culture = null)
{
return new DataGridPathSortDescription(propertyPath, descending ? ListSortDirection.Descending : ListSortDirection.Ascending, null, culture);
}
public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction, IComparer comparer)
{ {
return new DataGridPathSortDescription(propertyPath, descending, culture); return new DataGridPathSortDescription(propertyPath, direction, comparer, null);
} }
} }

5
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -1229,6 +1229,11 @@ namespace Avalonia.Controls
remove { AddHandler(SelectionChangedEvent, value); } remove { AddHandler(SelectionChangedEvent, value); }
} }
/// <summary>
/// Occurs when the <see cref="DataGridColumn"/> sorting request is triggered.
/// </summary>
public event EventHandler<DataGridColumnEventArgs> Sorting;
/// <summary> /// <summary>
/// Occurs when a <see cref="T:Avalonia.Controls.DataGridRow" /> /// Occurs when a <see cref="T:Avalonia.Controls.DataGridRow" />
/// object becomes available for reuse. /// object becomes available for reuse.

2
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -1047,4 +1047,4 @@ namespace Avalonia.Controls
} }
} }

97
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -161,13 +161,14 @@ namespace Avalonia.Controls
var sort = OwningColumn.GetSortDescription(); var sort = OwningColumn.GetSortDescription();
if (sort != null) if (sort != null)
{ {
CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending; CurrentSortingState = sort.Direction;
} }
} }
PseudoClasses.Set(":sortascending", PseudoClasses.Set(":sortascending",
CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending); CurrentSortingState == ListSortDirection.Ascending);
PseudoClasses.Set(":sortdescending", PseudoClasses.Set(":sortdescending",
CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending); CurrentSortingState == ListSortDirection.Descending);
} }
internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn) internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn)
@ -215,70 +216,76 @@ namespace Avalonia.Controls
internal void ProcessSort(KeyModifiers keyModifiers) internal void ProcessSort(KeyModifiers keyModifiers)
{ {
// if we can sort: // if we can sort:
// - DataConnection.AllowSort is true, and
// - AllowUserToSortColumns and CanSort are true, and // - AllowUserToSortColumns and CanSort are true, and
// - OwningColumn is bound, and // - OwningColumn is bound
// - SortDescriptionsCollection exists, and
// - the column's data type is comparable
// then try to sort // then try to sort
if (OwningColumn != null if (OwningColumn != null
&& OwningGrid != null && OwningGrid != null
&& OwningGrid.EditingRow == null && OwningGrid.EditingRow == null
&& OwningColumn != OwningGrid.ColumnsInternal.FillerColumn && OwningColumn != OwningGrid.ColumnsInternal.FillerColumn
&& OwningGrid.DataConnection.AllowSort
&& OwningGrid.CanUserSortColumns && OwningGrid.CanUserSortColumns
&& OwningColumn.CanUserSort && OwningColumn.CanUserSort)
&& OwningGrid.DataConnection.SortDescriptions != null)
{ {
DataGrid owningGrid = OwningGrid; var ea = new DataGridColumnEventArgs(OwningColumn);
OwningGrid.OnColumnSorting(ea);
DataGridSortDescription newSort; if (!ea.Handled && OwningGrid.DataConnection.AllowSort && OwningGrid.DataConnection.SortDescriptions != null)
{
// - DataConnection.AllowSort is true, and
// - SortDescriptionsCollection exists, and
// - the column's data type is comparable
KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift); DataGrid owningGrid = OwningGrid;
DataGridSortDescription newSort;
DataGridSortDescription sort = OwningColumn.GetSortDescription(); KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift);
IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView;
Debug.Assert(collectionView != null); DataGridSortDescription sort = OwningColumn.GetSortDescription();
using (collectionView.DeferRefresh()) IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView;
{ Debug.Assert(collectionView != null);
// if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand
if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0)
{
owningGrid.DataConnection.SortDescriptions.Clear();
}
// if ctrl is held down, we only clear the sort directions using (collectionView.DeferRefresh())
if (!ctrl)
{ {
if (sort != null) // if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand
if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0)
{ {
newSort = sort.SwitchSortDirection(); owningGrid.DataConnection.SortDescriptions.Clear();
}
// changing direction should not affect sort order, so we replace this column's // if ctrl is held down, we only clear the sort directions
// sort description instead of just adding it to the end of the collection if (!ctrl)
int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); {
if (oldIndex >= 0) if (sort != null)
{ {
owningGrid.DataConnection.SortDescriptions.Remove(sort); newSort = sort.SwitchSortDirection();
owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
// changing direction should not affect sort order, so we replace this column's
// sort description instead of just adding it to the end of the collection
int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
if (oldIndex >= 0)
{
owningGrid.DataConnection.SortDescriptions.Remove(sort);
owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
}
else
{
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
} }
else else
{ {
string propertyName = OwningColumn.GetSortPropertyName();
// no-opt if we couldn't find a property to sort on
if (string.IsNullOrEmpty(propertyName))
{
return;
}
newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
owningGrid.DataConnection.SortDescriptions.Add(newSort); owningGrid.DataConnection.SortDescriptions.Add(newSort);
} }
} }
else
{
string propertyName = OwningColumn.GetSortPropertyName();
// no-opt if we couldn't find a property to sort on
if (string.IsNullOrEmpty(propertyName))
{
return;
}
newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
} }
} }
} }

5
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@ -33,6 +33,11 @@ namespace Avalonia.Controls
ColumnReordering?.Invoke(this, e); ColumnReordering?.Invoke(this, e);
} }
protected internal virtual void OnColumnSorting(DataGridColumnEventArgs e)
{
Sorting?.Invoke(this, e);
}
/// <summary> /// <summary>
/// Adjusts the widths of all columns with DisplayIndex >= displayIndex such that the total /// Adjusts the widths of all columns with DisplayIndex >= displayIndex such that the total
/// width is adjusted by the given amount, if possible. If the total desired adjustment amount /// width is adjusted by the given amount, if possible. If the total desired adjustment amount

4
src/Avalonia.Controls.DataGrid/EventArgs.cs

@ -289,7 +289,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Provides data for <see cref="T:Avalonia.Controls.DataGrid" /> column-related events. /// Provides data for <see cref="T:Avalonia.Controls.DataGrid" /> column-related events.
/// </summary> /// </summary>
public class DataGridColumnEventArgs : EventArgs public class DataGridColumnEventArgs : HandledEventArgs
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumnEventArgs" /> class. /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumnEventArgs" /> class.
@ -566,4 +566,4 @@ namespace Avalonia.Controls
private set; private set;
} }
} }
} }

9
tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs

@ -1,4 +1,5 @@
using System; using System;
using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Xunit; using Xunit;
@ -18,7 +19,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections
new Item("c", "c"), new Item("c", "c"),
}; };
var expectedResult = items.OrderBy(i => i.Prop1).ToList(); var expectedResult = items.OrderBy(i => i.Prop1).ToList();
var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: false); var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), ListSortDirection.Ascending);
sortDescription.Initialize(typeof(Item)); sortDescription.Initialize(typeof(Item));
var result = sortDescription.OrderBy(items).ToList(); var result = sortDescription.OrderBy(items).ToList();
@ -36,7 +37,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections
new Item("c", "c"), new Item("c", "c"),
}; };
var expectedResult = items.OrderByDescending(i => i.Prop1).ToList(); var expectedResult = items.OrderByDescending(i => i.Prop1).ToList();
var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: true); var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), ListSortDirection.Descending);
sortDescription.Initialize(typeof(Item)); sortDescription.Initialize(typeof(Item));
var result = sortDescription.OrderBy(items).ToList(); var result = sortDescription.OrderBy(items).ToList();
@ -61,7 +62,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections
new Item("a", "b"), new Item("a", "b"),
new Item("a", "c"), new Item("a", "c"),
}; };
var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: false); var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), ListSortDirection.Ascending);
sortDescription.Initialize(typeof(Item)); sortDescription.Initialize(typeof(Item));
var result = sortDescription.ThenBy(items).ToList(); var result = sortDescription.ThenBy(items).ToList();
@ -86,7 +87,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections
new Item("a", "b"), new Item("a", "b"),
new Item("a", "a"), new Item("a", "a"),
}; };
var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: true); var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), ListSortDirection.Descending);
sortDescription.Initialize(typeof(Item)); sortDescription.Initialize(typeof(Item));
var result = sortDescription.ThenBy(items).ToList(); var result = sortDescription.ThenBy(items).ToList();

Loading…
Cancel
Save