Skip to content

Commit 3a3666b

Browse files
committed
2 parents a8d1140 + 3a18713 commit 3a3666b

File tree

7 files changed

+85
-32
lines changed

7 files changed

+85
-32
lines changed

examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,13 +2012,15 @@
20122012
</summary>
20132013
<param name="title">The title of the column to sort by.</param>
20142014
<param name="direction">The direction of sorting. The default is <see cref="F:Microsoft.FluentUI.AspNetCore.Components.SortDirection.Auto"/>. If the value is <see cref="F:Microsoft.FluentUI.AspNetCore.Components.SortDirection.Auto"/>, then it will toggle the direction on each call.</param>
2015+
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
20152016
</member>
20162017
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.SortByColumnAsync(System.Int32,Microsoft.FluentUI.AspNetCore.Components.SortDirection)">
20172018
<summary>
20182019
Sorts the grid by the specified column <paramref name="index"/>. If the index is out of range, nothing happens.
20192020
</summary>
20202021
<param name="index">The index of the column to sort by.</param>
20212022
<param name="direction">The direction of sorting. The default is <see cref="F:Microsoft.FluentUI.AspNetCore.Components.SortDirection.Auto"/>. If the value is <see cref="F:Microsoft.FluentUI.AspNetCore.Components.SortDirection.Auto"/>, then it will toggle the direction on each call.</param>
2023+
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
20222024
</member>
20232025
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RemoveSortByColumnAsync(Microsoft.FluentUI.AspNetCore.Components.ColumnBase{`0})">
20242026
<summary>
@@ -2033,13 +2035,15 @@
20332035
options UI that was previously displayed.
20342036
</summary>
20352037
<param name="column">The column whose options are to be displayed, if any are available.</param>
2038+
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
20362039
</member>
20372040
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ShowColumnResizeAsync(Microsoft.FluentUI.AspNetCore.Components.ColumnBase{`0})">
20382041
<summary>
20392042
Displays the column resize UI for the specified column, closing any other column
20402043
resize UI that was previously displayed.
20412044
</summary>
20422045
<param name="column">The column whose resize UI is to be displayed.</param>
2046+
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
20432047
</member>
20442048
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RefreshDataAsync">
20452049
<summary>
@@ -2287,19 +2291,21 @@
22872291
<param name="queryable">An <see cref="T:System.Linq.IQueryable`1" /> instance.</param>
22882292
<returns>True if this <see cref="T:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor"/> instance can perform asynchronous queries for the supplied <paramref name="queryable"/>, otherwise false.</returns>
22892293
</member>
2290-
<member name="M:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor.CountAsync``1(System.Linq.IQueryable{``0})">
2294+
<member name="M:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor.CountAsync``1(System.Linq.IQueryable{``0},System.Threading.CancellationToken)">
22912295
<summary>
22922296
Asynchronously counts the items in the <see cref="T:System.Linq.IQueryable`1" />, if supported.
22932297
</summary>
22942298
<typeparam name="T">The data type.</typeparam>
22952299
<param name="queryable">An <see cref="T:System.Linq.IQueryable`1" /> instance.</param>
2300+
<param name="cancellationToken">An <see cref="T:System.Threading.CancellationToken" /> instance.</param>
22962301
<returns>The number of items in <paramref name="queryable"/>.</returns>.
22972302
</member>
2298-
<member name="M:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor.ToArrayAsync``1(System.Linq.IQueryable{``0})">
2303+
<member name="M:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor.ToArrayAsync``1(System.Linq.IQueryable{``0},System.Threading.CancellationToken)">
22992304
<summary>
23002305
Asynchronously materializes the <see cref="T:System.Linq.IQueryable`1" /> as an array, if supported.
23012306
</summary>
23022307
<typeparam name="T">The data type.</typeparam>
2308+
<param name="cancellationToken">An <see cref="T:System.Threading.CancellationToken" /> instance.</param>
23032309
<param name="queryable">An <see cref="T:System.Linq.IQueryable`1" /> instance.</param>
23042310
<returns>The items in the <paramref name="queryable"/>.</returns>.
23052311
</member>

