diff --git a/.github/workflows/build-core-lib.yml b/.github/workflows/build-core-lib.yml
index 2d5de67676..af768943ae 100644
--- a/.github/workflows/build-core-lib.yml
+++ b/.github/workflows/build-core-lib.yml
@@ -64,6 +64,11 @@ jobs:
dotnet-version: 10.0.x
dotnet-quality: preview
+ - name: Setup .NET 11.0
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 11.0.100-preview.7.26381.103
+
# Build
- name: Restore
@@ -92,6 +97,10 @@ jobs:
done
working-directory: ${{ github.workspace }}
+ - name: Tests .NET 11.0
+ run: dotnet test ./tests/Core/Components.Tests.csproj --report-trx --results-directory ./TestResults --verbosity normal --configuration Release /p:ExampleNetVersion=net11.0
+ working-directory: ${{ github.workspace }}
+
- name: Publish Unit Tests
if: success() || failure()
continue-on-error: true
@@ -229,6 +238,11 @@ jobs:
dotnet-version: 10.0.x
dotnet-quality: preview
+ - name: Setup .NET 11.0
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 11.0.100-preview.7.26381.103
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
diff --git a/Directory.Build.props b/Directory.Build.props
index 9b75e6d4cd..6759ff2d0f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -22,9 +22,12 @@
- net8.0;net9.0;net10.0
+ net8.0;net9.0;net10.0;net11.0
$(NetVersion)
+
+ $(NoWarn);BL0016
+
5.0.0
5.0.0
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 86c7f18b95..b069bcfe39 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -122,10 +122,8 @@
-
-
diff --git a/_ListOutdatedDependencies.ps1 b/_ListOutdatedDependencies.ps1
index 10245dc8b3..a99f3b5a01 100644
--- a/_ListOutdatedDependencies.ps1
+++ b/_ListOutdatedDependencies.ps1
@@ -5,7 +5,7 @@
.DESCRIPTION
Temporarily switches the property in Directory.Build.props to each of
- net8.0, net9.0 and net10.0, runs `dotnet outdated` for the solution, and
+ net8.0, net9.0, net10.0 and net11.0, runs `dotnet outdated` for the solution, and
aggregates the results into a single de-duplicated summary list.
Also lists outdated npm packages (via npm-check-updates) for the Core.Scripts and
Charts.Scripts projects, without changing any package.json or lock file.
@@ -24,7 +24,7 @@ $ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot
$propsPath = Join-Path $repoRoot 'Directory.Build.props'
$slnPath = Join-Path $repoRoot 'Microsoft.FluentUI-v5.slnx'
-$netVersions = @('net8.0', 'net9.0', 'net10.0')
+$netVersions = @('net8.0', 'net9.0', 'net10.0', 'net11.0')
$netVersionPattern = 'net\d+\.\d+'
$exampleVersionPattern = 'net\d+\.\d+'
# Demo/Samples/Tests projects require net9.0+ (see ExampleNetVersion comment in Directory.Build.props).
diff --git a/eng/pipelines/build-all-lib.yml b/eng/pipelines/build-all-lib.yml
index 09892817e4..68e941e18c 100644
--- a/eng/pipelines/build-all-lib.yml
+++ b/eng/pipelines/build-all-lib.yml
@@ -142,6 +142,12 @@ extends:
version: 10.0.x
includePreviewVersions: true
+ - task: UseDotNet@2
+ displayName: 'Install .NET 11.0'
+ inputs:
+ version: 11.0.100-preview.7.26381.103
+ includePreviewVersions: true
+
# Set version number (exclude some folders)
- task: PowerShell@2
displayName: 'Versioning $(Build.BuildNumber)'
diff --git a/eng/pipelines/build-core-lib.yml b/eng/pipelines/build-core-lib.yml
index 24f2545a23..4ab757377f 100644
--- a/eng/pipelines/build-core-lib.yml
+++ b/eng/pipelines/build-core-lib.yml
@@ -165,6 +165,12 @@ extends:
version: 10.0.x
includePreviewVersions: true
+ - task: UseDotNet@2
+ displayName: 'Install .NET 11.0'
+ inputs:
+ version: 11.0.100-preview.7.26381.103
+ includePreviewVersions: true
+
# Set version number (exclude some folders)
- task: PowerShell@2
displayName: 'Versioning $(Build.BuildNumber)'
diff --git a/examples/Demo/FluentUI.Demo.Client/DebugPages/EditFormPage.razor b/examples/Demo/FluentUI.Demo.Client/DebugPages/EditFormPage.razor
index 149a3937d7..f9714dd150 100644
--- a/examples/Demo/FluentUI.Demo.Client/DebugPages/EditFormPage.razor
+++ b/examples/Demo/FluentUI.Demo.Client/DebugPages/EditFormPage.razor
@@ -132,7 +132,11 @@
void ValidateForm()
{
+#if NET11_0_OR_GREATER
+ _ = form.EditContext?.ValidateAsync();
+#else
form.EditContext?.Validate();
+#endif
}
void SetFormToValid()
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Pages/DataGridVirtualizePage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Pages/DataGridVirtualizePage.md
index 0b298035f6..b42f1002ef 100644
--- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Pages/DataGridVirtualizePage.md
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Pages/DataGridVirtualizePage.md
@@ -17,3 +17,55 @@ Enabling virtualization is just a matter of passing `Virtualize="true"`. For it
properly and reliably, every row rendered must have the same known height. **This is handled by the `FluentDataGrid` code**
{{ DataGridVirtualize }}
+
+## Positioning and anchoring in .NET 11
+
+When targeting .NET 11, a virtualized grid can open at a specific zero-based row index, scroll to a row
+programmatically, and keep the viewport anchored as provider data changes. Out-of-range indexes are clamped by
+the underlying `Virtualize` component.
+
+An `ItemComparer` should compare stable item identity rather than object references when an items provider
+returns new instances across requests.
+
+```razor
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+
+Back to top
+
+
+
+@code {
+ private FluentDataGrid? _grid;
+
+ private Task ScrollToTopAsync()
+ => _grid!.ScrollToItemAsync(0);
+
+ private sealed class ProductIdComparer : IEqualityComparer
+ {
+ public static ProductIdComparer Instance { get; } = new();
+
+ public bool Equals(Product? first, Product? second)
+ => first?.Id == second?.Id;
+
+ public int GetHashCode(Product product)
+ => product.Id.GetHashCode();
+ }
+}
+```
+
+`InitialItemIndex` is applied only on the first interactive render. Use `ScrollToItemAsync` after the grid has
+rendered for later navigation. Calling it before rendering or while `Virtualize` is disabled throws an
+`InvalidOperationException`. Cancellation and repeated-call behavior are delegated to the underlying virtualizer;
+when calls overlap, the last call wins.
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Drag/Examples/DragDropNested.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Drag/Examples/DragDropNested.razor
index 710356ce10..33c02f8e46 100644
--- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Drag/Examples/DragDropNested.razor
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Drag/Examples/DragDropNested.razor
@@ -175,8 +175,6 @@
targetRow.Columns.Add(source);
}
}
-
- StateHasChanged();
}
private void OnDropElement(FluentDragEventArgs e)
@@ -213,8 +211,6 @@
targetColumn.Elements.Add(source);
}
}
-
- StateHasChanged();
}
public class Form
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/General/Theme/Designer/ThemeDesigner.razor.cs b/examples/Demo/FluentUI.Demo.Client/Documentation/General/Theme/Designer/ThemeDesigner.razor.cs
index e12d627172..90317aa5e9 100644
--- a/examples/Demo/FluentUI.Demo.Client/Documentation/General/Theme/Designer/ThemeDesigner.razor.cs
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/General/Theme/Designer/ThemeDesigner.razor.cs
@@ -94,7 +94,5 @@ private async Task ResetThemeAsync()
await ThemeService.SetThemeToElementAsync(_themePreviewElement, settings);
await ThemeService.SetThemeAsync(settings);
await ThemeService.ClearStoredThemeSettingsAsync();
-
- StateHasChanged();
}
}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Mcp/Examples/AISkillsDownload.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Mcp/Examples/AISkillsDownload.razor
index fab409da8d..2e5f671a94 100644
--- a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Mcp/Examples/AISkillsDownload.razor
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Mcp/Examples/AISkillsDownload.razor
@@ -147,7 +147,6 @@
private async Task DownloadAllAsync()
{
_downloadingAll = true;
- StateHasChanged();
try
{
@@ -161,7 +160,6 @@
finally
{
_downloadingAll = false;
- StateHasChanged();
}
}
diff --git a/examples/Demo/FluentUI.Demo/Program.cs b/examples/Demo/FluentUI.Demo/Program.cs
index 1802348295..da00593f53 100644
--- a/examples/Demo/FluentUI.Demo/Program.cs
+++ b/examples/Demo/FluentUI.Demo/Program.cs
@@ -39,7 +39,9 @@
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
+#if !NET11_0_OR_GREATER
app.UseWebAssemblyDebugging();
+#endif
}
else
{
diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs
index 50e2ccf4d1..35aaf04704 100644
--- a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs
+++ b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs
@@ -81,7 +81,6 @@ protected override async Task OnInitializedAsync()
{
HttpClient.BaseAddress ??= new Uri(NavigationManager.BaseUri);
await McpDocumentationService.LoadAsync(HttpClient, "/mcp-documentation.json").ConfigureAwait(true);
- StateHasChanged();
}
}
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor b/src/Core/Components/DataGrid/FluentDataGrid.razor
index b11ca8b6c3..72d898e48a 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor
@@ -66,14 +66,7 @@
}
else
{
-
+ @((RenderFragment)RenderVirtualize)
}
}
else
@@ -91,6 +84,29 @@
@code {
+ // RenderTreeBuilder is required while Virtualize parameters are target-framework conditional.
+ // Replace this with Razor markup when older targets no longer require these #if directives.
+ private void RenderVirtualize(RenderTreeBuilder builder)
+ {
+ builder.OpenComponent>(0);
+ builder.AddAttribute(1, nameof(Virtualize<(int, TGridItem)>.ItemSize), ItemSize);
+ builder.AddAttribute(2, nameof(Virtualize<(int, TGridItem)>.OverscanCount), OverscanCount);
+#if NET9_0_OR_GREATER
+ builder.AddAttribute(3, nameof(Virtualize<(int, TGridItem)>.MaxItemCount), MaxItemCount);
+#endif
+#if NET11_0_OR_GREATER
+ builder.AddAttribute(4, nameof(Virtualize<(int, TGridItem)>.InitialItemIndex), InitialItemIndex);
+ builder.AddAttribute(5, nameof(Virtualize<(int, TGridItem)>.AnchorMode), AnchorMode);
+ builder.AddAttribute(6, nameof(Virtualize<(int, TGridItem)>.ItemComparer), _virtualizeItemComparer);
+#endif
+ builder.AddAttribute(7, nameof(Virtualize<(int, TGridItem)>.ItemsProvider), (ItemsProviderDelegate<(int, TGridItem)>)ProvideVirtualizedItemsAsync);
+ builder.AddAttribute(8, nameof(Virtualize<(int, TGridItem)>.ItemContent), (RenderFragment<(int RowIndex, TGridItem Data)>)(item => contentBuilder => RenderRow(contentBuilder, item.RowIndex, item.Data)));
+ builder.AddAttribute(9, nameof(Virtualize<(int, TGridItem)>.Placeholder), (RenderFragment)(placeholderContext => contentBuilder => RenderPlaceholderRow(contentBuilder, placeholderContext)));
+ builder.AddAttribute(10, nameof(Virtualize<(int, TGridItem)>.SpacerElement), "tr");
+ builder.AddComponentReferenceCapture(11, value => _virtualizeComponent = (Virtualize<(int, TGridItem)>)value);
+ builder.CloseComponent();
+ }
+
private void RenderNonVirtualizedRows(RenderTreeBuilder __builder)
{
var initialRowIndex = (GenerateHeader != DataGridGeneratedHeaderType.None) ? 2 : 1; // aria-rowindex is 1-based, plus 1 if there is a header
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs
index d76bba9ef5..b9bfab7e57 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs
@@ -34,10 +34,35 @@ private enum ColumnHeaderUiKind
Resize,
}
+#if NET11_0_OR_GREATER
+ // Adapts ItemComparer to the virtualizer's (row index, item) shape while comparing item identity only.
+ // Reading from the owner on each call ensures parameter updates take effect.
+ private sealed class VirtualizeItemComparer(FluentDataGrid owner) : IEqualityComparer<(int, TGridItem)>
+ {
+ private readonly FluentDataGrid _owner = owner;
+
+ private IEqualityComparer ItemComparer => _owner._itemComparer ?? EqualityComparer.Default;
+
+ public bool Equals((int, TGridItem) first, (int, TGridItem) second)
+ {
+ return ItemComparer.Equals(first.Item2, second.Item2);
+ }
+
+ public int GetHashCode((int, TGridItem) item)
+ {
+ return item.Item2 is null ? 0 : ItemComparer.GetHashCode(item.Item2);
+ }
+ }
+#endif
+
private const string JAVASCRIPT_FILE = FluentJSModule.JAVASCRIPT_ROOT + "DataGrid/FluentDataGrid.razor.js";
private ElementReference? _gridReference;
private Virtualize<(int, TGridItem)>? _virtualizeComponent;
+#if NET11_0_OR_GREATER
+ private IEqualityComparer? _itemComparer;
+ private readonly IEqualityComparer<(int, TGridItem)> _virtualizeItemComparer;
+#endif
private IAsyncQueryExecutor? _asyncQueryExecutor;
private AsyncServiceScope? _scope;
private bool _asyncQueryExecuted;
@@ -94,6 +119,9 @@ public FluentDataGrid(LibraryConfiguration configuration) : base(configuration)
_renderEmptyContent = RenderEmptyContent;
_renderLoadingContent = RenderLoadingContent;
_renderErrorContent = RenderErrorContent;
+ #if NET11_0_OR_GREATER
+ _virtualizeItemComparer = new VirtualizeItemComparer(this);
+ #endif
// As a special case, we don't issue the first data load request until we've collected the initial set of columns
// This is so we can apply default sort order (or any future per-column options) before loading data
@@ -169,6 +197,15 @@ public FluentDataGrid(LibraryConfiguration configuration) : base(configuration)
[Parameter]
public int OverscanCount { get; set; } = 3;
+#if NET9_0_OR_GREATER
+ ///
+ /// This is applicable only when using . It defines the maximum number of items
+ /// that can be rendered, even when the viewport would otherwise require more items.
+ ///
+ [Parameter]
+ public int MaxItemCount { get; set; } = 100;
+#endif
+
///
/// This is applicable only when using . It defines an expected height in pixels for
/// each row, allowing the virtualization mechanism to fetch the correct number of items to match the display
@@ -177,6 +214,39 @@ public FluentDataGrid(LibraryConfiguration configuration) : base(configuration)
[Parameter]
public float ItemSize { get; set; } = 32;
+#if NET11_0_OR_GREATER
+ ///
+ /// This is applicable only when using . Gets or sets the zero-based index of the
+ /// row to scroll to on first interactive render. Applied once when the grid first knows its item count and
+ /// ignored on subsequent re-renders; to scroll programmatically at any later point, call
+ /// . Out-of-range values are clamped. The default value,
+ /// 0, means no initial scroll.
+ ///
+ [Parameter]
+ public int InitialItemIndex { get; set; }
+
+ ///
+ /// This is applicable only when using . Gets or sets the anchor mode that controls
+ /// how the viewport behaves at the edges of the list
+ /// when new items arrive during virtualization. The default is .
+ ///
+ [Parameter]
+ public VirtualizeAnchorMode AnchorMode { get; set; } = VirtualizeAnchorMode.Start;
+
+ ///
+ /// This is applicable only when using . Gets or sets a comparer used during
+ /// virtualization to detect whether items were prepended or appended between data loads.
+ /// Provide a comparer that compares items by a stable unique identifier.
+ /// Defaults to .
+ ///
+ [Parameter]
+ public IEqualityComparer? ItemComparer
+ {
+ get => _itemComparer;
+ set => _itemComparer = value;
+ }
+#endif
+
///
/// If true, renders draggable handles around the column headers and adds a button to invoke a resize UI.
/// This allows the user to resize columns manually. Size changes are not persisted.
@@ -1336,6 +1406,30 @@ public async Task RefreshDataAsync(bool force = false)
await RefreshDataCoreAsync();
}
+#if NET11_0_OR_GREATER
+ ///
+ /// Scrolls the virtualized grid so the row at is aligned to the top of the scrollable area.
+ ///
+ ///
+ /// Each call cancels any previously-running operation.
+ /// Must be called on the renderer's synchronization context; background-thread callers should wrap with
+ /// .
+ ///
+ /// The zero-based index of the row to scroll to.
+ /// A token that lets the caller request cancellation.
+ /// A that completes when the target is aligned or superseded by another call.
+ public Task ScrollToItemAsync(int itemIndex, CancellationToken cancellationToken = default)
+ {
+ if (!Virtualize || _virtualizeComponent is null)
+ {
+ throw new InvalidOperationException(
+ $"{nameof(ScrollToItemAsync)} can only be used when {nameof(Virtualize)} is enabled and the grid has been rendered.");
+ }
+
+ return _virtualizeComponent.ScrollToItemAsync(itemIndex, cancellationToken);
+ }
+#endif
+
// Same as RefreshDataAsync, except without forcing a re-render. We use this from OnParametersSetAsync
// because in that case there's going to be a re-render anyway.
[SuppressMessage("Design", "MA0051:Method is too long", Justification = "Not going to do artificial optimization because of some random arbitrary determined line count number")]
diff --git a/src/Core/Components/PullToRefresh/FluentPullToRefresh.razor.cs b/src/Core/Components/PullToRefresh/FluentPullToRefresh.razor.cs
index d370b45dc1..89efa65e17 100644
--- a/src/Core/Components/PullToRefresh/FluentPullToRefresh.razor.cs
+++ b/src/Core/Components/PullToRefresh/FluentPullToRefresh.razor.cs
@@ -300,6 +300,7 @@ private async Task OnTouchEndAsync(TouchEventArgs e)
if (!hasMoreData)
{
SetPullStatus(PullStatus.NoData);
+ StateHasChanged();
await Task.Delay(StatusUpdateMessageTimeout);
}
else
diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs
index a7c99d9208..d25977a890 100644
--- a/src/Core/Components/Wizard/FluentWizard.razor.cs
+++ b/src/Core/Components/Wizard/FluentWizard.razor.cs
@@ -208,8 +208,6 @@ protected virtual async Task OnNextHandlerAsync(MouseEventArgs e)
{
await ValueChanged.InvokeAsync(Value);
}
-
- StateHasChanged();
}
}
@@ -235,8 +233,6 @@ protected virtual async Task OnPreviousHandlerAsync(MouseEventArgs e)
{
await ValueChanged.InvokeAsync(Value);
}
-
- StateHasChanged();
}
}
diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.cs b/src/Core/Components/Wizard/FluentWizardStep.razor.cs
index 367a9b207a..487e190c01 100644
--- a/src/Core/Components/Wizard/FluentWizardStep.razor.cs
+++ b/src/Core/Components/Wizard/FluentWizardStep.razor.cs
@@ -196,6 +196,8 @@ public void RegisterEditFormAndContext(EditForm editForm, EditContext editContex
///
/// Validates all registered EditContexts.
///
+#pragma warning disable CS0618
+ // TODO: Make this method asynchronous and use EditContext.ValidateAsync when the public API can be changed.
public bool ValidateEditContexts()
{
var isValid = true;
@@ -219,6 +221,7 @@ public bool ValidateEditContexts()
return isValid;
}
+#pragma warning restore CS0618
///
public override async ValueTask DisposeAsync()
diff --git a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj
index b4a583a111..8ff739757d 100644
--- a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj
+++ b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj
@@ -88,9 +88,7 @@
-
-
-
+
all
diff --git a/src/Tools/McpServer/Services/DocumentationService.cs b/src/Tools/McpServer/Services/DocumentationService.cs
index efd2860b77..5a32f3399d 100644
--- a/src/Tools/McpServer/Services/DocumentationService.cs
+++ b/src/Tools/McpServer/Services/DocumentationService.cs
@@ -220,7 +220,7 @@ private static string ExtractSummary(string content)
// Remove headers and get first meaningful paragraph
var lines = content.Split('\n')
.Select(l => l.Trim())
- .Where(l => !string.IsNullOrEmpty(l) && !l.StartsWith('#'))
+ .Where(l => !string.IsNullOrEmpty(l) && !l.StartsWith("#", StringComparison.Ordinal))
.Take(3);
var summary = string.Join(' ', lines);
diff --git a/src/Tools/McpServer/Services/MigrationService.cs b/src/Tools/McpServer/Services/MigrationService.cs
index d12f09dcc1..ab27cda30c 100644
--- a/src/Tools/McpServer/Services/MigrationService.cs
+++ b/src/Tools/McpServer/Services/MigrationService.cs
@@ -220,7 +220,7 @@ private static string ExtractSummary(string content)
{
var lines = content.Split('\n')
.Select(l => l.Trim())
- .Where(l => !string.IsNullOrEmpty(l) && !l.StartsWith('#'))
+ .Where(l => !string.IsNullOrEmpty(l) && !l.StartsWith("#", StringComparison.Ordinal))
.Take(3);
var summary = string.Join(' ', lines);
diff --git a/tests/Core/Components.Tests.csproj b/tests/Core/Components.Tests.csproj
index ca1112552c..ac0dcb9136 100644
--- a/tests/Core/Components.Tests.csproj
+++ b/tests/Core/Components.Tests.csproj
@@ -10,7 +10,7 @@
false
Microsoft.FluentUI.AspNetCore.Components.Tests
Microsoft.FluentUI.AspNetCore.Components.Tests
- xUnit1069
+ xUnit1069;BL0016
diff --git a/tests/Core/Components/DataGrid/FluentDataGridTests.razor b/tests/Core/Components/DataGrid/FluentDataGridTests.razor
index 2e73f2b3f9..f7f57e4fe5 100644
--- a/tests/Core/Components/DataGrid/FluentDataGridTests.razor
+++ b/tests/Core/Components/DataGrid/FluentDataGridTests.razor
@@ -369,6 +369,152 @@
builder.AddContent(0, text);
};
+#if NET9_0_OR_GREATER
+ [Fact]
+ public void FluentDataGrid_Virtualize_SetsMaxItemCount()
+ {
+ // Arrange && Act
+ var cut = Render>(
+ @
+
+ );
+
+ // Assert
+ var virtualize = cut.FindComponent>();
+ Assert.Equal(42, virtualize.Instance.MaxItemCount);
+ }
+#endif
+
+#if NET11_0_OR_GREATER
+ [Fact]
+ public void FluentDataGrid_Virtualize_SetsNet11VirtualizationParameters()
+ {
+ // Arrange
+ var customers = GetCustomers().ToArray();
+ var itemComparer = EqualityComparer.Create(
+ (first, second) => first?.Id == second?.Id,
+ customer => customer.Id);
+ GridItemsProvider itemsProvider = request =>
+ ValueTask.FromResult(GridItemsProviderResult.From(
+ customers
+ .Skip(request.StartIndex)
+ .Take(request.Count ?? customers.Length)
+ .Select(customer => customer with { })
+ .ToArray(),
+ customers.Length));
+
+ // Act
+ var cut = Render>(
+ @
+
+ );
+
+ // Assert
+ var virtualize = cut.FindComponent>().Instance;
+ var firstCustomer = customers[0];
+ var equivalentCustomer = firstCustomer with { };
+ var differentCustomer = customers[1];
+
+ Assert.Equal(2, virtualize.InitialItemIndex);
+ Assert.Equal(VirtualizeAnchorMode.End, virtualize.AnchorMode);
+ Assert.True(virtualize.ItemComparer.Equals((1, firstCustomer), (99, equivalentCustomer)));
+ Assert.False(virtualize.ItemComparer.Equals((1, firstCustomer), (1, differentCustomer)));
+ }
+
+ [Fact]
+ public void FluentDataGrid_ScrollToItemAsync_VirtualizationDisabled_ThrowsInvalidOperationException()
+ {
+ // Arrange
+ var cut = Render>(
+ @
+
+ );
+
+ // Act
+ var exception = Assert.Throws(() =>
+ {
+ _ = cut.Instance.ScrollToItemAsync(1, Xunit.TestContext.Current.CancellationToken);
+ });
+
+ // Assert
+ Assert.Contains(nameof(FluentDataGrid.Virtualize), exception.Message);
+ }
+
+ [Fact]
+ public void FluentDataGrid_ScrollToItemAsync_VirtualizerNotRendered_ThrowsInvalidOperationException()
+ {
+ // Arrange
+ GridItemsProvider itemsProvider = request =>
+ ValueTask.FromResult(GridItemsProviderResult.From(Array.Empty(), 0));
+
+ var cut = Render>(
+ @
+
+ );
+
+ // Act
+ var exception = Assert.Throws(() =>
+ {
+ _ = cut.Instance.ScrollToItemAsync(1, Xunit.TestContext.Current.CancellationToken);
+ });
+
+ // Assert
+ Assert.Contains("grid has been rendered", exception.Message);
+ }
+
+ [Fact]
+ public async Task FluentDataGrid_ScrollToItemAsync_CanceledToken_ThrowsOperationCanceledException()
+ {
+ // Arrange
+ FluentDataGrid? grid = null;
+ var cut = Render>(
+ @
+
+ );
+ using var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(Xunit.TestContext.Current.CancellationToken);
+ await cancellationSource.CancelAsync();
+
+ // Act && Assert
+ await Assert.ThrowsAnyAsync(async () =>
+ await cut.InvokeAsync(() => grid!.ScrollToItemAsync(10, cancellationSource.Token)));
+ }
+
+ [Fact]
+ public async Task FluentDataGrid_ScrollToItemAsync_RepeatedCalls_BothCallsCompleteSuccessfully()
+ {
+ // Arrange
+ FluentDataGrid? grid = null;
+ var cut = Render>(
+ @
+
+ );
+ Task? firstCall = null;
+ Task? secondCall = null;
+
+ // Act
+ await cut.InvokeAsync(() =>
+ {
+ firstCall = grid!.ScrollToItemAsync(500, Xunit.TestContext.Current.CancellationToken);
+ secondCall = grid.ScrollToItemAsync(750, Xunit.TestContext.Current.CancellationToken);
+ });
+
+ await firstCall!.WaitAsync(TimeSpan.FromSeconds(5), Xunit.TestContext.Current.CancellationToken);
+ await secondCall!.WaitAsync(TimeSpan.FromSeconds(5), Xunit.TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.True(firstCall.IsCompletedSuccessfully);
+ Assert.True(secondCall.IsCompletedSuccessfully);
+ }
+#endif
+
[Fact]
public async Task FluentDataGrid_Virtualize()
{
diff --git a/tests/Core/Components/PullToRefresh/FluentPullToRefreshTests.razor b/tests/Core/Components/PullToRefresh/FluentPullToRefreshTests.razor
index 003674ddc1..7362143407 100644
--- a/tests/Core/Components/PullToRefresh/FluentPullToRefreshTests.razor
+++ b/tests/Core/Components/PullToRefresh/FluentPullToRefreshTests.razor
@@ -338,13 +338,16 @@
public async Task FluentPullToRefresh_OnTouchEnd_WithMoreData_CompletesThenResets()
{
var refreshCalls = 0;
+ var refreshStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var refreshCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var cut = Render(parameters => parameters
.Add(p => p.EmulateTouch, false)
.Add(p => p.StatusUpdateMessageTimeout, 10)
- .Add(p => p.OnRefreshAsync, () =>
+ .Add(p => p.OnRefreshAsync, async () =>
{
refreshCalls++;
- return Task.FromResult(true);
+ refreshStarted.SetResult(true);
+ return await refreshCompletion.Task;
})
.Add(p => p.ChildContent, builder => builder.AddContent(0, "Content")));
@@ -352,7 +355,10 @@
var container = cut.Find("div.fluent-pull-container");
var touchEndTask = container.TriggerEventAsync("ontouchend", CreateTouchEventArgs(0));
- await cut.WaitForAssertionAsync(() => Assert.Equal(PullStatus.Completed, GetPullStatus(cut)));
+ await refreshStarted.Task;
+ var completedStatusTask = cut.WaitForAssertionAsync(() => Assert.Equal(PullStatus.Completed, GetPullStatus(cut)));
+ refreshCompletion.SetResult(true);
+ await completedStatusTask;
await touchEndTask;
@@ -364,17 +370,26 @@
[Fact]
public async Task FluentPullToRefresh_OnTouchEnd_NoMoreData_ShowsNoDataBeforeReset()
{
+ var refreshStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var refreshCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var cut = Render(parameters => parameters
.Add(p => p.EmulateTouch, false)
.Add(p => p.StatusUpdateMessageTimeout, 10)
- .Add(p => p.OnRefreshAsync, () => Task.FromResult(false))
+ .Add(p => p.OnRefreshAsync, async () =>
+ {
+ refreshStarted.SetResult(true);
+ return await refreshCompletion.Task;
+ })
.Add(p => p.ChildContent, builder => builder.AddContent(0, "Content")));
await SetDistanceAsync(cut, 80);
var container = cut.Find("div.fluent-pull-container");
var touchEndTask = container.TriggerEventAsync("ontouchend", CreateTouchEventArgs(0));
- await cut.WaitForAssertionAsync(() => Assert.Equal(PullStatus.NoData, GetPullStatus(cut)));
+ await refreshStarted.Task;
+ var noDataStatusTask = cut.WaitForAssertionAsync(() => Assert.Equal(PullStatus.NoData, GetPullStatus(cut)));
+ refreshCompletion.SetResult(false);
+ await noDataStatusTask;
await touchEndTask;
diff --git a/tests/Core/Components/Wizard/FluentWizardAdvancedTests.razor b/tests/Core/Components/Wizard/FluentWizardAdvancedTests.razor
index ea194acfa1..da9edc6cd2 100644
--- a/tests/Core/Components/Wizard/FluentWizardAdvancedTests.razor
+++ b/tests/Core/Components/Wizard/FluentWizardAdvancedTests.razor
@@ -66,7 +66,9 @@
// Act 2: make model valid, then finish again
model.Name = "John";
+ #pragma warning disable CS0618
await cut.InvokeAsync(() => editContext.Validate());
+ #pragma warning restore CS0618
await cut.InvokeAsync(() => wizard.FinishAsync(validateEditContexts: true));
// Assert 2
@@ -219,7 +221,9 @@
// Act: valid model
model.Name = "John";
+ #pragma warning disable CS0618
await cut.InvokeAsync(() => editContext.Validate());
+ #pragma warning restore CS0618
await cut.InvokeAsync(() => wizard.FinishAsync(validateEditContexts: true));
// Assert valid path