Skip to content

QuickGrid SSR support#65451

Open
dariatiurina wants to merge 12 commits intodotnet:mainfrom
dariatiurina:51249-quickgrid-ssr-sorting
Open

QuickGrid SSR support#65451
dariatiurina wants to merge 12 commits intodotnet:mainfrom
dariatiurina:51249-quickgrid-ssr-sorting

Conversation

@dariatiurina
Copy link
Copy Markdown
Contributor

@dariatiurina dariatiurina commented Feb 17, 2026

QuickGrid SSR support

Description

QuickGrid's Paginator and column sorting currently require interactivity because they rely on @onclick event handlers and in-memory state, which don't work with the StaticHtmlRenderer. Page index and sort state are stored in memory, so they're lost between SSR requests and can't be shared via URL.

This PR adds SSR support to both the Paginator component and column sorting in QuickGrid, so that pagination and sorting work without interactivity by persisting state in the URL query string using regular <a> links.

Problem

There were three main blockers preventing QuickGrid from working fully in SSR:

  1. @onclick is not supported in SSR — The StaticHtmlRenderer does not render event handler attributes like @onclick, so the pagination buttons and sort column headers had no effect.
  2. In-memory page statePaginationState stores the current page index in memory, which is lost between SSR requests. Users also cannot share URLs pointing to a specific page.
  3. In-memory sort state — The current sort column and direction are stored in memory on QuickGrid, which is also lost between SSR requests.

Considered solutions

  • Session / TempData — Persist state server-side, keeping public APIs unchanged. Discarded, because it still doesn't allow URL sharing between users.
  • Query string parameters — Persist state in the URL as ?page=N, ?sort=ColumnTitle, ?order=asc/desc. This option was chosen because it provides a familiar UX, enables link sharing, and works with both SSR and interactive rendering.

Implemented changes

QuickGrid (QuickGrid.razor.cs)

  • New QueryName parameter ([Parameter], defaults to "") — specifies a prefix for query string parameter names used to persist pagination and sort state. When empty, parameters are named page, sort, and order. When set (e.g., "products"), parameters become products_page, products_sort, and products_order. This allows multiple grids on the same page without URL parameter conflicts.
  • NavigationManager injection — used to read the current URL and navigate with updated query parameters.
  • QueryParameterValueSupplier — used to parse query string values from the current URL.
  • OnInitialized — parses the initial URL query string and subscribes to NavigationManager.LocationChanged to react to URL changes (e.g., browser back/forward).
  • OnParametersSetAsync — propagates the page query parameter name to the associated PaginationState so the Paginator uses the correct parameter name.
  • ReadSortFromQueryString() — parses ?sort=ColumnTitle&order=asc from the URL using QueryParameterValueSupplier and caches the result for use during column collection. The sort column is identified by its Title property.
  • GetSortUrl() / GetSortQueryStringUrl() — generates URLs with updated sort query parameters using NavigationManager.GetUriWithQueryParameters().
  • OnLocationChanged — responds to external URL changes (back/forward navigation) by re-reading sort state and refreshing data. Falls back to the default sort column/direction when sort parameters are removed from the URL.
  • Default sort tracking — stores the initial default sort column and direction (_defaultSortColumn, _defaultSortAscending) so that removing sort params from the URL restores the original sort order.
  • DisposeAsync — updated to unsubscribe from LocationChanged.

Column sorting (ColumnBase.razor / ColumnBase.razor.css)

  • Sortable column headers changed from <button> to <a> — each sortable column header now renders as an <a> element with an href pointing to a URL with updated sort query parameters (via Grid.GetSortUrl(this)). This works in both SSR and interactive rendering without branching.
  • Added null check for Title — sorting is only rendered when the column has a non-null Title (needed to identify the column in query parameters).
  • CSS updated — selectors changed from button.col-title to a.col-title, with text-decoration: none and color: inherit added for link styling consistency.

Paginator rendering (Paginator.razor / Paginator.razor.css)

  • Navigation buttons changed from <button> to <a> — each pagination button (first, previous, next, last) is now an <a> element with an href pointing to the URL for the target page. Disabled state uses aria-disabled="true" and pointer-events: none instead of the HTML disabled attribute.
  • CSS updated — selectors changed from nav button to nav a, with aria-disabled selectors replacing [disabled].

