Skip to content

Commit 8591e50

Browse files
EvandroMoSouEMotaSouzavnbaaij
authored
[DataGrid] Provide alternate way to refresh when working with remote data. (#3423)
* Fix DataGrid remote data calling API multiple times. * Add vnbaaij requests --------- Co-authored-by: Evandro Mota de Souza <emotasouza@PRODAM.SP.GOV.BR> Co-authored-by: Vincent Baaij <vnbaaij@outlook.com>
1 parent 0c99f40 commit 8591e50

File tree

9 files changed

+223
-20
lines changed

9 files changed

+223
-20
lines changed

examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,11 @@
12501250
"show options" UI and invoke the grid's <see cref="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ShowColumnOptionsAsync(Microsoft.FluentUI.AspNetCore.Components.ColumnBase{`0})" />).
12511251
</summary>
12521252
</member>
1253+
<member name="P:Microsoft.FluentUI.AspNetCore.Components.ColumnBase`1.SortName">
1254+
<summary>
1255+
Gets or sets the name used to assist the external ordering.
1256+
</summary>
1257+
</member>
12531258
<member name="P:Microsoft.FluentUI.AspNetCore.Components.ColumnBase`1.Sortable">
12541259
<summary>
12551260
Gets or sets a value indicating whether the data should be sortable by this column.
@@ -1850,6 +1855,13 @@
18501855
You should supply either <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.Items"/> or <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ItemsProvider"/>, but not both.
18511856
</summary>
18521857
</member>
1858+
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RefreshItems">
1859+
<summary>
1860+
Gets or sets a callback which will be called if there is a change in pagination, ordering or if a RefreshDataAsync is forced.
1861+
1862+
You must supply <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.Items"/> if you use this callback.
1863+
</summary>
1864+
</member>
18531865
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ItemsProvider">
18541866
<summary>
18551867
Gets or sets a callback that supplies data for the rid.
@@ -2158,7 +2170,7 @@
21582170
<param name="index">The column index whose resize UI is to be displayed.</param>
21592171
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
21602172
</member>
2161-
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RefreshDataAsync">
2173+
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RefreshDataAsync(System.Boolean)">
21622174
<summary>
21632175
Instructs the grid to re-fetch and render the current data from the supplied data source
21642176
(either <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.Items"/> or <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ItemsProvider"/>).

examples/Demo/Shared/Pages/DataGrid/Examples/DataGridRemoteData.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
foodRecallProvider = async req =>
3939
{
4040
var url = NavManager.GetUriWithQueryParameters("https://api.fda.gov/food/enforcement.json", new Dictionary<string, object?>
41-
{
41+
{
4242
{ "skip", req.StartIndex },
4343
{ "limit", req.Count },
44-
});
44+
});
4545

4646
var response = await Http.GetFromJsonAsync<FoodRecallQueryResult>(url, req.CancellationToken);
4747

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
@using Microsoft.FluentUI.AspNetCore.Components
2+
3+
@inject HttpClient Http
4+
@inject NavigationManager NavManager
5+
6+
<FluentAccordion>
7+
<FluentAccordionItem Heading="Filter(s)" Expanded="true">
8+
<FluentIcon Value="@(new Icons.Regular.Size20.FilterAdd())" Color="@Color.Neutral" Slot="start" />
9+
<FluentGrid Spacing="1" Justify="JustifyContent.FlexStart" Style="padding: 5px;">
10+
<FluentGridItem xs="12" sm="6" md="4">
11+
<FluentTextField @bind-Value=_stateFilter Label="State Filter"></FluentTextField>
12+
</FluentGridItem>
13+
</FluentGrid>
14+
<FluentStack Orientation="Orientation.Horizontal" HorizontalAlignment="HorizontalAlignment.End">
15+
<FluentButton IconStart="@(new Icons.Regular.Size16.Broom())"
16+
Disabled="loading"
17+
OnClick="ClearFilters">
18+
Clear
19+
</FluentButton>
20+
<FluentButton IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
21+
Appearance="Appearance.Accent"
22+
Loading="loading"
23+
OnClick="DataGridRefreshDataAsync">
24+
Search
25+
</FluentButton>
26+
</FluentStack>
27+
</FluentAccordionItem>
28+
</FluentAccordion>
29+
<br />
30+
<div style="height: 370px; overflow:auto;" tabindex="-1">
31+
<FluentDataGrid @ref="dataGrid"
32+
Items="foodRecallItems"
33+
RefreshItems="RefreshItemsAsync"
34+
OnRowDoubleClick="@(()=>DemoLogger.WriteLine("Row double clicked!"))"
35+
ItemSize="46"
36+
GenerateHeader="GenerateHeaderOption.Sticky"
37+
TGridItem="FoodRecall"
38+
Loading="loading"
39+
Pagination="pagination">
40+
<PropertyColumn Title="ID" Property="@(c => c!.Event_Id)" />
41+
<PropertyColumn Property="@(c => c!.State)" Style="color: #af5f00 ;" />
42+
<PropertyColumn Property="@(c => c!.City)" />
43+
<PropertyColumn Title="Company" Property="@(c => c!.Recalling_Firm)" Tooltip="true" />
44+
<PropertyColumn Property="@(c => c!.Status)" />
45+
<PropertyColumn Title="Termination Date" Property="@(c => c!.Termination_Date)" SortName="termination_date" Sortable="true" />
46+
<TemplateColumn Title="Actions" Align="@Align.End">
47+
<FluentButton aria-label="Edit item" IconEnd="@(new Icons.Regular.Size16.Edit())" OnClick="@(() => DemoLogger.WriteLine("Edit clicked"))" />
48+
<FluentButton aria-label="Delete item" IconEnd="@(new Icons.Regular.Size16.Delete())" OnClick="@(() => DemoLogger.WriteLine("Delete clicked"))" />
49+
</TemplateColumn>
50+
</FluentDataGrid>
51+
</div>
52+
<FluentPaginator State="@pagination" />
53+
54+
@code {
55+
56+
FluentDataGrid<FoodRecall> dataGrid = default!;
57+
IQueryable<FoodRecall> foodRecallItems = default!;
58+
bool loading = true;
59+
PaginationState pagination = new PaginationState { ItemsPerPage = 10 };
60+
string _stateFilter = "NY";
61+
62+
protected async Task RefreshItemsAsync(GridItemsProviderRequest<FoodRecall> req)
63+
{
64+
loading = true;
65+
await InvokeAsync(StateHasChanged);
66+
67+
var filters = new Dictionary<string, object?>
68+
{
69+
{ "skip", req.StartIndex },
70+
{ "limit", req.Count },
71+
};
72+
73+
if (!string.IsNullOrWhiteSpace(_stateFilter))
74+
filters.Add("search", $"state:{_stateFilter}");
75+
76+
if (req.SortByColumn != null)
77+
filters.Add("sort", req.SortByColumn.SortName + (req.SortByAscending ? ":asc" : ":desc"));
78+
79+
var url = NavManager.GetUriWithQueryParameters("https://api.fda.gov/food/enforcement.json", filters);
80+
81+
var response = await Http.GetFromJsonAsync<FoodRecallQueryResult>(url);
82+
83+
// Simulate a slow data retrieval process
84+
if (req.Count is null)
85+
{
86+
await Task.Delay(2500);
87+
}
88+
89+
foodRecallItems = response!.Results.AsQueryable();
90+
await pagination.SetTotalItemCountAsync(response!.Meta.Results.Total);
91+
92+
loading = false;
93+
await InvokeAsync(StateHasChanged);
94+
}
95+
96+
public void ClearFilters()
97+
{
98+
_stateFilter = null;
99+
}
100+
101+
public async Task DataGridRefreshDataAsync()
102+
{
103+
await dataGrid.RefreshDataAsync(true);
104+
}
105+
106+
}

examples/Demo/Shared/Pages/DataGrid/Pages/DataGridRemoteDataPage.razor

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,16 @@
3333
</p>
3434
</Description>
3535
</DemoSection>
36+
<DemoSection Title="Remote data with RefreshItems" Component="@typeof(DataGridRemoteData2)">
37+
<Description>
38+
<p>
39+
If the external endpoint controls filtering, paging and sorting you can use <code>Items</code> combined with <code>RefreshItems</code>.
40+
</p>
41+
<p>
42+
The method defined in <code>RefreshItems</code> will be called once, and only once, if there is a change in the pagination or ordering.
43+
</p>
44+
<p>
45+
Meanwhile, you can control the filtering with elements present on the page itself and force a call to <code>RefreshItems</code> with the force option in the RefreshDataAsync.
46+
</p>
47+
</Description>
48+
</DemoSection>

examples/Demo/Shared/SampleData/FoodRecall.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class FoodRecall
1010
public string City { get; set; }
1111
public string State { get; set; }
1212
public string Recalling_Firm { get; set; }
13+
public string Termination_Date { get; set; }
1314
}
1415

