Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,11 @@
"show options" UI and invoke the grid's <see cref="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ShowColumnOptionsAsync(Microsoft.FluentUI.AspNetCore.Components.ColumnBase{`0})" />).
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.ColumnBase`1.SortName">
<summary>
Gets or sets the name used to assist the external ordering.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.ColumnBase`1.Sortable">
<summary>
Gets or sets a value indicating whether the data should be sortable by this column.
Expand Down Expand Up @@ -1850,6 +1855,13 @@
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.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RefreshItems">
<summary>
Gets or sets a callback which will be called if there is a change in pagination, ordering or if a RefreshDataAsync is forced.

You must supply <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.Items"/> if you use this callback.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ItemsProvider">
<summary>
Gets or sets a callback that supplies data for the rid.
Expand Down Expand Up @@ -2158,7 +2170,7 @@
<param name="index">The column index whose resize UI is to be displayed.</param>
<returns>A <see cref="T:System.Threading.Tasks.Task"/> representing the completion of the operation.</returns>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RefreshDataAsync">
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.RefreshDataAsync(System.Boolean)">
<summary>
Instructs the grid to re-fetch and render the current data from the supplied data source
(either <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.Items"/> or <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ItemsProvider"/>).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
foodRecallProvider = async req =>
{
var url = NavManager.GetUriWithQueryParameters("https://api.fda.gov/food/enforcement.json", new Dictionary<string, object?>
{
{
{ "skip", req.StartIndex },
{ "limit", req.Count },
});
});

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
ο»Ώ@using Microsoft.FluentUI.AspNetCore.Components

@inject HttpClient Http
@inject NavigationManager NavManager

<FluentAccordion>
<FluentAccordionItem Heading="Filter(s)" Expanded="true">
<FluentIcon Value="@(new Icons.Regular.Size20.FilterAdd())" Color="@Color.Neutral" Slot="start" />
<FluentGrid Spacing="1" Justify="JustifyContent.FlexStart" Style="padding: 5px;">
<FluentGridItem xs="12" sm="6" md="4">
<FluentTextField @bind-Value=_stateFilter Label="State Filter"></FluentTextField>
</FluentGridItem>
</FluentGrid>
<FluentStack Orientation="Orientation.Horizontal" HorizontalAlignment="HorizontalAlignment.End">
<FluentButton IconStart="@(new Icons.Regular.Size16.Broom())"
Disabled="loading"
OnClick="ClearFilters">
Clear
</FluentButton>
<FluentButton IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
Appearance="Appearance.Accent"
Loading="loading"
OnClick="DataGridRefreshDataAsync">
Search
</FluentButton>
</FluentStack>
</FluentAccordionItem>
</FluentAccordion>
<br />
<div style="height: 370px; overflow:auto;" tabindex="-1">
<FluentDataGrid @ref="dataGrid"
Items="foodRecallItems"
RefreshItems="RefreshItemsAsync"
OnRowDoubleClick="@(()=>DemoLogger.WriteLine("Row double clicked!"))"
ItemSize="46"
GenerateHeader="GenerateHeaderOption.Sticky"
TGridItem="FoodRecall"
Loading="loading"
Pagination="pagination">
<PropertyColumn Title="ID" Property="@(c => c!.Event_Id)" />
<PropertyColumn Property="@(c => c!.State)" Style="color: #af5f00 ;" />
<PropertyColumn Property="@(c => c!.City)" />
<PropertyColumn Title="Company" Property="@(c => c!.Recalling_Firm)" Tooltip="true" />
<PropertyColumn Property="@(c => c!.Status)" />
<PropertyColumn Title="Termination Date" Property="@(c => c!.Termination_Date)" SortName="termination_date" Sortable="true" />
<TemplateColumn Title="Actions" Align="@Align.End">
<FluentButton aria-label="Edit item" IconEnd="@(new Icons.Regular.Size16.Edit())" OnClick="@(() => DemoLogger.WriteLine("Edit clicked"))" />
<FluentButton aria-label="Delete item" IconEnd="@(new Icons.Regular.Size16.Delete())" OnClick="@(() => DemoLogger.WriteLine("Delete clicked"))" />
</TemplateColumn>
</FluentDataGrid>
</div>
<FluentPaginator State="@pagination" />

