diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 66c9e597ae..2ed255231a 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -2104,6 +2104,23 @@ A default fragment is used if loading content is not specified. + + + Gets or sets the callback that is invoked when the asynchronous loading state of items changes and is used. + + The callback receives a value when items start loading + and a value when the loading process completes. + + + + Gets or sets a delegate that determines whether a given exception should be handled. + + + + + Gets or sets the content to render when an error occurs. + + Sets to automatically fit the columns to the available width as best it can. diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor b/src/Core/Components/DataGrid/FluentDataGrid.razor index 071d862b77..25dbcae26a 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor @@ -40,7 +40,11 @@ } - @if (EffectiveLoadingValue) + @if (_lastError != null) + { + @_renderErrorContent + } + else if (EffectiveLoadingValue) { @_renderLoadingContent } @@ -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) { @@ -260,4 +268,36 @@ } + + 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(); + } + + + + @if (ErrorContent is null) + { + @("An error occurred while retrieving data.") + } + else + { + @ErrorContent(_lastError) + } + + + } } diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index 4527b49fb0..d8f771f01c 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -24,6 +24,7 @@ public partial class FluentDataGrid : 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 _menuReferences = []; /// @@ -280,6 +281,26 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve [Parameter] public RenderFragment? LoadingContent { get; set; } + /// + /// Gets or sets the callback that is invoked when the asynchronous loading state of items changes and is used. + /// + /// The callback receives a value when items start loading + /// and a value when the loading process completes. + [Parameter] + public EventCallback OnItemsLoading { get; set; } + + /// + /// Gets or sets a delegate that determines whether a given exception should be handled. + /// + [Parameter] + public Func? HandleLoadingError { get; set; } + + /// + /// Gets or sets the content to render when an error occurs. + /// + [Parameter] + public RenderFragment? ErrorContent { get; set; } + /// /// Sets to automatically fit the columns to the available width as best it can. /// @@ -378,9 +399,9 @@ public partial class FluentDataGrid : 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; @@ -394,6 +415,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve private GridItemsProvider? _lastAssignedItemsProvider; private CancellationTokenSource? _pendingDataLoadCancellationTokenSource; + private Exception? _lastError; private GridItemsProviderRequest? _lastRequest; private bool _forceRefreshData; @@ -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 @@ -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(); @@ -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> ResolveItemsRequestAsync(GridItemsProviderRequest request) { + if (_lastError != null) + { + _lastError = null; + StateHasChanged(); + } + try { if (ItemsProvider is not null) @@ -875,6 +904,10 @@ private async ValueTask> 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? result; @@ -898,6 +931,23 @@ private async ValueTask> 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(), 0); }