1516
public class FoodRecallQueryResult

src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ public abstract partial class ColumnBase<TGridItem>
9393
[Parameter]
9494
public RenderFragment? ColumnOptions { get; set; }
9595

96+
/// <summary>
97+
/// Gets or sets the name used to assist the external ordering.
98+
/// </summary>
99+
[Parameter]
100+
public string? SortName { get; set; }
101+
96102
/// <summary>
97103
/// Gets or sets a value indicating whether the data should be sortable by this column.
98104
///

src/Core/Components/DataGrid/Columns/PropertyColumn.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ protected override void OnParametersSet()
114114
{
115115
Title = memberExpression.Member.Name;
116116
}
117+
118+
SortName = SortName ?? memberExpression.Member.Name;
117119
}
118120
}
119121
}

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

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
5353
[Parameter]
5454
public IQueryable<TGridItem>? Items { get; set; }
5555

56+
/// <summary>
57+
/// Gets or sets a callback which will be called if there is a change in pagination, ordering or if a RefreshDataAsync is forced.
58+
///
59+
/// You must supply <see cref="Items"/> if you use this callback.
60+
/// </summary>
61+
[Parameter]
62+
public Func<GridItemsProviderRequest<TGridItem>, Task>? RefreshItems { get; set; }
63+
5664
/// <summary>
5765
/// Gets or sets a callback that supplies data for the rid.
5866
///
@@ -352,6 +360,9 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
352360
private GridItemsProvider<TGridItem>? _lastAssignedItemsProvider;
353361
private CancellationTokenSource? _pendingDataLoadCancellationTokenSource;
354362

