Skip to content

Commit 2004083

Browse files
QuickGrid:
-Adds Multi column sorting capability -Exposes SortColumns so that individual columns can easily apply their own logic based on whether or not they're currently being sorted by. Use cases like swapping an icon out in markup, changing available options, etc.
1 parent 062e663 commit 2004083

File tree

4 files changed

+95
-32
lines changed

4 files changed

+95
-32
lines changed

src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/GridSort.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public sealed class GridSort<TGridItem>
1515
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.";
1616

1717
private readonly Func<IQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> _first;
18+
private readonly Func<IOrderedQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> _thenFirst;
1819
private List<Func<IOrderedQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>>>? _then;
1920

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

26-
internal GridSort(Func<IQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> first, (LambdaExpression, bool) firstExpression)
27+
internal GridSort(
28+
Func<IQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> first,
29+
(LambdaExpression, bool) firstExpression,
30+
Func<IOrderedQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>> thenFirst)
2731
{
2832
_first = first;
2933
_firstExpression = firstExpression;
34+
_thenFirst = thenFirst;
3035
_then = default;
3136
_thenExpressions = default;
3237
}
@@ -39,7 +44,8 @@ internal GridSort(Func<IQueryable<TGridItem>, bool, IOrderedQueryable<TGridItem>
3944
/// <returns>A <see cref="GridSort{T}"/> instance representing the specified sorting rule.</returns>
4045
public static GridSort<TGridItem> ByAscending<U>(Expression<Func<TGridItem, U>> expression)
4146
=> new((queryable, asc) => asc ? queryable.OrderBy(expression) : queryable.OrderByDescending(expression),
42-
(expression, true));
47+
(expression, true),
48+
(queryable, asc) => asc ? queryable.ThenBy(expression) : queryable.ThenByDescending(expression));
4349

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

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

88-
internal IOrderedQueryable<TGridItem> Apply(IQueryable<TGridItem> queryable, bool ascending)
95+
internal IOrderedQueryable<TGridItem> Apply(IQueryable<TGridItem> queryable, bool ascending, bool firstColumn)
8996
{
90-
var orderedQueryable = _first(queryable, ascending);
97+
var orderedQueryable = ApplyCore(queryable, ascending, firstColumn);
9198

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

110+
private IOrderedQueryable<TGridItem> ApplyCore(IQueryable<TGridItem> queryable, bool ascending, bool firstColumn)
111+
{
112+
if (firstColumn)
113+
{
114+
return _first(queryable, ascending);
115+
}
116+
117+
if (queryable is not IOrderedQueryable<TGridItem> src)
118+
{
119+
throw new InvalidOperationException($"Expected {typeof(IOrderedQueryable<TGridItem>)} since this is not the first sort column.");
120+
}
121+
122+
return _thenFirst(src, ascending);
123+
}
124+
103125
internal IReadOnlyCollection<SortedProperty> ToPropertyList(bool ascending)
104126
{
105127
if (ascending)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Microsoft.AspNetCore.Components.QuickGrid;
2+
3+
/// <summary>
4+
/// Provides information about the column that has sorting applied.
5+
/// </summary>
6+
/// <typeparam name="TGridItem">The type of data represented by each row in the grid.</typeparam>
7+
public class SortColumn<TGridItem>
8+
{
9+
/// <summary>
10+
/// The column that has sorting applied.
11+
/// </summary>
12+
public ColumnBase<TGridItem>? Column { get; init; }
13+
14+
/// <summary>
15+
/// Whether or not the sort is ascending.
16+
/// </summary>
17+
public bool Ascending { get; internal set; }
18+
}

src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/GridItemsProviderRequest.cs

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,25 @@ public readonly struct GridItemsProviderRequest<TGridItem>
2222
public int? Count { get; init; }
2323

2424
/// <summary>
25-
/// Specifies which column represents the sort order.
26-
///
25+
/// Specifies which columns are currently being sorted.
26+
///
2727
/// Rather than inferring the sort rules manually, you should normally call either <see cref="ApplySorting(IQueryable{TGridItem})"/>
28-
/// or <see cref="GetSortByProperties"/>, since they also account for <see cref="SortByColumn" /> and <see cref="SortByAscending" /> automatically.
28+
/// or <see cref="GetSortByProperties"/>, since they also account for <see cref="SortColumn{TGridItem}.Column"/>
29+
/// and <see cref="SortColumn{TGridItem}.Ascending"/> automatically.
2930
/// </summary>
30-
public ColumnBase<TGridItem>? SortByColumn { get; init; }
31-
32-
/// <summary>
33-
/// Specifies the current sort direction.
34-
///
35-
/// Rather than inferring the sort rules manually, you should normally call either <see cref="ApplySorting(IQueryable{TGridItem})"/>
36-
/// or <see cref="GetSortByProperties"/>, since they also account for <see cref="SortByColumn" /> and <see cref="SortByAscending" /> automatically.
37-
/// </summary>
38-
public bool SortByAscending { get; init; }
31+
public IReadOnlyList<SortColumn<TGridItem>> SortColumns { get; init; }
3932

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

4538
internal GridItemsProviderRequest(
46-
int startIndex, int? count, ColumnBase<TGridItem>? sortByColumn, bool sortByAscending,
47-
CancellationToken cancellationToken)
39+
int startIndex, int? count, IReadOnlyList<SortColumn<TGridItem>> sortColumns, CancellationToken cancellationToken)
4840
{
4941
StartIndex = startIndex;
5042
Count = count;
51-
SortByColumn = sortByColumn;
52-
SortByAscending = sortByAscending;
43+
SortColumns = sortColumns;
5344
CancellationToken = cancellationToken;
5445
}
5546

@@ -58,13 +49,26 @@ internal GridItemsProviderRequest(
5849
/// </summary>
5950
/// <param name="source">An <see cref="IQueryable{TGridItem}"/>.</param>
6051
/// <returns>A new <see cref="IQueryable{TGridItem}"/> representing the <paramref name="source"/> with sorting rules applied.</returns>
61-
public IQueryable<TGridItem> ApplySorting(IQueryable<TGridItem> source) =>
62-
SortByColumn?.SortBy?.Apply(source, SortByAscending) ?? source;
52+
public IQueryable<TGridItem> ApplySorting(IQueryable<TGridItem> source)
53+
{
54+
for (var i = 0; i < SortColumns.Count; i++)
55+
{
56+
var sortColumn = SortColumns[i];
57+
source = sortColumn.Column.SortBy.Apply(source, sortColumn.Ascending, i == 0);
58+
}
59+
60+
return source;
61+
}
6362

6463
/// <summary>
6564
/// Produces a collection of (property name, direction) pairs representing the sorting rules.
6665
/// </summary>
6766
/// <returns>A collection of (property name, direction) pairs representing the sorting rules</returns>
68-
public IReadOnlyCollection<SortedProperty> GetSortByProperties() =>
69-
SortByColumn?.SortBy?.ToPropertyList(SortByAscending) ?? Array.Empty<SortedProperty>();
67+
public IEnumerable<IReadOnlyCollection<SortedProperty>> GetSortByProperties()
68+
{
69+
foreach (var sortColumn in SortColumns)
70+
{
71+
yield return sortColumn.Column.SortBy?.ToPropertyList(sortColumn.Ascending) ?? [];
72+
}
73+
}
7074
}

src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,13 @@ private void FinishCollectingColumns()
242242
_collectingColumns = false;
243243
}
244244

