diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index eeddcfa75e..147ece7c0b 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1954,7 +1954,7 @@ Gets or sets a value indicating whether column resize handles should extend the full height of the grid. - When true, columns can be resized by dragging from any row. When false, columns can only be resized + When true, columns can be resized by dragging from any row. When false, columns can only be resized by dragging from the column header. Default is true. @@ -2140,6 +2140,13 @@ Gets or sets a value indicating whether the grids' first cell should be focused. + + + Gets or sets a value indicating whether the grid's dataset is not expected to change during its lifetime. + When set to true, reduces automatic refresh checks for better performance with static datasets. + Default is false to maintain backward compatibility. + + Constructs an instance of . @@ -2283,12 +2290,6 @@ - - - Computes a hash code for the given items. - To limit the effect on performance, only the given maximum number (default 250) of items will be considered. - - Gets or sets the reference to the item that holds this cell's values. diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index 1cd7123d28..d6a8c91176 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -117,7 +117,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve /// /// Gets or sets a value indicating whether column resize handles should extend the full height of the grid. - /// When true, columns can be resized by dragging from any row. When false, columns can only be resized + /// When true, columns can be resized by dragging from any row. When false, columns can only be resized /// by dragging from the column header. Default is true. /// [Parameter] @@ -335,6 +335,14 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve [Parameter] public bool AutoFocus { get; set; } = false; + /// + /// Gets or sets a value indicating whether the grid's dataset is not expected to change during its lifetime. + /// When set to true, reduces automatic refresh checks for better performance with static datasets. + /// Default is false to maintain backward compatibility. + /// + [Parameter] + public bool IsFixed { get; set; } + // Returns Loading if set (controlled). If not controlled, // we assume the grid is loading until the next data load completes internal bool EffectiveLoadingValue => Loading ?? ItemsProvider is not null; @@ -382,7 +390,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve // things have changed, and to discard earlier load attempts that were superseded. private PaginationState? _lastRefreshedPaginationState; private IQueryable? _lastAssignedItems; - private int _lastAssignedItemsHashCode; + private GridItemsProvider? _lastAssignedItemsProvider; private CancellationTokenSource? _pendingDataLoadCancellationTokenSource; @@ -443,18 +451,14 @@ protected override Task OnParametersSetAsync() throw new InvalidOperationException($"FluentDataGrid cannot use both {nameof(Virtualize)} and {nameof(MultiLine)} at the same time."); } - var currentItemsHash = FluentDataGrid.ComputeItemsHash(Items); - var itemsChanged = currentItemsHash != _lastAssignedItemsHashCode; - // Perform a re-query only if the data source or something else has changed - var dataSourceHasChanged = itemsChanged || !Equals(ItemsProvider, _lastAssignedItemsProvider); + var dataSourceHasChanged = !Equals(ItemsProvider, _lastAssignedItemsProvider) || !ReferenceEquals(Items, _lastAssignedItems); if (dataSourceHasChanged) { _scope?.Dispose(); _scope = ScopeFactory.CreateAsyncScope(); _lastAssignedItemsProvider = ItemsProvider; _lastAssignedItems = Items; - _lastAssignedItemsHashCode = currentItemsHash; _asyncQueryExecutor = AsyncQueryExecutorSupplier.GetAsyncQueryExecutor(_scope.Value.ServiceProvider, Items); } @@ -761,6 +765,25 @@ private async Task RefreshDataCoreAsync() if (RefreshItems is not null) { + if (IsFixed) + { + if (_forceRefreshData || _lastRequest == null) + { + _forceRefreshData = false; + _lastRequest = request; + await RefreshItems.Invoke(request); + } + } + else + { + if (_forceRefreshData || _lastRequest == null || !_lastRequest.Value.IsSameRequest(request)) + { + _forceRefreshData = false; + _lastRequest = request; + await RefreshItems.Invoke(request); + } + } + if (_forceRefreshData || _lastRequest == null || !_lastRequest.Value.IsSameRequest(request)) { _forceRefreshData = false; @@ -1115,31 +1138,5 @@ public async Task ResetColumnWidthsAsync() await Module.InvokeVoidAsync("resetColumnWidths", _gridReference); } } - - /// - /// Computes a hash code for the given items. - /// To limit the effect on performance, only the given maximum number (default 250) of items will be considered. - /// - private static int ComputeItemsHash(IEnumerable? items, int maxItems = 250) - { - if (items == null) - { - return 0; - } - unchecked - { - var hash = 19; - var count = 0; - foreach (var item in items) - { - if (++count > maxItems) - { - break; - } - hash = (hash * 31) + (item?.GetHashCode() ?? 0); - } - return hash; - } - } } diff --git a/tests/Core/DataGrid/FluentDataGridIsFixedTests.razor b/tests/Core/DataGrid/FluentDataGridIsFixedTests.razor new file mode 100644 index 0000000000..e29315c110 --- /dev/null +++ b/tests/Core/DataGrid/FluentDataGridIsFixedTests.razor @@ -0,0 +1,168 @@ +@using Xunit +@inherits TestContext + +@code { + public FluentDataGridIsFixedTests() + { + var dataGridModule = JSInterop.SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/DataGrid/FluentDataGrid.razor.js"); + dataGridModule.SetupModule("init", _ => true); + + // Register services + Services.AddSingleton(LibraryConfiguration.ForUnitTests); + Services.AddScoped(factory => new KeyCodeService()); + } + + [Fact] + public void FluentDataGrid_IsFixed_Default_Value_Is_False() + { + // Arrange && Act + var cut = Render>( + @ + + + + ); + + // Assert + var dataGrid = cut.Instance; + Assert.False(dataGrid.IsFixed); + } + + [Fact] + public void FluentDataGrid_IsFixed_Can_Be_Set_To_True() + { + // Arrange && Act + var cut = Render>( + @ + + + + ); + + // Assert + var dataGrid = cut.Instance; + Assert.True(dataGrid.IsFixed); + } + + [Fact] + public async Task FluentDataGrid_IsFixed_True_Allows_Data_Changes_Without_Automatic_Refresh() + { + // Arrange + var items = GetCustomers().AsQueryable(); + + var cut = Render>( + @ + + + + ); + + var dataGrid = cut.Instance; + + // Act - Update items (simulating data change) + var newItems = GetCustomers().Concat(new[] { new Customer(4, "New Customer") }).AsQueryable(); + cut.SetParametersAndRender(parameters => parameters + .Add(p => p.Items, newItems)); + + // Assert - With IsFixed=true, the grid should still work correctly + Assert.True(dataGrid.IsFixed); + } + + [Fact] + public async Task FluentDataGrid_IsFixed_True_Still_Allows_Pagination() + { + // Arrange + var pagination = new PaginationState { ItemsPerPage = 2 }; + + var cut = Render>( + @ + + + + ); + + // Act - Change pagination + await cut.InvokeAsync(() => pagination.SetCurrentPageIndexAsync(1)); + + // Assert - Should still work with IsFixed=true + Assert.Equal(1, pagination.CurrentPageIndex); + } + + [Fact] + public async Task FluentDataGrid_IsFixed_False_Allows_Normal_Refresh_Behavior() + { + // Arrange + var refreshCallCount = 0; + async ValueTask> GetItems(GridItemsProviderRequest request) + { + refreshCallCount++; + await Task.Delay(1); // Simulate async work + return GridItemsProviderResult.From( + GetCustomers().ToArray(), + GetCustomers().Count()); + } + + var cut = Render>( + @ + + + + ); + + // Wait for initial load + await Task.Delay(100); + var dataGrid = cut.Instance; + + // Act - Explicitly refresh + await cut.InvokeAsync(() => dataGrid.RefreshDataAsync(force: true)); + await Task.Delay(100); + + // Assert - With IsFixed=false, explicit refresh should still work + Assert.True(refreshCallCount >= 2, + $"Expected at least 2 refresh calls (initial + explicit). Got {refreshCallCount} calls."); + } + + [Fact] + public async Task FluentDataGrid_IsFixed_True_Still_Allows_Explicit_Refresh() + { + // Arrange + var refreshCallCount = 0; + async ValueTask> GetItems(GridItemsProviderRequest request) + { + refreshCallCount++; + await Task.Delay(1); // Simulate async work + return GridItemsProviderResult.From( + GetCustomers().ToArray(), + GetCustomers().Count()); + } + + var cut = Render>( + @ + + + + ); + + // Wait for initial load + await Task.Delay(100); + var dataGrid = cut.Instance; + + // Act - Explicitly refresh even with IsFixed=true + await cut.InvokeAsync(() => dataGrid.RefreshDataAsync(force: true)); + await Task.Delay(100); + + // Assert - Explicit refresh should still work with IsFixed=true + Assert.True(refreshCallCount >= 2, + $"Expected at least 2 refresh calls (initial + explicit). Got {refreshCallCount} calls."); + } + + // Sample data... + private IEnumerable GetCustomers() + { + yield return new Customer(1, "Denis Voituron"); + yield return new Customer(2, "Vincent Baaij"); + yield return new Customer(3, "Bill Gates"); + } + + private record Customer(int Id, string Name); +}