363+
private GridItemsProviderRequest<TGridItem>? _lastRequest;
364+
private bool _forceRefreshData;
365+
355366
// If the PaginationState mutates, it raises this event. We use it to trigger a re-render.
356367
private readonly EventCallbackSubscriber<PaginationState> _currentPageItemsChanged;
357368
public bool? SortByAscending => _sortByAscending;
@@ -671,8 +682,9 @@ public void SetLoadingState(bool? loading)
671682
/// (either <see cref="Items"/> or <see cref="ItemsProvider"/>).
672683
/// </summary>
673684
/// <returns>A <see cref="Task"/> that represents the completion of the operation.</returns>
674-
public async Task RefreshDataAsync()
685+
public async Task RefreshDataAsync(bool force = false)
675686
{
687+
_forceRefreshData = force;
676688
await RefreshDataCoreAsync();
677689
}
678690

@@ -691,24 +703,39 @@ private async Task RefreshDataCoreAsync()
691703
// (2) We won't know what slice of data to query for
692704
await _virtualizeComponent.RefreshDataAsync();
693705
_pendingDataLoadCancellationTokenSource = null;
706+
707+
StateHasChanged();
708+
return;
709+
}
710+
711+
// If we're not using Virtualize, we build and execute a request against the items provider directly
712+
var startIndex = Pagination is null ? 0 : (Pagination.CurrentPageIndex * Pagination.ItemsPerPage);
713+
GridItemsProviderRequest<TGridItem> request = new(
714+
startIndex, Pagination?.ItemsPerPage, _sortByColumn, _sortByAscending, thisLoadCts.Token);
715+
_lastRefreshedPaginationState = Pagination;
716+
717+
if (RefreshItems is not null)
718+
{
719+
if (_forceRefreshData || _lastRequest == null || !_lastRequest.Value.IsSameRequest(request))
720+
{
721+
_forceRefreshData = false;
722+
_lastRequest = request;
723+
await RefreshItems.Invoke(request);
724+
}
694725
}
695-
else
726+
727+
var result = await ResolveItemsRequestAsync(request);
728+
if (!thisLoadCts.IsCancellationRequested)
696729
{
697-
// If we're not using Virtualize, we build and execute a request against the items provider directly
698-
var startIndex = Pagination is null ? 0 : (Pagination.CurrentPageIndex * Pagination.ItemsPerPage);
699-
GridItemsProviderRequest<TGridItem> request = new(
700-
startIndex, Pagination?.ItemsPerPage, _sortByColumn, _sortByAscending, thisLoadCts.Token);
701-
_lastRefreshedPaginationState = Pagination;
702-
var result = await ResolveItemsRequestAsync(request);
703-
if (!thisLoadCts.IsCancellationRequested)
730+
_internalGridContext.Items = result.Items;
731+
_internalGridContext.TotalItemCount = result.TotalItemCount;
732+
if (RefreshItems is null)
704733
{
705-
_internalGridContext.Items = result.Items;
706-
_internalGridContext.TotalItemCount = result.TotalItemCount;
707734
Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
708-
_pendingDataLoadCancellationTokenSource = null;
709735
}
710-
_internalGridContext.ResetRowIndexes(startIndex);
736+
_pendingDataLoadCancellationTokenSource = null;
711737
}
738+
_internalGridContext.ResetRowIndexes(startIndex);
712739

