Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ Microsoft.AspNetCore.Components.Web.SupplyParameterFromSessionAttribute.Name.set
Microsoft.AspNetCore.Components.Web.SupplyParameterFromSessionAttribute.SupplyParameterFromSessionAttribute() -> void
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.AnchorMode.get -> Microsoft.AspNetCore.Components.Web.Virtualization.VirtualizeAnchorMode
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.AnchorMode.set -> void
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.InitialIndex.get -> int
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.InitialIndex.set -> void
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.InitialItemIndex.get -> int
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.InitialItemIndex.set -> void
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.ItemComparer.get -> System.Collections.Generic.IEqualityComparer<TItem>!
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.ItemComparer.set -> void
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.ScrollToIndexAsync(int itemIndex, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.ScrollToItemAsync(int itemIndex, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.Web.Virtualization.VirtualizeAnchorMode
Microsoft.AspNetCore.Components.Web.Virtualization.VirtualizeAnchorMode.Beginning = 1 -> Microsoft.AspNetCore.Components.Web.Virtualization.VirtualizeAnchorMode
Microsoft.AspNetCore.Components.Web.Virtualization.VirtualizeAnchorMode.End = 2 -> Microsoft.AspNetCore.Components.Web.Virtualization.VirtualizeAnchorMode
Expand Down
38 changes: 19 additions & 19 deletions src/Components/Web/src/Virtualization/Virtualize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,11 @@ public IEqualityComparer<TItem> ItemComparer
/// <summary>
/// Gets or sets the zero-based index of the item to scroll to on first interactive render.
/// Applied once when the component first knows its item count and ignored on subsequent re-renders;
/// to scroll programmatically at any later point, call <see cref="ScrollToIndexAsync(int, CancellationToken)"/>.
/// to scroll programmatically at any later point, call <see cref="ScrollToItemAsync(int, CancellationToken)"/>.
/// Out-of-range values are clamped. The default value, <c>0</c>, means no initial scroll.
/// </summary>
[Parameter]
public int InitialIndex { get; set; }
public int InitialItemIndex { get; set; }

private IEqualityComparer<TItem> _itemComparer = EqualityComparer<TItem>.Default;

Expand All @@ -232,28 +232,28 @@ public async Task RefreshDataAsync()
/// Scrolls the viewport so the item at <paramref name="itemIndex"/> is aligned to the start of the visible area.
/// </summary>
/// <remarks>
/// Each call cancels any previously-running <see cref="ScrollToIndexAsync(int, CancellationToken)"/> (last call wins).
/// Each call cancels any previously-running <see cref="ScrollToItemAsync(int, CancellationToken)"/> (last call wins).
/// Must be called on the renderer's synchronization context; background-thread callers should wrap with
/// <see cref="ComponentBase.InvokeAsync(Func{Task})"/> to await completion.
/// </remarks>
/// <param name="itemIndex">The zero-based index of the item to scroll to.</param>
/// <param name="cancellationToken">A token that lets the caller request cancellation.</param>
/// <returns>A <see cref="Task"/> that completes when the target is aligned or superseded by another call,
/// or faults with <see cref="OperationCanceledException"/> if <paramref name="cancellationToken"/> is cancelled.</returns>
public Task ScrollToIndexAsync(int itemIndex, CancellationToken cancellationToken = default)
public Task ScrollToItemAsync(int itemIndex, CancellationToken cancellationToken = default)
{
if (_jsInterop is null)
{
// Throw synchronously so misuse is reported on the call site, not on the Task.
throw new InvalidOperationException(
$"{nameof(ScrollToIndexAsync)} cannot be called before the {nameof(Virtualize<TItem>)} has been initialized for interactive rendering. " +
$"Use the {nameof(InitialIndex)} parameter to set the initial scroll position.");
$"{nameof(ScrollToItemAsync)} cannot be called before the {nameof(Virtualize<TItem>)} has been initialized for interactive rendering. " +
$"Use the {nameof(InitialItemIndex)} parameter to set the initial scroll position.");
}

return ScrollToIndexAsyncCore(itemIndex, cancellationToken);
return ScrollToItemAsyncCore(itemIndex, cancellationToken);
}

private async Task ScrollToIndexAsyncCore(int itemIndex, CancellationToken cancellationToken)
private async Task ScrollToItemAsyncCore(int itemIndex, CancellationToken cancellationToken)
{
// Cancel-and-switch (last call wins); finally block guards cleanup by ref-equality.
_currentScrollCts?.Cancel();
Expand Down Expand Up @@ -436,11 +436,11 @@ protected override void OnParametersSet()
_placeholder = Placeholder ?? DefaultPlaceholder;
_emptyContent = EmptyContent;

// Pre-position the window at InitialIndex before the first render so the initial
// Pre-position the window at InitialItemIndex before the first render so the initial
// ItemsProvider fetch targets the right slice and avoids a flash of item 0.
if (!_initialScrollApplied && InitialIndex > 0)
if (!_initialScrollApplied && InitialItemIndex > 0)
{
MoveWindowToContain(InitialIndex);
MoveWindowToContain(InitialItemIndex);
}
}

Expand Down Expand Up @@ -479,7 +479,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
}

