diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/GridSort.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/GridSort.cs index 0e891b2e206f..51a65171736f 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/GridSort.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/GridSort.cs @@ -15,6 +15,7 @@ public sealed class GridSort 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, bool, IOrderedQueryable> _first; + private readonly Func, bool, IOrderedQueryable> _thenFirst; private List, bool, IOrderedQueryable>>? _then; private (LambdaExpression, bool) _firstExpression; @@ -23,10 +24,14 @@ public sealed class GridSort private IReadOnlyCollection? _cachedPropertyListAscending; private IReadOnlyCollection? _cachedPropertyListDescending; - internal GridSort(Func, bool, IOrderedQueryable> first, (LambdaExpression, bool) firstExpression) + internal GridSort( + Func, bool, IOrderedQueryable> first, + (LambdaExpression, bool) firstExpression, + Func, bool, IOrderedQueryable> thenFirst) { _first = first; _firstExpression = firstExpression; + _thenFirst = thenFirst; _then = default; _thenExpressions = default; } @@ -39,7 +44,8 @@ internal GridSort(Func, bool, IOrderedQueryable /// A instance representing the specified sorting rule. public static GridSort ByAscending(Expression> expression) => new((queryable, asc) => asc ? queryable.OrderBy(expression) : queryable.OrderByDescending(expression), - (expression, true)); + (expression, true), + (queryable, asc) => asc ? queryable.ThenBy(expression) : queryable.ThenByDescending(expression)); /// /// Produces a instance that sorts according to the specified , descending. @@ -49,7 +55,8 @@ public static GridSort ByAscending(Expression> /// A instance representing the specified sorting rule. public static GridSort ByDescending(Expression> expression) => new((queryable, asc) => asc ? queryable.OrderByDescending(expression) : queryable.OrderBy(expression), - (expression, false)); + (expression, false), + (queryable, asc) => asc ? queryable.ThenByDescending(expression) : queryable.ThenBy(expression)); /// /// Updates a instance by appending a further sorting rule. @@ -85,9 +92,9 @@ public GridSort ThenDescending(Expression> expr return this; } - internal IOrderedQueryable Apply(IQueryable queryable, bool ascending) + internal IOrderedQueryable Apply(IQueryable queryable, bool ascending, bool firstColumn) { - var orderedQueryable = _first(queryable, ascending); + var orderedQueryable = ApplyCore(queryable, ascending, firstColumn); if (_then is not null) { @@ -100,6 +107,21 @@ internal IOrderedQueryable Apply(IQueryable queryable, boo return orderedQueryable; } + private IOrderedQueryable ApplyCore(IQueryable queryable, bool ascending, bool firstColumn) + { + if (firstColumn) + { + return _first(queryable, ascending); + } + + if (queryable is not IOrderedQueryable src) + { + throw new InvalidOperationException($"Expected {typeof(IOrderedQueryable)} since this is not the first sort column."); + } + + return _thenFirst(src, ascending); + } + internal IReadOnlyCollection ToPropertyList(bool ascending) { if (ascending) diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/SortColumn.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/SortColumn.cs new file mode 100644 index 000000000000..361ae0efc9c8 --- /dev/null +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/SortColumn.cs @@ -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; + +/// +/// Provides information about the column that has sorting applied. +/// +/// The type of data represented by each row in the grid. +public class SortColumn +{ + /// + /// The column that has sorting applied. + /// + public ColumnBase? Column { get; init; } + + /// + /// Whether or not the sort is ascending. + /// + public bool Ascending { get; internal set; } +} diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/GridItemsProviderRequest.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/GridItemsProviderRequest.cs index 5c634a28ce0b..1163a78d9619 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/GridItemsProviderRequest.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/GridItemsProviderRequest.cs @@ -21,13 +21,29 @@ public readonly struct GridItemsProviderRequest /// public int? Count { get; init; } + /// + /// Specifies which columns are currently being sorted. + /// + /// Rather than inferring the sort rules manually, you should normally call either + /// or , since they also account for + /// and automatically. + /// + public IReadOnlyList> SortColumns { get; init; } + /// /// Specifies which column represents the sort order. /// /// Rather than inferring the sort rules manually, you should normally call either /// or , since they also account for and automatically. /// - public ColumnBase? SortByColumn { get; init; } + [Obsolete("Use " + nameof(SortColumns) + " instead.")] + public ColumnBase? SortByColumn + { + get => _sortByColumn; + init => _sortByColumn = value; + } + + private readonly ColumnBase? _sortByColumn; /// /// Specifies the current sort direction. @@ -35,7 +51,14 @@ public readonly struct GridItemsProviderRequest /// Rather than inferring the sort rules manually, you should normally call either /// or , since they also account for and automatically. /// - public bool SortByAscending { get; init; } + [Obsolete("Use " + nameof(SortColumns) + " instead.")] + public bool SortByAscending + { + get => _sortByAscending; + init => _sortByAscending = value; + } + + private readonly bool _sortByAscending; /// /// A token that indicates if the request should be cancelled. @@ -43,13 +66,19 @@ public readonly struct GridItemsProviderRequest public CancellationToken CancellationToken { get; init; } internal GridItemsProviderRequest( - int startIndex, int? count, ColumnBase? sortByColumn, bool sortByAscending, - CancellationToken cancellationToken) + int startIndex, int? count, IReadOnlyList> 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; } @@ -58,13 +87,44 @@ internal GridItemsProviderRequest( /// /// An . /// A new representing the with sorting rules applied. - public IQueryable ApplySorting(IQueryable source) => - SortByColumn?.SortBy?.Apply(source, SortByAscending) ?? source; + public IQueryable ApplySorting(IQueryable 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; + } /// /// Produces a collection of (property name, direction) pairs representing the sorting rules. /// /// A collection of (property name, direction) pairs representing the sorting rules + [Obsolete("Use " + nameof(GetSortColumnProperties) + " instead.")] public IReadOnlyCollection GetSortByProperties() => - SortByColumn?.SortBy?.ToPropertyList(SortByAscending) ?? Array.Empty(); + SortByColumn?.SortBy?.ToPropertyList(SortByAscending) ?? Array.Empty(); + + /// + /// Produces a collection of collections representing the applied sort rules for the grid. + /// + /// + /// 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.). + /// + /// + /// An of of representing the full sorting expression. + /// + public IEnumerable> GetSortColumnProperties() + { + foreach (var sortColumn in SortColumns) + { + yield return sortColumn.Column?.SortBy?.ToPropertyList(sortColumn.Ascending) ?? []; + } + } } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt index a5806f90a9db..cf1b8e2114ac 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt @@ -1,4 +1,14 @@ #nullable enable +Microsoft.AspNetCore.Components.QuickGrid.GridItemsProviderRequest.GetSortColumnProperties() -> System.Collections.Generic.IEnumerable!>! +Microsoft.AspNetCore.Components.QuickGrid.GridItemsProviderRequest.SortColumns.get -> System.Collections.Generic.IReadOnlyList!>! +Microsoft.AspNetCore.Components.QuickGrid.GridItemsProviderRequest.SortColumns.init -> void +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.AddUpdateSortByColumnAsync(Microsoft.AspNetCore.Components.QuickGrid.ColumnBase! column, Microsoft.AspNetCore.Components.QuickGrid.SortDirection direction = Microsoft.AspNetCore.Components.QuickGrid.SortDirection.Auto) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.HideColumnOptionsAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.RowClass.get -> System.Func? -Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.RowClass.set -> void \ No newline at end of file +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.RowClass.set -> void +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.SortColumns.get -> System.Collections.Generic.IReadOnlyList!>! +Microsoft.AspNetCore.Components.QuickGrid.SortColumn +Microsoft.AspNetCore.Components.QuickGrid.SortColumn.Ascending.get -> bool +Microsoft.AspNetCore.Components.QuickGrid.SortColumn.Column.get -> Microsoft.AspNetCore.Components.QuickGrid.ColumnBase? +Microsoft.AspNetCore.Components.QuickGrid.SortColumn.Column.init -> void +Microsoft.AspNetCore.Components.QuickGrid.SortColumn.SortColumn() -> void diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index 363ad846cbf7..7c9511420075 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -242,6 +242,13 @@ private void FinishCollectingColumns() _collectingColumns = false; } + private readonly List> _sortByColumns = []; + + /// + /// The list of columns that have sorting applied. + /// + public IReadOnlyList> SortColumns => _sortByColumns; + /// /// Sets the grid's current sort column to the specified . /// @@ -250,16 +257,36 @@ private void FinishCollectingColumns() /// A representing the completion of the operation. public Task SortByColumnAsync(ColumnBase column, SortDirection direction = SortDirection.Auto) { - _sortByAscending = direction switch + _sortByColumns.RemoveAll(sbc => sbc.Column != column); + return AddUpdateSortByColumnAsync(column, direction); + } + + /// + /// Adds or updates sorting of the specified . + /// If the column is not already being tracked it will be appended, otherwise it's direction is updated with it's position unchanged. + /// + /// The column that defines the new sort order. + /// The direction of sorting. If the value is , then it will toggle the direction on each call. + /// A representing the completion of the operation. + /// + public Task AddUpdateSortByColumnAsync(ColumnBase 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(); } @@ -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( - startIndex, Pagination?.ItemsPerPage, _sortByColumn, _sortByAscending, thisLoadCts.Token); + startIndex, Pagination?.ItemsPerPage, SortColumns, thisLoadCts.Token); var result = await ResolveItemsRequestAsync(request); if (!thisLoadCts.IsCancellationRequested) { @@ -356,7 +383,7 @@ private async Task RefreshDataCoreAsync() } var providerRequest = new GridItemsProviderRequest( - startIndex, count, _sortByColumn, _sortByAscending, request.CancellationToken); + startIndex, count, SortColumns, request.CancellationToken); var providerResult = await ResolveItemsRequestAsync(providerRequest); if (!request.CancellationToken.IsCancellationRequested)