@@ -24,6 +24,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
2424 private const string JAVASCRIPT_FILE = "./_content/Microsoft.FluentUI.AspNetCore.Components/Components/DataGrid/FluentDataGrid.razor.js" ;
2525 public const string EMPTY_CONTENT_ROW_CLASS = "empty-content-row" ;
2626 public const string LOADING_CONTENT_ROW_CLASS = "loading-content-row" ;
27+ public const string ERROR_CONTENT_ROW_CLASS = "error-content-row" ;
2728 public List < FluentMenu > _menuReferences = [ ] ;
2829
2930 /// <summary />
@@ -280,6 +281,26 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
280281 [ Parameter ]
281282 public RenderFragment ? LoadingContent { get ; set ; }
282283
284+ /// <summary>
285+ /// Gets or sets the callback that is invoked when the asynchronous loading state of items changes and <see cref="IAsyncQueryExecutor"/> is used.
286+ /// </summary>
287+ /// <remarks>The callback receives a <see langword="true"/> value when items start loading
288+ /// and a <see langword="false"/> value when the loading process completes.</remarks>
289+ [ Parameter ]
290+ public EventCallback < bool > OnItemsLoading { get ; set ; }
291+
292+ /// <summary>
293+ /// Gets or sets a delegate that determines whether a given exception should be handled.
294+ /// </summary>
295+ [ Parameter ]
296+ public Func < Exception , bool > ? HandleLoadingError { get ; set ; }
297+
298+ /// <summary>
299+ /// Gets or sets the content to render when an error occurs.
300+ /// </summary>
301+ [ Parameter ]
302+ public RenderFragment < Exception > ? ErrorContent { get ; set ; }
303+
283304 /// <summary>
284305 /// Sets <see cref="GridTemplateColumns"/> to automatically fit the columns to the available width as best it can.
285306 /// </summary>
@@ -378,9 +399,9 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
378399 // Caches of method->delegate conversions
379400 private readonly RenderFragment _renderColumnHeaders ;
380401 private readonly RenderFragment _renderNonVirtualizedRows ;
381-
382402 private readonly RenderFragment _renderEmptyContent ;
383403 private readonly RenderFragment _renderLoadingContent ;
404+ private readonly RenderFragment _renderErrorContent ;
384405
385406 private string ? _internalGridTemplateColumns ;
386407
@@ -394,6 +415,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
394415 private GridItemsProvider < TGridItem > ? _lastAssignedItemsProvider ;
395416 private CancellationTokenSource ? _pendingDataLoadCancellationTokenSource ;
396417
418+ private Exception ? _lastError ;
397419 private GridItemsProviderRequest < TGridItem > ? _lastRequest ;
398420 private bool _forceRefreshData ;
399421
@@ -416,6 +438,7 @@ public FluentDataGrid()
416438 _renderNonVirtualizedRows = RenderNonVirtualizedRows ;
417439 _renderEmptyContent = RenderEmptyContent ;
418440 _renderLoadingContent = RenderLoadingContent ;
441+ _renderErrorContent = RenderErrorContent ;
419442
420443 // As a special case, we don't issue the first data load request until we've collected the initial set of columns
421444 // 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()
842865 {
843866 Pagination ? . SetTotalItemCountAsync ( _internalGridContext . TotalItemCount ) ;
844867 }
845- if ( _internalGridContext . TotalItemCount > 0 && Loading is null )
868+ if ( ( _internalGridContext . TotalItemCount > 0 && Loading is null ) || _lastError != null )
846869 {
847870 Loading = false ;
848871 StateHasChanged ( ) ;
@@ -861,6 +884,12 @@ private async Task RefreshDataCoreAsync()
861884 // Normalizes all the different ways of configuring a data source so they have common GridItemsProvider-shaped API
862885 private async ValueTask < GridItemsProviderResult < TGridItem > > ResolveItemsRequestAsync ( GridItemsProviderRequest < TGridItem > request )
863886 {
887+ if ( _lastError != null )
888+ {
889+ _lastError = null ;
890+ StateHasChanged ( ) ;
891+ }
892+
864893 try
865894 {
866895 if ( ItemsProvider is not null )
@@ -875,6 +904,10 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
875904 }
876905 else if ( Items is not null )
877906 {
907+ if ( _asyncQueryExecutor is not null )
908+ {
909+ await OnItemsLoading . InvokeAsync ( true ) ;
910+ }
878911 var totalItemCount = _asyncQueryExecutor is null ? Items . Count ( ) : await _asyncQueryExecutor . CountAsync ( Items , request . CancellationToken ) ;
879912 _internalGridContext . TotalItemCount = totalItemCount ;
880913 IQueryable < TGridItem > ? result ;
@@ -898,6 +931,23 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
898931 {
899932 // No-op; we canceled the operation, so it's fine to suppress this exception.
900933 }
934+ catch ( Exception ex ) when ( HandleLoadingError ? . Invoke ( ex ) == true )
935+ {
936+ _lastError = ex . GetBaseException ( ) ;
937+ }
938+ finally
939+ {
940+ if ( Items is not null && _asyncQueryExecutor is not null )
941+ {
942+ if ( Loading == true )
943+ {
944+ Loading = false ;
945+ StateHasChanged ( ) ;
946+ }
947+ await OnItemsLoading . InvokeAsync ( false ) ;
948+ }
949+ }
950+
901951 return GridItemsProviderResult . From ( Array . Empty < TGridItem > ( ) , 0 ) ;
902952 }
903953
0 commit comments