// If a mutation captured an anchor snapshot, restore it to keep the same row at the
// same viewport offset. Skip while a ScrollToIndexAsync is in flight — we are
// same viewport offset. Skip while a ScrollToItemAsync is in flight — we are
// intentionally moving the viewport.
var shouldRestore = _pendingAnchorRestore && !_pendingScrollToBottom && _currentScrollCts is null;
_pendingAnchorRestore = false;
Expand All @@ -492,14 +492,14 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
await _jsInterop.RefreshObserversAsync();
}

// Apply InitialIndex once: drive the first fetch via ScrollToIndexAsync rather than
// Apply InitialItemIndex once: drive the first fetch via ScrollToItemAsync rather than
// letting the spacer-IO callback fire at scrollTop=0 and reset the window to index 0.
if (!_initialScrollApplied && _jsInterop is not null)
{
if (InitialIndex > 0)
if (InitialItemIndex > 0)
{
_initialScrollApplied = true;
await ScrollToIndexAsync(InitialIndex);
await ScrollToItemAsync(InitialItemIndex);
}
else if (_itemCount > 0)
{
Expand Down Expand Up @@ -629,9 +629,9 @@ private bool ProcessMeasurements(float spacerSeparation)

private bool ShouldSuppressSpacerCallback()
{
// Before the initial ScrollToIndexAsync runs, ignore IO callbacks: at scrollTop=0
// Before the initial ScrollToItemAsync runs, ignore IO callbacks: at scrollTop=0
// they would compute itemsBefore=0 and overwrite the pre-positioned window.
if (!_initialScrollApplied && InitialIndex > 0)
if (!_initialScrollApplied && InitialItemIndex > 0)
{
return true;
}
Expand Down Expand Up @@ -820,7 +820,7 @@ private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess)
{
var result = await _itemsProvider(request);

// InitialIndex out-of-range or TotalItemCount shrank between fetches: re-clamp
// InitialItemIndex out-of-range or TotalItemCount shrank between fetches: re-clamp
if (!cancellationToken.IsCancellationRequested
&& result.TotalItemCount > 0
&& _itemsBefore >= result.TotalItemCount)
Expand Down Expand Up @@ -915,7 +915,7 @@ private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess)
// Cache this exception so the renderer can throw it.
_refreshException = e;

// Surface the exception to any waiting ScrollToIndexAsync caller.
// Surface the exception to any waiting ScrollToItemAsync caller.
_nextRenderTcs?.TrySetException(e);

// Re-render the component to throw the exception.
Expand Down
36 changes: 18 additions & 18 deletions src/Components/Web/test/Virtualization/VirtualizeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -978,9 +978,9 @@ public void ScrollToIndexAsync_ThrowsBeforeJsInteropInitialized()
var virtualize = new Virtualize<int>();

var ex = Assert.Throws<InvalidOperationException>(
(Action)(() => { _ = virtualize.ScrollToIndexAsync(0); }));
Assert.Contains(nameof(Virtualize<int>.ScrollToIndexAsync), ex.Message);
Assert.Contains(nameof(Virtualize<int>.InitialIndex), ex.Message);
(Action)(() => { _ = virtualize.ScrollToItemAsync(0); }));
Assert.Contains(nameof(Virtualize<int>.ScrollToItemAsync), ex.Message);
Assert.Contains(nameof(Virtualize<int>.InitialItemIndex), ex.Message);
}