245+
private List<SortColumn<TGridItem>> _sortByColumns = [];
246+
247+
/// <summary>
248+
/// The list of columns that have sorting applied.
249+
/// </summary>
250+
public IReadOnlyList<SortColumn<TGridItem>> SortColumns => _sortByColumns;
251+
245252
/// <summary>
246253
/// Sets the grid's current sort column to the specified <paramref name="column"/>.
247254
/// </summary>
@@ -250,16 +257,28 @@ private void FinishCollectingColumns()
250257
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
251258
public Task SortByColumnAsync(ColumnBase<TGridItem> column, SortDirection direction = SortDirection.Auto)
252259
{
253-
_sortByAscending = direction switch
260+
_sortByColumns.RemoveAll(sbc => sbc.Column != column);
261+
return AddUpdateSortByColumnAsync(column, direction);
262+
}
263+
264+
public Task AddUpdateSortByColumnAsync(ColumnBase<TGridItem> column, SortDirection direction = SortDirection.Auto)
265+
{
266+
var sortBy = _sortByColumns.FirstOrDefault(sbc => sbc.Column == column);
267+
268+
if (sortBy == null)
269+
{
270+
sortBy = new() { Column = column };
271+
_sortByColumns.Add(sortBy);
272+
}
273+
274+
sortBy.Ascending = direction switch
254275
{
255276
SortDirection.Ascending => true,
256277
SortDirection.Descending => false,
257-
SortDirection.Auto => _sortByColumn != column || !_sortByAscending,
258-
_ => throw new NotSupportedException($"Unknown sort direction {direction}"),
278+
SortDirection.Auto => !sortBy.Ascending,
279+
_ => throw new NotSupportedException($"Unknown sort direction {direction}")
259280
};
260281

261-
_sortByColumn = column;
262-
263282
StateHasChanged(); // We want to see the updated sort order in the header, even before the data query is completed
264283
return RefreshDataAsync();
265284
}
@@ -320,7 +339,7 @@ private async Task RefreshDataCoreAsync()
320339
_lastRefreshedPaginationStateHash = Pagination?.GetHashCode();
321340
var startIndex = Pagination is null ? 0 : (Pagination.CurrentPageIndex * Pagination.ItemsPerPage);
322341
var request = new GridItemsProviderRequest<TGridItem>(
323-
startIndex, Pagination?.ItemsPerPage, _sortByColumn, _sortByAscending, thisLoadCts.Token);
342+
startIndex, Pagination?.ItemsPerPage, SortColumns, thisLoadCts.Token);
324343
var result = await ResolveItemsRequestAsync(request);
325344
if (!thisLoadCts.IsCancellationRequested)
326345
{
@@ -356,7 +375,7 @@ private async Task RefreshDataCoreAsync()
356375
}
357376

358377
var providerRequest = new GridItemsProviderRequest<TGridItem>(
359-
startIndex, count, _sortByColumn, _sortByAscending, request.CancellationToken);
378+
startIndex, count, SortColumns, request.CancellationToken);
360379
var providerResult = await ResolveItemsRequestAsync(providerRequest);
361380

362381
if (!request.CancellationToken.IsCancellationRequested)

0 commit comments

Comments
 (0)