Paginator logic (Paginator.razor.cs)

  • NavigationManager injection — used to generate page URLs via GetUriWithQueryParameter().
  • QueryParameterValueSupplier — used to parse the current page from the URL query string.
  • OnInitialized — subscribes to NavigationManager.LocationChanged.
  • OnParametersSetOnParametersSetAsync — changed to async to support reading the query string on first render and setting the initial page index.
  • ReadPageIndexFromQueryString() — parses the 1-based page number from the URL and converts it to a 0-based index.
  • GetPageUrl() — generates URLs for pagination links. Page 1 omits the query parameter (clean URL); other pages use ?page=N (1-based).
  • OnLocationChanged — responds to external URL changes (back/forward navigation) by re-reading page state and updating accordingly.
  • Dispose — unsubscribes from LocationChanged.

PaginationState (PaginationState.cs)

  • QueryName internal property — set by QuickGrid so the Paginator reads the correct query parameter name (e.g., page or products_page).

Shared infrastructure changes

  • Moved to shared sourceQueryParameterValueSupplier.cs, QueryParameterNameComparer.cs, StringSegmentAccumulator.cs, and UrlValueConstraint.cs moved from Components/src/Routing/ to Shared/src/ so they can be consumed by both the Components project and QuickGrid.
  • QueryParameterValueSupplier.GetQueryString() — new static method that extracts the query string portion from a URL. Previously a local static method in SupplyParameterFromQueryValueProvider, now shared.
  • QueryStringEnumerable.cs — added as a shared source compile include in the QuickGrid project.

Project references

  • Microsoft.AspNetCore.Components.csproj — added shared source includes for the moved routing files.
  • Microsoft.AspNetCore.Components.QuickGrid.csproj — added shared source includes for QueryParameterValueSupplier, QueryParameterNameComparer, StringSegmentAccumulator, UrlValueConstraint, and QueryStringEnumerable.

Public API changes (PublicAPI.Unshipped.txt)

+Microsoft.AspNetCore.Components.QuickGrid.QuickGrid<TGridItem>.QueryName.get -> string!
+Microsoft.AspNetCore.Components.QuickGrid.QuickGrid<TGridItem>.QueryName.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<TGridItem>.OnInitialized() -> void
*REMOVED*override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnParametersSet() -> void

Breaking change

The sort column headers and paginator buttons have changed from <button> elements to <a> (link) elements. Any custom CSS targeting button.col-title or nav button in the paginator will need to be updated to target a.col-title or nav a respectively.

Additionally, when multiple QuickGrid components are used on the same page, each one now must have a unique QueryName value. Since QueryName defaults to "", having two or more grids without explicitly setting different QueryName values will cause them to share the same query parameters (page, sort, order) for pagination and sorting, and interfere with each other. This applies to both SSR and interactive rendering.

Before (worked implicitly):

<QuickGrid>...</QuickGrid>
<QuickGrid>...</QuickGrid>

After (requires unique QueryName per grid):

<QuickGrid>...</QuickGrid>
<QuickGrid QueryName="products">...</QuickGrid>

Shared PaginationState limitation

Each QuickGrid propagates its page query parameter name to the associated PaginationState during OnParametersSetAsync, so that the connected Paginator uses the same query parameter name. This means multiple QuickGrid components must not share the same PaginationState instance if they have different QueryName values — the last grid to render will overwrite the query name on the shared state, causing the Paginator to read from the wrong query parameter. Each grid should have its own PaginationState instance.

Usage example

@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid Items="@people" Pagination="@pagination">
    <PropertyColumn Property="@(p => p.Name)" Sortable="true" />
    <PropertyColumn Property="@(p => p.Age)" Sortable="true" />
    <PropertyColumn Property="@(p => p.City)" Sortable="true" />
</QuickGrid>
<Paginator State="@pagination" />

@* Multiple grids on the same page with different query names *@
<QuickGrid Items="@cities" Pagination="@pagination2" QueryName="cities">
    <PropertyColumn Property="@(c => c.Name)" Sortable="true" />
    <PropertyColumn Property="@(c => c.Country)" Sortable="true" />