@code {

FluentDataGrid<FoodRecall> dataGrid = default!;
IQueryable<FoodRecall> foodRecallItems = default!;
bool loading = true;
PaginationState pagination = new PaginationState { ItemsPerPage = 10 };
string _stateFilter = "NY";

protected async Task RefreshItemsAsync(GridItemsProviderRequest<FoodRecall> req)
{
loading = true;
await InvokeAsync(StateHasChanged);

var filters = new Dictionary<string, object?>
{
{ "skip", req.StartIndex },
{ "limit", req.Count },
};

if (!string.IsNullOrWhiteSpace(_stateFilter))
filters.Add("search", $"state:{_stateFilter}");

if (req.SortByColumn != null)
filters.Add("sort", req.SortByColumn.SortName + (req.SortByAscending ? ":asc" : ":desc"));

var url = NavManager.GetUriWithQueryParameters("https://api.fda.gov/food/enforcement.json", filters);

var response = await Http.GetFromJsonAsync<FoodRecallQueryResult>(url);

// Simulate a slow data retrieval process
if (req.Count is null)
{
await Task.Delay(2500);
}

foodRecallItems = response!.Results.AsQueryable();
await pagination.SetTotalItemCountAsync(response!.Meta.Results.Total);

loading = false;
await InvokeAsync(StateHasChanged);
}

public void ClearFilters()
{
_stateFilter = null;

Check warning on line 98 in examples/Demo/Shared/Pages/DataGrid/Examples/DataGridRemoteData2.razor

View workflow job for this annotation

GitHub Actions / Build and Deploy Demo site

Cannot convert null literal to non-nullable reference type.

Check warning on line 98 in examples/Demo/Shared/Pages/DataGrid/Examples/DataGridRemoteData2.razor

View workflow job for this annotation

GitHub Actions / Build and Deploy Demo site

Cannot convert null literal to non-nullable reference type.

Check warning on line 98 in examples/Demo/Shared/Pages/DataGrid/Examples/DataGridRemoteData2.razor

View workflow job for this annotation

GitHub Actions / Build and Deploy Demo site

Cannot convert null literal to non-nullable reference type.
}

public async Task DataGridRefreshDataAsync()
{
await dataGrid.RefreshDataAsync(true);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,16 @@
</p>
</Description>
</DemoSection>
<DemoSection Title="Remote data with RefreshItems" Component="@typeof(DataGridRemoteData2)">
<Description>
<p>
If the external endpoint controls filtering, paging and sorting you can use <code>Items</code> combined with <code>RefreshItems</code>.
</p>
<p>
The method defined in <code>RefreshItems</code> will be called once, and only once, if there is a change in the pagination or ordering.
</p>
<p>
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.
</p>
</Description>
</DemoSection>
1 change: 1 addition & 0 deletions examples/Demo/Shared/SampleData/FoodRecall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class FoodRecall
public string City { get; set; }
public string State { get; set; }
public string Recalling_Firm { get; set; }
public string Termination_Date { get; set; }
}

public class FoodRecallQueryResult
Expand Down
6 changes: 6 additions & 0 deletions src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ public abstract partial class ColumnBase<TGridItem>
[Parameter]
public RenderFragment? ColumnOptions { get; set; }

/// <summary>
/// Gets or sets the name used to assist the external ordering.
/// </summary>
[Parameter]
public string? SortName { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the data should be sortable by this column.
///
Expand Down
2 changes: 2 additions & 0 deletions src/Core/Components/DataGrid/Columns/PropertyColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ protected override void OnParametersSet()
{
Title = memberExpression.Member.Name;
}

SortName = SortName ?? memberExpression.Member.Name;
}
}
}
Expand Down
72 changes: 55 additions & 17 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
[Parameter]
public IQueryable<TGridItem>? Items { get; set; }

/// <summary>
/// Gets or sets a callback which will be called if there is a change in pagination, ordering or if a RefreshDataAsync is forced.
///
/// You must supply <see cref="Items"/> if you use this callback.
/// </summary>
[Parameter]
public Func<GridItemsProviderRequest<TGridItem>, Task>? RefreshItems { get; set; }

/// <summary>
/// Gets or sets a callback that supplies data for the rid.
///
Expand Down Expand Up @@ -352,6 +360,9 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
private GridItemsProvider<TGridItem>? _lastAssignedItemsProvider;
private CancellationTokenSource? _pendingDataLoadCancellationTokenSource;

private GridItemsProviderRequest<TGridItem>? _lastRequest;
private bool _forceRefreshData;

// If the PaginationState mutates, it raises this event. We use it to trigger a re-render.
private readonly EventCallbackSubscriber<PaginationState> _currentPageItemsChanged;
public bool? SortByAscending => _sortByAscending;
Expand Down Expand Up @@ -671,8 +682,9 @@ public void SetLoadingState(bool? loading)
/// (either <see cref="Items"/> or <see cref="ItemsProvider"/>).
/// </summary>
/// <returns>A <see cref="Task"/> that represents the completion of the operation.</returns>
public async Task RefreshDataAsync()
public async Task RefreshDataAsync(bool force = false)
{
_forceRefreshData = force;
await RefreshDataCoreAsync();
}

