From 1d9c7a7ac2b301ad43664a123d3fcfd1c4c547c0 Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Tue, 17 Feb 2026 17:58:16 +0100 Subject: [PATCH 01/14] Add QuickGrid SSR support for pagination and sorting Persist pagination and sort state in URL query string parameters so QuickGrid works without interactivity. In SSR mode, buttons render as enhanced forms with antiforgery tokens instead of @onclick handlers. --- .../src/Columns/ColumnBase.razor | 24 ++- .../src/Columns/ColumnBase.razor.cs | 4 + ...oft.AspNetCore.Components.QuickGrid.csproj | 1 + .../src/Pagination/PaginationState.cs | 14 +- .../src/Pagination/Paginator.razor | 45 +++- .../src/Pagination/Paginator.razor.cs | 92 ++++++++- .../src/PublicAPI.Unshipped.txt | 7 + .../src/QuickGrid.razor.cs | 139 ++++++++++++- .../Tests/QuickGridNoInteractivityTest.cs | 193 ++++++++++++++++++ .../Components.TestServer.csproj | 1 + .../Pages/QuickGrid/QuickGridComponent.razor | 101 +++++++++ 11 files changed, 599 insertions(+), 22 deletions(-) create mode 100644 src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor index f9d50dfdc2b7..01bcc886de60 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor @@ -1,10 +1,11 @@ @using Microsoft.AspNetCore.Components.Rendering @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.Forms @namespace Microsoft.AspNetCore.Components.QuickGrid @typeparam TGridItem @{ - InternalGridContext.Grid.AddColumn(this, InitialSortDirection, IsDefaultSortColumn); + ColumnIndex = InternalGridContext.Grid.AddColumn(this, InitialSortDirection, IsDefaultSortColumn); } @code @@ -24,10 +25,23 @@ if (Sortable.HasValue ? Sortable.Value : IsSortableByDefault()) { - + if (RendererInfo.IsInteractive) + { + + } + else + { +
+ + + + } } else { diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs index fec920a2c092..aefc0e233051 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs @@ -105,6 +105,10 @@ public abstract partial class ColumnBase /// True if the column should be sortable by default, otherwise false. protected virtual bool IsSortableByDefault() => false; + internal int ColumnIndex { get; set; } + + private string FormName => $"{Grid.QueryName}_col{ColumnIndex}_sort_form"; + /// /// Constructs an instance of . /// diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj index 8603bc6d164e..e8dab12adc70 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj @@ -17,6 +17,7 @@ <_CurrentProjectDiscoveredScopedCssFiles Include="@(ThemeCssFiles)" RelativePath="%(Identity)" BasePath="_content/$(AssemblyName)" /> + diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs index 2da161a7af9a..b566e8fd8b70 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs @@ -38,6 +38,7 @@ public class PaginationState internal EventCallbackSubscribable CurrentPageItemsChanged { get; } = new(); internal EventCallbackSubscribable TotalItemCountChangedSubscribable { get; } = new(); + internal string QueryName { get; set; } = "QuickGrid"; /// public override int GetHashCode() @@ -51,7 +52,18 @@ public override int GetHashCode() /// A representing the completion of the operation. public Task SetCurrentPageIndexAsync(int pageIndex) { - CurrentPageIndex = pageIndex; + if (pageIndex < 0) + { + CurrentPageIndex = 0; + } + else if (LastPageIndex.HasValue && pageIndex > LastPageIndex.Value) + { + CurrentPageIndex = LastPageIndex.Value; + } + else + { + CurrentPageIndex = pageIndex; + } return CurrentPageItemsChanged.InvokeCallbacksAsync(this); } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor index 054b09ba20a5..73ef33bc782c 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor @@ -1,4 +1,5 @@ -@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Web @namespace Microsoft.AspNetCore.Components.QuickGrid
@@ -15,14 +16,40 @@ }
} diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs index bd34bec6272d..26067a9cdad5 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.Components.QuickGrid; @@ -11,6 +13,16 @@ namespace Microsoft.AspNetCore.Components.QuickGrid; public partial class Paginator : IDisposable { private readonly EventCallbackSubscriber _totalItemCountChanged; + private bool _hasReadQueryString; + private bool _suppressNextLocationChange; + + [Inject] + private NavigationManager NavigationManager { get; set; } = default!; + private string QueryName => State.QueryName; + private string FormNameFirst => $"Paginator_{QueryName}_GoFirst"; + private string FormNamePrevious => $"Paginator_{QueryName}_GoPrevious"; + private string FormNameNext => $"Paginator_{QueryName}_GoNext"; + private string FormNameLast => $"Paginator_{QueryName}_GoLast"; /// /// Specifies the associated . This parameter is required. @@ -39,14 +51,84 @@ public Paginator() private bool CanGoBack => State.CurrentPageIndex > 0; private bool CanGoForwards => State.CurrentPageIndex < State.LastPageIndex; - private Task GoToPageAsync(int pageIndex) - => State.SetCurrentPageIndexAsync(pageIndex); + private async Task GoToPageAsync(int pageIndex) + { + int? pageValue = pageIndex == 0 ? null : pageIndex + 1; + var newUri = NavigationManager.GetUriWithQueryParameter(QueryName, pageValue); + await State.SetCurrentPageIndexAsync(pageIndex); + _suppressNextLocationChange = true; + NavigationManager.NavigateTo(newUri); + } /// - protected override void OnParametersSet() - => _totalItemCountChanged.SubscribeOrMove(State.TotalItemCountChangedSubscribable); + protected override void OnInitialized() + { + NavigationManager.LocationChanged += OnLocationChanged; + } + + /// + protected override Task OnParametersSetAsync() + { + _totalItemCountChanged.SubscribeOrMove(State.TotalItemCountChangedSubscribable); + if (!_hasReadQueryString) + { + _hasReadQueryString = true; + var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; + if (pageFromQuery != State.CurrentPageIndex) + { + return State.SetCurrentPageIndexAsync(pageFromQuery); + } + } + return Task.CompletedTask; + } + + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + if (_suppressNextLocationChange) + { + _suppressNextLocationChange = false; + return; + } + var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; + if (pageFromQuery != State.CurrentPageIndex) + { + await InvokeAsync(async () => + { + await State.SetCurrentPageIndexAsync(pageFromQuery); + StateHasChanged(); + }); + } + } + + private int? ReadPageIndexFromQueryString() + { + var uri = NavigationManager.Uri; + var queryStart = uri.IndexOf('?'); + if (queryStart < 0) + { + return null; + } + + var queryEnd = uri.IndexOf('#', queryStart); + var query = uri.AsMemory(queryStart..((queryEnd < 0) ? uri.Length : queryEnd)); + var enumerable = new QueryStringEnumerable(query); + + foreach (var pair in enumerable) + { + if (pair.DecodeName().Span.Equals(QueryName, StringComparison.OrdinalIgnoreCase) + && int.TryParse(pair.DecodeValue().Span, out var page) + && page > 0) + { + return page - 1; + } + } + return null; + } /// public void Dispose() - => _totalItemCountChanged.Dispose(); + { + NavigationManager.LocationChanged -= OnLocationChanged; + _totalItemCountChanged.Dispose(); + } } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt index 8aa8edca4b77..5e66d67d11db 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt @@ -1,3 +1,10 @@ #nullable enable +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryName.get -> string! +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryName.set -> void +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.Dispose() -> void Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnRowClick.get -> Microsoft.AspNetCore.Components.EventCallback Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnRowClick.set -> void +override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnInitialized() -> void +override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnParametersSetAsync() -> System.Threading.Tasks.Task! +override Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnInitialized() -> void +*REMOVED*override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnParametersSet() -> void diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index f8d34dc34925..dec313b71adb 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -3,7 +3,9 @@ using System.Linq; using Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; +using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web.Virtualization; +using Microsoft.AspNetCore.Internal; using Microsoft.JSInterop; using Microsoft.AspNetCore.Components.Forms; @@ -14,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.QuickGrid; /// /// The type of data represented by each row in the grid. [CascadingTypeParameter(nameof(TGridItem))] -public partial class QuickGrid : IAsyncDisposable +public partial class QuickGrid : IAsyncDisposable, IDisposable { /// /// A queryable source of data for the grid. @@ -114,8 +116,17 @@ public partial class QuickGrid : IAsyncDisposable /// [Parameter] public EventCallback OnRowClick { get; set; } + /// + /// Name of the query string parameter used to persist the current page index and sort order in the URL. + /// Defaults to "QuickGrid". The sort parameter is derived by appending "_sort" to this value. + /// When set, this value is propagated to the associated + /// so that connected components such as use the same query parameter name. + /// + [Parameter] public string QueryName { get; set; } = "QuickGrid"; + [Inject] private IServiceProvider Services { get; set; } = default!; [Inject] private IJSRuntime JS { get; set; } = default!; + [Inject] private NavigationManager NavigationManager { get; set; } = default!; private ElementReference _tableReference; private Virtualize<(int, TGridItem)>? _virtualizeComponent; @@ -162,6 +173,12 @@ public partial class QuickGrid : IAsyncDisposable private bool _firstRefreshDataAsync = true; + private bool _hasReadSortFromQueryString; + private bool _suppressNextLocationChange; + private (int ColumnIndex, bool Ascending)? _cachedSortFromQuery; + + private string SortQueryParameterName => $"{QueryName}_sort"; + /// /// Constructs an instance of . /// @@ -181,12 +198,23 @@ public QuickGrid() columnsFirstCollectedSubscriber.SubscribeOrMove(_internalGridContext.ColumnsFirstCollected); } + /// + protected override void OnInitialized() + { + NavigationManager.LocationChanged += OnLocationChanged; + } + /// protected override Task OnParametersSetAsync() { // The associated pagination state may have been added/removed/replaced _currentPageItemsChanged.SubscribeOrMove(Pagination?.CurrentPageItemsChanged); + if (Pagination is { } pagination) + { + pagination.QueryName = QueryName; + } + if (Items is not null && ItemsProvider is not null) { throw new InvalidOperationException($"{nameof(QuickGrid)} requires one of {nameof(Items)} or {nameof(ItemsProvider)}, but both were specified."); @@ -233,24 +261,37 @@ protected override async Task OnAfterRenderAsync(bool firstRender) } // Invoked by descendant columns at a special time during rendering - internal void AddColumn(ColumnBase column, SortDirection? initialSortDirection, bool isDefaultSortColumn) + internal int AddColumn(ColumnBase column, SortDirection? initialSortDirection, bool isDefaultSortColumn) { if (_collectingColumns) { _columns.Add(column); + var columnIndex = _columns.Count - 1; if (isDefaultSortColumn && _sortByColumn is null && initialSortDirection.HasValue) { _sortByColumn = column; _sortByAscending = initialSortDirection.Value != SortDirection.Descending; } + + if (!_hasReadSortFromQueryString + && _cachedSortFromQuery is { } sortFromQuery + && sortFromQuery.ColumnIndex == columnIndex) + { + _sortByColumn = column; + _sortByAscending = sortFromQuery.Ascending; + _hasReadSortFromQueryString = true; + } + return columnIndex; } + return -1; } private void StartCollectingColumns() { _columns.Clear(); _collectingColumns = true; + _cachedSortFromQuery = !_hasReadSortFromQueryString ? ReadSortFromQueryString() : null; } private void FinishCollectingColumns() @@ -276,10 +317,97 @@ public Task SortByColumnAsync(ColumnBase column, SortDirection direct _sortByColumn = column; + UpdateSortQueryString(); + StateHasChanged(); // We want to see the updated sort order in the header, even before the data query is completed return RefreshDataAsync(); } + private void UpdateSortQueryString() + { + string? sortValue = _sortByColumn is not null + ? $"{_columns.IndexOf(_sortByColumn)}_{(_sortByAscending ? "asc" : "desc")}" + : null; + var newUri = NavigationManager.GetUriWithQueryParameter(SortQueryParameterName, sortValue); + _suppressNextLocationChange = true; + NavigationManager.NavigateTo(newUri); + } + + private (int ColumnIndex, bool Ascending)? ReadSortFromQueryString() + { + var uri = NavigationManager.Uri; + var queryStart = uri.IndexOf('?'); + if (queryStart < 0) + { + return null; + } + + var queryEnd = uri.IndexOf('#', queryStart); + var query = uri.AsMemory(queryStart..((queryEnd < 0) ? uri.Length : queryEnd)); + var enumerable = new QueryStringEnumerable(query); + var paramName = SortQueryParameterName; + + foreach (var pair in enumerable) + { + if (pair.DecodeName().Span.Equals(paramName, StringComparison.OrdinalIgnoreCase)) + { + var value = pair.DecodeValue().Span; + var lastUnderscore = value.LastIndexOf('_'); + if (lastUnderscore > 0 && lastUnderscore < value.Length - 1) + { + var indexSpan = value[..lastUnderscore]; + var direction = value[(lastUnderscore + 1)..]; + if (int.TryParse(indexSpan, out var columnIndex) && columnIndex >= 0) + { + if (direction.Equals("asc", StringComparison.OrdinalIgnoreCase)) + { + return (columnIndex, true); + } + else if (direction.Equals("desc", StringComparison.OrdinalIgnoreCase)) + { + return (columnIndex, false); + } + } + } + } + } + return null; + } + + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + if (_suppressNextLocationChange) + { + _suppressNextLocationChange = false; + return; + } + + var sortFromQuery = ReadSortFromQueryString(); + var currentColumnIndex = _sortByColumn is not null ? _columns.IndexOf(_sortByColumn) : -1; + var currentAscending = _sortByAscending; + + if (sortFromQuery is null && _sortByColumn is not null + || sortFromQuery is not null && (sortFromQuery.Value.ColumnIndex != currentColumnIndex + || sortFromQuery.Value.Ascending != currentAscending)) + { + await InvokeAsync(async () => + { + if (sortFromQuery is null) + { + _sortByColumn = null; + } + else if (sortFromQuery.Value.ColumnIndex >= 0 && sortFromQuery.Value.ColumnIndex < _columns.Count) + { + _sortByColumn = _columns[sortFromQuery.Value.ColumnIndex]; + _sortByAscending = sortFromQuery.Value.Ascending; + } + + await RefreshDataCoreAsync(); + StateHasChanged(); + }); + } + } + /// /// Displays the UI for the specified column, closing any other column /// options UI that was previously displayed. @@ -454,9 +582,16 @@ private string GridClass() _ => column.Class, }; + /// + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + } + /// public async ValueTask DisposeAsync() { + Dispose(); _wasDisposed = true; _currentPageItemsChanged.Dispose(); diff --git a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs new file mode 100644 index 000000000000..d46229e75989 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.E2ETesting; +using Xunit.Abstractions; +using OpenQA.Selenium; +using TestServer; + +namespace Microsoft.AspNetCore.Components.E2ETest.Tests; + +public class QuickGridNoInteractivityTest : ServerTestBase>> +{ + public QuickGridNoInteractivityTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + + [Fact] + public void PaginatorDisplaysCorrectItemCount() + { + Navigate($"{ServerPathBase}/quickgrid"); + + var paginator = Browser.FindElement(By.CssSelector(".first-paginator .paginator")); + + var paginatorCount = paginator.FindElement(By.CssSelector("div > strong")).Text; + var currentPageNumber = paginator.FindElement(By.CssSelector("nav > div > strong:nth-child(1)")).Text; + var totalPageNumber = paginator.FindElement(By.CssSelector("nav > div > strong:nth-child(2)")).Text; + + Assert.Equal("43", paginatorCount); + Assert.Equal("1", currentPageNumber); + Assert.Equal("5", totalPageNumber); + } + + [Fact] + public void PaginatorGoNextShowsNextPage() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Browser.FindElement(By.CssSelector(".first-paginator .go-next")).Click(); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Assert.Contains("QuickGrid=2", Browser.Url); + + var grid = Browser.FindElement(By.ClassName("quickgrid")); + var rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; + Assert.Equal(10, rowCount); + } + + [Fact] + public void PaginatorLinkLoadsCorrectPage() + { + Navigate($"{ServerPathBase}/quickgrid?QuickGrid=3"); + + Browser.Equal("3", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } + + [Fact] + public void PaginatorGoPreviousFromSecondPage() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Browser.FindElement(By.CssSelector(".first-paginator .go-next")).Click(); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).Click(); + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } + + [Fact] + public void PaginatorNavigationButtonsDisabledCorrectly() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("disabled")); + Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("disabled")); + Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("disabled")); + + Browser.FindElement(By.CssSelector(".first-paginator .go-last")).Click(); + Browser.Equal("5", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("disabled")); + Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("disabled")); + } + + [Fact] + public void MultiplePaginatorsWorkIndependently() + { + Navigate($"{ServerPathBase}/quickgrid"); + Browser.FindElement(By.CssSelector(".second-paginator .go-next")).Click(); + + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".second-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + var grid1 = Browser.FindElement(By.CssSelector("#grid .quickgrid")); + Assert.Equal(10, grid1.FindElements(By.CssSelector("tbody > tr")).Count); + var grid2 = Browser.FindElement(By.CssSelector("#grid2 .quickgrid")); + Assert.Equal(5, grid2.FindElements(By.CssSelector("tbody > tr")).Count); + } + + [Fact] + public void PaginatorOutOfRangePageClampsToLastPage() + { + Navigate($"{ServerPathBase}/quickgrid?QuickGrid=999"); + + Browser.Equal("5", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } + + [Fact] + public void PaginatorInvalidPageValueDefaultsToFirstPage() + { + Navigate($"{ServerPathBase}/quickgrid?QuickGrid=abc"); + + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } + + [Fact] + public void CanColumnSortByInt() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("QuickGrid_sort=0_asc", Browser.Url); + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); + + Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("QuickGrid_sort=0_desc", Browser.Url); + + var firstRow = Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1)")); + Assert.Equal("Matti", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); + Assert.Equal("Karttunen", firstRow.FindElement(By.CssSelector("td:nth-child(3)")).Text); + Assert.Equal("1981-06-04", firstRow.FindElement(By.CssSelector("td:nth-child(4)")).Text); + } + + [Fact] + public void SortLinkLoadsCorrectOrder() + { + Navigate($"{ServerPathBase}/quickgrid?QuickGrid_sort=0_desc"); + + Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Equal("Matti", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + } + + [Fact] + public void SortNavigationWithPagination() + { + Navigate($"{ServerPathBase}/quickgrid?QuickGrid=2"); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); + Browser.True(() => Browser.Url.Contains("QuickGrid_sort=0_asc")); + var firstRow = Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1)")); + Assert.NotNull(firstRow); + } + + [Fact] + public void TwoGridsSortIndependently() + { + Navigate($"{ServerPathBase}/quickgrid"); + + // Sort first grid by PersonId ascending + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Sort second grid by Name ascending + Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) button.col-title")).Click(); + Browser.Equal("Beijing", () => Browser.FindElement(By.CssSelector("#grid2 table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Assert.Equal("10895", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + Assert.Contains("QuickGrid_sort=0_asc", Browser.Url); + Assert.Contains("QuickGrid2_sort=1_asc", Browser.Url); + + // Sort second grid by Name descending + Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) button.col-title")).Click(); + Browser.Equal("Tokyo", () => Browser.FindElement(By.CssSelector("#grid2 table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + + // First grid should remain unchanged + Assert.Equal("10895", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("QuickGrid_sort=0_asc", Browser.Url); + Assert.Contains("QuickGrid2_sort=1_desc", Browser.Url); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj index bbd94ca13df1..b4f37687efa9 100644 --- a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj +++ b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj @@ -85,6 +85,7 @@ + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor new file mode 100644 index 000000000000..8b254258b606 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor @@ -0,0 +1,101 @@ +@page "/quickgrid" +@using Microsoft.AspNetCore.Components.QuickGrid + +

QuickGrid Test

+ +
+ + + + + + +
+
+ +
+ +

Second QuickGrid

+ +
+ + + + + +
+
+ +
+ +@code { + record Person(int PersonId, string FirstName, string LastName, DateOnly BirthDate); + record City(int CityId, string Name, string Country); + + PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; + PaginationState pagination2 = new PaginationState { ItemsPerPage = 5 }; + + IQueryable cities = new[] + { + new City(1, "Tokyo", "Japan"), + new City(2, "Delhi", "India"), + new City(3, "Shanghai", "China"), + new City(4, "São Paulo", "Brazil"), + new City(5, "Mexico City", "Mexico"), + new City(6, "Cairo", "Egypt"), + new City(7, "Mumbai", "India"), + new City(8, "Beijing", "China"), + new City(9, "Dhaka", "Bangladesh"), + new City(10, "Osaka", "Japan"), + new City(11, "New York", "USA"), + new City(12, "Karachi", "Pakistan"), + new City(13, "Buenos Aires", "Argentina"), + }.AsQueryable(); + + IQueryable people = new[] + { + new Person(11203, "Julie", "Smith", new DateOnly(1958, 10, 10)), + new Person(11205, "Nur", "Sari", new DateOnly(1922, 4, 27)), + new Person(11898, "Jose", "Hernandez", new DateOnly(2011, 5, 3)), + new Person(10895, "Jean", "Martin", new DateOnly(1985, 3, 16)), + new Person(10944, "António", "Langa", new DateOnly(1991, 12, 1)), + new Person(12130, "Kenji", "Sato", new DateOnly(2004, 1, 9)), + new Person(12238, "Sven", "Ottlieb", new DateOnly(1973, 11, 15)), + new Person(12345, "Liu", "Wang", new DateOnly(1999, 6, 30)), + new Person(12346, "Giovanni", "Rovelli", new DateOnly(2000, 7, 31)), + new Person(12347, "Eduardo", "Martins", new DateOnly(2001, 8, 1)), + new Person(12348, "Martín", "Sommer", new DateOnly(2002, 9, 2)), + new Person(12349, "Victoria", "Ashworth", new DateOnly(2003, 10, 3)), + new Person(12350, "Hannah", "Moos", new DateOnly(2004, 11, 4)), + new Person(12351, "Palle", "Ibsen", new DateOnly(2005, 12, 5)), + new Person(12352, "Lúcia", "Carvalho", new DateOnly(2006, 1, 6)), + new Person(12353, "Horst", "Kloss", new DateOnly(2007, 2, 7)), + new Person(12354, "Sergio", "Gutiérrez", new DateOnly(2008, 3, 8)), + new Person(12355, "Janine", "Labrune", new DateOnly(2009, 4, 9)), + new Person(12356, "Ann", "Devon", new DateOnly(2010, 5, 10)), + new Person(12357, "Roland", "Mendel", new DateOnly(2011, 6, 11)), + new Person(12358, "Aria", "Cruz", new DateOnly(2012, 7, 12)), + new Person(12359, "Diego", "Roel", new DateOnly(2001, 8, 13)), + new Person(12360, "Martine", "Rancé", new DateOnly(2005, 9, 14)), + new Person(12361, "Maria", "Larsson", new DateOnly(1998, 10, 15)), + new Person(12362, "Peter", "Lewis", new DateOnly(2016, 11, 16)), + new Person(12363, "Carine", "Schmitt", new DateOnly(2017, 12, 13)), + new Person(12364, "Paolo", "Accorti", new DateOnly(2018, 5, 18)), + new Person(12365, "Lino", "Rodriguez", new DateOnly(1980, 2, 19)), + new Person(12367, "Bernardo", "Batista", new DateOnly(1979, 4, 21)), + new Person(12368, "Lúcia", "Carvalho", new DateOnly(1976, 5, 22)), + new Person(12369, "Guillermo", "Fernández", new DateOnly(1983, 6, 23)), + new Person(12370, "Georg", "Pipps", new DateOnly(1982, 7, 24)), + new Person(12371, "Mario", "Pontes", new DateOnly(1981, 8, 25)), + new Person(12372, "Anabela", "Camino", new DateOnly(1980, 9, 26)), + new Person(12380, "Karl", "Jablonski", new DateOnly(1981, 5, 3)), + new Person(12381, "Matti", "Karttunen", new DateOnly(1981, 6, 4)), + new Person(12373, "Helvetius", "Nagy", new DateOnly(1980, 10, 27)), + new Person(12374, "Rita", "Müller", new DateOnly(1980, 11, 28)), + new Person(12375, "Pirkko", "Koskitalo", new DateOnly(1980, 12, 29)), + new Person(12376, "Paula", "Parente", new DateOnly(1981, 1, 30)), + new Person(12377, "Karl", "Jablonski", new DateOnly(1981, 2, 10)), + new Person(12378, "Matti", "Karttunen", new DateOnly(1981, 3, 1)), + new Person(12379, "Zbyszek", "Piestrzeniewicz", new DateOnly(1981, 4, 2)), + }.AsQueryable(); +} From b0264493a44bce5d6f7bcdc94cd0a2bb6728f1cb Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Wed, 18 Feb 2026 11:20:39 +0100 Subject: [PATCH 02/14] Optimization --- .../src/Columns/ColumnBase.razor.cs | 2 +- .../src/Infrastructure/QueryStringHelper.cs | 36 +++++++++++ .../src/Pagination/Paginator.razor.cs | 21 +------ .../src/QuickGrid.razor.cs | 60 ++++++------------- .../test/GridRaceConditionTest.cs | 12 ++++ .../Tests/QuickGridNoInteractivityTest.cs | 16 ++--- 6 files changed, 79 insertions(+), 68 deletions(-) create mode 100644 src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Infrastructure/QueryStringHelper.cs diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs index aefc0e233051..9324fb93f946 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs @@ -107,7 +107,7 @@ public abstract partial class ColumnBase internal int ColumnIndex { get; set; } - private string FormName => $"{Grid.QueryName}_col{ColumnIndex}_sort_form"; + private string FormName => $"{Grid.QueryName}_{ColumnIndex}"; /// /// Constructs an instance of . diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Infrastructure/QueryStringHelper.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Infrastructure/QueryStringHelper.cs new file mode 100644 index 000000000000..dc9289186d58 --- /dev/null +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Infrastructure/QueryStringHelper.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Internal; + +namespace Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; + +internal static class QueryStringHelper +{ + /// + /// Reads the decoded value of the first query string parameter matching + /// from the given , or returns if not found. + /// + internal static string? ReadQueryStringValue(string uri, string parameterName) + { + var queryStart = uri.IndexOf('?'); + if (queryStart < 0) + { + return null; + } + + var queryEnd = uri.IndexOf('#', queryStart); + var query = uri.AsMemory(queryStart..((queryEnd < 0) ? uri.Length : queryEnd)); + var enumerable = new QueryStringEnumerable(query); + + foreach (var pair in enumerable) + { + if (pair.DecodeName().Span.Equals(parameterName, StringComparison.OrdinalIgnoreCase)) + { + return pair.DecodeValue().ToString(); + } + } + + return null; + } +} diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs index 26067a9cdad5..ca4b4b1c1b53 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; using Microsoft.AspNetCore.Components.Routing; -using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.Components.QuickGrid; @@ -102,26 +101,12 @@ await InvokeAsync(async () => private int? ReadPageIndexFromQueryString() { - var uri = NavigationManager.Uri; - var queryStart = uri.IndexOf('?'); - if (queryStart < 0) + var value = QueryStringHelper.ReadQueryStringValue(NavigationManager.Uri, QueryName); + if (value is not null && int.TryParse(value, out var page) && page > 0) { - return null; + return page - 1; } - var queryEnd = uri.IndexOf('#', queryStart); - var query = uri.AsMemory(queryStart..((queryEnd < 0) ? uri.Length : queryEnd)); - var enumerable = new QueryStringEnumerable(query); - - foreach (var pair in enumerable) - { - if (pair.DecodeName().Span.Equals(QueryName, StringComparison.OrdinalIgnoreCase) - && int.TryParse(pair.DecodeValue().Span, out var page) - && page > 0) - { - return page - 1; - } - } return null; } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index dec313b71adb..bbf21a2b6ff0 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web.Virtualization; -using Microsoft.AspNetCore.Internal; using Microsoft.JSInterop; using Microsoft.AspNetCore.Components.Forms; @@ -177,7 +176,7 @@ public partial class QuickGrid : IAsyncDisposable, IDisposable private bool _suppressNextLocationChange; private (int ColumnIndex, bool Ascending)? _cachedSortFromQuery; - private string SortQueryParameterName => $"{QueryName}_sort"; + private string SortQueryParameterName => $"{QueryName}_s"; /// /// Constructs an instance of . @@ -335,41 +334,23 @@ private void UpdateSortQueryString() private (int ColumnIndex, bool Ascending)? ReadSortFromQueryString() { - var uri = NavigationManager.Uri; - var queryStart = uri.IndexOf('?'); - if (queryStart < 0) + var value = QueryStringHelper.ReadQueryStringValue(NavigationManager.Uri, SortQueryParameterName); + if (value is null) { return null; } - var queryEnd = uri.IndexOf('#', queryStart); - var query = uri.AsMemory(queryStart..((queryEnd < 0) ? uri.Length : queryEnd)); - var enumerable = new QueryStringEnumerable(query); - var paramName = SortQueryParameterName; - - foreach (var pair in enumerable) + var lastUnderscore = value.LastIndexOf('_'); + if (lastUnderscore > 0 && lastUnderscore < value.Length - 1 + && int.TryParse(value.AsSpan(0, lastUnderscore), out var columnIndex) + && columnIndex >= 0) { - if (pair.DecodeName().Span.Equals(paramName, StringComparison.OrdinalIgnoreCase)) + return value.AsSpan(lastUnderscore + 1) switch { - var value = pair.DecodeValue().Span; - var lastUnderscore = value.LastIndexOf('_'); - if (lastUnderscore > 0 && lastUnderscore < value.Length - 1) - { - var indexSpan = value[..lastUnderscore]; - var direction = value[(lastUnderscore + 1)..]; - if (int.TryParse(indexSpan, out var columnIndex) && columnIndex >= 0) - { - if (direction.Equals("asc", StringComparison.OrdinalIgnoreCase)) - { - return (columnIndex, true); - } - else if (direction.Equals("desc", StringComparison.OrdinalIgnoreCase)) - { - return (columnIndex, false); - } - } - } - } + var d when d.Equals("asc", StringComparison.OrdinalIgnoreCase) => (columnIndex, true), + var d when d.Equals("desc", StringComparison.OrdinalIgnoreCase) => (columnIndex, false), + _ => null, + }; } return null; } @@ -383,23 +364,20 @@ private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) } var sortFromQuery = ReadSortFromQueryString(); - var currentColumnIndex = _sortByColumn is not null ? _columns.IndexOf(_sortByColumn) : -1; - var currentAscending = _sortByAscending; + var currentSort = _sortByColumn is not null ? (_columns.IndexOf(_sortByColumn), _sortByAscending) : ((int ColumnIndex, bool Ascending)?)null; - if (sortFromQuery is null && _sortByColumn is not null - || sortFromQuery is not null && (sortFromQuery.Value.ColumnIndex != currentColumnIndex - || sortFromQuery.Value.Ascending != currentAscending)) + if (sortFromQuery != currentSort) { await InvokeAsync(async () => { - if (sortFromQuery is null) + if (sortFromQuery is { } sort && sort.ColumnIndex >= 0 && sort.ColumnIndex < _columns.Count) { - _sortByColumn = null; + _sortByColumn = _columns[sort.ColumnIndex]; + _sortByAscending = sort.Ascending; } - else if (sortFromQuery.Value.ColumnIndex >= 0 && sortFromQuery.Value.ColumnIndex < _columns.Count) + else { - _sortByColumn = _columns[sortFromQuery.Value.ColumnIndex]; - _sortByAscending = sortFromQuery.Value.Ascending; + _sortByColumn = null; } await RefreshDataCoreAsync(); diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridRaceConditionTest.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridRaceConditionTest.cs index ffa4e6d3b409..fb335d294c46 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridRaceConditionTest.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridRaceConditionTest.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; using Xunit.Sdk; +using Microsoft.AspNetCore.Components.Routing; namespace Microsoft.AspNetCore.Components.QuickGrid.Tests; @@ -21,6 +22,7 @@ public async Task CanCorrectlyDisposeAsync() var testJsRuntime = new TestJsRuntime(moduleLoadCompletion, moduleImportStarted); var serviceProvider = new ServiceCollection() .AddSingleton(testJsRuntime) + .AddSingleton() .BuildServiceProvider(); var renderer = new TestRenderer(serviceProvider); @@ -57,6 +59,7 @@ public async Task FailingQuickGridCallsInitAfterDisposal() var testJsRuntime = new TestJsRuntime(moduleLoadCompletion, moduleImportStarted); var serviceProvider = new ServiceCollection() .AddSingleton(testJsRuntime) + .AddSingleton() .BuildServiceProvider(); var renderer = new TestRenderer(serviceProvider); @@ -177,6 +180,15 @@ public ValueTask InvokeAsync(string identifier, CancellationToke InvokeAsync(identifier, args); } +internal class TestNavigationManager : NavigationManager, IHostEnvironmentNavigationManager +{ + public TestNavigationManager() => Initialize("https://localhost/", "https://localhost/"); + + void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri) => Initialize(baseUri, uri); + + protected override void NavigateToCore(string uri, bool forceLoad) => Uri = uri; +} + internal class TestJSObjectReference(TestJsRuntime jsRuntime) : IJSObjectReference { private readonly TestJsRuntime _jsRuntime = jsRuntime; diff --git a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs index d46229e75989..66df0ba2ce84 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs @@ -131,11 +131,11 @@ public void CanColumnSortByInt() Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Assert.Contains("QuickGrid_sort=0_asc", Browser.Url); + Assert.Contains("QuickGrid_s=0_asc", Browser.Url); Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Assert.Contains("QuickGrid_sort=0_desc", Browser.Url); + Assert.Contains("QuickGrid_s=0_desc", Browser.Url); var firstRow = Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1)")); Assert.Equal("Matti", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); @@ -146,7 +146,7 @@ public void CanColumnSortByInt() [Fact] public void SortLinkLoadsCorrectOrder() { - Navigate($"{ServerPathBase}/quickgrid?QuickGrid_sort=0_desc"); + Navigate($"{ServerPathBase}/quickgrid?QuickGrid_s=0_desc"); Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); Assert.Equal("Matti", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); @@ -159,7 +159,7 @@ public void SortNavigationWithPagination() Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); - Browser.True(() => Browser.Url.Contains("QuickGrid_sort=0_asc")); + Browser.True(() => Browser.Url.Contains("QuickGrid_s=0_asc")); var firstRow = Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1)")); Assert.NotNull(firstRow); } @@ -178,8 +178,8 @@ public void TwoGridsSortIndependently() Browser.Equal("Beijing", () => Browser.FindElement(By.CssSelector("#grid2 table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); Assert.Equal("10895", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Assert.Contains("QuickGrid_sort=0_asc", Browser.Url); - Assert.Contains("QuickGrid2_sort=1_asc", Browser.Url); + Assert.Contains("QuickGrid_s=0_asc", Browser.Url); + Assert.Contains("QuickGrid2_s=1_asc", Browser.Url); // Sort second grid by Name descending Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) button.col-title")).Click(); @@ -187,7 +187,7 @@ public void TwoGridsSortIndependently() // First grid should remain unchanged Assert.Equal("10895", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Assert.Contains("QuickGrid_sort=0_asc", Browser.Url); - Assert.Contains("QuickGrid2_sort=1_desc", Browser.Url); + Assert.Contains("QuickGrid_s=0_asc", Browser.Url); + Assert.Contains("QuickGrid2_s=1_desc", Browser.Url); } } From 0dfcc68efcc2f975a88ac4259a2bbc1c483aedba Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Wed, 18 Feb 2026 12:10:52 +0100 Subject: [PATCH 03/14] Fixed tests --- .../Tests/QuickGridNoInteractivityTest.cs | 2 +- .../Pages/QuickGrid/QuickGridComponent.razor | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs index 66df0ba2ce84..e39221ed4fe2 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs @@ -175,7 +175,7 @@ public void TwoGridsSortIndependently() // Sort second grid by Name ascending Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) button.col-title")).Click(); - Browser.Equal("Beijing", () => Browser.FindElement(By.CssSelector("#grid2 table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Bangalore", () => Browser.FindElement(By.CssSelector("#grid2 table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); Assert.Equal("10895", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); Assert.Contains("QuickGrid_s=0_asc", Browser.Url); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor index 8b254258b606..93e16f957890 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor @@ -18,7 +18,7 @@

Second QuickGrid

- + @@ -35,7 +35,9 @@ PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; PaginationState pagination2 = new PaginationState { ItemsPerPage = 5 }; - IQueryable cities = new[] + GridItemsProvider cityProvider; + + static readonly City[] allCities = new[] { new City(1, "Tokyo", "Japan"), new City(2, "Delhi", "India"), @@ -50,7 +52,36 @@ new City(11, "New York", "USA"), new City(12, "Karachi", "Pakistan"), new City(13, "Buenos Aires", "Argentina"), - }.AsQueryable(); + new City(14, "Istanbul", "Turkey"), + new City(15, "Chongqing", "China"), + new City(16, "Lagos", "Nigeria"), + new City(17, "Kolkata", "India"), + new City(18, "Manila", "Philippines"), + new City(19, "Guangzhou", "China"), + new City(20, "Tianjin", "China"), + new City(21, "Lahore", "Pakistan"), + new City(22, "Bangalore", "India"), + new City(23, "Paris", "France"), + new City(24, "Bogotá", "Colombia"), + new City(25, "Jakarta", "Indonesia"), + new City(26, "Lima", "Peru"), + new City(27, "Bangkok", "Thailand"), + new City(28, "Hyderabad", "India"), + new City(29, "Seoul", "South Korea"), + new City(30, "London", "United Kingdom"), + }; + + protected override void OnInitialized() + { + cityProvider = async req => + { + var query = allCities.AsQueryable(); + query = req.ApplySorting(query); + var totalCount = query.Count(); + var items = query.Skip(req.StartIndex).Take(req.Count ?? totalCount).ToList(); + return GridItemsProviderResult.From(items, totalCount); + }; + } IQueryable people = new[] { From 15d0ee13ea5a2bd300f6bf2a6280015bb3a433e3 Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Mon, 23 Feb 2026 13:45:40 +0100 Subject: [PATCH 04/14] Fix multiple paginators --- .../src/Pagination/Paginator.razor.cs | 11 ++- .../src/QuickGrid.razor.cs | 2 +- .../Tests/QuickGridNoInteractivityTest.cs | 14 ++++ .../test/E2ETest/Tests/QuickGridTest.cs | 16 ++++- .../test/testassets/BasicTestApp/Index.razor | 1 + .../QuickGridDualPaginatorComponent.razor | 70 +++++++++++++++++++ .../Pages/QuickGrid/QuickGridComponent.razor | 25 +++++-- 7 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridDualPaginatorComponent.razor diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs index ca4b4b1c1b53..50de870fae19 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs @@ -14,6 +14,7 @@ public partial class Paginator : IDisposable private readonly EventCallbackSubscriber _totalItemCountChanged; private bool _hasReadQueryString; private bool _suppressNextLocationChange; + private int _lastRenderedPageIndex; [Inject] private NavigationManager NavigationManager { get; set; } = default!; @@ -55,6 +56,7 @@ private async Task GoToPageAsync(int pageIndex) int? pageValue = pageIndex == 0 ? null : pageIndex + 1; var newUri = NavigationManager.GetUriWithQueryParameter(QueryName, pageValue); await State.SetCurrentPageIndexAsync(pageIndex); + _lastRenderedPageIndex = State.CurrentPageIndex; _suppressNextLocationChange = true; NavigationManager.NavigateTo(newUri); } @@ -73,6 +75,7 @@ protected override Task OnParametersSetAsync() { _hasReadQueryString = true; var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; + _lastRenderedPageIndex = pageFromQuery; if (pageFromQuery != State.CurrentPageIndex) { return State.SetCurrentPageIndexAsync(pageFromQuery); @@ -89,11 +92,15 @@ private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) return; } var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; - if (pageFromQuery != State.CurrentPageIndex) + if (pageFromQuery != _lastRenderedPageIndex) { + _lastRenderedPageIndex = pageFromQuery; await InvokeAsync(async () => { - await State.SetCurrentPageIndexAsync(pageFromQuery); + if (pageFromQuery != State.CurrentPageIndex) + { + await State.SetCurrentPageIndexAsync(pageFromQuery); + } StateHasChanged(); }); } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index bbf21a2b6ff0..ba2a9a709e7b 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -117,7 +117,7 @@ public partial class QuickGrid : IAsyncDisposable, IDisposable /// /// Name of the query string parameter used to persist the current page index and sort order in the URL. - /// Defaults to "QuickGrid". The sort parameter is derived by appending "_sort" to this value. + /// Defaults to "QuickGrid". The sort parameter is derived by appending "_s" to this value. /// When set, this value is propagated to the associated /// so that connected components such as use the same query parameter name. /// diff --git a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs index e39221ed4fe2..a2f77d2ba796 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs @@ -190,4 +190,18 @@ public void TwoGridsSortIndependently() Assert.Contains("QuickGrid_s=0_asc", Browser.Url); Assert.Contains("QuickGrid2_s=1_desc", Browser.Url); } + + [Fact] + public void DualPaginatorsNavigatesAndBothUpdate() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".third-top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".third-bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + Browser.FindElement(By.CssSelector(".third-top-paginator .go-next")).Click(); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".third-top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".third-bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } } diff --git a/src/Components/test/E2ETest/Tests/QuickGridTest.cs b/src/Components/test/E2ETest/Tests/QuickGridTest.cs index fcafb8c4ec1b..fdf900cc2b79 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridTest.cs @@ -245,7 +245,7 @@ public void FilterUsingRefreshDataDoesNotCauseExtraRefresh() Browser.Equal("2", () => app.FindElement(By.Id("items-provider-calls")).Text); } - + [Fact] public void OnRowClickTriggersCallback() { @@ -284,4 +284,18 @@ public void OnRowClickAppliesCursorPointerStyle() return row ? getComputedStyle(row).cursor : null;"); Assert.Equal("pointer", cursorStyle); } + + [Fact] + public void DualPaginatorsNavigatesAndBothUpdate() + { + app = Browser.MountTestComponent(); + + Browser.Equal("1", () => app.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("1", () => app.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + app.FindElement(By.CssSelector(".top-paginator .go-next")).Click(); + + Browser.Equal("2", () => app.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("2", () => app.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } } diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index f21970563113..f6e33908b0df 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -100,6 +100,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridDualPaginatorComponent.razor b/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridDualPaginatorComponent.razor new file mode 100644 index 000000000000..74ef09c5726d --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridDualPaginatorComponent.razor @@ -0,0 +1,70 @@ +@using Microsoft.AspNetCore.Components.QuickGrid + +