src/Core/Components/DataGrid/FluentDataGrid.razor.cs

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using Microsoft.AspNetCore.Components;
66
using Microsoft.AspNetCore.Components.Web.Virtualization;
7+
using Microsoft.Extensions.DependencyInjection;
78
using Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure;
89
using Microsoft.FluentUI.AspNetCore.Components.Extensions;
910
using Microsoft.FluentUI.AspNetCore.Components.Infrastructure;
@@ -27,7 +28,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
2728
private LibraryConfiguration LibraryConfiguration { get; set; } = default!;
2829

2930
[Inject]
30-
private IServiceProvider Services { get; set; } = default!;
31+
private IServiceScopeFactory ScopeFactory { get; set; } = default!;
3132

3233
[Inject]
3334
private IJSRuntime JSRuntime { get; set; } = default!;
@@ -255,6 +256,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
255256
// IQueryable only exposes synchronous query APIs. IAsyncQueryExecutor is an adapter that lets us invoke any
256257
// async query APIs that might be available. We have built-in support for using EF Core's async query APIs.
257258
private IAsyncQueryExecutor? _asyncQueryExecutor;
259+
private AsyncServiceScope? _scope;
258260

259261
// We cascade the InternalGridContext to descendants, which in turn call it to add themselves to _columns
260262
// This happens on every render so that the column list can be updated dynamically
@@ -351,9 +353,11 @@ protected override Task OnParametersSetAsync()
351353
var dataSourceHasChanged = !Equals(Items, _lastAssignedItems) || !Equals(ItemsProvider, _lastAssignedItemsProvider);
352354
if (dataSourceHasChanged)
353355
{
356+
_scope?.Dispose();
357+
_scope = ScopeFactory.CreateAsyncScope();
354358
_lastAssignedItemsProvider = ItemsProvider;
355359
_lastAssignedItems = Items;
356-
_asyncQueryExecutor = AsyncQueryExecutorSupplier.GetAsyncQueryExecutor(Services, Items);
360+
_asyncQueryExecutor = AsyncQueryExecutorSupplier.GetAsyncQueryExecutor(_scope.Value.ServiceProvider, Items);
357361
}
358362

