Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…to dev
  • Loading branch information
vnbaaij committed Sep 23, 2024
2 parents a8d1140 + 3a18713 commit 3a3666b
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 32 deletions.
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);
}
2 changes: 1 addition & 1 deletion src/Core/Components/List/FluentSelect.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
@typeparam TOption
<CascadingValue Value="_internalListContext" Name="ListContext" TValue="InternalListContext<TOption>" IsFixed="true">
@InlineStyleValue
<FluentInputLabel ForId="@Id" Label="@Label" AriaLabel="@AriaLabel" Required="@Required" ChildContent="@LabelTemplate" />
<FluentInputLabel ForId="@Id" Label="@Label" AriaLabel="@GetAriaLabelWithRequired()" Required="@Required" ChildContent="@LabelTemplate" />
<fluent-select @ref=Element
id=@Id
class="@ClassValue"
Expand Down
12 changes: 12 additions & 0 deletions src/Core/Components/List/FluentSelect.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
[CascadingTypeParameter(nameof(TOption))]
public partial class FluentSelect<TOption> : ListComponentBase<TOption> where TOption : notnull
{
/// <summary>
/// Gets the `Required` aria label.
/// </summary>
public static string RequiredAriaLabel = "Required";

/// <summary />
protected virtual MarkupString InlineStyleValue => new InlineStyleBuilder()
.AddStyle($"#{Id}::part(listbox)", "max-height", Height, !string.IsNullOrWhiteSpace(Height))
Expand Down Expand Up @@ -39,4 +44,11 @@ public partial class FluentSelect<TOption> : ListComponentBase<TOption> where TO
/// </summary>
[Parameter]
public Appearance? Appearance { get; set; }

private string? GetAriaLabelWithRequired()
{
var label = AriaLabel ?? Label ?? Title ?? string.Empty;

Check warning on line 50 in src/Core/Components/List/FluentSelect.razor.cs

View workflow job for this annotation

GitHub Actions / Build and deploy Demo site

'ListComponentBase<TOption>.Title' is obsolete: 'Use AriaLabel instead'

Check warning on line 50 in src/Core/Components/List/FluentSelect.razor.cs

View workflow job for this annotation

GitHub Actions / Build and Test Core Lib

'ListComponentBase<TOption>.Title' is obsolete: 'Use AriaLabel instead'

Check warning on line 50 in src/Core/Components/List/FluentSelect.razor.cs

View workflow job for this annotation

GitHub Actions / Build and Test Core Lib

'ListComponentBase<TOption>.Title' is obsolete: 'Use AriaLabel instead'

Check warning on line 50 in src/Core/Components/List/FluentSelect.razor.cs

View workflow job for this annotation

GitHub Actions / Build and Test Core Lib

'ListComponentBase<TOption>.Title' is obsolete: 'Use AriaLabel instead'

Check warning on line 50 in src/Core/Components/List/FluentSelect.razor.cs

View workflow job for this annotation

GitHub Actions / Build and Deploy Demo site

'ListComponentBase<TOption>.Title' is obsolete: 'Use AriaLabel instead'

return label + (Required ? $", {RequiredAriaLabel}" : string.Empty);
}
}
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();
}

0 comments on commit 3a3666b

Please sign in to comment.