713740
StateHasChanged();
714741
}
@@ -750,7 +777,10 @@ private async Task RefreshDataCoreAsync()
750777
_internalGridContext.TotalItemCount = providerResult.TotalItemCount;
751778
_internalGridContext.TotalViewItemCount = Pagination?.ItemsPerPage ?? providerResult.TotalItemCount;
752779

753-
Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
780+
if (RefreshItems is null)
781+
{
782+
Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
783+
}
754784
if (_internalGridContext.TotalItemCount > 0 && Loading is null)
755785
{
756786
Loading = false;
@@ -787,10 +817,18 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
787817
{
788818
var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items, request.CancellationToken);
789819
_internalGridContext.TotalItemCount = totalItemCount;
790-
var result = request.ApplySorting(Items).Skip(request.StartIndex);
791-
if (request.Count.HasValue)
820+
IQueryable<TGridItem>? result;
821+
if (RefreshItems is null)
822+
{
823+
result = request.ApplySorting(Items).Skip(request.StartIndex);
824+
if (request.Count.HasValue)
825+
{
826+
result = result.Take(request.Count.Value);
827+
}
828+
}
829+
else
792830
{
793-
result = result.Take(request.Count.Value);
831+
result = Items;
794832
}
795833
var resultArray = _asyncQueryExecutor is null ? [.. result] : await _asyncQueryExecutor.ToArrayAsync(result, request.CancellationToken);
796834
return GridItemsProviderResult.From(resultArray, totalItemCount);

src/Core/Components/DataGrid/GridItemsProviderRequest.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,29 @@ public IQueryable<TGridItem> ApplySorting(IQueryable<TGridItem> source) =>
6262
/// <returns>A collection of (property name, direction) pairs representing the sorting rules</returns>
6363
public IReadOnlyCollection<SortedProperty> GetSortByProperties() =>
6464
SortByColumn?.SortBy?.ToPropertyList(SortByAscending) ?? Array.Empty<SortedProperty>();
65+
66+
public bool IsSameRequest(GridItemsProviderRequest<TGridItem> req)
67+
{
68+
if (StartIndex != req.StartIndex)
69+
{
70+
return false;
71+
}
72+
73+
if (Count != req.Count)
74+
{
75+
return false;
76+
}
77+
78+
if (SortByColumn?.Index != req.SortByColumn?.Index)
79+
{
80+
return false;
81+
}
82+
83+
if (SortByAscending != req.SortByAscending)
84+
{
85+
return false;
86+
}
87+
88+
return true;
89+
}
6590
}

0 commit comments

Comments
 (0)