diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml
index f3957dd44..9ffb41a6e 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 e56fcdd15..04ab45fc9 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 69322a97a..5e6f17d7f 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/Core/Components/List/FluentSelect.razor b/src/Core/Components/List/FluentSelect.razor
index 3ec4f2187..de964582b 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);
+ }
}
diff --git a/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs b/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs
index 9df687001..cadf2921e 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 43172efa7..d76546e1a 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();
}