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);
}