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 @@ -2104,6 +2104,23 @@
A default fragment is used if loading content is not specified.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.OnItemsLoading">
<summary>
Gets or sets the callback that is invoked when the asynchronous loading state of items changes and <see cref="T:Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure.IAsyncQueryExecutor"/> is used.
</summary>
<remarks>The callback receives a <see langword="true"/> value when items start loading
and a <see langword="false"/> value when the loading process completes.</remarks>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.HandleLoadingError">
<summary>
Gets or sets a delegate that determines whether a given exception should be handled.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ErrorContent">
<summary>
Gets or sets the content to render when an error occurs.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.AutoFit">
<summary>
Sets <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.GridTemplateColumns"/> to automatically fit the columns to the available width as best it can.
Expand Down
44 changes: 42 additions & 2 deletions src/Core/Components/DataGrid/FluentDataGrid.razor
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@
</thead>
}
<tbody>
@if (EffectiveLoadingValue)
@if (_lastError != null)
{
@_renderErrorContent
}
else if (EffectiveLoadingValue)
{
@_renderLoadingContent
}
Expand Down Expand Up @@ -83,7 +87,11 @@
{
var initialRowIndex = (GenerateHeader != GenerateHeaderOption.None) ? 2 : 1; // aria-rowindex is 1-based, plus 1 if there is a header
var rowIndex = initialRowIndex;
if (_internalGridContext.Items.Any())
if (_lastError != null)
{
RenderErrorContent(__builder);
}
else if (_internalGridContext.Items.Any())
{
foreach (var item in _internalGridContext.Items)
{
Expand Down Expand Up @@ -260,4 +268,36 @@
</FluentDataGridCell>
</FluentDataGridRow>
}

private void RenderErrorContent(RenderTreeBuilder __builder)
{
if (_lastError == null)
{
return;
}

string? style = null;
string? colspan = null;
if (DisplayMode == DataGridDisplayMode.Grid)
{
style = $"grid-column: 1 / {_columns.Count + 1}";
}
else
{
colspan = _columns.Count.ToString();
}

<FluentDataGridRow Class="@ERROR_CONTENT_ROW_CLASS" TGridItem="TGridItem">
<FluentDataGridCell Class="empty-content-cell" Style="@style" colspan="@colspan">
@if (ErrorContent is null)
{
@("An error occurred while retrieving data.")
}
else
{
@ErrorContent(_lastError)
}
</FluentDataGridCell>
</FluentDataGridRow>
}
}
54 changes: 52 additions & 2 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
private const string JAVASCRIPT_FILE = "./_content/Microsoft.FluentUI.AspNetCore.Components/Components/DataGrid/FluentDataGrid.razor.js";
public const string EMPTY_CONTENT_ROW_CLASS = "empty-content-row";
public const string LOADING_CONTENT_ROW_CLASS = "loading-content-row";
public const string ERROR_CONTENT_ROW_CLASS = "error-content-row";
public List<FluentMenu> _menuReferences = [];

/// <summary />
Expand Down Expand Up @@ -280,6 +281,26 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
[Parameter]
public RenderFragment? LoadingContent { get; set; }

/// <summary>
/// Gets or sets the callback that is invoked when the asynchronous loading state of items changes and <see cref="IAsyncQueryExecutor"/> is used.
/// </summary>
/// <remarks>The callback receives a <see langword="true"/> value when items start loading
/// and a <see langword="false"/> value when the loading process completes.</remarks>
[Parameter]
public EventCallback<bool> OnItemsLoading { get; set; }

/// <summary>
/// Gets or sets a delegate that determines whether a given exception should be handled.
/// </summary>
[Parameter]
public Func<Exception, bool>? HandleLoadingError { get; set; }

/// <summary>
/// Gets or sets the content to render when an error occurs.
/// </summary>
[Parameter]
public RenderFragment<Exception>? ErrorContent { get; set; }

/// <summary>
/// Sets <see cref="GridTemplateColumns"/> to automatically fit the columns to the available width as best it can.
/// </summary>
Expand Down Expand Up @@ -378,9 +399,9 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
// Caches of method->delegate conversions
private readonly RenderFragment _renderColumnHeaders;
private readonly RenderFragment _renderNonVirtualizedRows;

private readonly RenderFragment _renderEmptyContent;
private readonly RenderFragment _renderLoadingContent;
private readonly RenderFragment _renderErrorContent;

private string? _internalGridTemplateColumns;

Expand All @@ -394,6 +415,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
private GridItemsProvider<TGridItem>? _lastAssignedItemsProvider;
private CancellationTokenSource? _pendingDataLoadCancellationTokenSource;

private Exception? _lastError;
private GridItemsProviderRequest<TGridItem>? _lastRequest;
private bool _forceRefreshData;

Expand All @@ -416,6 +438,7 @@ public FluentDataGrid()
_renderNonVirtualizedRows = RenderNonVirtualizedRows;
_renderEmptyContent = RenderEmptyContent;
_renderLoadingContent = RenderLoadingContent;
_renderErrorContent = RenderErrorContent;

// As a special case, we don't issue the first data load request until we've collected the initial set of columns
// This is so we can apply default sort order (or any future per-column options) before loading data
Expand Down Expand Up @@ -842,7 +865,7 @@ private async Task RefreshDataCoreAsync()
{
Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
}
if (_internalGridContext.TotalItemCount > 0 && Loading is null)
if ((_internalGridContext.TotalItemCount > 0 && Loading is null) || _lastError != null)
{
Loading = false;
StateHasChanged();
Expand All @@ -861,6 +884,12 @@ 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 (_lastError != null)
{
_lastError = null;
StateHasChanged();
}

try
{
if (ItemsProvider is not null)
Expand All @@ -875,6 +904,10 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
}
else if (Items is not null)
{
if (_asyncQueryExecutor is not null)
{
await OnItemsLoading.InvokeAsync(true);
}
var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items, request.CancellationToken);
_internalGridContext.TotalItemCount = totalItemCount;
IQueryable<TGridItem>? result;
Expand All @@ -898,6 +931,23 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
{
// No-op; we canceled the operation, so it's fine to suppress this exception.
}
catch (Exception ex) when (HandleLoadingError?.Invoke(ex) == true)
{
_lastError = ex.GetBaseException();
}
finally
{
if (Items is not null && _asyncQueryExecutor is not null)
{
if (Loading == true)
{
Loading = false;
StateHasChanged();
}
await OnItemsLoading.InvokeAsync(false);
}
}

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

Expand Down
Loading