private static readonly RenderFragment<int> SimpleItemTemplate = item => b => b.AddContent(0, item);
Expand All @@ -996,7 +996,7 @@ await renderer.Dispatcher.InvokeAsync(() =>
callbacks.OnAfterSpacerVisible(0f, 500f, 500f));

Task task = null;
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToIndexAsync(-5); });
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToItemAsync(-5); });
await task;
Assert.True(task.IsCompletedSuccessfully);
// Clamp post-condition: negative index lands the window at the start.
Expand All @@ -1014,7 +1014,7 @@ await renderer.Dispatcher.InvokeAsync(() =>
callbacks.OnAfterSpacerVisible(0f, 500f, 500f));

Task task = null;
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToIndexAsync(99_999); });
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToItemAsync(99_999); });

await task;
Assert.True(task.IsCompletedSuccessfully);
Expand All @@ -1031,7 +1031,7 @@ public async Task ScrollToIndexAsync_EmptyListCompletesAsNoOp()
var (virtualize, renderer) = await CreateRenderedVirtualize(itemSize: 50f, totalItems: 0);

Task task = null;
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToIndexAsync(0); });
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToItemAsync(0); });

await task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.True(task.IsCompletedSuccessfully);
Expand All @@ -1046,7 +1046,7 @@ public async Task ScrollToIndexAsync_AlreadyCancelledTokenProducesCancelledTask(
cts.Cancel();

Task task = null;
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToIndexAsync(10, cts.Token); });
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToItemAsync(10, cts.Token); });

await Assert.ThrowsAnyAsync<OperationCanceledException>(() => task);
}
Expand All @@ -1062,8 +1062,8 @@ await renderer.Dispatcher.InvokeAsync(() =>

var (firstTask, secondTask) = await renderer.Dispatcher.InvokeAsync(() =>
{
var first = virtualize.ScrollToIndexAsync(500);
var second = virtualize.ScrollToIndexAsync(750);
var first = virtualize.ScrollToItemAsync(500);
var second = virtualize.ScrollToItemAsync(750);
return (first, second);
});

Expand All @@ -1086,7 +1086,7 @@ public async Task ScrollToIndexAsync_ProviderThrows_FaultsTaskInsteadOfHanging()
itemSize: 50f, totalItems: 100, customProvider: throwingProvider);

Task task = null;
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToIndexAsync(50); });
await renderer.Dispatcher.InvokeAsync(() => { task = virtualize.ScrollToItemAsync(50); });

var ex = await Assert.ThrowsAnyAsync<Exception>(async () => await task.WaitAsync(TimeSpan.FromSeconds(5)));
Assert.Same(sentinel, ex);
Expand All @@ -1103,7 +1103,7 @@ public async Task InitialIndex_ParameterRoundTrip()
builder.OpenComponent<Virtualize<int>>(0);
builder.AddComponentParameter(1, "ItemSize", 50f);
builder.AddComponentParameter(2, "Items", (ICollection<int>)Enumerable.Range(1, 100).ToList());
builder.AddComponentParameter(3, "InitialIndex", 42);
builder.AddComponentParameter(3, "InitialItemIndex", 42);
builder.AddComponentParameter(4, "ChildContent", (RenderFragment<int>)(item => b =>
{
b.OpenElement(0, "span");
Expand All @@ -1124,7 +1124,7 @@ public async Task InitialIndex_ParameterRoundTrip()
await testRenderer.RenderRootComponentAsync(componentId);

Assert.NotNull(renderedVirtualize);
Assert.Equal(42, renderedVirtualize.InitialIndex);
Assert.Equal(42, renderedVirtualize.InitialItemIndex);
}

[Fact]
Expand All @@ -1146,7 +1146,7 @@ public async Task InitialIndex_DefaultZeroMeansNoInitialScroll()
await testRenderer.RenderRootComponentAsync(componentId);

Assert.NotNull(renderedVirtualize);
Assert.Equal(0, renderedVirtualize.InitialIndex);
Assert.Equal(0, renderedVirtualize.InitialItemIndex);
// No initial-scroll request was issued; component opens at item 0.
Assert.Equal(0, renderedVirtualize._itemsBefore);
}
Expand All @@ -1162,7 +1162,7 @@ public async Task InitialIndex_NegativeClampsToZero()
builder.OpenComponent<Virtualize<int>>(0);
builder.AddComponentParameter(1, "ItemSize", 50f);
builder.AddComponentParameter(2, "Items", Enumerable.Range(1, 100).ToList() as ICollection<int>);
builder.AddComponentParameter(3, "InitialIndex", -5);
builder.AddComponentParameter(3, "InitialItemIndex", -5);
builder.AddComponentParameter(4, "ChildContent", (RenderFragment<int>)(item => b => b.AddContent(0, item.ToString(System.Globalization.CultureInfo.InvariantCulture))));
builder.AddComponentReferenceCapture(5, c => renderedVirtualize = c as Virtualize<int>);
builder.CloseComponent();
Expand All @@ -1178,8 +1178,8 @@ public async Task InitialIndex_NegativeClampsToZero()
await testRenderer.RenderRootComponentAsync(componentId);

Assert.NotNull(renderedVirtualize);
Assert.Equal(-5, renderedVirtualize.InitialIndex);
// Negative InitialIndex must clamp to 0 (no out-of-range seeding of _itemsBefore).
Assert.Equal(-5, renderedVirtualize.InitialItemIndex);
// Negative InitialItemIndex must clamp to 0 (no out-of-range seeding of _itemsBefore).
Assert.Equal(0, renderedVirtualize._itemsBefore);
}

