Skip to content

QuickGrid: Adds Multi Column Sorting #62604

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public sealed class GridSort<TGridItem>
private const string ExpressionNotRepresentableMessage = "The supplied expression can't be represented as a property name for sorting. Only simple member expressions, such as @(x => x.SomeProperty), can be converted to property names.";

private readonly Func<IQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> _first;
private readonly Func<IOrderedQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> _thenFirst;
private List<Func<IOrderedQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>>>? _then;

private (LambdaExpression, bool) _firstExpression;
Expand All @@ -23,10 +24,14 @@ public sealed class GridSort<TGridItem>
private IReadOnlyCollection<SortedProperty>? _cachedPropertyListAscending;
private IReadOnlyCollection<SortedProperty>? _cachedPropertyListDescending;

internal GridSort(Func<IQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> first, (LambdaExpression, bool) firstExpression)
internal GridSort(
Func<IQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> first,
(LambdaExpression, bool) firstExpression,
Func<IOrderedQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> thenFirst)
{
_first = first;
_firstExpression = firstExpression;
_thenFirst = thenFirst;
_then = default;
_thenExpressions = default;
}
Expand All @@ -39,7 +44,8 @@ internal GridSort(Func<IQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>
/// <returns>A <see cref="GridSort{T}"/> instance representing the specified sorting rule.</returns>
public static GridSort<TGridItem> ByAscending<U>(Expression<Func<TGridItem, U>> expression)
=> new((queryable, asc) => asc ? queryable.OrderBy(expression) : queryable.OrderByDescending(expression),
(expression, true));
(expression, true),
(queryable, asc) => asc ? queryable.ThenBy(expression) : queryable.ThenByDescending(expression));

/// <summary>
/// Produces a <see cref="GridSort{T}"/> instance that sorts according to the specified <paramref name="expression"/>, descending.
Expand All @@ -49,7 +55,8 @@ public static GridSort<TGridItem> ByAscending<U>(Expression<Func<TGridItem, U>>
/// <returns>A <see cref="GridSort{T}"/> instance representing the specified sorting rule.</returns>
public static GridSort<TGridItem> ByDescending<U>(Expression<Func<TGridItem, U>> expression)
=> new((queryable, asc) => asc ? queryable.OrderByDescending(expression) : queryable.OrderBy(expression),
(expression, false));
(expression, false),
(queryable, asc) => asc ? queryable.ThenByDescending(expression) : queryable.ThenBy(expression));

/// <summary>
/// Updates a <see cref="GridSort{T}"/> instance by appending a further sorting rule.
Expand Down Expand Up @@ -85,9 +92,9 @@ public GridSort<TGridItem> ThenDescending<U>(Expression<Func<TGridItem, U>> expr
return this;
}

