Skip to content

Commit 89d2472

Browse files
committed
Implement #4177
1 parent be46fe0 commit 89d2472

File tree

2 files changed

+108
-9
lines changed

2 files changed

+108
-9
lines changed

src/Core/Components/DataGrid/FluentDataGrid.razor

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@
4040
</thead>
4141
}
4242
<tbody>
43-
@if (EffectiveLoadingValue)
43+
@if (_lastError != null)
44+
{
45+
@_renderErrorContent
46+
}
47+
else if (EffectiveLoadingValue)
4448
{
4549
@_renderLoadingContent
4650
}
@@ -83,7 +87,11 @@
8387
{
8488
var initialRowIndex = (GenerateHeader != DataGridGeneratedHeaderType.None) ? 2 : 1; // aria-rowindex is 1-based, plus 1 if there is a header
8589
var rowIndex = initialRowIndex;
86-
if (_internalGridContext.Items.Any())
90+
if (_lastError != null)
91+
{
92+
RenderErrorContent(__builder);
93+
}
94+
else if (_internalGridContext.Items.Any())
8795
{
8896
foreach (var item in _internalGridContext.Items)
8997
{
@@ -261,4 +269,36 @@
261269
</FluentDataGridCell>
262270
</FluentDataGridRow>
263271
}
272+
273+
private void RenderErrorContent(RenderTreeBuilder __builder)
274+
{
275+
if (_lastError == null)
276+
{
277+
return;
278+
}
279+
280+
string? style = null;
281+
string? colspan = null;
282+
if (DisplayMode == DataGridDisplayMode.Grid)
283+
{
284+
style = $"grid-column: 1 / {_columns.Count + 1}";
285+
}
286+
else
287+
{
288+
colspan = _columns.Count.ToString();
289+
}
290+
291+
<FluentDataGridRow Class="@ERROR_CONTENT_ROW_CLASS" TGridItem="TGridItem">
292+
<FluentDataGridCell Class="empty-content-cell" Style="@style" colspan="@colspan">
293+
@if (ErrorContent is null)
294+
{
295+
@("An error occurred while retrieving data.")
296+
}
297+
else
298+
{
299+
@ErrorContent(_lastError)
300+
}
301+
</FluentDataGridCell>
302+
</FluentDataGridRow>
303+
}
264304
}

src/Core/Components/DataGrid/FluentDataGrid.razor.cs

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
2626

2727
internal const string EMPTY_CONTENT_ROW_CLASS = "empty-content-row";
2828
internal const string LOADING_CONTENT_ROW_CLASS = "loading-content-row";
29+
internal const string ERROR_CONTENT_ROW_CLASS = "error-content-row";
2930

3031
private ElementReference? _gridReference;
3132
private Virtualize<(int, TGridItem)>? _virtualizeComponent;
@@ -44,11 +45,13 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
4445
private readonly RenderFragment _renderNonVirtualizedRows;
4546
private readonly RenderFragment _renderEmptyContent;
4647
private readonly RenderFragment _renderLoadingContent;
48+
private readonly RenderFragment _renderErrorContent;
4749
private string? _internalGridTemplateColumns;
4850
private PaginationState? _lastRefreshedPaginationState;
4951
private IQueryable<TGridItem>? _lastAssignedItems;
5052
private GridItemsProvider<TGridItem>? _lastAssignedItemsProvider;
5153
private CancellationTokenSource? _pendingDataLoadCancellationTokenSource;
54+
private Exception? _lastError;
5255
private GridItemsProviderRequest<TGridItem>? _lastRequest;
5356
private bool _forceRefreshData;
5457
private readonly EventCallbackSubscriber<PaginationState> _currentPageItemsChanged;
@@ -64,11 +67,12 @@ public FluentDataGrid(LibraryConfiguration configuration) : base(configuration)
6467
_renderNonVirtualizedRows = RenderNonVirtualizedRows;
6568
_renderEmptyContent = RenderEmptyContent;
6669
_renderLoadingContent = RenderLoadingContent;
70+
_renderErrorContent = RenderErrorContent;
6771

68-
// As a special case, we don't issue the first data load request until we've collected the initial set of columns
69-
// This is so we can apply default sort order (or any future per-column options) before loading data
70-
// We use EventCallbackSubscriber to safely hook this async operation into the synchronous rendering flow
71-
EventCallbackSubscriber<object?>? columnsFirstCollectedSubscriber = new(
72+
// As a special case, we don't issue the first data load request until we've collected the initial set of columns
73+
// This is so we can apply default sort order (or any future per-column options) before loading data
74+
// We use EventCallbackSubscriber to safely hook this async operation into the synchronous rendering flow
75+
EventCallbackSubscriber<object?>? columnsFirstCollectedSubscriber = new(
7276
EventCallback.Factory.Create<object?>(this, RefreshDataCoreAsync));
7377
columnsFirstCollectedSubscriber.SubscribeOrMove(_internalGridContext.ColumnsFirstCollected);
7478
}
@@ -323,6 +327,26 @@ public FluentDataGrid(LibraryConfiguration configuration) : base(configuration)
323327
[Parameter]
324328
public RenderFragment? LoadingContent { get; set; }
325329

330+
/// <summary>
331+
/// Gets or sets the callback that is invoked when the asynchronous loading state of items changes and <see cref="IAsyncQueryExecutor"/> is used.
332+
/// </summary>
333+
/// <remarks>The callback receives a <see langword="true"/> value when items start loading
334+
/// and a <see langword="false"/> value when the loading process completes.</remarks>
335+
[Parameter]
336+
public EventCallback<bool> OnItemsLoading { get; set; }
337+
338+
/// <summary>
339+
/// Gets or sets a delegate that determines whether a given exception should be handled.
340+
/// </summary>
341+
[Parameter]
342+
public Func<Exception, bool>? HandleLoadingError { get; set; }
343+
344+
/// <summary>
345+
/// Gets or sets the content to render when an error occurs.
346+
/// </summary>
347+
[Parameter]
348+
public RenderFragment<Exception>? ErrorContent { get; set; }
349+
326350
/// <summary>
327351
/// Sets <see cref="GridTemplateColumns"/> to automatically fit the columns to the available width as best it can.
328352
/// </summary>
@@ -821,7 +845,7 @@ private async Task RefreshDataCoreAsync()
821845
Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
822846
}
823847