Expand All @@ -1195,7 +1195,7 @@ public async Task InitialIndex_BeyondCountClampsToEnd()
builder.OpenComponent<Virtualize<int>>(0);
builder.AddComponentParameter(1, "ItemSize", 50f);
builder.AddComponentParameter(2, "Items", items as ICollection<int>);
builder.AddComponentParameter(3, "InitialIndex", 99999);
builder.AddComponentParameter(3, "InitialItemIndex", 99999);
builder.AddComponentParameter(4, "ChildContent", (RenderFragment<int>)(item => b => b.AddContent(0, item.ToString(System.Globalization.CultureInfo.InvariantCulture))));
builder.AddComponentReferenceCapture(5, c => renderedVirtualize = c as Virtualize<int>);
builder.CloseComponent();
Expand All @@ -1211,7 +1211,7 @@ public async Task InitialIndex_BeyondCountClampsToEnd()
await testRenderer.RenderRootComponentAsync(componentId);

Assert.NotNull(renderedVirtualize);
Assert.Equal(99999, renderedVirtualize.InitialIndex);
Assert.Equal(99999, renderedVirtualize.InitialItemIndex);
// For a fixed Items collection, the seed clamps using the now-known count: max(0, Count - capacity).
// capacity = OverscanCount*2 + 1 = 31 with default OverscanCount=15, so _itemsBefore = max(0, 100 - 31) = 69.
Assert.Equal(69, renderedVirtualize._itemsBefore);
Expand Down
6 changes: 3 additions & 3 deletions src/Components/test/E2ETest/Tests/VirtualizationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4195,7 +4195,7 @@ public void ScrollToItem_ForwardThenBackward()
[Fact]
public void ScrollToItem_WithProviderDelay_NoPlaceholderAtTarget()
{
// When ScrollToIndexAsync completes, the target row must show content, not placeholder.
// When ScrollToItemAsync completes, the target row must show content, not placeholder.
MountAnchorModeForScrollToItem(delay: true);
var js = (IJavaScriptExecutor)Browser;

Expand Down Expand Up @@ -4269,7 +4269,7 @@ public void InitialIndex_BeyondCount_ClampsToEnd(bool delay)
if (!last) return false;
// Scroller must be pinned at max (within 2px) — proves the clamp targeted the end.
return Math.abs((c.scrollTop + c.clientHeight) - c.scrollHeight) <= 2;
"), "Expected last item (999) rendered and scroller pinned at end after clamping InitialIndex=100000.");
"), "Expected last item (999) rendered and scroller pinned at end after clamping InitialItemIndex=100000.");
}

[Theory]
Expand Down Expand Up @@ -4326,7 +4326,7 @@ public void ScrollToItem_ExternalCancellation_TaskCancels()
[Fact]
public void ScrollToItem_UserScrollDuringProviderFetch_UserScrollWins()
{
// While the provider is fetching for ScrollToIndexAsync, a real user scroll must win.
// While the provider is fetching for ScrollToItemAsync, a real user scroll must win.
MountAnchorModeForScrollToItem();
var js = (IJavaScriptExecutor)Browser;
var container = Browser.Exists(By.Id("scroll-container"));
Expand Down
Loading
Loading