359363
var paginationStateHasChanged =
@@ -471,6 +475,7 @@ public Task SortByColumnAsync(ColumnBase<TGridItem> column, SortDirection direct
471475
/// </summary>
472476
/// <param name="title">The title of the column to sort by.</param>
473477
/// <param name="direction">The direction of sorting. The default is <see cref="SortDirection.Auto"/>. If the value is <see cref="SortDirection.Auto"/>, then it will toggle the direction on each call.</param>
478+
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
474479
public Task SortByColumnAsync(string title, SortDirection direction = SortDirection.Auto)
475480
{
476481
var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false);
@@ -483,6 +488,7 @@ public Task SortByColumnAsync(string title, SortDirection direction = SortDirect
483488
/// </summary>
484489
/// <param name="index">The index of the column to sort by.</param>
485490
/// <param name="direction">The direction of sorting. The default is <see cref="SortDirection.Auto"/>. If the value is <see cref="SortDirection.Auto"/>, then it will toggle the direction on each call.</param>
491+
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
486492
public Task SortByColumnAsync(int index, SortDirection direction = SortDirection.Auto)
487493
{
488494
return index >= 0 && index < _columns.Count ? SortByColumnAsync(_columns[index], direction) : Task.CompletedTask;
@@ -510,6 +516,7 @@ public Task RemoveSortByColumnAsync(ColumnBase<TGridItem> column)
510516
/// options UI that was previously displayed.
511517
/// </summary>
512518
/// <param name="column">The column whose options are to be displayed, if any are available.</param>
519+
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
513520
public Task ShowColumnOptionsAsync(ColumnBase<TGridItem> column)
514521
{
515522
_displayOptionsForColumn = column;
@@ -523,6 +530,7 @@ public Task ShowColumnOptionsAsync(ColumnBase<TGridItem> column)
523530
/// resize UI that was previously displayed.
524531
/// </summary>
525532
/// <param name="column">The column whose resize UI is to be displayed.</param>
533+
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
526534
public Task ShowColumnResizeAsync(ColumnBase<TGridItem> column)
527535
{
528536
_displayResizeForColumn = column;
@@ -640,31 +648,37 @@ private async Task RefreshDataCoreAsync()
640648
// Normalizes all the different ways of configuring a data source so they have common GridItemsProvider-shaped API
641649
private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestAsync(GridItemsProviderRequest<TGridItem> request)
642650
{
643-
if (ItemsProvider is not null)
651+
try
644652
{
645-
var gipr = await ItemsProvider(request);
646-
if (gipr.Items is not null)
653+
if (ItemsProvider is not null)
647654
{
648-
Loading = false;
655+
var gipr = await ItemsProvider(request);
656+
if (gipr.Items is not null)
657+
{
658+
Loading = false;
659+
}
660+
return gipr;
649661
}
650-
return gipr;
651-
}
652-
else if (Items is not null)
653-
{
654-
var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items);
655-
_internalGridContext.TotalItemCount = totalItemCount;
656-
var result = request.ApplySorting(Items).Skip(request.StartIndex);
657-
if (request.Count.HasValue)
662+
else if (Items is not null)
658663
{
659-
result = result.Take(request.Count.Value);
664+
var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items, request.CancellationToken);
665+
_internalGridContext.TotalItemCount = totalItemCount;
666+
var result = request.ApplySorting(Items).Skip(request.StartIndex);
667+
if (request.Count.HasValue)
668+
{
669+
result = result.Take(request.Count.Value);
670+
}
671+
var resultArray = _asyncQueryExecutor is null ? [.. result] : await _asyncQueryExecutor.ToArrayAsync(result, request.CancellationToken);
672+
return GridItemsProviderResult.From(resultArray, totalItemCount);
660673
}
661-
var resultArray = _asyncQueryExecutor is null ? [.. result] : await _asyncQueryExecutor.ToArrayAsync(result);
662-
return GridItemsProviderResult.From(resultArray, totalItemCount);
663674
}
664-
else
675+
catch (OperationCanceledException oce) when (oce.CancellationToken == request.CancellationToken)
665676
{
666-
return GridItemsProviderResult.From(Array.Empty<TGridItem>(), 0);
677+
// No-op; we canceled the operation, so it's fine to suppress this exception.
667678
}
679+
680+
Loading = false;
681+
return GridItemsProviderResult.From(Array.Empty<TGridItem>(), 0);
668682
}
669683

670684
private string AriaSortValue(ColumnBase<TGridItem> column)
@@ -674,8 +688,8 @@ private string AriaSortValue(ColumnBase<TGridItem> column)
674688

675689
private string? ColumnHeaderClass(ColumnBase<TGridItem> column)
676690
=> _sortByColumn == column
677-
? $"{ColumnClass(column)} {(_sortByAscending ? "col-sort-asc" : "col-sort-desc")}"
678-
: ColumnClass(column);
691+
? $"{ColumnClass(column)} {(_sortByAscending ? "col-sort-asc" : "col-sort-desc")}"
692+
: ColumnClass(column);
679693

680694
private string? GridClass()
681695
{
@@ -701,6 +715,7 @@ private string AriaSortValue(ColumnBase<TGridItem> column)
701715
public async ValueTask DisposeAsync()
702716
{
703717
_currentPageItemsChanged.Dispose();
718+
_scope?.Dispose();
704719

705720
try
706721
{

src/Core/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ public interface IAsyncQueryExecutor
1818
/// </summary>
1919
/// <typeparam name="T">The data type.</typeparam>
2020
/// <param name="queryable">An <see cref="IQueryable{T}" /> instance.</param>
21+
/// <param name="cancellationToken">An <see cref="CancellationToken" /> instance.</param>
2122
/// <returns>The number of items in <paramref name="queryable"/>.</returns>.
22-
Task<int> CountAsync<T>(IQueryable<T> queryable);
23+
Task<int> CountAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default);
2324

2425
/// <summary>
2526
/// Asynchronously materializes the <see cref="IQueryable{T}" /> as an array, if supported.
2627
/// </summary>
2728
/// <typeparam name="T">The data type.</typeparam>
29+
/// <param name="cancellationToken">An <see cref="CancellationToken" /> instance.</param>
2830
/// <param name="queryable">An <see cref="IQueryable{T}" /> instance.</param>
2931
/// <returns>The items in the <paramref name="queryable"/>.</returns>.
30-
Task<T[]> ToArrayAsync<T>(IQueryable<T> queryable);
32+
Task<T[]> ToArrayAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default);
3133
}

src/Core/Components/List/FluentSelect.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
@typeparam TOption
44
<CascadingValue Value="_internalListContext" Name="ListContext" TValue="InternalListContext<TOption>" IsFixed="true">
55
@InlineStyleValue
6-
<FluentInputLabel ForId="@Id" Label="@Label" AriaLabel="@AriaLabel" Required="@Required" ChildContent="@LabelTemplate" />
6+
<FluentInputLabel ForId="@Id" Label="@Label" AriaLabel="@GetAriaLabelWithRequired()" Required="@Required" ChildContent="@LabelTemplate" />
77
<fluent-select @ref=Element
88
id=@Id
99
class="@ClassValue"

src/Core/Components/List/FluentSelect.razor.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
66
[CascadingTypeParameter(nameof(TOption))]
77
public partial class FluentSelect<TOption> : ListComponentBase<TOption> where TOption : notnull
88
{
9+
/// <summary>
10+
/// Gets the `Required` aria label.
11+
/// </summary>
12+
public static string RequiredAriaLabel = "Required";
13+
914
/// <summary />
1015
protected virtual MarkupString InlineStyleValue => new InlineStyleBuilder()
1116
.AddStyle($"#{Id}::part(listbox)", "max-height", Height, !string.IsNullOrWhiteSpace(Height))
@@ -39,4 +44,11 @@ public partial class FluentSelect<TOption> : ListComponentBase<TOption> where TO
3944
/// </summary>
4045
[Parameter]
4146
public Appearance? Appearance { get; set; }
47+
48+
private string? GetAriaLabelWithRequired()
49+
{
50+
var label = AriaLabel ?? Label ?? Title ?? string.Empty;
51+
52+
return label + (Required ? $", {RequiredAriaLabel}" : string.Empty);
53+
}
4254
}

src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ public static class EntityFrameworkAdapterServiceCollectionExtensions
1414
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
1515
public static void AddDataGridEntityFrameworkAdapter(this IServiceCollection services)
1616
{
17-
services.AddSingleton<IAsyncQueryExecutor, EntityFrameworkAsyncQueryExecutor>();
17+
services.AddScoped<IAsyncQueryExecutor, EntityFrameworkAsyncQueryExecutor>();
1818
}
1919
}

src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAsyncQueryExecutor.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,32 @@
44

55
namespace Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter;
66

7-
internal class EntityFrameworkAsyncQueryExecutor : IAsyncQueryExecutor
7+
internal class EntityFrameworkAsyncQueryExecutor : IAsyncQueryExecutor, IDisposable
88
{
9+
private readonly SemaphoreSlim _lock = new(1);
10+
911
public bool IsSupported<T>(IQueryable<T> queryable)
1012
=> queryable.Provider is IAsyncQueryProvider;
1113

12-
public Task<int> CountAsync<T>(IQueryable<T> queryable)
13-
=> queryable.CountAsync();
14+
public Task<int> CountAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken)
15+
=> ExecuteAsync(() => queryable.CountAsync(cancellationToken));
16+
17+
public Task<T[]> ToArrayAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken)
18+
=> ExecuteAsync(() => queryable.ToArrayAsync(cancellationToken));
19+
20+
private async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> operation)
21+
{
22+
await _lock.WaitAsync();
23+
24+
try
25+
{
26+
return await operation();
27+
}
28+
finally
29+
{
30+
_lock.Release();
31+
}
32+
}
1433

15-
public Task<T[]> ToArrayAsync<T>(IQueryable<T> queryable)
16-
=> queryable.ToArrayAsync();
34+
void IDisposable.Dispose() => _lock.Dispose();
1735
}

0 commit comments

Comments
 (0)