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 @@ -1954,7 +1954,7 @@
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ResizeColumnOnAllRows">
<summary>
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.
</summary>
</member>
Expand Down Expand Up @@ -2140,6 +2140,13 @@
Gets or sets a value indicating whether the grids' first cell should be focused.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.IsFixed">
<summary>
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.
</summary>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.#ctor">
<summary>
Constructs an instance of <see cref="T:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1"/>.
Expand Down Expand Up @@ -2283,12 +2290,6 @@
</summary>
<returns></returns>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ComputeItemsHash(System.Collections.Generic.IEnumerable{`0},System.Int32)">
<summary>
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.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGridCell`1.Item">
<summary>
Gets or sets the reference to the item that holds this cell's values.
Expand Down
63 changes: 30 additions & 33 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve

/// <summary>
/// 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.
/// </summary>
[Parameter]
Expand Down Expand Up @@ -335,6 +335,14 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
[Parameter]
public bool AutoFocus { get; set; } = false;

/// <summary>
/// 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.
/// </summary>
[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;
Expand Down Expand Up @@ -382,7 +390,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
// things have changed, and to discard earlier load attempts that were superseded.
private PaginationState? _lastRefreshedPaginationState;
private IQueryable<TGridItem>? _lastAssignedItems;
private int _lastAssignedItemsHashCode;

private GridItemsProvider<TGridItem>? _lastAssignedItemsProvider;
private CancellationTokenSource? _pendingDataLoadCancellationTokenSource;

Expand Down Expand Up @@ -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<TGridItem>.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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1115,31 +1138,5 @@ public async Task ResetColumnWidthsAsync()
await Module.InvokeVoidAsync("resetColumnWidths", _gridReference);
}
}

/// <summary>
/// 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.
/// </summary>
private static int ComputeItemsHash(IEnumerable<TGridItem>? 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;
}
}
}

168 changes: 168 additions & 0 deletions tests/Core/DataGrid/FluentDataGridIsFixedTests.razor
Original file line number Diff line number Diff line change
@@ -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<IKeyCodeService>(factory => new KeyCodeService());
}

[Fact]
public void FluentDataGrid_IsFixed_Default_Value_Is_False()
{
// Arrange && Act
var cut = Render<FluentDataGrid<Customer>>(
@<FluentDataGrid Items="@GetCustomers().AsQueryable()">
<ChildContent>
<PropertyColumn Property="@(x => x.Name)" />
</ChildContent>
</FluentDataGrid>);

// Assert
var dataGrid = cut.Instance;
Assert.False(dataGrid.IsFixed);
}

[Fact]
public void FluentDataGrid_IsFixed_Can_Be_Set_To_True()
{
// Arrange && Act
var cut = Render<FluentDataGrid<Customer>>(
@<FluentDataGrid Items="@GetCustomers().AsQueryable()" IsFixed="true">
<ChildContent>
<PropertyColumn Property="@(x => x.Name)" />
</ChildContent>
</FluentDataGrid>);

// 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<FluentDataGrid<Customer>>(
@<FluentDataGrid Items="@items" IsFixed="true">
<ChildContent>
<PropertyColumn Property="@(i => i.Name)" />
</ChildContent>
</FluentDataGrid>);

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<FluentDataGrid<Customer>>(
@<FluentDataGrid Items="@GetCustomers().AsQueryable()" IsFixed="true" Pagination="@pagination">
<ChildContent>
<PropertyColumn Property="@(i => i.Name)" />
</ChildContent>
</FluentDataGrid>);

// 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<GridItemsProviderResult<Customer>> GetItems(GridItemsProviderRequest<Customer> request)
{
refreshCallCount++;
await Task.Delay(1); // Simulate async work
return GridItemsProviderResult.From(
GetCustomers().ToArray(),
GetCustomers().Count());
}

var cut = Render<FluentDataGrid<Customer>>(
@<FluentDataGrid TGridItem="Customer" ItemsProvider="@GetItems" IsFixed="false">
<ChildContent>
<PropertyColumn Property="@(i => i.Name)" />
</ChildContent>
</FluentDataGrid>);

// 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<GridItemsProviderResult<Customer>> GetItems(GridItemsProviderRequest<Customer> request)
{
refreshCallCount++;
await Task.Delay(1); // Simulate async work
return GridItemsProviderResult.From(
GetCustomers().ToArray(),
GetCustomers().Count());
}

var cut = Render<FluentDataGrid<Customer>>(
@<FluentDataGrid TGridItem="Customer" ItemsProvider="@GetItems" IsFixed="true">
<ChildContent>
<PropertyColumn Property="@(i => i.Name)" />
</ChildContent>
</FluentDataGrid>);

// 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<Customer> 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);
}
Loading