Expand All @@ -691,24 +703,39 @@ private async Task RefreshDataCoreAsync()
// (2) We won't know what slice of data to query for
await _virtualizeComponent.RefreshDataAsync();
_pendingDataLoadCancellationTokenSource = null;

StateHasChanged();
return;
}

// If we're not using Virtualize, we build and execute a request against the items provider directly
var startIndex = Pagination is null ? 0 : (Pagination.CurrentPageIndex * Pagination.ItemsPerPage);
GridItemsProviderRequest<TGridItem> request = new(
startIndex, Pagination?.ItemsPerPage, _sortByColumn, _sortByAscending, thisLoadCts.Token);
_lastRefreshedPaginationState = Pagination;

if (RefreshItems is not null)
{
if (_forceRefreshData || _lastRequest == null || !_lastRequest.Value.IsSameRequest(request))
{
_forceRefreshData = false;
_lastRequest = request;
await RefreshItems.Invoke(request);
}
}
else

var result = await ResolveItemsRequestAsync(request);
if (!thisLoadCts.IsCancellationRequested)
{
// If we're not using Virtualize, we build and execute a request against the items provider directly
var startIndex = Pagination is null ? 0 : (Pagination.CurrentPageIndex * Pagination.ItemsPerPage);
GridItemsProviderRequest<TGridItem> request = new(
startIndex, Pagination?.ItemsPerPage, _sortByColumn, _sortByAscending, thisLoadCts.Token);
_lastRefreshedPaginationState = Pagination;
var result = await ResolveItemsRequestAsync(request);
if (!thisLoadCts.IsCancellationRequested)
_internalGridContext.Items = result.Items;
_internalGridContext.TotalItemCount = result.TotalItemCount;
if (RefreshItems is null)
{
_internalGridContext.Items = result.Items;
_internalGridContext.TotalItemCount = result.TotalItemCount;
Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
_pendingDataLoadCancellationTokenSource = null;
}
_internalGridContext.ResetRowIndexes(startIndex);
_pendingDataLoadCancellationTokenSource = null;
}
_internalGridContext.ResetRowIndexes(startIndex);

StateHasChanged();
}
Expand Down Expand Up @@ -750,7 +777,10 @@ private async Task RefreshDataCoreAsync()
_internalGridContext.TotalItemCount = providerResult.TotalItemCount;
_internalGridContext.TotalViewItemCount = Pagination?.ItemsPerPage ?? providerResult.TotalItemCount;

Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
if (RefreshItems is null)
{
Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
}
if (_internalGridContext.TotalItemCount > 0 && Loading is null)
{
Loading = false;
Expand Down Expand Up @@ -787,10 +817,18 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
{
var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items, request.CancellationToken);
_internalGridContext.TotalItemCount = totalItemCount;
var result = request.ApplySorting(Items).Skip(request.StartIndex);
if (request.Count.HasValue)
IQueryable<TGridItem>? result;
if (RefreshItems is null)
{
result = request.ApplySorting(Items).Skip(request.StartIndex);
if (request.Count.HasValue)
{
result = result.Take(request.Count.Value);
}
}
else
{
result = result.Take(request.Count.Value);
result = Items;
}
var resultArray = _asyncQueryExecutor is null ? [.. result] : await _asyncQueryExecutor.ToArrayAsync(result, request.CancellationToken);
return GridItemsProviderResult.From(resultArray, totalItemCount);
Expand Down
25 changes: 25 additions & 0 deletions src/Core/Components/DataGrid/GridItemsProviderRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,29 @@ public IQueryable<TGridItem> ApplySorting(IQueryable<TGridItem> source) =>
/// <returns>A collection of (property name, direction) pairs representing the sorting rules</returns>
public IReadOnlyCollection<SortedProperty> GetSortByProperties() =>
SortByColumn?.SortBy?.ToPropertyList(SortByAscending) ?? Array.Empty<SortedProperty>();

public bool IsSameRequest(GridItemsProviderRequest<TGridItem> req)
{
if (StartIndex != req.StartIndex)
{
return false;
}

if (Count != req.Count)
{
return false;
}

if (SortByColumn?.Index != req.SortByColumn?.Index)
{
return false;
}

if (SortByAscending != req.SortByAscending)
{
return false;
}

return true;
}
}
Loading