</QuickGrid>
<Paginator State="@pagination2" />

@code {
    PaginationState pagination = new PaginationState { ItemsPerPage = 10 };
    PaginationState pagination2 = new PaginationState { ItemsPerPage = 5 };
    private IQueryable<Person> people = GetPeople().AsQueryable();
    private IQueryable<City> cities = GetCities().AsQueryable();
}

Fixes #51249

@github-actions github-actions bot added the area-blazor Includes: Blazor, Razor Components label Feb 17, 2026
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.
@dariatiurina dariatiurina force-pushed the 51249-quickgrid-ssr-sorting branch from 92bb4b5 to 1d9c7a7 Compare February 17, 2026 16:58
@dariatiurina dariatiurina self-assigned this Feb 17, 2026
@dariatiurina dariatiurina marked this pull request as ready for review February 23, 2026 13:20
@dariatiurina dariatiurina requested a review from a team as a code owner February 23, 2026 13:20
Copilot AI review requested due to automatic review settings February 23, 2026 13:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds static SSR (non-interactive) support to QuickGrid pagination and column sorting by persisting state in the URL query string and rendering SSR-compatible form posts for interactions.

Changes:

  • Introduces QuickGrid<TGridItem>.QueryName and URL query-string synchronization for sort state (including back/forward handling).
  • Updates Paginator and sortable column headers to render SSR-friendly <form method="post" data-enhance ...> interactions with antiforgery.
  • Adds E2E coverage for no-interactivity pagination/sorting scenarios plus supporting test pages/components.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs Adds QueryName, query-string sort persistence, and navigation event handling/disposal.
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor SSR branch for sortable headers using form posts; wires ColumnIndex from grid.
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.cs Stores ColumnIndex and derives SSR form name from QueryName.
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor SSR branch for pagination buttons using form posts + antiforgery.
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs Reads/writes current page to query string; listens to navigation changes.
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs Adds internal QueryName and clamps page index based on known bounds.
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Infrastructure/QueryStringHelper.cs Adds helper to read first matching query parameter via QueryStringEnumerable.
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj Includes shared QueryStringEnumerable.cs in compilation.
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt Declares new/changed public API surface for shipping.
src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridRaceConditionTest.cs Registers a test NavigationManager for new injection usage.
src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs New E2E tests validating SSR paging/sorting + URL persistence.
src/Components/test/E2ETest/Tests/QuickGridTest.cs Adds interactive-mode dual-paginator coverage.
src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor Adds SSR test page with multiple grids/paginators and distinct QueryNames.
src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj Adds QuickGrid reference needed by the new test page.
src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridDualPaginatorComponent.razor Adds interactive test component for dual paginators.
src/Components/test/testassets/BasicTestApp/Index.razor Adds menu entry for the new interactive test component.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@dariatiurina
Copy link
Copy Markdown
Contributor Author

dariatiurina commented Feb 23, 2026

It is also a fix for #57289

@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Mar 2, 2026
@dariatiurina dariatiurina removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Mar 3, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 24 changed files in this pull request and generated 4 comments.

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime.
To make sure no conflicting changes have occurred, please rerun validation before merging. You can do this by leaving an /azp run comment here (requires commit rights), or by simply closing and reopening.

@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Mar 11, 2026
ilonatommy added a commit that referenced this pull request Mar 20, 2026
)

These tests were unquarantined in #65864 but are failing again across
multiple PRs (#64964, #65871, #65451) in the Helix x64 Subset 2 job.
Re-quarantining only the RazorRuntimeCompilationHostingStartupTest
methods, not the RazorBuildTest ones.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ilonatommy added a commit that referenced this pull request Mar 20, 2026
) (#65881)

These tests were unquarantined in #65864 but are failing again across
multiple PRs (#64964, #65871, #65451) in the Helix x64 Subset 2 job.
Re-quarantining only the RazorRuntimeCompilationHostingStartupTest
methods, not the RazorBuildTest ones.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun

Projects

None yet

Development

Successfully merging this pull request may close these issues.

QuickGrid support for static server-side rendering (static SSR)

2 participants