QuickGrid Dual Paginator Component

+ +
+ +
+
+ + + + + + +
+
+ +
+ +@code { + record Person(int PersonId, string FirstName, string LastName, DateOnly BirthDate); + PaginationState pagination = new PaginationState { ItemsPerPage = 5 }; + + IQueryable people = new[] + { + new Person(11203, "Julie", "Smith", new DateOnly(1958, 10, 10)), + new Person(11205, "Nur", "Sari", new DateOnly(1922, 4, 27)), + new Person(11898, "Jose", "Hernandez", new DateOnly(2011, 5, 3)), + new Person(10895, "Jean", "Martin", new DateOnly(1985, 3, 16)), + new Person(10944, "António", "Langa", new DateOnly(1991, 12, 1)), + new Person(12130, "Kenji", "Sato", new DateOnly(2004, 1, 9)), + new Person(12238, "Sven", "Ottlieb", new DateOnly(1973, 11, 15)), + new Person(12345, "Liu", "Wang", new DateOnly(1999, 6, 30)), + new Person(12346, "Giovanni", "Rovelli", new DateOnly(2000, 7, 31)), + new Person(12347, "Eduardo", "Martins", new DateOnly(2001, 8, 1)), + new Person(12348, "Martín", "Sommer", new DateOnly(2002, 9, 2)), + new Person(12349, "Victoria", "Ashworth", new DateOnly(2003, 10, 3)), + new Person(12350, "Hannah", "Moos", new DateOnly(2004, 11, 4)), + new Person(12351, "Palle", "Ibsen", new DateOnly(2005, 12, 5)), + new Person(12352, "Lúcia", "Carvalho", new DateOnly(2006, 1, 6)), + new Person(12353, "Horst", "Kloss", new DateOnly(2007, 2, 7)), + new Person(12354, "Sergio", "Gutiérrez", new DateOnly(2008, 3, 8)), + new Person(12355, "Janine", "Labrune", new DateOnly(2009, 4, 9)), + new Person(12356, "Ann", "Devon", new DateOnly(2010, 5, 10)), + new Person(12357, "Roland", "Mendel", new DateOnly(2011, 6, 11)), + new Person(12358, "Aria", "Cruz", new DateOnly(2012, 7, 12)), + new Person(12359, "Diego", "Roel", new DateOnly(2001, 8, 13)), + new Person(12360, "Martine", "Rancé", new DateOnly(2005, 9, 14)), + new Person(12361, "Maria", "Larsson", new DateOnly(1998, 10, 15)), + new Person(12362, "Peter", "Lewis", new DateOnly(2016, 11, 16)), + new Person(12363, "Carine", "Schmitt", new DateOnly(2017, 12, 13)), + new Person(12364, "Paolo", "Accorti", new DateOnly(2018, 5, 18)), + new Person(12365, "Lino", "Rodriguez", new DateOnly(1980, 2, 19)), + new Person(12367, "Bernardo", "Batista", new DateOnly(1979, 4, 21)), + new Person(12368, "Lúcia", "Carvalho", new DateOnly(1976, 5, 22)), + new Person(12369, "Guillermo", "Fernández", new DateOnly(1983, 6, 23)), + new Person(12370, "Georg", "Pipps", new DateOnly(1982, 7, 24)), + new Person(12371, "Mario", "Pontes", new DateOnly(1981, 8, 25)), + new Person(12372, "Anabela", "Camino", new DateOnly(1980, 9, 26)), + new Person(12380, "Karl", "Jablonski", new DateOnly(1981, 5, 3)), + new Person(12381, "Matti", "Karttunen", new DateOnly(1981, 6, 4)), + new Person(12373, "Helvetius", "Nagy", new DateOnly(1980, 10, 27)), + new Person(12374, "Rita", "Müller", new DateOnly(1980, 11, 28)), + new Person(12375, "Pirkko", "Koskitalo", new DateOnly(1980, 12, 29)), + new Person(12376, "Paula", "Parente", new DateOnly(1981, 1, 30)), + new Person(12377, "Karl", "Jablonski", new DateOnly(1981, 2, 10)), + new Person(12378, "Matti", "Karttunen", new DateOnly(1981, 3, 1)), + new Person(12379, "Zbyszek", "Piestrzeniewicz", new DateOnly(1981, 4, 2)), + }.AsQueryable(); +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor index 93e16f957890..9b15e9335c40 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor @@ -1,7 +1,6 @@ @page "/quickgrid" @using Microsoft.AspNetCore.Components.QuickGrid - -

QuickGrid Test

+@using Microsoft.AspNetCore.Components.Forms
@@ -15,8 +14,6 @@
-

Second QuickGrid

-
@@ -28,12 +25,32 @@
+ +
+ +
+
+
+ + + + + + +
+ +
+ +
+
+ @code { record Person(int PersonId, string FirstName, string LastName, DateOnly BirthDate); record City(int CityId, string Name, string Country); PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; PaginationState pagination2 = new PaginationState { ItemsPerPage = 5 }; + PaginationState pagination3 = new PaginationState { ItemsPerPage = 5 }; GridItemsProvider cityProvider; From a628d45fde4e9953d7b828d64ea44040f3f33e55 Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Mon, 23 Feb 2026 15:27:06 +0100 Subject: [PATCH 05/14] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/QuickGrid.razor.cs | 5 +++++ .../RazorComponents/Pages/QuickGrid/QuickGridComponent.razor | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index ba2a9a709e7b..0bfb0524f788 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -121,6 +121,11 @@ public partial class QuickGrid : IAsyncDisposable, IDisposable /// When set, this value is propagated to the associated /// so that connected components such as use the same query parameter name. ///
+ /// + /// When rendering multiple instances on the same page, assign a unique + /// value to each instance. Reusing the default value causes the grids to share + /// query-string keys and can make their paging and sorting state interfere with each other. + /// [Parameter] public string QueryName { get; set; } = "QuickGrid"; [Inject] private IServiceProvider Services { get; set; } = default!; diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor index 9b15e9335c40..eca19b0dcee3 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor @@ -90,13 +90,13 @@ protected override void OnInitialized() { - cityProvider = async req => + cityProvider = req => { var query = allCities.AsQueryable(); query = req.ApplySorting(query); var totalCount = query.Count(); var items = query.Skip(req.StartIndex).Take(req.Count ?? totalCount).ToList(); - return GridItemsProviderResult.From(items, totalCount); + return ValueTask.FromResult(GridItemsProviderResult.From(items, totalCount)); }; } From e18101f745c5a94f6dac6411616bc6f84505dfa2 Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Mon, 23 Feb 2026 16:31:40 +0100 Subject: [PATCH 06/14] Fixed sorting in the case of the rerendering of the columns --- .../src/Columns/ColumnBase.razor | 7 ++++++- .../src/QuickGrid.razor.cs | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor index 01bcc886de60..99dc3a01e38a 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor @@ -5,7 +5,12 @@ @namespace Microsoft.AspNetCore.Components.QuickGrid @typeparam TGridItem @{ - ColumnIndex = InternalGridContext.Grid.AddColumn(this, InitialSortDirection, IsDefaultSortColumn); + var columnIndex = InternalGridContext.Grid.AddColumn(this, InitialSortDirection, IsDefaultSortColumn); + + if (columnIndex >= 0) + { + ColumnIndex = columnIndex; + } } @code diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index 0bfb0524f788..14f85c72a27b 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -301,6 +301,11 @@ private void StartCollectingColumns() private void FinishCollectingColumns() { _collectingColumns = false; + + if (_sortByColumn is not null && !_columns.Contains(_sortByColumn)) + { + _sortByColumn = null; + } } /// From a6fcdb4e397e762695b62226075ea150761d0c3b Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Thu, 26 Feb 2026 18:34:00 +0100 Subject: [PATCH 07/14] Feedback --- .../Microsoft.AspNetCore.Components.csproj | 4 + .../SupplyParameterFromQueryValueProvider.cs | 18 +--- .../src/Columns/ColumnBase.razor | 31 ++---- .../src/Columns/ColumnBase.razor.cs | 4 - .../src/Columns/ColumnBase.razor.css | 6 +- .../src/Infrastructure/QueryStringHelper.cs | 36 ------- ...oft.AspNetCore.Components.QuickGrid.csproj | 4 + .../src/Pagination/PaginationState.cs | 2 +- .../src/Pagination/Paginator.razor | 45 ++------ .../src/Pagination/Paginator.razor.cs | 61 ++++------- .../src/Pagination/Paginator.razor.css | 12 ++- .../src/PublicAPI.Unshipped.txt | 1 - .../src/QuickGrid.razor.cs | 100 ++++++++---------- .../src}/QueryParameterNameComparer.cs | 0 .../src}/QueryParameterValueSupplier.cs | 15 +++ .../src}/StringSegmentAccumulator.cs | 0 .../src}/UrlValueConstraint.cs | 0 .../Tests/QuickGridNoInteractivityTest.cs | 55 +++++----- .../test/E2ETest/Tests/QuickGridTest.cs | 61 +++++------ 19 files changed, 174 insertions(+), 281 deletions(-) delete mode 100644 src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Infrastructure/QueryStringHelper.cs rename src/Components/{Components/src/Routing => Shared/src}/QueryParameterNameComparer.cs (100%) rename src/Components/{Components/src/Routing => Shared/src}/QueryParameterValueSupplier.cs (81%) rename src/Components/{Components/src/Routing => Shared/src}/StringSegmentAccumulator.cs (100%) rename src/Components/{Components/src/Routing => Shared/src}/UrlValueConstraint.cs (100%) diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index ba3ae6420645..2b0d22175e38 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -18,6 +18,10 @@ + + + + diff --git a/src/Components/Components/src/Routing/SupplyParameterFromQueryValueProvider.cs b/src/Components/Components/src/Routing/SupplyParameterFromQueryValueProvider.cs index eeae2040d2ed..769756ffc882 100644 --- a/src/Components/Components/src/Routing/SupplyParameterFromQueryValueProvider.cs +++ b/src/Components/Components/src/Routing/SupplyParameterFromQueryValueProvider.cs @@ -74,9 +74,9 @@ private bool TryUpdateUri() return false; } - var query = GetQueryString(navigationManager.Uri); + var query = QueryParameterValueSupplier.GetQueryString(navigationManager.Uri); - if (!query.Span.SequenceEqual(GetQueryString(_lastUri).Span)) + if (!query.Span.SequenceEqual(QueryParameterValueSupplier.GetQueryString(_lastUri).Span)) { _queryChanged = true; _queryParameterValueSupplier.ReadParametersFromQuery(query); @@ -84,20 +84,6 @@ private bool TryUpdateUri() _lastUri = navigationManager.Uri; return true; - - static ReadOnlyMemory GetQueryString(string? url) - { - var queryStartPos = url?.IndexOf('?') ?? -1; - - if (queryStartPos < 0) - { - return default; - } - - Debug.Assert(url is not null); - var queryEndPos = url.IndexOf('#', queryStartPos); - return url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); - } } private void SubscribeToLocationChanges() diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor index 99dc3a01e38a..899a64ebadff 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor @@ -1,16 +1,10 @@ @using Microsoft.AspNetCore.Components.Rendering @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.AspNetCore.Components.Forms @namespace Microsoft.AspNetCore.Components.QuickGrid @typeparam TGridItem @{ - var columnIndex = InternalGridContext.Grid.AddColumn(this, InitialSortDirection, IsDefaultSortColumn); - - if (columnIndex >= 0) - { - ColumnIndex = columnIndex; - } + InternalGridContext.Grid.AddColumn(this, InitialSortDirection, IsDefaultSortColumn); } @code @@ -28,25 +22,12 @@ } - if (Sortable.HasValue ? Sortable.Value : IsSortableByDefault()) + if ((Sortable.HasValue ? Sortable.Value : IsSortableByDefault()) && Title is not null) { - if (RendererInfo.IsInteractive) - { - - } - else - { -
- - - - } + +
@Title
+ +
} else { diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs index 9324fb93f946..fec920a2c092 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs @@ -105,10 +105,6 @@ public abstract partial class ColumnBase /// True if the column should be sortable by default, otherwise false. protected virtual bool IsSortableByDefault() => false; - internal int ColumnIndex { get; set; } - - private string FormName => $"{Grid.QueryName}_{ColumnIndex}"; - /// /// Constructs an instance of . /// diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css index 11477eb380d6..1d1de079f9cf 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css @@ -6,12 +6,14 @@ padding: 0; } -/* If the column is sortable, its title is rendered as a button element for accessibility and to support navigation by tab */ -button.col-title { +/* If the column is sortable, its title is rendered as a link element for accessibility and to support navigation by tab */ +a.col-title { border: none; background: none; position: relative; cursor: pointer; + text-decoration: none; + color: inherit; } .col-justify-center .col-title { diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Infrastructure/QueryStringHelper.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Infrastructure/QueryStringHelper.cs deleted file mode 100644 index dc9289186d58..000000000000 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Infrastructure/QueryStringHelper.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Internal; - -namespace Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; - -internal static class QueryStringHelper -{ - /// - /// Reads the decoded value of the first query string parameter matching - /// from the given , or returns if not found. - /// - internal static string? ReadQueryStringValue(string uri, string parameterName) - { - var queryStart = uri.IndexOf('?'); - if (queryStart < 0) - { - return null; - } - - var queryEnd = uri.IndexOf('#', queryStart); - var query = uri.AsMemory(queryStart..((queryEnd < 0) ? uri.Length : queryEnd)); - var enumerable = new QueryStringEnumerable(query); - - foreach (var pair in enumerable) - { - if (pair.DecodeName().Span.Equals(parameterName, StringComparison.OrdinalIgnoreCase)) - { - return pair.DecodeValue().ToString(); - } - } - - return null; - } -} diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj index e8dab12adc70..16dc8318a297 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj @@ -17,6 +17,10 @@ <_CurrentProjectDiscoveredScopedCssFiles Include="@(ThemeCssFiles)" RelativePath="%(Identity)" BasePath="_content/$(AssemblyName)" /> + + + + diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs index b566e8fd8b70..9a006f719d02 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs @@ -38,7 +38,7 @@ public class PaginationState internal EventCallbackSubscribable CurrentPageItemsChanged { get; } = new(); internal EventCallbackSubscribable TotalItemCountChangedSubscribable { get; } = new(); - internal string QueryName { get; set; } = "QuickGrid"; + internal string QueryName { get; set; } = ""; /// public override int GetHashCode() diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor index 73ef33bc782c..fc44e59b62a6 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor @@ -1,5 +1,4 @@ -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web @namespace Microsoft.AspNetCore.Components.QuickGrid
@@ -16,40 +15,14 @@ }
} diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs index 50de870fae19..d1a9f6f832dd 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs @@ -12,17 +12,10 @@ namespace Microsoft.AspNetCore.Components.QuickGrid; public partial class Paginator : IDisposable { private readonly EventCallbackSubscriber _totalItemCountChanged; - private bool _hasReadQueryString; - private bool _suppressNextLocationChange; - private int _lastRenderedPageIndex; [Inject] private NavigationManager NavigationManager { get; set; } = default!; private string QueryName => State.QueryName; - private string FormNameFirst => $"Paginator_{QueryName}_GoFirst"; - private string FormNamePrevious => $"Paginator_{QueryName}_GoPrevious"; - private string FormNameNext => $"Paginator_{QueryName}_GoNext"; - private string FormNameLast => $"Paginator_{QueryName}_GoLast"; /// /// Specifies the associated . This parameter is required. @@ -41,24 +34,17 @@ public Paginator() { // The "total item count" handler doesn't need to do anything except cause this component to re-render _totalItemCountChanged = new(new EventCallback(this, null)); + _queryParameterValueSupplier = new(); } - private Task GoFirstAsync() => GoToPageAsync(0); - private Task GoPreviousAsync() => GoToPageAsync(State.CurrentPageIndex - 1); - private Task GoNextAsync() => GoToPageAsync(State.CurrentPageIndex + 1); - private Task GoLastAsync() => GoToPageAsync(State.LastPageIndex.GetValueOrDefault(0)); - private bool CanGoBack => State.CurrentPageIndex > 0; private bool CanGoForwards => State.CurrentPageIndex < State.LastPageIndex; + private readonly QueryParameterValueSupplier _queryParameterValueSupplier; - private async Task GoToPageAsync(int pageIndex) + private string GetPageUrl(int pageIndex) { int? pageValue = pageIndex == 0 ? null : pageIndex + 1; - var newUri = NavigationManager.GetUriWithQueryParameter(QueryName, pageValue); - await State.SetCurrentPageIndexAsync(pageIndex); - _lastRenderedPageIndex = State.CurrentPageIndex; - _suppressNextLocationChange = true; - NavigationManager.NavigateTo(newUri); + return NavigationManager.GetUriWithQueryParameter(QueryName, pageValue); } /// @@ -71,44 +57,35 @@ protected override void OnInitialized() protected override Task OnParametersSetAsync() { _totalItemCountChanged.SubscribeOrMove(State.TotalItemCountChangedSubscribable); - if (!_hasReadQueryString) + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); + var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; + if (pageFromQuery != State.CurrentPageIndex) { - _hasReadQueryString = true; - var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; - _lastRenderedPageIndex = pageFromQuery; - if (pageFromQuery != State.CurrentPageIndex) - { - return State.SetCurrentPageIndexAsync(pageFromQuery); - } + return State.SetCurrentPageIndexAsync(pageFromQuery); } return Task.CompletedTask; } private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) { - if (_suppressNextLocationChange) - { - _suppressNextLocationChange = false; - return; - } + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; - if (pageFromQuery != _lastRenderedPageIndex) + await InvokeAsync(async () => { - _lastRenderedPageIndex = pageFromQuery; - await InvokeAsync(async () => + if (pageFromQuery != State.CurrentPageIndex) { - if (pageFromQuery != State.CurrentPageIndex) - { - await State.SetCurrentPageIndexAsync(pageFromQuery); - } - StateHasChanged(); - }); - } + await State.SetCurrentPageIndexAsync(pageFromQuery); + } + + // Always re-render so that page link URLs reflect the current URI + // (e.g., preserving sort query parameters added by QuickGrid). + StateHasChanged(); + }); } private int? ReadPageIndexFromQueryString() { - var value = QueryStringHelper.ReadQueryStringValue(NavigationManager.Uri, QueryName); + var value = _queryParameterValueSupplier.GetQueryParameterValue(typeof(string), QueryName) as string; if (value is not null && int.TryParse(value, out var page) && page > 0) { return page - 1; diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css index d996a6d1beb7..0bc26dfee116 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css @@ -17,22 +17,26 @@ nav { align-items: center; } - nav button { + nav a { border: 0; background: none center center / 1rem no-repeat; width: 2rem; height: 2rem; + display: inline-block; + text-decoration: none; + font-size: 0; } - nav button[disabled] { + nav a:not([href]) { opacity: 0.4; + pointer-events: none; } - nav button:not([disabled]):hover { + nav a[href]:hover { background-color: #eee; } - nav button:not([disabled]):active { + nav a[href]:active { background-color: #aaa; } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt index 5e66d67d11db..033fd8769244 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt @@ -1,7 +1,6 @@ #nullable enable Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryName.get -> string! Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryName.set -> void -Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.Dispose() -> void Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnRowClick.get -> Microsoft.AspNetCore.Components.EventCallback Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnRowClick.set -> void override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnInitialized() -> void diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index 14f85c72a27b..d7940f849806 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -2,11 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web.Virtualization; using Microsoft.JSInterop; -using Microsoft.AspNetCore.Components.Forms; namespace Microsoft.AspNetCore.Components.QuickGrid; @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Components.QuickGrid; /// /// The type of data represented by each row in the grid. [CascadingTypeParameter(nameof(TGridItem))] -public partial class QuickGrid : IAsyncDisposable, IDisposable +public partial class QuickGrid : IAsyncDisposable { /// /// A queryable source of data for the grid. @@ -116,17 +116,10 @@ public partial class QuickGrid : IAsyncDisposable, IDisposable [Parameter] public EventCallback OnRowClick { get; set; } /// - /// Name of the query string parameter used to persist the current page index and sort order in the URL. - /// Defaults to "QuickGrid". The sort parameter is derived by appending "_s" to this value. - /// When set, this value is propagated to the associated - /// so that connected components such as use the same query parameter name. + /// The parameter from which the page and sorting URL parameters are derived. The default value is an empty string, which results in query parameters named "page", "sort", and "order". If you provide a non-empty value, for example "products", + /// then the query parameters will be "products_page", "products_sort", and "products_order". This allows you to use multiple components on the same page without their URL parameters conflicting with each other. /// - /// - /// When rendering multiple instances on the same page, assign a unique - /// value to each instance. Reusing the default value causes the grids to share - /// query-string keys and can make their paging and sorting state interfere with each other. - /// - [Parameter] public string QueryName { get; set; } = "QuickGrid"; + [Parameter] public string QueryName { get; set; } = ""; [Inject] private IServiceProvider Services { get; set; } = default!; [Inject] private IJSRuntime JS { get; set; } = default!; @@ -179,9 +172,12 @@ public partial class QuickGrid : IAsyncDisposable, IDisposable private bool _hasReadSortFromQueryString; private bool _suppressNextLocationChange; - private (int ColumnIndex, bool Ascending)? _cachedSortFromQuery; + private (string ColumnTitle, bool Ascending)? _cachedSortFromQuery; - private string SortQueryParameterName => $"{QueryName}_s"; + private string SortQueryParameterNameBy => QueryName == "" ? "sort" : $"{QueryName}_sort"; + private string SortQueryParameterNameOrder => QueryName == "" ? "order" : $"{QueryName}_order"; + private string PageQueryParameterName => QueryName == "" ? "page" : $"{QueryName}_page"; + private readonly QueryParameterValueSupplier _queryParameterValueSupplier; /// /// Constructs an instance of . @@ -193,6 +189,7 @@ public QuickGrid() _currentPageItemsChanged = new(EventCallback.Factory.Create(this, RefreshDataCoreAsync)); _renderColumnHeaders = RenderColumnHeaders; _renderNonVirtualizedRows = RenderNonVirtualizedRows; + _queryParameterValueSupplier = new(); // 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 @@ -205,6 +202,7 @@ public QuickGrid() /// protected override void OnInitialized() { + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); NavigationManager.LocationChanged += OnLocationChanged; } @@ -216,7 +214,7 @@ protected override Task OnParametersSetAsync() if (Pagination is { } pagination) { - pagination.QueryName = QueryName; + pagination.QueryName = PageQueryParameterName; } if (Items is not null && ItemsProvider is not null) @@ -265,12 +263,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender) } // Invoked by descendant columns at a special time during rendering - internal int AddColumn(ColumnBase column, SortDirection? initialSortDirection, bool isDefaultSortColumn) + internal void AddColumn(ColumnBase column, SortDirection? initialSortDirection, bool isDefaultSortColumn) { if (_collectingColumns) { _columns.Add(column); - var columnIndex = _columns.Count - 1; if (isDefaultSortColumn && _sortByColumn is null && initialSortDirection.HasValue) { @@ -280,15 +277,13 @@ internal int AddColumn(ColumnBase column, SortDirection? initialSortD if (!_hasReadSortFromQueryString && _cachedSortFromQuery is { } sortFromQuery - && sortFromQuery.ColumnIndex == columnIndex) + && sortFromQuery.ColumnTitle == column.Title) { _sortByColumn = column; _sortByAscending = sortFromQuery.Ascending; _hasReadSortFromQueryString = true; } - return columnIndex; } - return -1; } private void StartCollectingColumns() @@ -325,40 +320,44 @@ public Task SortByColumnAsync(ColumnBase column, SortDirection direct }; _sortByColumn = column; - - UpdateSortQueryString(); - - StateHasChanged(); // We want to see the updated sort order in the header, even before the data query is completed + var newUri = GetSortQueryStringUrl(_sortByColumn, _sortByAscending); + _suppressNextLocationChange = true; + NavigationManager.NavigateTo(newUri); return RefreshDataAsync(); } - private void UpdateSortQueryString() + internal string GetSortUrl(ColumnBase column, SortDirection direction = SortDirection.Auto) { - string? sortValue = _sortByColumn is not null - ? $"{_columns.IndexOf(_sortByColumn)}_{(_sortByAscending ? "asc" : "desc")}" - : null; - var newUri = NavigationManager.GetUriWithQueryParameter(SortQueryParameterName, sortValue); - _suppressNextLocationChange = true; - NavigationManager.NavigateTo(newUri); + var ascending = direction switch + { + SortDirection.Ascending => true, + SortDirection.Descending => false, + SortDirection.Auto => _sortByColumn != column || !_sortByAscending, + _ => throw new NotSupportedException($"Unknown sort direction {direction}"), + }; + + return GetSortQueryStringUrl(column, ascending); } - private (int ColumnIndex, bool Ascending)? ReadSortFromQueryString() + private string GetSortQueryStringUrl(ColumnBase? column, bool ascending) { - var value = QueryStringHelper.ReadQueryStringValue(NavigationManager.Uri, SortQueryParameterName); - if (value is null) + return NavigationManager.GetUriWithQueryParameters(new Dictionary { - return null; - } + [SortQueryParameterNameBy] = column?.Title, + [SortQueryParameterNameOrder] = ascending ? "asc" : "desc", + }); + } - var lastUnderscore = value.LastIndexOf('_'); - if (lastUnderscore > 0 && lastUnderscore < value.Length - 1 - && int.TryParse(value.AsSpan(0, lastUnderscore), out var columnIndex) - && columnIndex >= 0) + private (string ColumnTitle, bool Ascending)? ReadSortFromQueryString() + { + var column = _queryParameterValueSupplier.GetQueryParameterValue(typeof(string), SortQueryParameterNameBy) as string; + var order = _queryParameterValueSupplier.GetQueryParameterValue(typeof(string), SortQueryParameterNameOrder) as string; + if (column is not null && order is not null) { - return value.AsSpan(lastUnderscore + 1) switch + return order switch { - var d when d.Equals("asc", StringComparison.OrdinalIgnoreCase) => (columnIndex, true), - var d when d.Equals("desc", StringComparison.OrdinalIgnoreCase) => (columnIndex, false), + var d when d.Equals("asc", StringComparison.OrdinalIgnoreCase) => (column, true), + var d when d.Equals("desc", StringComparison.OrdinalIgnoreCase) => (column, false), _ => null, }; } @@ -373,16 +372,17 @@ private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) return; } + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); var sortFromQuery = ReadSortFromQueryString(); - var currentSort = _sortByColumn is not null ? (_columns.IndexOf(_sortByColumn), _sortByAscending) : ((int ColumnIndex, bool Ascending)?)null; + var currentSort = _sortByColumn is not null ? (_sortByColumn.Title, _sortByAscending) : ((string? ColumnTitle, bool Ascending)?)null; if (sortFromQuery != currentSort) { await InvokeAsync(async () => { - if (sortFromQuery is { } sort && sort.ColumnIndex >= 0 && sort.ColumnIndex < _columns.Count) + if (sortFromQuery is { } sort && sort.ColumnTitle is not null) { - _sortByColumn = _columns[sort.ColumnIndex]; + _sortByColumn = _columns.FirstOrDefault(c => c.Title == sort.ColumnTitle); _sortByAscending = sort.Ascending; } else @@ -570,16 +570,10 @@ private string GridClass() _ => column.Class, }; - /// - public void Dispose() - { - NavigationManager.LocationChanged -= OnLocationChanged; - } - /// public async ValueTask DisposeAsync() { - Dispose(); + NavigationManager.LocationChanged -= OnLocationChanged; _wasDisposed = true; _currentPageItemsChanged.Dispose(); diff --git a/src/Components/Components/src/Routing/QueryParameterNameComparer.cs b/src/Components/Shared/src/QueryParameterNameComparer.cs similarity index 100% rename from src/Components/Components/src/Routing/QueryParameterNameComparer.cs rename to src/Components/Shared/src/QueryParameterNameComparer.cs diff --git a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs b/src/Components/Shared/src/QueryParameterValueSupplier.cs similarity index 81% rename from src/Components/Components/src/Routing/QueryParameterValueSupplier.cs rename to src/Components/Shared/src/QueryParameterValueSupplier.cs index d5e75c9003f6..6367c0ee6a2c 100644 --- a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs +++ b/src/Components/Shared/src/QueryParameterValueSupplier.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Internal; @@ -51,4 +52,18 @@ public void ReadParametersFromQuery(ReadOnlyMemory query) return default; } + + public static ReadOnlyMemory GetQueryString(string? url) + { + var queryStartPos = url?.IndexOf('?') ?? -1; + + if (queryStartPos < 0) + { + return default; + } + + Debug.Assert(url is not null); + var queryEndPos = url.IndexOf('#', queryStartPos); + return url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); + } } diff --git a/src/Components/Components/src/Routing/StringSegmentAccumulator.cs b/src/Components/Shared/src/StringSegmentAccumulator.cs similarity index 100% rename from src/Components/Components/src/Routing/StringSegmentAccumulator.cs rename to src/Components/Shared/src/StringSegmentAccumulator.cs diff --git a/src/Components/Components/src/Routing/UrlValueConstraint.cs b/src/Components/Shared/src/UrlValueConstraint.cs similarity index 100% rename from src/Components/Components/src/Routing/UrlValueConstraint.cs rename to src/Components/Shared/src/UrlValueConstraint.cs diff --git a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs index a2f77d2ba796..ca5dc5ff7466 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs @@ -47,7 +47,7 @@ public void PaginatorGoNextShowsNextPage() Browser.FindElement(By.CssSelector(".first-paginator .go-next")).Click(); Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); - Assert.Contains("QuickGrid=2", Browser.Url); + Assert.Contains("page=2", Browser.Url); var grid = Browser.FindElement(By.ClassName("quickgrid")); var rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; @@ -57,7 +57,7 @@ public void PaginatorGoNextShowsNextPage() [Fact] public void PaginatorLinkLoadsCorrectPage() { - Navigate($"{ServerPathBase}/quickgrid?QuickGrid=3"); + Navigate($"{ServerPathBase}/quickgrid?page=3"); Browser.Equal("3", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); } @@ -75,12 +75,12 @@ public void PaginatorGoPreviousFromSecondPage() } [Fact] - public void PaginatorNavigationButtonsDisabledCorrectly() + public void PaginatorNavigationLinksDisabledCorrectly() { Navigate($"{ServerPathBase}/quickgrid"); - Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("disabled")); - Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("disabled")); + Assert.NotNull(Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("disabled")); + Assert.NotNull(Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("disabled")); Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("disabled")); Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("disabled")); @@ -89,8 +89,8 @@ public void PaginatorNavigationButtonsDisabledCorrectly() Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("disabled")); Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("disabled")); - Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("disabled")); - Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("disabled")); + Assert.NotNull(Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("disabled")); + Assert.NotNull(Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("disabled")); } [Fact] @@ -111,7 +111,7 @@ public void MultiplePaginatorsWorkIndependently() [Fact] public void PaginatorOutOfRangePageClampsToLastPage() { - Navigate($"{ServerPathBase}/quickgrid?QuickGrid=999"); + Navigate($"{ServerPathBase}/quickgrid?page=999"); Browser.Equal("5", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); } @@ -119,7 +119,7 @@ public void PaginatorOutOfRangePageClampsToLastPage() [Fact] public void PaginatorInvalidPageValueDefaultsToFirstPage() { - Navigate($"{ServerPathBase}/quickgrid?QuickGrid=abc"); + Navigate($"{ServerPathBase}/quickgrid?page=abc"); Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); } @@ -129,13 +129,15 @@ public void CanColumnSortByInt() { Navigate($"{ServerPathBase}/quickgrid"); - Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) a.col-title")).Click(); Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Assert.Contains("QuickGrid_s=0_asc", Browser.Url); - Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) a.col-title")).Click(); Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Assert.Contains("QuickGrid_s=0_desc", Browser.Url); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=desc", Browser.Url); var firstRow = Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1)")); Assert.Equal("Matti", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); @@ -146,7 +148,7 @@ public void CanColumnSortByInt() [Fact] public void SortLinkLoadsCorrectOrder() { - Navigate($"{ServerPathBase}/quickgrid?QuickGrid_s=0_desc"); + Navigate($"{ServerPathBase}/quickgrid?sort=PersonId&order=desc"); Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); Assert.Equal("Matti", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); @@ -155,11 +157,12 @@ public void SortLinkLoadsCorrectOrder() [Fact] public void SortNavigationWithPagination() { - Navigate($"{ServerPathBase}/quickgrid?QuickGrid=2"); + Navigate($"{ServerPathBase}/quickgrid?page=2"); Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); - Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); - Browser.True(() => Browser.Url.Contains("QuickGrid_s=0_asc")); + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) a.col-title")).Click(); + Browser.True(() => Browser.Url.Contains("sort=PersonId")); + Browser.True(() => Browser.Url.Contains("order=asc")); var firstRow = Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1)")); Assert.NotNull(firstRow); } @@ -170,25 +173,29 @@ public void TwoGridsSortIndependently() Navigate($"{ServerPathBase}/quickgrid"); // Sort first grid by PersonId ascending - Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) button.col-title")).Click(); + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) a.col-title")).Click(); Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); // Sort second grid by Name ascending - Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) button.col-title")).Click(); + Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) a.col-title")).Click(); Browser.Equal("Bangalore", () => Browser.FindElement(By.CssSelector("#grid2 table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); Assert.Equal("10895", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Assert.Contains("QuickGrid_s=0_asc", Browser.Url); - Assert.Contains("QuickGrid2_s=1_asc", Browser.Url); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + Assert.Contains("QuickGrid2_sort=Name", Browser.Url); + Assert.Contains("QuickGrid2_order=asc", Browser.Url); // Sort second grid by Name descending - Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) button.col-title")).Click(); + Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) a.col-title")).Click(); Browser.Equal("Tokyo", () => Browser.FindElement(By.CssSelector("#grid2 table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); // First grid should remain unchanged Assert.Equal("10895", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Assert.Contains("QuickGrid_s=0_asc", Browser.Url); - Assert.Contains("QuickGrid2_s=1_desc", Browser.Url); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + Assert.Contains("QuickGrid2_sort=Name", Browser.Url); + Assert.Contains("QuickGrid2_order=desc", Browser.Url); } [Fact] diff --git a/src/Components/test/E2ETest/Tests/QuickGridTest.cs b/src/Components/test/E2ETest/Tests/QuickGridTest.cs index fdf900cc2b79..10547aa080c5 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridTest.cs @@ -34,58 +34,46 @@ protected override void InitializeAsyncCore() [Fact] public void CanColumnSortByInt() { - var grid = app.FindElement(By.CssSelector("#grid > table")); - var idColumnSortButton = grid.FindElement(By.CssSelector("thead > tr > th:nth-child(1) > div > button")); // Click twice to sort by descending - idColumnSortButton.Click(); - idColumnSortButton.Click(); - - var firstRow = grid.FindElement(By.CssSelector("tbody > tr:nth-child(1)")); + Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")).Click(); + Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")).Click(); //Compare first row to expected result - Assert.Equal("12381", firstRow.FindElement(By.CssSelector("td:nth-child(1)")).Text); - Assert.Equal("Matti", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); - Assert.Equal("Karttunen", firstRow.FindElement(By.CssSelector("td:nth-child(3)")).Text); - Assert.Equal("1981-06-04", firstRow.FindElement(By.CssSelector("td:nth-child(4)")).Text); + Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Matti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Karttunen", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-06-04", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); } [Fact] public void CanColumnSortByString() { - var grid = app.FindElement(By.CssSelector("#grid > table")); - var firstNameColumnSortButton = grid.FindElement(By.CssSelector("thead > tr > th:nth-child(2) > div > button.col-title")); // Click twice to sort by descending - firstNameColumnSortButton.Click(); - firstNameColumnSortButton.Click(); - - var firstRow = grid.FindElement(By.CssSelector("tbody > tr:nth-child(1)")); + Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")).Click(); + Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")).Click(); //Compare first row to expected result - Assert.Equal("12379", firstRow.FindElement(By.CssSelector("td:nth-child(1)")).Text); - Assert.Equal("Zbyszek", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); - Assert.Equal("Piestrzeniewicz", firstRow.FindElement(By.CssSelector("td:nth-child(3)")).Text); - Assert.Equal("1981-04-02", firstRow.FindElement(By.CssSelector("td:nth-child(4)")).Text); + Browser.Equal("12379", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Zbyszek", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Piestrzeniewicz", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-04-02", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); } [Fact] public void CanColumnSortByDateOnly() { - var grid = app.FindElement(By.CssSelector("#grid > table")); - var birthDateColumnSortButton = grid.FindElement(By.CssSelector("thead > tr > th:nth-child(4) > div > button")); // Click twice to sort by descending - birthDateColumnSortButton.Click(); - birthDateColumnSortButton.Click(); - - var firstRow = grid.FindElement(By.CssSelector("tbody > tr:nth-child(1)")); + Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")).Click(); + Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")).Click(); //Compare first row to expected result - Assert.Equal("12364", firstRow.FindElement(By.CssSelector("td:nth-child(1)")).Text); - Assert.Equal("Paolo", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); - Assert.Equal("Accorti", firstRow.FindElement(By.CssSelector("td:nth-child(3)")).Text); - Assert.Equal("2018-05-18", firstRow.FindElement(By.CssSelector("td:nth-child(4)")).Text); + Browser.Equal("12364", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Paolo", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Accorti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("2018-05-18", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); } [Fact] @@ -97,8 +85,7 @@ public void PaginatorCorrectItemsPerPage() app.FindElement(By.ClassName("go-next")).Click(); - rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; - Assert.Equal(10, rowCount); + Browser.Equal(10, () => Browser.FindElement(By.ClassName("quickgrid")).FindElements(By.CssSelector("tbody > tr")).Count); } [Fact] @@ -290,12 +277,12 @@ public void DualPaginatorsNavigatesAndBothUpdate() { app = Browser.MountTestComponent(); - Browser.Equal("1", () => app.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); - Browser.Equal("1", () => app.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); - app.FindElement(By.CssSelector(".top-paginator .go-next")).Click(); + Browser.FindElement(By.CssSelector(".top-paginator .go-next")).Click(); - Browser.Equal("2", () => app.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); - Browser.Equal("2", () => app.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); } } From 7898379ae5e2719f9e43b194bbe467eae4f7483a Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Tue, 3 Mar 2026 15:06:20 +0100 Subject: [PATCH 08/14] Fix tests --- .../E2ETest/Tests/QuickGridInteractiveTest.cs | 116 +++++++++++++++ .../Tests/QuickGridNoInteractivityTest.cs | 16 +- .../test/E2ETest/Tests/QuickGridTest.cs | 70 --------- .../QuickGrid/QuickGridInteractive.razor | 138 ++++++++++++++++++ 4 files changed, 262 insertions(+), 78 deletions(-) create mode 100644 src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor diff --git a/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs b/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs new file mode 100644 index 000000000000..09a20cf4c91d --- /dev/null +++ b/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.Tests; + +public class QuickGridInteractiveTest : ServerTestBase>> +{ + public QuickGridInteractiveTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + + [Fact] + public void CanColumnSortByInt() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + // Click to sort ascending, wait for result + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")); + + //Compare first row to expected result + Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Matti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Karttunen", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-06-04", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + } + + [Fact] + public void CanColumnSortByString() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + // Click to sort ascending, wait for result + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")); + Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")); + + //Compare first row to expected result + Browser.Equal("12379", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Zbyszek", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Piestrzeniewicz", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-04-02", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + } + + [Fact] + public void CanColumnSortByDateOnly() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + // Click to sort ascending, wait for result + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")); + Browser.Equal("11205", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")); + + //Compare first row to expected result + Browser.Equal("12364", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Paolo", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Accorti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("2018-05-18", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + } + + [Fact] + public void PaginatorCorrectItemsPerPage() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var grid = Browser.FindElement(By.ClassName("quickgrid")); + var rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; + Assert.Equal(10, rowCount); + + Browser.Click(By.CssSelector(".first-paginator .go-next")); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal(10, () => Browser.FindElement(By.CssSelector("#grid .quickgrid")).FindElements(By.CssSelector("tbody > tr")).Count); + } + + [Fact] + public void DualPaginatorsNavigatesAndBothUpdate() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid2 > table")); + + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + Browser.Click(By.CssSelector(".top-paginator .go-next")); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } +} diff --git a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs index ca5dc5ff7466..b4998f47ae2c 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs @@ -79,18 +79,18 @@ public void PaginatorNavigationLinksDisabledCorrectly() { Navigate($"{ServerPathBase}/quickgrid"); - Assert.NotNull(Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("disabled")); - Assert.NotNull(Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("disabled")); - Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("disabled")); - Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("aria-disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("aria-disabled")); + Assert.Equal("false", Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("aria-disabled")); + Assert.Equal("false", Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("aria-disabled")); Browser.FindElement(By.CssSelector(".first-paginator .go-last")).Click(); Browser.Equal("5", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); - Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("disabled")); - Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("disabled")); - Assert.NotNull(Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("disabled")); - Assert.NotNull(Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("disabled")); + Assert.Equal("false", Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("aria-disabled")); + Assert.Equal("false", Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("aria-disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("aria-disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("aria-disabled")); } [Fact] diff --git a/src/Components/test/E2ETest/Tests/QuickGridTest.cs b/src/Components/test/E2ETest/Tests/QuickGridTest.cs index 10547aa080c5..0d4f6e04e0bf 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridTest.cs @@ -31,63 +31,6 @@ protected override void InitializeAsyncCore() app = Browser.MountTestComponent(); } - [Fact] - public void CanColumnSortByInt() - { - - // Click twice to sort by descending - Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")).Click(); - Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")).Click(); - - //Compare first row to expected result - Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Browser.Equal("Matti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); - Browser.Equal("Karttunen", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); - Browser.Equal("1981-06-04", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); - } - - [Fact] - public void CanColumnSortByString() - { - - // Click twice to sort by descending - Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")).Click(); - Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")).Click(); - - //Compare first row to expected result - Browser.Equal("12379", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Browser.Equal("Zbyszek", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); - Browser.Equal("Piestrzeniewicz", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); - Browser.Equal("1981-04-02", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); - } - - [Fact] - public void CanColumnSortByDateOnly() - { - - // Click twice to sort by descending - Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")).Click(); - Browser.FindElement(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")).Click(); - - //Compare first row to expected result - Browser.Equal("12364", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - Browser.Equal("Paolo", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); - Browser.Equal("Accorti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); - Browser.Equal("2018-05-18", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); - } - - [Fact] - public void PaginatorCorrectItemsPerPage() - { - var grid = app.FindElement(By.ClassName("quickgrid")); - var rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; - Assert.Equal(10, rowCount); - - app.FindElement(By.ClassName("go-next")).Click(); - - Browser.Equal(10, () => Browser.FindElement(By.ClassName("quickgrid")).FindElements(By.CssSelector("tbody > tr")).Count); - } - [Fact] public void PaginatorDisplaysCorrectItemCount() { @@ -272,17 +215,4 @@ public void OnRowClickAppliesCursorPointerStyle() Assert.Equal("pointer", cursorStyle); } - [Fact] - public void DualPaginatorsNavigatesAndBothUpdate() - { - app = Browser.MountTestComponent(); - - Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); - Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); - - Browser.FindElement(By.CssSelector(".top-paginator .go-next")).Click(); - - Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); - Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); - } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor new file mode 100644 index 000000000000..6b526f85b85b --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor @@ -0,0 +1,138 @@ +@page "/quickgrid-interactive" +@rendermode RenderMode.InteractiveServer +@using Microsoft.AspNetCore.Components.QuickGrid + +

Sample QuickGrid Component

+ +
+ + + + + + + + + + + + +
+
+ +
+ +
+ +
+
+ + + + + + +
+
+ +
+ +
+ @if (clickedPerson is not null) + { +

PersonId: @clickedPerson.PersonId

+

Name: @clickedPerson.FirstName @clickedPerson.LastName

+

Click count: @clickCount

+ } + else + { +

No row clicked yet

+ } +
+ +@code { + record Person(int PersonId, string FirstName, string LastName, DateOnly BirthDate); + PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; + PaginationState pagination2 = new PaginationState { ItemsPerPage = 5 }; + string firstNameFilter; + QuickGrid quickGridRef; + Person clickedPerson; + int clickCount; + + void HandleRowClick(Person person) + { + clickedPerson = person; + clickCount++; + } + + int ComputeAge(DateOnly birthDate) + => DateTime.Now.Year - birthDate.Year - (birthDate.DayOfYear < DateTime.Now.DayOfYear ? 0 : 1); + + string HighlightJulie(Person person) => person.FirstName == "Julie" ? "highlight" : null; + + IQueryable FilteredPeople + { + get + { + var result = people; + + if (!string.IsNullOrEmpty(firstNameFilter)) + { + result = result.Where(p => p.FirstName.Contains(firstNameFilter, StringComparison.CurrentCultureIgnoreCase)); + } + + return result; + } + } + + // Changes to this list affect the E2E tests. + // If you change this list, you must also update the E2E tests. + IQueryable people = new[] + { + new Person(11203, "Julie", "Smith", new DateOnly(1958, 10, 10)), + new Person(11205, "Nur", "Sari", new DateOnly(1922, 4, 27)), + new Person(11898, "Jose", "Hernandez", new DateOnly(2011, 5, 3)), + new Person(10895, "Jean", "Martin", new DateOnly(1985, 3, 16)), + new Person(10944, "António", "Langa", new DateOnly(1991, 12, 1)), + new Person(12130, "Kenji", "Sato", new DateOnly(2004, 1, 9)), + new Person(12238, "Sven", "Ottlieb", new DateOnly(1973, 11, 15)), + new Person(12345, "Liu", "Wang", new DateOnly(1999, 6, 30)), + new Person(12346, "Giovanni", "Rovelli", new DateOnly(2000, 7, 31)), + new Person(12347, "Eduardo", "Martins", new DateOnly(2001, 8, 1)), + new Person(12348, "Martín", "Sommer", new DateOnly(2002, 9, 2)), + new Person(12349, "Victoria", "Ashworth", new DateOnly(2003, 10, 3)), + new Person(12350, "Hannah", "Moos", new DateOnly(2004, 11, 4)), + new Person(12351, "Palle", "Ibsen", new DateOnly(2005, 12, 5)), + new Person(12352, "Lúcia", "Carvalho", new DateOnly(2006, 1, 6)), + new Person(12353, "Horst", "Kloss", new DateOnly(2007, 2, 7)), + new Person(12354, "Sergio", "Gutiérrez", new DateOnly(2008, 3, 8)), + new Person(12355, "Janine", "Labrune", new DateOnly(2009, 4, 9)), + new Person(12356, "Ann", "Devon", new DateOnly(2010, 5, 10)), + new Person(12357, "Roland", "Mendel", new DateOnly(2011, 6, 11)), + new Person(12358, "Aria", "Cruz", new DateOnly(2012, 7, 12)), + new Person(12359, "Diego", "Roel", new DateOnly(2001, 8, 13)), + new Person(12360, "Martine", "Rancé", new DateOnly(2005, 9, 14)), + new Person(12361, "Maria", "Larsson", new DateOnly(1998, 10, 15)), + new Person(12362, "Peter", "Lewis", new DateOnly(2016, 11, 16)), + new Person(12363, "Carine", "Schmitt", new DateOnly(2017, 12, 13)), + new Person(12364, "Paolo", "Accorti", new DateOnly(2018, 5, 18)), + new Person(12365, "Lino", "Rodriguez", new DateOnly(1980, 2, 19)), + new Person(12367, "Bernardo", "Batista", new DateOnly(1979, 4, 21)), + new Person(12368, "Lúcia", "Carvalho", new DateOnly(1976, 5, 22)), + new Person(12369, "Guillermo", "Fernández", new DateOnly(1983, 6, 23)), + new Person(12370, "Georg", "Pipps", new DateOnly(1982, 7, 24)), + new Person(12371, "Mario", "Pontes", new DateOnly(1981, 8, 25)), + new Person(12372, "Anabela", "Camino", new DateOnly(1980, 9, 26)), + new Person(12380, "Karl", "Jablonski", new DateOnly(1981, 5, 3)), + new Person(12381, "Matti", "Karttunen", new DateOnly(1981, 6, 4)), + new Person(12373, "Helvetius", "Nagy", new DateOnly(1980, 10, 27)), + new Person(12374, "Rita", "Müller", new DateOnly(1980, 11, 28)), + new Person(12375, "Pirkko", "Koskitalo", new DateOnly(1980, 12, 29)), + new Person(12376, "Paula", "Parente", new DateOnly(1981, 1, 30)), + new Person(12377, "Karl", "Jablonski", new DateOnly(1981, 2, 10)), + new Person(12378, "Matti", "Karttunen", new DateOnly(1981, 3, 1)), + new Person(12379, "Zbyszek", "Piestrzeniewicz", new DateOnly(1981, 4, 2)), + }.AsQueryable(); +} From 028cb6d3d90fe96b1dc1ecf3d55d50b1c2807464 Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Tue, 3 Mar 2026 16:53:10 +0100 Subject: [PATCH 09/14] Small optimizations --- .../src/Pagination/Paginator.razor.css | 6 +++--- .../src/QuickGrid.razor.cs | 18 ++---------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css index 0bc26dfee116..af33388c773f 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css @@ -27,16 +27,16 @@ nav { font-size: 0; } - nav a:not([href]) { + nav a[aria-disabled="true"] { opacity: 0.4; pointer-events: none; } - nav a[href]:hover { + nav a:not([aria-disabled="true"]):hover { background-color: #eee; } - nav a[href]:active { + nav a:not([aria-disabled="true"]):active { background-color: #aaa; } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index d7940f849806..c23b218658b5 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -171,7 +171,6 @@ public partial class QuickGrid : IAsyncDisposable private bool _firstRefreshDataAsync = true; private bool _hasReadSortFromQueryString; - private bool _suppressNextLocationChange; private (string ColumnTitle, bool Ascending)? _cachedSortFromQuery; private string SortQueryParameterNameBy => QueryName == "" ? "sort" : $"{QueryName}_sort"; @@ -321,20 +320,13 @@ public Task SortByColumnAsync(ColumnBase column, SortDirection direct _sortByColumn = column; var newUri = GetSortQueryStringUrl(_sortByColumn, _sortByAscending); - _suppressNextLocationChange = true; NavigationManager.NavigateTo(newUri); return RefreshDataAsync(); } - internal string GetSortUrl(ColumnBase column, SortDirection direction = SortDirection.Auto) + internal string GetSortUrl(ColumnBase column) { - var ascending = direction switch - { - SortDirection.Ascending => true, - SortDirection.Descending => false, - SortDirection.Auto => _sortByColumn != column || !_sortByAscending, - _ => throw new NotSupportedException($"Unknown sort direction {direction}"), - }; + var ascending = _sortByColumn != column || !_sortByAscending; return GetSortQueryStringUrl(column, ascending); } @@ -366,12 +358,6 @@ var d when d.Equals("desc", StringComparison.OrdinalIgnoreCase) => (column, fals private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) { - if (_suppressNextLocationChange) - { - _suppressNextLocationChange = false; - return; - } - _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); var sortFromQuery = ReadSortFromQueryString(); var currentSort = _sortByColumn is not null ? (_sortByColumn.Title, _sortByAscending) : ((string? ColumnTitle, bool Ascending)?)null; From 2390df89e78e85f1ec3993a9241c6b4d26c4a30a Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Tue, 3 Mar 2026 17:49:15 +0100 Subject: [PATCH 10/14] Fixes and improvements --- .../src/Pagination/PaginationState.cs | 14 +---- .../src/Pagination/Paginator.razor.cs | 3 - .../src/QuickGrid.razor.cs | 58 ++++++++++--------- .../test/E2ETest/Tests/QuickGridTest.cs | 1 - 4 files changed, 32 insertions(+), 44 deletions(-) diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs index 9a006f719d02..2da161a7af9a 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs @@ -38,7 +38,6 @@ public class PaginationState internal EventCallbackSubscribable CurrentPageItemsChanged { get; } = new(); internal EventCallbackSubscribable TotalItemCountChangedSubscribable { get; } = new(); - internal string QueryName { get; set; } = ""; /// public override int GetHashCode() @@ -52,18 +51,7 @@ public override int GetHashCode() /// A representing the completion of the operation. public Task SetCurrentPageIndexAsync(int pageIndex) { - if (pageIndex < 0) - { - CurrentPageIndex = 0; - } - else if (LastPageIndex.HasValue && pageIndex > LastPageIndex.Value) - { - CurrentPageIndex = LastPageIndex.Value; - } - else - { - CurrentPageIndex = pageIndex; - } + CurrentPageIndex = pageIndex; return CurrentPageItemsChanged.InvokeCallbacksAsync(this); } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs index d1a9f6f832dd..20ebbbee4626 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs @@ -76,9 +76,6 @@ await InvokeAsync(async () => { await State.SetCurrentPageIndexAsync(pageFromQuery); } - - // Always re-render so that page link URLs reflect the current URI - // (e.g., preserving sort query parameters added by QuickGrid). StateHasChanged(); }); } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index c23b218658b5..ee96e7fa8705 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -2,11 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; -using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web.Virtualization; using Microsoft.JSInterop; +using Microsoft.AspNetCore.Components.Forms; namespace Microsoft.AspNetCore.Components.QuickGrid; @@ -144,6 +144,8 @@ public partial class QuickGrid : IAsyncDisposable private ColumnBase? _displayOptionsForColumn; private ColumnBase? _sortByColumn; private bool _sortByAscending; + private ColumnBase? _defaultSortColumn; + private bool _defaultSortAscending; private bool _checkColumnOptionsPosition; // The associated ES6 module, which uses document-level event listeners @@ -170,7 +172,6 @@ public partial class QuickGrid : IAsyncDisposable private bool _firstRefreshDataAsync = true; - private bool _hasReadSortFromQueryString; private (string ColumnTitle, bool Ascending)? _cachedSortFromQuery; private string SortQueryParameterNameBy => QueryName == "" ? "sort" : $"{QueryName}_sort"; @@ -272,15 +273,16 @@ internal void AddColumn(ColumnBase column, SortDirection? initialSort { _sortByColumn = column; _sortByAscending = initialSortDirection.Value != SortDirection.Descending; + _defaultSortColumn = column; + _defaultSortAscending = _sortByAscending; } - if (!_hasReadSortFromQueryString - && _cachedSortFromQuery is { } sortFromQuery + if (_cachedSortFromQuery is { } sortFromQuery && sortFromQuery.ColumnTitle == column.Title) { _sortByColumn = column; _sortByAscending = sortFromQuery.Ascending; - _hasReadSortFromQueryString = true; + _cachedSortFromQuery = null; } } } @@ -289,17 +291,12 @@ private void StartCollectingColumns() { _columns.Clear(); _collectingColumns = true; - _cachedSortFromQuery = !_hasReadSortFromQueryString ? ReadSortFromQueryString() : null; + _cachedSortFromQuery ??= ReadSortFromQueryString(); } private void FinishCollectingColumns() { _collectingColumns = false; - - if (_sortByColumn is not null && !_columns.Contains(_sortByColumn)) - { - _sortByColumn = null; - } } /// @@ -327,7 +324,6 @@ public Task SortByColumnAsync(ColumnBase column, SortDirection direct internal string GetSortUrl(ColumnBase column) { var ascending = _sortByColumn != column || !_sortByAscending; - return GetSortQueryStringUrl(column, ascending); } @@ -359,26 +355,34 @@ var d when d.Equals("desc", StringComparison.OrdinalIgnoreCase) => (column, fals private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) { _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); - var sortFromQuery = ReadSortFromQueryString(); - var currentSort = _sortByColumn is not null ? (_sortByColumn.Title, _sortByAscending) : ((string? ColumnTitle, bool Ascending)?)null; + var currentSort = _sortByColumn is not null ? (_sortByColumn.Title, _sortByAscending) : ((string ColumnTitle, bool Ascending)?)null; - if (sortFromQuery != currentSort) + if (ReadSortFromQueryString() is { } sortFromQuery) { - await InvokeAsync(async () => + if (sortFromQuery != currentSort) { - if (sortFromQuery is { } sort && sort.ColumnTitle is not null) + await InvokeAsync(async () => { - _sortByColumn = _columns.FirstOrDefault(c => c.Title == sort.ColumnTitle); - _sortByAscending = sort.Ascending; - } - else + _sortByColumn = _columns.FirstOrDefault(c => c.Title == sortFromQuery.ColumnTitle); + _sortByAscending = sortFromQuery.Ascending; + await RefreshDataCoreAsync(); + StateHasChanged(); + }); + } + } + else if (currentSort is not null) + { + var defaultSort = _defaultSortColumn is not null ? (_defaultSortColumn.Title, _defaultSortAscending) : ((string ColumnTitle, bool Ascending)?)null; + if (currentSort != defaultSort) + { + await InvokeAsync(async () => { - _sortByColumn = null; - } - - await RefreshDataCoreAsync(); - StateHasChanged(); - }); + _sortByColumn = _defaultSortColumn; + _sortByAscending = _defaultSortAscending; + await RefreshDataCoreAsync(); + StateHasChanged(); + }); + } } } diff --git a/src/Components/test/E2ETest/Tests/QuickGridTest.cs b/src/Components/test/E2ETest/Tests/QuickGridTest.cs index 0d4f6e04e0bf..6eee73bb5127 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridTest.cs @@ -214,5 +214,4 @@ public void OnRowClickAppliesCursorPointerStyle() return row ? getComputedStyle(row).cursor : null;"); Assert.Equal("pointer", cursorStyle); } - } From 1273bc7633ae32e285b8ba185fde9ea07174a4ca Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Tue, 3 Mar 2026 18:13:51 +0100 Subject: [PATCH 11/14] Fix --- .../src/Pagination/PaginationState.cs | 1 + .../src/QuickGrid.razor.cs | 40 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs index 2da161a7af9a..a86eb2b5c017 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs @@ -36,6 +36,7 @@ public class PaginationState /// public event EventHandler? TotalItemCountChanged; + internal string QueryName { get; set; } = ""; internal EventCallbackSubscribable CurrentPageItemsChanged { get; } = new(); internal EventCallbackSubscribable TotalItemCountChangedSubscribable { get; } = new(); diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index ee96e7fa8705..decf5d208d31 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -355,34 +355,30 @@ var d when d.Equals("desc", StringComparison.OrdinalIgnoreCase) => (column, fals private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) { _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); - var currentSort = _sortByColumn is not null ? (_sortByColumn.Title, _sortByAscending) : ((string ColumnTitle, bool Ascending)?)null; + var sortFromQuery = ReadSortFromQueryString(); - if (ReadSortFromQueryString() is { } sortFromQuery) + if (sortFromQuery is { } sort + && _columns.FirstOrDefault(c => c.Title == sort.ColumnTitle) is { } column + && (column != _sortByColumn || sort.Ascending != _sortByAscending)) { - if (sortFromQuery != currentSort) + await InvokeAsync(async () => { - await InvokeAsync(async () => - { - _sortByColumn = _columns.FirstOrDefault(c => c.Title == sortFromQuery.ColumnTitle); - _sortByAscending = sortFromQuery.Ascending; - await RefreshDataCoreAsync(); - StateHasChanged(); - }); - } + _sortByColumn = column; + _sortByAscending = sort.Ascending; + await RefreshDataCoreAsync(); + StateHasChanged(); + }); } - else if (currentSort is not null) + else if (sortFromQuery is null + && (_sortByColumn != _defaultSortColumn || _sortByAscending != _defaultSortAscending)) { - var defaultSort = _defaultSortColumn is not null ? (_defaultSortColumn.Title, _defaultSortAscending) : ((string ColumnTitle, bool Ascending)?)null; - if (currentSort != defaultSort) + await InvokeAsync(async () => { - await InvokeAsync(async () => - { - _sortByColumn = _defaultSortColumn; - _sortByAscending = _defaultSortAscending; - await RefreshDataCoreAsync(); - StateHasChanged(); - }); - } + _sortByColumn = _defaultSortColumn; + _sortByAscending = _defaultSortAscending; + await RefreshDataCoreAsync(); + StateHasChanged(); + }); } } From 45823a2e93227251d894e174141ab574397cff0c Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Wed, 4 Mar 2026 12:07:43 +0100 Subject: [PATCH 12/14] Fix the bug with the unchanged href values in the QuickGrid --- .../src/QuickGrid.razor.cs | 3 +- .../E2ETest/Tests/QuickGridInteractiveTest.cs | 38 +++++++++++++++++++ .../QuickGrid/QuickGridInteractive.razor | 2 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index decf5d208d31..40f2ce950cf9 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -366,7 +366,6 @@ await InvokeAsync(async () => _sortByColumn = column; _sortByAscending = sort.Ascending; await RefreshDataCoreAsync(); - StateHasChanged(); }); } else if (sortFromQuery is null @@ -377,9 +376,9 @@ await InvokeAsync(async () => _sortByColumn = _defaultSortColumn; _sortByAscending = _defaultSortAscending; await RefreshDataCoreAsync(); - StateHasChanged(); }); } + await InvokeAsync(StateHasChanged); } /// diff --git a/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs b/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs index 09a20cf4c91d..cbb98f4daded 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs @@ -32,6 +32,8 @@ public void CanColumnSortByInt() // Click to sort ascending, wait for result Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")); Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); // Click again to sort descending Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")); @@ -41,6 +43,8 @@ public void CanColumnSortByInt() Browser.Equal("Matti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); Browser.Equal("Karttunen", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); Browser.Equal("1981-06-04", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=desc", Browser.Url); } [Fact] @@ -52,6 +56,8 @@ public void CanColumnSortByString() // Click to sort ascending, wait for result Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")); Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("sort=FirstName", Browser.Url); + Assert.Contains("order=asc", Browser.Url); // Click again to sort descending Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")); @@ -61,6 +67,8 @@ public void CanColumnSortByString() Browser.Equal("Zbyszek", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); Browser.Equal("Piestrzeniewicz", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); Browser.Equal("1981-04-02", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Contains("sort=FirstName", Browser.Url); + Assert.Contains("order=desc", Browser.Url); } [Fact] @@ -72,6 +80,8 @@ public void CanColumnSortByDateOnly() // Click to sort ascending, wait for result Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")); Browser.Equal("11205", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("sort=BirthDate", Browser.Url); + Assert.Contains("order=asc", Browser.Url); // Click again to sort descending Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")); @@ -81,6 +91,8 @@ public void CanColumnSortByDateOnly() Browser.Equal("Paolo", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); Browser.Equal("Accorti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); Browser.Equal("2018-05-18", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Contains("sort=BirthDate", Browser.Url); + Assert.Contains("order=desc", Browser.Url); } [Fact] @@ -112,5 +124,31 @@ public void DualPaginatorsNavigatesAndBothUpdate() Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Assert.Contains("people_page=2", Browser.Url); + } + + [Fact] + public void TwoGridsSortIndependently() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + Browser.Exists(By.CssSelector("#grid2 > table")); + + // Sort first grid by PersonId ascending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Sort second grid by FirstName ascending (uses QueryName="people" prefix) + Browser.Click(By.CssSelector("#grid2 > table thead > tr > th:nth-child(2) > div > a.col-title")); + Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid2 > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // First grid should remain sorted + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Verify URL contains both unprefixed and prefixed sort parameters + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + Assert.Contains("people_sort=FirstName", Browser.Url); + Assert.Contains("people_order=asc", Browser.Url); } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor index 6b526f85b85b..244de0b6b56b 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor @@ -28,7 +28,7 @@
- + From b9aa96d3de5785311f217825d07f3c51c9da57db Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Fri, 24 Apr 2026 14:31:53 +0200 Subject: [PATCH 13/14] AppContextSwitch enabled --- .../src/Columns/ColumnBase.razor | 28 +++- .../src/Columns/ColumnBase.razor.css | 3 +- .../src/Pagination/Paginator.razor | 32 ++-- .../src/Pagination/Paginator.razor.cs | 29 +++- .../src/Pagination/Paginator.razor.css | 14 +- .../src/PublicAPI.Unshipped.txt | 4 +- .../src/QuickGrid.razor.cs | 29 ++-- .../src/QuickGridFeatureFlags.cs | 10 ++ .../Tests/QuickGridInteractiveCompatTest.cs | 153 ++++++++++++++++++ .../E2ETest/Tests/QuickGridInteractiveTest.cs | 2 +- .../RazorComponentEndpointsStartup.cs | 9 ++ .../Pages/QuickGrid/QuickGridComponent.razor | 6 +- .../QuickGrid/QuickGridInteractive.razor | 4 +- 13 files changed, 280 insertions(+), 43 deletions(-) create mode 100644 src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGridFeatureFlags.cs create mode 100644 src/Components/test/E2ETest/Tests/QuickGridInteractiveCompatTest.cs diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor index 899a64ebadff..46beae4969ed 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor @@ -1,4 +1,4 @@ -@using Microsoft.AspNetCore.Components.Rendering +@using Microsoft.AspNetCore.Components.Rendering @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @namespace Microsoft.AspNetCore.Components.QuickGrid @@ -22,12 +22,28 @@ } - if ((Sortable.HasValue ? Sortable.Value : IsSortableByDefault()) && Title is not null) + if (Sortable.HasValue ? Sortable.Value : IsSortableByDefault()) { - -
@Title
- -
+ @if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting && Title is not null) + { + +
@Title
+ +
+ } + else if (!QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting && Title is not null) + { + + } + else + { +
+
@Title
+
+ } } else { diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css index 1d1de079f9cf..756257d3d570 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css @@ -7,7 +7,8 @@ } /* If the column is sortable, its title is rendered as a link element for accessibility and to support navigation by tab */ -a.col-title { +a.col-title, +button.col-title { border: none; background: none; position: relative; diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor index fc44e59b62a6..51b229f27909 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor @@ -1,4 +1,4 @@ -@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web @namespace Microsoft.AspNetCore.Components.QuickGrid
@@ -15,14 +15,28 @@ }
}
diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs index 20ebbbee4626..26b90a34237b 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs @@ -41,6 +41,12 @@ public Paginator() private bool CanGoForwards => State.CurrentPageIndex < State.LastPageIndex; private readonly QueryParameterValueSupplier _queryParameterValueSupplier; + private Task GoFirstAsync() => GoToPageAsync(0); + private Task GoPreviousAsync() => GoToPageAsync(State.CurrentPageIndex - 1); + private Task GoNextAsync() => GoToPageAsync(State.CurrentPageIndex + 1); + private Task GoLastAsync() => GoToPageAsync(State.LastPageIndex.GetValueOrDefault(0)); + private Task GoToPageAsync(int pageIndex) => State.SetCurrentPageIndexAsync(pageIndex); + private string GetPageUrl(int pageIndex) { int? pageValue = pageIndex == 0 ? null : pageIndex + 1; @@ -50,19 +56,27 @@ private string GetPageUrl(int pageIndex) /// protected override void OnInitialized() { - NavigationManager.LocationChanged += OnLocationChanged; + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + NavigationManager.LocationChanged += OnLocationChanged; + } } /// protected override Task OnParametersSetAsync() { _totalItemCountChanged.SubscribeOrMove(State.TotalItemCountChangedSubscribable); - _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); - var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; - if (pageFromQuery != State.CurrentPageIndex) + + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) { - return State.SetCurrentPageIndexAsync(pageFromQuery); + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); + var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; + if (pageFromQuery != State.CurrentPageIndex) + { + return State.SetCurrentPageIndexAsync(pageFromQuery); + } } + return Task.CompletedTask; } @@ -94,7 +108,10 @@ await InvokeAsync(async () => /// public void Dispose() { - NavigationManager.LocationChanged -= OnLocationChanged; + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + NavigationManager.LocationChanged -= OnLocationChanged; + } _totalItemCountChanged.Dispose(); } } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css index af33388c773f..a15905bfc6b8 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css @@ -17,7 +17,8 @@ nav { align-items: center; } - nav a { + nav a, + nav button { border: 0; background: none center center / 1rem no-repeat; width: 2rem; @@ -25,18 +26,23 @@ nav { display: inline-block; text-decoration: none; font-size: 0; + cursor: pointer; + padding: 0; } - nav a[aria-disabled="true"] { + nav a[aria-disabled="true"], + nav button:disabled { opacity: 0.4; pointer-events: none; } - nav a:not([aria-disabled="true"]):hover { + nav a:not([aria-disabled="true"]):hover, + nav button:not(:disabled):hover { background-color: #eee; } - nav a:not([aria-disabled="true"]):active { + nav a:not([aria-disabled="true"]):active, + nav button:not(:disabled):active { background-color: #aaa; } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt index 033fd8769244..bc71853880cf 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt @@ -1,6 +1,6 @@ #nullable enable -Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryName.get -> string! -Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryName.set -> void +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryParameterNamePrefix.get -> string! +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryParameterNamePrefix.set -> void Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnRowClick.get -> Microsoft.AspNetCore.Components.EventCallback Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnRowClick.set -> void override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnInitialized() -> void diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index 40f2ce950cf9..af620bc09e17 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -119,7 +119,7 @@ public partial class QuickGrid : IAsyncDisposable /// The parameter from which the page and sorting URL parameters are derived. The default value is an empty string, which results in query parameters named "page", "sort", and "order". If you provide a non-empty value, for example "products", /// then the query parameters will be "products_page", "products_sort", and "products_order". This allows you to use multiple components on the same page without their URL parameters conflicting with each other. ///
- [Parameter] public string QueryName { get; set; } = ""; + [Parameter] public string QueryParameterNamePrefix { get; set; } = ""; [Inject] private IServiceProvider Services { get; set; } = default!; [Inject] private IJSRuntime JS { get; set; } = default!; @@ -174,9 +174,9 @@ public partial class QuickGrid : IAsyncDisposable private (string ColumnTitle, bool Ascending)? _cachedSortFromQuery; - private string SortQueryParameterNameBy => QueryName == "" ? "sort" : $"{QueryName}_sort"; - private string SortQueryParameterNameOrder => QueryName == "" ? "order" : $"{QueryName}_order"; - private string PageQueryParameterName => QueryName == "" ? "page" : $"{QueryName}_page"; + private string SortQueryParameterNameBy => QueryParameterNamePrefix == "" ? "sort" : $"{QueryParameterNamePrefix}_sort"; + private string SortQueryParameterNameOrder => QueryParameterNamePrefix == "" ? "order" : $"{QueryParameterNamePrefix}_order"; + private string PageQueryParameterName => QueryParameterNamePrefix == "" ? "page" : $"{QueryParameterNamePrefix}_page"; private readonly QueryParameterValueSupplier _queryParameterValueSupplier; /// @@ -202,8 +202,11 @@ public QuickGrid() /// protected override void OnInitialized() { - _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); - NavigationManager.LocationChanged += OnLocationChanged; + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); + NavigationManager.LocationChanged += OnLocationChanged; + } } /// @@ -316,8 +319,13 @@ public Task SortByColumnAsync(ColumnBase column, SortDirection direct }; _sortByColumn = column; - var newUri = GetSortQueryStringUrl(_sortByColumn, _sortByAscending); - NavigationManager.NavigateTo(newUri); + + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + var newUri = GetSortQueryStringUrl(_sortByColumn, _sortByAscending); + NavigationManager.NavigateTo(newUri); + } + return RefreshDataAsync(); } @@ -558,7 +566,10 @@ private string GridClass() /// public async ValueTask DisposeAsync() { - NavigationManager.LocationChanged -= OnLocationChanged; + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + NavigationManager.LocationChanged -= OnLocationChanged; + } _wasDisposed = true; _currentPageItemsChanged.Dispose(); diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGridFeatureFlags.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGridFeatureFlags.cs new file mode 100644 index 000000000000..1c91fe421ac1 --- /dev/null +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGridFeatureFlags.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.QuickGrid; + +internal static class QuickGridFeatureFlags +{ + internal static bool EnableUrlBasedQuickGridNavigationAndSorting => + !AppContext.TryGetSwitch("Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting", out var isEnabled) || isEnabled; +} diff --git a/src/Components/test/E2ETest/Tests/QuickGridInteractiveCompatTest.cs b/src/Components/test/E2ETest/Tests/QuickGridInteractiveCompatTest.cs new file mode 100644 index 000000000000..ac0e0673ad3d --- /dev/null +++ b/src/Components/test/E2ETest/Tests/QuickGridInteractiveCompatTest.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.Tests; + +/// +/// Tests QuickGrid with URL-driven navigation disabled via the +/// Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting AppContext switch. +/// Mirrors but asserts button-based sorting and pagination +/// instead of anchor-based navigation with URL query parameters. +/// +public class QuickGridInteractiveCompatTest : ServerTestBase>> +{ + public QuickGridInteractiveCompatTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + protected override void InitializeAsyncCore() + { + _serverFixture.AdditionalArguments.Add("--DisableUrlDrivenNavigation=true"); + base.InitializeAsyncCore(); + } + + public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + + [Fact] + public void CanColumnSortByInt() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var initialUrl = Browser.Url; + + // Click button to sort ascending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > button.col-title")); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // URL should not change in compat mode + Assert.Equal(initialUrl, Browser.Url); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > button.col-title")); + + Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Matti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Karttunen", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-06-04", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + + // URL should still not change + Assert.Equal(initialUrl, Browser.Url); + } + + [Fact] + public void CanColumnSortByString() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var initialUrl = Browser.Url; + + // Click button to sort ascending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > button.col-title")); + Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Equal(initialUrl, Browser.Url); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > button.col-title")); + + Browser.Equal("12379", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Zbyszek", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Piestrzeniewicz", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-04-02", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Equal(initialUrl, Browser.Url); + } + + [Fact] + public void CanColumnSortByDateOnly() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var initialUrl = Browser.Url; + + // Click button to sort ascending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > button.col-title")); + Browser.Equal("11205", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Equal(initialUrl, Browser.Url); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > button.col-title")); + + Browser.Equal("12364", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Paolo", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Accorti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("2018-05-18", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Equal(initialUrl, Browser.Url); + } + + [Fact] + public void PaginatorCorrectItemsPerPage() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var grid = Browser.FindElement(By.ClassName("quickgrid")); + var rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; + Assert.Equal(10, rowCount); + + Browser.Click(By.CssSelector(".first-paginator .go-next")); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal(10, () => Browser.FindElement(By.CssSelector("#grid .quickgrid")).FindElements(By.CssSelector("tbody > tr")).Count); + + // URL should not change in compat mode + Assert.DoesNotContain("page=", Browser.Url); + } + + [Fact] + public void TwoGridsSortIndependently() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + Browser.Exists(By.CssSelector("#grid2 > table")); + + var initialUrl = Browser.Url; + + // Sort first grid by PersonId ascending (button, not anchor) + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > button.col-title")); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Sort second grid by FirstName ascending (button, not anchor) + Browser.Click(By.CssSelector("#grid2 > table thead > tr > th:nth-child(2) > div > button.col-title")); + Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid2 > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // First grid should remain sorted + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // URL should not change in compat mode + Assert.Equal(initialUrl, Browser.Url); + } +} diff --git a/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs b/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs index cbb98f4daded..d9f31d046afd 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs @@ -138,7 +138,7 @@ public void TwoGridsSortIndependently() Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")); Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); - // Sort second grid by FirstName ascending (uses QueryName="people" prefix) + // Sort second grid by FirstName ascending (uses QueryParameterNamePrefix="people" prefix) Browser.Click(By.CssSelector("#grid2 > table thead > tr > th:nth-child(2) > div > a.col-title")); Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid2 > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 3361507131f3..220260c9066c 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -31,6 +31,15 @@ public RazorComponentEndpointsStartup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + if (Configuration.GetValue("DisableUrlDrivenNavigation")) + { + AppContext.SetSwitch("Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting", false); + } + else + { + AppContext.SetSwitch("Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting", true); + } + services.AddValidation(); services.AddRazorComponents(options => diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor index eca19b0dcee3..392401ecf9dc 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor @@ -1,4 +1,4 @@ -@page "/quickgrid" +@page "/quickgrid" @using Microsoft.AspNetCore.Components.QuickGrid @using Microsoft.AspNetCore.Components.Forms @@ -15,7 +15,7 @@
- + @@ -31,7 +31,7 @@
- + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor index 244de0b6b56b..3d8614f27f40 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor @@ -1,4 +1,4 @@ -@page "/quickgrid-interactive" +@page "/quickgrid-interactive" @rendermode RenderMode.InteractiveServer @using Microsoft.AspNetCore.Components.QuickGrid @@ -28,7 +28,7 @@
- + From df8c4104379392d74421fc93509bfa792e1d5247 Mon Sep 17 00:00:00 2001 From: Daria Tiurina Date: Fri, 24 Apr 2026 15:18:32 +0200 Subject: [PATCH 14/14] Clean-up --- .../src/Pagination/Paginator.razor.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs index 26b90a34237b..fadf0784b88f 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs @@ -37,22 +37,24 @@ public Paginator() _queryParameterValueSupplier = new(); } - private bool CanGoBack => State.CurrentPageIndex > 0; - private bool CanGoForwards => State.CurrentPageIndex < State.LastPageIndex; private readonly QueryParameterValueSupplier _queryParameterValueSupplier; - private Task GoFirstAsync() => GoToPageAsync(0); - private Task GoPreviousAsync() => GoToPageAsync(State.CurrentPageIndex - 1); - private Task GoNextAsync() => GoToPageAsync(State.CurrentPageIndex + 1); - private Task GoLastAsync() => GoToPageAsync(State.LastPageIndex.GetValueOrDefault(0)); - private Task GoToPageAsync(int pageIndex) => State.SetCurrentPageIndexAsync(pageIndex); - private string GetPageUrl(int pageIndex) { int? pageValue = pageIndex == 0 ? null : pageIndex + 1; return NavigationManager.GetUriWithQueryParameter(QueryName, pageValue); } + private Task GoFirstAsync() => GoToPageAsync(0); + private Task GoPreviousAsync() => GoToPageAsync(State.CurrentPageIndex - 1); + private Task GoNextAsync() => GoToPageAsync(State.CurrentPageIndex + 1); + private Task GoLastAsync() => GoToPageAsync(State.LastPageIndex.GetValueOrDefault(0)); + + private bool CanGoBack => State.CurrentPageIndex > 0; + private bool CanGoForwards => State.CurrentPageIndex < State.LastPageIndex; + private Task GoToPageAsync(int pageIndex) + => State.SetCurrentPageIndexAsync(pageIndex); + /// protected override void OnInitialized() {