@@ -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