You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a ScrollToItemAsync(int itemIndex) method and a FirstVisibleItemIndex parameter to the Virtualize<TItem> component, enabling programmatic scrolling to any item by index and reading the current first visible item index.
Users are forced into fragile workarounds: JS interop to manually compute scroll offsets, which breaks unpredictably with variable-height items and virtualization window shifts.
Real-world scenarios blocked by this gap:
Map-pin-to-list sync (10K+ items, clicking a pin scrolls to the corresponding list card)
Table-of-contents navigation in document viewers
Return-to-position after edit-and-back-navigate
Chat/log viewers jumping to a specific message
Setting initial scroll position without visible flash-of-wrong-content
The infrastructure needed for this feature — anchor snapshotting, IO suppression, and convergence — is being introduced by #66262 (AnchorMode). This design reuses that mechanism for scroll-to-item.
In scope
ScrollToItemAsync(int itemIndex) — programmatically scroll to any item by zero-based index. Works with both Items and ItemsProvider. Returns a Task that completes when the target item is fully rendered, precisely aligned, and all IO/resize cycles have settled.
FirstVisibleItemIndex parameter — dual-purpose: setter sets the initial scroll position on first render (latched once, ignored on re-renders); getter returns the zero-based index of the first item currently visible in the viewport, reported from JS via the existing IO callbacks.
Variable-height item support — the scroll uses actual DOM measurement, not pixel-offset estimation, for final alignment.
Async ItemsProvider support — convergence handles placeholder → real item transitions.
Race-safe rapid calls — monotonic request IDs ensure only the latest call wins.
Risks / unknowns
Multi-element templates:ChildContent can render multiple root DOM elements per item. Sibling-walking can't reliably identify item N. Mitigated by adding a temporary wrapper element with data-virtualize-item="{index}" during scroll-to-item operations (removed after convergence).
IO suppression scope: AnchorMode suppresses IntersectionObserver callbacks until user scroll. ScrollToItemAsync must auto-re-arm observers after alignment (viewport intentionally changed). Incorrect re-arm timing could cause feedback loops or stale distribution.
Server-side Blazor latency: Each convergence cycle is a C#→JS round-trip. Convergence will take more cycles but should complete correctly. Need to validate there's no visible flicker.
Convergence guard: If item height estimates are wildly off, convergence could loop. A max iteration count (e.g., 10 render cycles) with a forced fallback prevents this.
Detailed design
Two-stage scroll approach
Rather than manually setting internal _itemsBefore state (which bypasses distribution invariants), the design uses two stages:
Stage 1 — Estimated jump (JS-driven):
JS sets scrollTop to an estimated pixel offset (targetIndex × avgItemHeight). This triggers the normal IntersectionObserver cycle which naturally adjusts the rendered window to include the target index.
Stage 2 — Precise alignment (after render):
Once the target item is in the rendered DOM, JS locates it via a data-virtualize-item attribute, measures its actual position, and adjusts scrollTop to align it with the container's top. The Task completes at this point.
Reused infrastructure from AnchorMode:
Mechanism
ScrollToItem use
updateAnchorSnapshot()
Save target item position after precise alignment
ignoreAnchorScroll flag
Skip synthetic scroll events from estimated jump and precise alignment
suppressSpacerCallbacks
Suppress stale IO from estimated jump, auto-re-arm after alignment via microtask
Convergence observing
Keep target item stable as async items load
Key difference: AnchorMode suppresses IO until next user scroll (viewport unchanged). ScrollToItem re-arms observers automatically after alignment because the viewport intentionally changed.
Race handling
A monotonic _scrollRequestId is incremented on each ScrollToItemAsync call. All JS calls include the ID. Stale operations are ignored in both C# (OnAfterRenderAsync) and JS (alignToItem). A new call cancels the previous TaskCompletionSource.
Multi-element templates
During scroll-to-item, BuildRenderTree wraps each item in a lightweight element with data-virtualize-item="{logicalIndex}". JS uses querySelector to find the target. The wrapper is removed after convergence completes.
API surface
// New methodpublicTaskScrollToItemAsync(intitemIndex);// New parameter — setter: initial scroll position (latched once);// getter: current first visible item index (updated from JS on scroll)[Parameter]publicint?FirstVisibleItemIndex{get;set;}
Considered alternatives
Directly setting _itemsBefore in C# — rejected because it bypasses UpdateItemDistribution invariants (clamping, skip-refresh logic, render sequencing), causing duplicate renders and mismatched state.
DOM sibling-walking to find target item — rejected because ChildContent can render multiple root elements per item, making child index unreliable for identifying specific logical items.
Always rendering wrapper elements — rejected as a breaking change to existing DOM structure with unnecessary performance overhead for the common case.
Waiting for user scroll to re-arm IO (matching AnchorMode) — rejected because ScrollToItemAsync intentionally changes the viewport. Waiting would deadlock virtualization.
Summary
Add a
ScrollToItemAsync(int itemIndex)method and aFirstVisibleItemIndexparameter to theVirtualize<TItem>component, enabling programmatic scrolling to any item by index and reading the current first visible item index.Motivation and goals
Virtualizehas no public API to scroll to a specific item. This is the most-requested Virtualize feature (Virtualize component Row Index Enhancements (API to scroll to item) #26943).The infrastructure needed for this feature — anchor snapshotting, IO suppression, and convergence — is being introduced by #66262 (
AnchorMode). This design reuses that mechanism for scroll-to-item.In scope
ScrollToItemAsync(int itemIndex)— programmatically scroll to any item by zero-based index. Works with bothItemsandItemsProvider. Returns aTaskthat completes when the target item is fully rendered, precisely aligned, and all IO/resize cycles have settled.FirstVisibleItemIndexparameter — dual-purpose: setter sets the initial scroll position on first render (latched once, ignored on re-renders); getter returns the zero-based index of the first item currently visible in the viewport, reported from JS via the existing IO callbacks.ItemsProvidersupport — convergence handles placeholder → real item transitions.Risks / unknowns
ChildContentcan render multiple root DOM elements per item. Sibling-walking can't reliably identify item N. Mitigated by adding a temporary wrapper element withdata-virtualize-item="{index}"during scroll-to-item operations (removed after convergence).IntersectionObservercallbacks until user scroll.ScrollToItemAsyncmust auto-re-arm observers after alignment (viewport intentionally changed). Incorrect re-arm timing could cause feedback loops or stale distribution.Detailed design
Two-stage scroll approach
Rather than manually setting internal
_itemsBeforestate (which bypasses distribution invariants), the design uses two stages:Stage 1 — Estimated jump (JS-driven):
JS sets
scrollTopto an estimated pixel offset (targetIndex × avgItemHeight). This triggers the normalIntersectionObservercycle which naturally adjusts the rendered window to include the target index.Stage 2 — Precise alignment (after render):
Once the target item is in the rendered DOM, JS locates it via a
data-virtualize-itemattribute, measures its actual position, and adjustsscrollTopto align it with the container's top. TheTaskcompletes at this point.Reused infrastructure from AnchorMode:
updateAnchorSnapshot()ignoreAnchorScrollflagsuppressSpacerCallbacksKey difference: AnchorMode suppresses IO until next user scroll (viewport unchanged). ScrollToItem re-arms observers automatically after alignment because the viewport intentionally changed.
Race handling
A monotonic
_scrollRequestIdis incremented on eachScrollToItemAsynccall. All JS calls include the ID. Stale operations are ignored in both C# (OnAfterRenderAsync) and JS (alignToItem). A new call cancels the previousTaskCompletionSource.Multi-element templates
During scroll-to-item,
BuildRenderTreewraps each item in a lightweight element withdata-virtualize-item="{logicalIndex}". JS usesquerySelectorto find the target. The wrapper is removed after convergence completes.API surface
Considered alternatives
Directly setting
_itemsBeforein C# — rejected because it bypassesUpdateItemDistributioninvariants (clamping, skip-refresh logic, render sequencing), causing duplicate renders and mismatched state.DOM sibling-walking to find target item — rejected because
ChildContentcan render multiple root elements per item, making child index unreliable for identifying specific logical items.Always rendering wrapper elements — rejected as a breaking change to existing DOM structure with unnecessary performance overhead for the common case.
Waiting for user scroll to re-arm IO (matching AnchorMode) — rejected because
ScrollToItemAsyncintentionally changes the viewport. Waiting would deadlock virtualization.