824-
if (_internalGridContext.TotalItemCount > 0 && Loading is null)
848+
if ((_internalGridContext.TotalItemCount > 0 && Loading is null) || _lastError != null)
825849
{
826850
Loading = false;
827851
_ = InvokeAsync(StateHasChanged);
@@ -841,6 +865,7 @@ private async Task RefreshDataCoreAsync()
841865
// Normalizes all the different ways of configuring a data source so they have common GridItemsProvider-shaped API
842866
private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestAsync(GridItemsProviderRequest<TGridItem> request)
843867
{
868+
CheckAndResetLastError();
844869
try
845870
{
846871
if (ItemsProvider is not null)
@@ -857,6 +882,11 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
857882

858883
if (Items is not null)
859884
{
885+
if (_asyncQueryExecutor is not null)
886+
{
887+
await OnItemsLoading.InvokeAsync(true);
888+
}
889+
860890
var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items, request.CancellationToken);
861891
_internalGridContext.TotalItemCount = totalItemCount;
862892
IQueryable<TGridItem>? result;
@@ -877,14 +907,43 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
877907
return GridItemsProviderResult.From(resultArray, totalItemCount);
878908
}
879909
}
880-
catch (OperationCanceledException oce) when (oce.CancellationToken == request.CancellationToken)
910+
catch (OperationCanceledException oce) when (oce.CancellationToken == request.CancellationToken) // No-op; we canceled the operation, so it's fine to suppress this exception.
881911
{
882-
// No-op; we canceled the operation, so it's fine to suppress this exception.
912+
}
913+
catch (Exception ex) when (HandleLoadingError?.Invoke(ex) == true)
914+
{
915+
_lastError = ex.GetBaseException();
916+
}
917+
finally
918+
{
919+
if (Items is not null && _asyncQueryExecutor is not null)
920+
{
921+
CheckAndResetLoading();
922+
await OnItemsLoading.InvokeAsync(false);
923+
}
883924
}
884925

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

929+
private void CheckAndResetLoading()
930+
{
931+
if (Loading == true)
932+
{
933+
Loading = false;
934+
StateHasChanged();
935+
}
936+
}
937+
938+
private void CheckAndResetLastError()
939+
{
940+
if (_lastError != null)
941+
{
942+
_lastError = null;
943+
StateHasChanged();
944+
}
945+
}
946+
888947
private string AriaSortValue(ColumnBase<TGridItem> column)
889948
=> _sortByColumn == column
890949
? (_sortByAscending ? "ascending" : "descending")

0 commit comments

Comments
 (0)