From 260842de0e14659dc1584e032278b52c9e259dff Mon Sep 17 00:00:00 2001 From: Miguel Hasse de Oliveira Date: Mon, 23 Sep 2024 16:12:34 +0100 Subject: [PATCH 1/2] [DataGrid] EF Core Adapter - A second operation was started (#2653) * Fix: A second operation was started in DataGrid with EF Core (#801) * Removed additions not relevant for the PR subject as requested by Vincent Baaij --------- Co-authored-by: Vincent Baaij --- ...crosoft.FluentUI.AspNetCore.Components.xml | 10 +++- .../DataGrid/FluentDataGrid.razor.cs | 57 ++++++++++++------- .../Infrastructure/IAsyncQueryExecutor.cs | 6 +- ...eworkAdapterServiceCollectionExtensions.cs | 2 +- .../EntityFrameworkAsyncQueryExecutor.cs | 28 +++++++-- 5 files changed, 72 insertions(+), 31 deletions(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index f3957dd44e..9ffb41a6ef 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -2012,6 +2012,7 @@ The title of the column to sort by. The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. + A representing the completion of the operation. @@ -2019,6 +2020,7 @@ The index of the column to sort by. The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. + A representing the completion of the operation. @@ -2033,6 +2035,7 @@ options UI that was previously displayed. The column whose options are to be displayed, if any are available. + A representing the completion of the operation. @@ -2040,6 +2043,7 @@ resize UI that was previously displayed. The column whose resize UI is to be displayed. + A representing the completion of the operation. @@ -2287,19 +2291,21 @@ An instance. True if this instance can perform asynchronous queries for the supplied , otherwise false. - + Asynchronously counts the items in the , if supported. The data type. An instance. + An instance. The number of items in .. - + Asynchronously materializes the as an array, if supported. The data type. + An instance. An instance. The items in the .. diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index e56fcdd158..04ab45fc9f 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web.Virtualization; +using Microsoft.Extensions.DependencyInjection; using Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure; using Microsoft.FluentUI.AspNetCore.Components.Extensions; using Microsoft.FluentUI.AspNetCore.Components.Infrastructure; @@ -27,7 +28,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve private LibraryConfiguration LibraryConfiguration { get; set; } = default!; [Inject] - private IServiceProvider Services { get; set; } = default!; + private IServiceScopeFactory ScopeFactory { get; set; } = default!; [Inject] private IJSRuntime JSRuntime { get; set; } = default!; @@ -255,6 +256,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve // IQueryable only exposes synchronous query APIs. IAsyncQueryExecutor is an adapter that lets us invoke any // async query APIs that might be available. We have built-in support for using EF Core's async query APIs. private IAsyncQueryExecutor? _asyncQueryExecutor; + private AsyncServiceScope? _scope; // We cascade the InternalGridContext to descendants, which in turn call it to add themselves to _columns // This happens on every render so that the column list can be updated dynamically @@ -351,9 +353,11 @@ protected override Task OnParametersSetAsync() var dataSourceHasChanged = !Equals(Items, _lastAssignedItems) || !Equals(ItemsProvider, _lastAssignedItemsProvider); if (dataSourceHasChanged) { + _scope?.Dispose(); + _scope = ScopeFactory.CreateAsyncScope(); _lastAssignedItemsProvider = ItemsProvider; _lastAssignedItems = Items; - _asyncQueryExecutor = AsyncQueryExecutorSupplier.GetAsyncQueryExecutor(Services, Items); + _asyncQueryExecutor = AsyncQueryExecutorSupplier.GetAsyncQueryExecutor(_scope.Value.ServiceProvider, Items); } var paginationStateHasChanged = @@ -471,6 +475,7 @@ public Task SortByColumnAsync(ColumnBase column, SortDirection direct /// /// The title of the column to sort by. /// The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. + /// A representing the completion of the operation. public Task SortByColumnAsync(string title, SortDirection direction = SortDirection.Auto) { var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false); @@ -483,6 +488,7 @@ public Task SortByColumnAsync(string title, SortDirection direction = SortDirect /// /// The index of the column to sort by. /// The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. + /// A representing the completion of the operation. public Task SortByColumnAsync(int index, SortDirection direction = SortDirection.Auto) { return index >= 0 && index < _columns.Count ? SortByColumnAsync(_columns[index], direction) : Task.CompletedTask; @@ -510,6 +516,7 @@ public Task RemoveSortByColumnAsync(ColumnBase column) /// options UI that was previously displayed. /// /// The column whose options are to be displayed, if any are available. + /// A representing the completion of the operation. public Task ShowColumnOptionsAsync(ColumnBase column) { _displayOptionsForColumn = column; @@ -523,6 +530,7 @@ public Task ShowColumnOptionsAsync(ColumnBase column) /// resize UI that was previously displayed. /// /// The column whose resize UI is to be displayed. + /// A representing the completion of the operation. public Task ShowColumnResizeAsync(ColumnBase column) { _displayResizeForColumn = column; @@ -640,31 +648,37 @@ private async Task RefreshDataCoreAsync() // Normalizes all the different ways of configuring a data source so they have common GridItemsProvider-shaped API private async ValueTask> ResolveItemsRequestAsync(GridItemsProviderRequest request) { - if (ItemsProvider is not null) + try { - var gipr = await ItemsProvider(request); - if (gipr.Items is not null) + if (ItemsProvider is not null) { - Loading = false; + var gipr = await ItemsProvider(request); + if (gipr.Items is not null) + { + Loading = false; + } + return gipr; } - return gipr; - } - else if (Items is not null) - { - var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items); - _internalGridContext.TotalItemCount = totalItemCount; - var result = request.ApplySorting(Items).Skip(request.StartIndex); - if (request.Count.HasValue) + else if (Items is not null) { - result = result.Take(request.Count.Value); + var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items, request.CancellationToken); + _internalGridContext.TotalItemCount = totalItemCount; + var result = request.ApplySorting(Items).Skip(request.StartIndex); + if (request.Count.HasValue) + { + result = result.Take(request.Count.Value); + } + var resultArray = _asyncQueryExecutor is null ? [.. result] : await _asyncQueryExecutor.ToArrayAsync(result, request.CancellationToken); + return GridItemsProviderResult.From(resultArray, totalItemCount); } - var resultArray = _asyncQueryExecutor is null ? [.. result] : await _asyncQueryExecutor.ToArrayAsync(result); - return GridItemsProviderResult.From(resultArray, totalItemCount); } - else + catch (OperationCanceledException oce) when (oce.CancellationToken == request.CancellationToken) { - return GridItemsProviderResult.From(Array.Empty(), 0); + // No-op; we canceled the operation, so it's fine to suppress this exception. } + + Loading = false; + return GridItemsProviderResult.From(Array.Empty(), 0); } private string AriaSortValue(ColumnBase column) @@ -674,8 +688,8 @@ private string AriaSortValue(ColumnBase column) private string? ColumnHeaderClass(ColumnBase column) => _sortByColumn == column - ? $"{ColumnClass(column)} {(_sortByAscending ? "col-sort-asc" : "col-sort-desc")}" - : ColumnClass(column); + ? $"{ColumnClass(column)} {(_sortByAscending ? "col-sort-asc" : "col-sort-desc")}" + : ColumnClass(column); private string? GridClass() { @@ -701,6 +715,7 @@ private string AriaSortValue(ColumnBase column) public async ValueTask DisposeAsync() { _currentPageItemsChanged.Dispose(); + _scope?.Dispose(); try { diff --git a/src/Core/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs b/src/Core/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs index 69322a97af..5e6f17d7fb 100644 --- a/src/Core/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs +++ b/src/Core/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs @@ -18,14 +18,16 @@ public interface IAsyncQueryExecutor /// /// The data type. /// An instance. + /// An instance. /// The number of items in .. - Task CountAsync(IQueryable queryable); + Task CountAsync(IQueryable queryable, CancellationToken cancellationToken = default); /// /// Asynchronously materializes the as an array, if supported. /// /// The data type. + /// An instance. /// An instance. /// The items in the .. - Task ToArrayAsync(IQueryable queryable); + Task ToArrayAsync(IQueryable queryable, CancellationToken cancellationToken = default); } diff --git a/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs b/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs index 9df6870018..cadf2921ef 100644 --- a/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs +++ b/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs @@ -14,6 +14,6 @@ public static class EntityFrameworkAdapterServiceCollectionExtensions /// The . public static void AddDataGridEntityFrameworkAdapter(this IServiceCollection services) { - services.AddSingleton(); + services.AddScoped(); } } diff --git a/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAsyncQueryExecutor.cs b/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAsyncQueryExecutor.cs index 43172efa7d..d76546e1a1 100644 --- a/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAsyncQueryExecutor.cs +++ b/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAsyncQueryExecutor.cs @@ -4,14 +4,32 @@ namespace Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter; -internal class EntityFrameworkAsyncQueryExecutor : IAsyncQueryExecutor +internal class EntityFrameworkAsyncQueryExecutor : IAsyncQueryExecutor, IDisposable { + private readonly SemaphoreSlim _lock = new(1); + public bool IsSupported(IQueryable queryable) => queryable.Provider is IAsyncQueryProvider; - public Task CountAsync(IQueryable queryable) - => queryable.CountAsync(); + public Task CountAsync(IQueryable queryable, CancellationToken cancellationToken) + => ExecuteAsync(() => queryable.CountAsync(cancellationToken)); + + public Task ToArrayAsync(IQueryable queryable, CancellationToken cancellationToken) + => ExecuteAsync(() => queryable.ToArrayAsync(cancellationToken)); + + private async Task ExecuteAsync(Func> operation) + { + await _lock.WaitAsync(); + + try + { + return await operation(); + } + finally + { + _lock.Release(); + } + } - public Task ToArrayAsync(IQueryable queryable) - => queryable.ToArrayAsync(); + void IDisposable.Dispose() => _lock.Dispose(); } From 3a187132dc8a487c425f8cce590645428e2c1d48 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 23 Sep 2024 18:04:59 +0200 Subject: [PATCH 2/2] Fix of the unannounced `Required` keyword with FluentSelect (#2706) --- src/Core/Components/List/FluentSelect.razor | 2 +- src/Core/Components/List/FluentSelect.razor.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Core/Components/List/FluentSelect.razor b/src/Core/Components/List/FluentSelect.razor index 3ec4f2187e..de964582b6 100644 --- a/src/Core/Components/List/FluentSelect.razor +++ b/src/Core/Components/List/FluentSelect.razor @@ -3,7 +3,7 @@ @typeparam TOption @InlineStyleValue - + : ListComponentBase where TOption : notnull { + /// + /// Gets the `Required` aria label. + /// + public static string RequiredAriaLabel = "Required"; + /// protected virtual MarkupString InlineStyleValue => new InlineStyleBuilder() .AddStyle($"#{Id}::part(listbox)", "max-height", Height, !string.IsNullOrWhiteSpace(Height)) @@ -39,4 +44,11 @@ public partial class FluentSelect : ListComponentBase where TO /// [Parameter] public Appearance? Appearance { get; set; } + + private string? GetAriaLabelWithRequired() + { + var label = AriaLabel ?? Label ?? Title ?? string.Empty; + + return label + (Required ? $", {RequiredAriaLabel}" : string.Empty); + } }