Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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,21 @@
</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.RemoveSortByColumnAsync">
<summary>
Removes the grid's sort on double click for the currently sorted column if it's not a default sort column.
</summary>
<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 +2041,29 @@
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.ShowColumnOptionsAsync(System.String)">
<summary>
Displays the <see cref="P:Microsoft.FluentUI.AspNetCore.Components.ColumnBase`1.ColumnOptions"/> UI for the specified column <paramref name="title"/> found first,
closing any other column options UI that was previously displayed. If the title is not found, nothing happens.
</summary>
<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.ShowColumnOptionsAsync(System.Int32)">
<summary>
Displays the <see cref="P:Microsoft.FluentUI.AspNetCore.Components.ColumnBase`1.ColumnOptions"/> UI for the specified column <paramref name="index"/>,
closing any other column options UI that was previously displayed. If the index is out of range, nothing happens.
</summary>
<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 +2311,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
89 changes: 64 additions & 25 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,18 +475,26 @@ 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);

return column is not null ? SortByColumnAsync(column, direction) : Task.CompletedTask;
}

/// <summary>
/// Removes the grid's sort on double click for the currently sorted column if it's not a default sort column.
/// </summary>
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
public Task RemoveSortByColumnAsync() => (_sortByColumn != null) ? RemoveSortByColumnAsync(_sortByColumn) : Task.CompletedTask;

/// <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="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 +522,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 @@ -518,11 +531,33 @@ public Task ShowColumnOptionsAsync(ColumnBase<TGridItem> column)
return Task.CompletedTask;
}

/// <summary>
/// Displays the <see cref="ColumnBase{TGridItem}.ColumnOptions"/> UI for the specified column <paramref name="title"/> found first,
/// closing any other column options UI that was previously displayed. If the title is not found, nothing happens.
/// </summary>
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
public Task ShowColumnOptionsAsync(string title)
{
var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false);
return (column is not null) ? ShowColumnOptionsAsync(column) : Task.CompletedTask;
}

/// <summary>
/// Displays the <see cref="ColumnBase{TGridItem}.ColumnOptions"/> UI for the specified column <paramref name="index"/>,
/// closing any other column options UI that was previously displayed. If the index is out of range, nothing happens.
/// </summary>
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
public Task ShowColumnOptionsAsync(int index)
{
return (index >= 0 && index < _columns.Count) ? ShowColumnOptionsAsync(_columns[index]) : Task.CompletedTask;
}

/// <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="Task"/> representing the completion of the operation.</returns>
public Task ShowColumnResizeAsync(ColumnBase<TGridItem> column)
{
_displayResizeForColumn = column;
Expand All @@ -541,10 +576,7 @@ public void SetLoadingState(bool loading)
/// (either <see cref="Items"/> or <see cref="ItemsProvider"/>).
/// </summary>
/// <returns>A <see cref="Task"/> that represents the completion of the operation.</returns>
public async Task RefreshDataAsync()
{
await RefreshDataCoreAsync();
}
public Task RefreshDataAsync() => RefreshDataCoreAsync();

// Same as RefreshDataAsync, except without forcing a re-render. We use this from OnParametersSetAsync
// because in that case there's going to be a re-render anyway.
Expand Down Expand Up @@ -640,31 +672,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 +712,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 +739,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();
}