internal IOrderedQueryable<TGridItem> Apply(IQueryable<TGridItem> queryable, bool ascending)
internal IOrderedQueryable<TGridItem> Apply(IQueryable<TGridItem> queryable, bool ascending, bool firstColumn)
{
var orderedQueryable = _first(queryable, ascending);
var orderedQueryable = ApplyCore(queryable, ascending, firstColumn);

if (_then is not null)
{
Expand All @@ -100,6 +107,21 @@ internal IOrderedQueryable<TGridItem> Apply(IQueryable<TGridItem> queryable, boo
return orderedQueryable;
}

private IOrderedQueryable<TGridItem> ApplyCore(IQueryable<TGridItem> queryable, bool ascending, bool firstColumn)
{
if (firstColumn)
{
return _first(queryable, ascending);
}

if (queryable is not IOrderedQueryable<TGridItem> src)
{
throw new InvalidOperationException($"Expected {typeof(IOrderedQueryable<TGridItem>)} since this is not the first sort column.");
}

return _thenFirst(src, ascending);
}

internal IReadOnlyCollection<SortedProperty> ToPropertyList(bool ascending)
{
if (ascending)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.QuickGrid;

/// <summary>
/// Provides information about the column that has sorting applied.
/// </summary>
/// <typeparam name="TGridItem">The type of data represented by each row in the grid.</typeparam>
public class SortColumn<TGridItem>
{
/// <summary>
/// The column that has sorting applied.
/// </summary>
public ColumnBase<TGridItem>? Column { get; init; }

/// <summary>
/// Whether or not the sort is ascending.
/// </summary>
public bool Ascending { get; internal set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,64 @@ public readonly struct GridItemsProviderRequest<TGridItem>
/// </summary>
public int? Count { get; init; }

/// <summary>
/// Specifies which columns are currently being sorted.
///
/// Rather than inferring the sort rules manually, you should normally call either <see cref="ApplySorting(IQueryable{TGridItem})"/>
/// or <see cref="GetSortByProperties"/>, since they also account for <see cref="SortColumn{TGridItem}.Column"/>
/// and <see cref="SortColumn{TGridItem}.Ascending"/> automatically.
/// </summary>
public IReadOnlyList<SortColumn<TGridItem>> SortColumns { get; init; }

/// <summary>
/// Specifies which column represents the sort order.
///
/// Rather than inferring the sort rules manually, you should normally call either <see cref="ApplySorting(IQueryable{TGridItem})"/>
/// or <see cref="GetSortByProperties"/>, since they also account for <see cref="SortByColumn" /> and <see cref="SortByAscending" /> automatically.
/// </summary>
public ColumnBase<TGridItem>? SortByColumn { get; init; }
[Obsolete("Use " + nameof(SortColumns) + " instead.")]
public ColumnBase<TGridItem>? SortByColumn
{
get => _sortByColumn;
init => _sortByColumn = value;
}

private readonly ColumnBase<TGridItem>? _sortByColumn;

/// <summary>
/// Specifies the current sort direction.
///
/// Rather than inferring the sort rules manually, you should normally call either <see cref="ApplySorting(IQueryable{TGridItem})"/>
/// or <see cref="GetSortByProperties"/>, since they also account for <see cref="SortByColumn" /> and <see cref="SortByAscending" /> automatically.
/// </summary>
public bool SortByAscending { get; init; }
[Obsolete("Use " + nameof(SortColumns) + " instead.")]
public bool SortByAscending
{
get => _sortByAscending;
init => _sortByAscending = value;
}

private readonly bool _sortByAscending;

/// <summary>
/// A token that indicates if the request should be cancelled.
/// </summary>
public CancellationToken CancellationToken { get; init; }

internal GridItemsProviderRequest(
int startIndex, int? count, ColumnBase<TGridItem>? sortByColumn, bool sortByAscending,
CancellationToken cancellationToken)
int startIndex, int? count, IReadOnlyList<SortColumn<TGridItem>> sortColumns, CancellationToken cancellationToken)
{
StartIndex = startIndex;
Count = count;
SortByColumn = sortByColumn;
SortByAscending = sortByAscending;
SortColumns = sortColumns;

if (sortColumns.Any())
{
var sortColumn = sortColumns[0];
_sortByColumn = sortColumn.Column;
_sortByAscending = sortColumn.Ascending;
}

CancellationToken = cancellationToken;
}

Expand All @@ -58,13 +87,44 @@ internal GridItemsProviderRequest(
/// </summary>
/// <param name="source">An <see cref="IQueryable{TGridItem}"/>.</param>
/// <returns>A new <see cref="IQueryable{TGridItem}"/> representing the <paramref name="source"/> with sorting rules applied.</returns>
public IQueryable<TGridItem> ApplySorting(IQueryable<TGridItem> source) =>
SortByColumn?.SortBy?.Apply(source, SortByAscending) ?? source;
public IQueryable<TGridItem> ApplySorting(IQueryable<TGridItem> source)
{
for (var i = 0; i < SortColumns.Count; i++)
{
var sortColumn = SortColumns[i];

if (sortColumn.Column?.SortBy != null)
{
source = sortColumn.Column.SortBy.Apply(source, sortColumn.Ascending, i == 0);
}
}

return source;
}

/// <summary>
/// Produces a collection of (property name, direction) pairs representing the sorting rules.
/// </summary>
/// <returns>A collection of (property name, direction) pairs representing the sorting rules</returns>
[Obsolete("Use " + nameof(GetSortColumnProperties) + " instead.")]
public IReadOnlyCollection<SortedProperty> GetSortByProperties() =>
SortByColumn?.SortBy?.ToPropertyList(SortByAscending) ?? Array.Empty<SortedProperty>();
SortByColumn?.SortBy?.ToPropertyList(SortByAscending) ?? Array.Empty<SortedProperty>();

/// <summary>
/// Produces a collection of collections representing the applied sort rules for the grid.
/// </summary>
/// <remarks>
/// Each item in the returned sequence is a collection of property name and sort direction pairs that define one sorting expression.
/// The sequence reflects the sort precedence (first, second, etc.).
/// </remarks>
/// <returns>
/// An <see cref="IEnumerable{T}"/> of <see cref="IReadOnlyCollection{T}"/> of <see cref="SortedProperty"/> representing the full sorting expression.
/// </returns>
public IEnumerable<IReadOnlyCollection<SortedProperty>> GetSortColumnProperties()
{
foreach (var sortColumn in SortColumns)
{
yield return sortColumn.Column?.SortBy?.ToPropertyList(sortColumn.Ascending) ?? [];
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
#nullable enable
Microsoft.AspNetCore.Components.QuickGrid.GridItemsProviderRequest<TGridItem>.GetSortColumnProperties() -> System.Collections.Generic.IEnumerable<System.Collections.Generic.IReadOnlyCollection<Microsoft.AspNetCore.Components.QuickGrid.SortedProperty>!>!
Microsoft.AspNetCore.Components.QuickGrid.GridItemsProviderRequest<TGridItem>.SortColumns.get -> System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.QuickGrid.SortColumn<TGridItem>!>!
Microsoft.AspNetCore.Components.QuickGrid.GridItemsProviderRequest<TGridItem>.SortColumns.init -> void
Microsoft.AspNetCore.Components.QuickGrid.QuickGrid<TGridItem>.AddUpdateSortByColumnAsync(Microsoft.AspNetCore.Components.QuickGrid.ColumnBase<TGridItem>! column, Microsoft.AspNetCore.Components.QuickGrid.SortDirection direction = Microsoft.AspNetCore.Components.QuickGrid.SortDirection.Auto) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.QuickGrid.QuickGrid<TGridItem>.HideColumnOptionsAsync() -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.QuickGrid.QuickGrid<TGridItem>.RowClass.get -> System.Func<TGridItem, string?>?
Microsoft.AspNetCore.Components.QuickGrid.QuickGrid<TGridItem>.RowClass.set -> void
Microsoft.AspNetCore.Components.QuickGrid.QuickGrid<TGridItem>.RowClass.set -> void
Microsoft.AspNetCore.Components.QuickGrid.QuickGrid<TGridItem>.SortColumns.get -> System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.QuickGrid.SortColumn<TGridItem>!>!
Microsoft.AspNetCore.Components.QuickGrid.SortColumn<TGridItem>
Microsoft.AspNetCore.Components.QuickGrid.SortColumn<TGridItem>.Ascending.get -> bool
Microsoft.AspNetCore.Components.QuickGrid.SortColumn<TGridItem>.Column.get -> Microsoft.AspNetCore.Components.QuickGrid.ColumnBase<TGridItem>?
Microsoft.AspNetCore.Components.QuickGrid.SortColumn<TGridItem>.Column.init -> void
Microsoft.AspNetCore.Components.QuickGrid.SortColumn<TGridItem>.SortColumn() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ private void FinishCollectingColumns()
_collectingColumns = false;
}

private readonly List<SortColumn<TGridItem>> _sortByColumns = [];

/// <summary>
/// The list of columns that have sorting applied.
/// </summary>
public IReadOnlyList<SortColumn<TGridItem>> SortColumns => _sortByColumns;

/// <summary>
/// Sets the grid's current sort column to the specified <paramref name="column"/>.
/// </summary>
Expand All @@ -250,16 +257,36 @@ private void FinishCollectingColumns()
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
public Task SortByColumnAsync(ColumnBase<TGridItem> column, SortDirection direction = SortDirection.Auto)
{
_sortByAscending = direction switch
_sortByColumns.RemoveAll(sbc => sbc.Column != column);
return AddUpdateSortByColumnAsync(column, direction);
}

/// <summary>
/// Adds or updates sorting of the specified <paramref name="column"/>.
/// If the column is not already being tracked it will be appended, otherwise it's direction is updated with it's position unchanged.
/// </summary>
/// <param name="column">The column that defines the new sort order.</param>
/// <param name="direction">The direction of sorting. If the value is <see cref="SortDirection.Auto"/>, then it will toggle the direction on each call.</param>
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
/// <exception cref="NotSupportedException"></exception>
public Task AddUpdateSortByColumnAsync(ColumnBase<TGridItem> column, SortDirection direction = SortDirection.Auto)
{
var sortBy = _sortByColumns.FirstOrDefault(sbc => sbc.Column == column);

if (sortBy == null)
{
sortBy = new() { Column = column };
_sortByColumns.Add(sortBy);
}

sortBy.Ascending = direction switch
{
SortDirection.Ascending => true,
SortDirection.Descending => false,
SortDirection.Auto => _sortByColumn != column || !_sortByAscending,
_ => throw new NotSupportedException($"Unknown sort direction {direction}"),
SortDirection.Auto => !sortBy.Ascending,
_ => throw new NotSupportedException($"Unknown sort direction {direction}")
};

_sortByColumn = column;

StateHasChanged(); // We want to see the updated sort order in the header, even before the data query is completed
return RefreshDataAsync();
}
Expand Down Expand Up @@ -320,7 +347,7 @@ private async Task RefreshDataCoreAsync()
_lastRefreshedPaginationStateHash = Pagination?.GetHashCode();
var startIndex = Pagination is null ? 0 : (Pagination.CurrentPageIndex * Pagination.ItemsPerPage);
var request = new GridItemsProviderRequest<TGridItem>(
startIndex, Pagination?.ItemsPerPage, _sortByColumn, _sortByAscending, thisLoadCts.Token);
startIndex, Pagination?.ItemsPerPage, SortColumns, thisLoadCts.Token);
var result = await ResolveItemsRequestAsync(request);
if (!thisLoadCts.IsCancellationRequested)
{
Expand Down Expand Up @@ -356,7 +383,7 @@ private async Task RefreshDataCoreAsync()
}

var providerRequest = new GridItemsProviderRequest<TGridItem>(
startIndex, count, _sortByColumn, _sortByAscending, request.CancellationToken);
startIndex, count, SortColumns, request.CancellationToken);
var providerResult = await ResolveItemsRequestAsync(providerRequest);

if (!request.CancellationToken.IsCancellationRequested)
Expand Down
Loading