Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2012,13 +2012,15 @@
</summary>
<param name="title">The title of the column to sort by.</param>
<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>
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.SortByColumnAsync(System.Int32,Microsoft.FluentUI.AspNetCore.Components.SortDirection)">
<summary>
Sorts the grid by the specified column <paramref name="index"/>. If the index is out of range, nothing happens.
</summary>
<param name="index">The index of the column to sort by.</param>
<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>
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RemoveSortByColumnAsync(Microsoft.FluentUI.AspNetCore.Components.ColumnBase{`0})">
<summary>
Expand All @@ -2033,13 +2035,15 @@
options UI that was previously displayed.
</summary>
<param name="column">The column whose options are to be displayed, if any are available.</param>
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ShowColumnResizeAsync(Microsoft.FluentUI.AspNetCore.Components.ColumnBase{`0})">
<summary>
Displays the column resize UI for the specified column, closing any other column
resize UI that was previously displayed.
</summary>
<param name="column">The column whose resize UI is to be displayed.</param>
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RefreshDataAsync">
<summary>
Expand Down Expand Up @@ -2287,19 +2291,21 @@
<param name="queryable">An <see cref="T:System.Linq.IQueryable`1" /> instance.</param>
<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>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor.CountAsync``1(System.Linq.IQueryable{``0})">
<member name="M:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor.CountAsync``1(System.Linq.IQueryable{``0},System.Threading.CancellationToken)">
<summary>
Asynchronously counts the items in the <see cref="T:System.Linq.IQueryable`1" />, if supported.
</summary>
<typeparam name="T">The data type.</typeparam>
<param name="queryable">An <see cref="T:System.Linq.IQueryable`1" /> instance.</param>
<param name="cancellationToken">An <see cref="T:System.Threading.CancellationToken" /> instance.</param>
<returns>The number of items in <paramref name="queryable"/>.</returns>.
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor.ToArrayAsync``1(System.Linq.IQueryable{``0})">
<member name="M:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor.ToArrayAsync``1(System.Linq.IQueryable{``0},System.Threading.CancellationToken)">
<summary>
Asynchronously materializes the <see cref="T:System.Linq.IQueryable`1" /> as an array, if supported.
</summary>
<typeparam name="T">The data type.</typeparam>
<param name="cancellationToken">An <see cref="T:System.Threading.CancellationToken" /> instance.</param>
<param name="queryable">An <see cref="T:System.Linq.IQueryable`1" /> instance.</param>
<returns>The items in the <paramref name="queryable"/>.</returns>.
</member>
Expand Down
57 changes: 36 additions & 21 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,7 +28,7 @@ public partial class FluentDataGrid<TGridItem> : 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!;
Expand Down Expand Up @@ -255,6 +256,7 @@ public partial class FluentDataGrid<TGridItem> : 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
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -471,6 +475,7 @@ public Task SortByColumnAsync(ColumnBase<TGridItem> column, SortDirection direct
/// </summary>
/// <param name="title">The title of the column to sort by.</param>
/// <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>
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
public Task SortByColumnAsync(string title, SortDirection direction = SortDirection.Auto)
{
var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false);
Expand All @@ -483,6 +488,7 @@ public Task SortByColumnAsync(string title, SortDirection direction = SortDirect
/// </summary>
/// <param name="index">The index of the column to sort by.</param>
/// <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>
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
public Task SortByColumnAsync(int index, SortDirection direction = SortDirection.Auto)
{
return index >= 0 && index < _columns.Count ? SortByColumnAsync(_columns[index], direction) : Task.CompletedTask;
Expand Down Expand Up @@ -510,6 +516,7 @@ public Task RemoveSortByColumnAsync(ColumnBase<TGridItem> column)
/// options UI that was previously displayed.
/// </summary>
/// <param name="column">The column whose options are to be displayed, if any are available.</param>
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
public Task ShowColumnOptionsAsync(ColumnBase<TGridItem> column)
{
_displayOptionsForColumn = column;
Expand All @@ -523,6 +530,7 @@ public Task ShowColumnOptionsAsync(ColumnBase<TGridItem> column)
/// resize UI that was previously displayed.
/// </summary>
/// <param name="column">The column whose resize UI is to be displayed.</param>
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
public Task ShowColumnResizeAsync(ColumnBase<TGridItem> column)
{
_displayResizeForColumn = column;
Expand Down Expand Up @@ -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<GridItemsProviderResult<TGridItem>> ResolveItemsRequestAsync(GridItemsProviderRequest<TGridItem> 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<TGridItem>(), 0);
// No-op; we canceled the operation, so it's fine to suppress this exception.
}

Loading = false;
return GridItemsProviderResult.From(Array.Empty<TGridItem>(), 0);
}

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

private string? ColumnHeaderClass(ColumnBase<TGridItem> 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()
{
Expand All @@ -701,6 +715,7 @@ private string AriaSortValue(ColumnBase<TGridItem> column)
public async ValueTask DisposeAsync()
{
_currentPageItemsChanged.Dispose();
_scope?.Dispose();

try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ public interface IAsyncQueryExecutor
/// </summary>
/// <typeparam name="T">The data type.</typeparam>
/// <param name="queryable">An <see cref="IQueryable{T}" /> instance.</param>
/// <param name="cancellationToken">An <see cref="CancellationToken" /> instance.</param>
/// <returns>The number of items in <paramref name="queryable"/>.</returns>.
Task<int> CountAsync<T>(IQueryable<T> queryable);
Task<int> CountAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously materializes the <see cref="IQueryable{T}" /> as an array, if supported.
/// </summary>
/// <typeparam name="T">The data type.</typeparam>
/// <param name="cancellationToken">An <see cref="CancellationToken" /> instance.</param>
/// <param name="queryable">An <see cref="IQueryable{T}" /> instance.</param>
/// <returns>The items in the <paramref name="queryable"/>.</returns>.
Task<T[]> ToArrayAsync<T>(IQueryable<T> queryable);
Task<T[]> ToArrayAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ public static class EntityFrameworkAdapterServiceCollectionExtensions
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
public static void AddDataGridEntityFrameworkAdapter(this IServiceCollection services)
{
services.AddSingleton<IAsyncQueryExecutor, EntityFrameworkAsyncQueryExecutor>();
services.AddScoped<IAsyncQueryExecutor, EntityFrameworkAsyncQueryExecutor>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(IQueryable<T> queryable)
=> queryable.Provider is IAsyncQueryProvider;

public Task<int> CountAsync<T>(IQueryable<T> queryable)
=> queryable.CountAsync();
public Task<int> CountAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken)
=> ExecuteAsync(() => queryable.CountAsync(cancellationToken));

public Task<T[]> ToArrayAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken)
=> ExecuteAsync(() => queryable.ToArrayAsync(cancellationToken));

private async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> operation)
{
await _lock.WaitAsync();

try
{
return await operation();
}
finally
{
_lock.Release();
}
}

public Task<T[]> ToArrayAsync<T>(IQueryable<T> queryable)
=> queryable.ToArrayAsync();
void IDisposable.Dispose() => _lock.Dispose();
}