dotnet / dotnet/aspnetcore

Virtualize AnchorMode.End does not position at the end after the initial ItemsProvider result

Open
#69,302 0 comments 0 reactions 0 assignees View on GitHub
area-blazor feature-blazor-virtualization
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

Related but distinct:

- #65742 added anchor modes.
- #66799 is the `AnchorMode` API proposal.
- #68099 covers an `InitialItemIndex` startup race.

### Describe the bug

A `Virtualize` using an `ItemsProvider` and `AnchorMode="VirtualizeAnchorMode.End"` remains at the beginning of the list after its initial provider result is rendered.

This affects chat and log-viewer scenarios. If the provider initially returns enough existing items to overflow the viewport, the user sees the oldest items instead of the newest items at the end.

End anchoring works for subsequent appends after the viewport has reached the bottom. The problem is specifically the initial transition from an empty virtualizer to its first populated provider result.

### Expected behavior

An initially empty viewport is also at its end. When its first provider result changes the item count from zero to a nonzero value, `AnchorMode.End` should position the viewport at the end.

This matches the chat/log-viewer use case described for `AnchorMode.End`.

### Actual behavior

The first provider result is displayed at the beginning of the list. The user must manually scroll to the bottom before End following becomes established.

### Minimal reproduction

```razor
@page "/"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.Web.Virtualization




@context.Text



@code {
private readonly Message[] _messages =
[.. Enumerable.Range(1, 100).Select(i => new Message(i, $"Message {i}"))];

private readonly IEqualityComparer _messageComparer =
EqualityComparer.Default;

private ValueTask> LoadMessagesAsync(
ItemsProviderRequest request)
{
var items = _messages
.Skip(request.StartIndex)
.Take(request.Count);

return ValueTask.FromResult(
new ItemsProviderResult(items, _messages.Length));
}

private sealed record Message(int Id, string Text);
}
```

Load the page and wait for the initial provider result. The viewport remains at `Message 1` instead of being positioned at `Message 100`.

### Possible cause

In `Virtualize.RefreshDataCoreAsync`, append handling is gated by:

```csharp
var previousItemCount = _itemCount;
var countDelta = result.TotalItemCount - previousItemCount;
var itemsAdded = countDelta > 0 && previousItemCount > 0;
```

For the initial provider result, `previousItemCount` is zero, so neither `ShouldFollowAppendedTailAsync` nor `AdvanceWindowToAppendedTailAsync` runs.

On the JavaScript side, End mode initializes `bottomTracking.following` to `true`, but `bottomTracking.reached` and `bottomTracking.wasAtBottomLastRender` are initially false. `refreshObservedElements` therefore does not pin the first populated render:

```ts
if ((anchorModeIs.end || bottomTracking.following) &&
(bottomTracking.wasAtBottomLastRender || bottomTracking.reached)) {
scrollElement.scrollTop = scrollElement.scrollHeight;
}
```

The empty initial viewport should arguably establish the at-bottom state, or the first `0 -> N` provider result should enter the End-following path.

### Additional context

This was reproduced in two independent Aspire Dashboard views backed by `FluentDataGrid`, which forwards its `AnchorMode` to `Virtualize`:

- Structured Logs
- Traces

Both remained away from the end after initial loading. Subsequent appended content followed correctly once the viewport had manually reached the bottom.

### Version

- .NET SDK: `11.0.100-rc.1.26425.128`
- Microsoft.AspNetCore.App: `11.0.0-rc.1.26425.128`
- OS: Windows 11, `win-x64`
- Browser: Chromium

Contributor guide

Open the contributing guide

Research direction

Start with Virtualize.RefreshDataCoreAsync and trace the initial 0-to-N ItemsProvider result, then inspect the JavaScript refreshObservedElements logic and bottomTracking state. Run the minimal reproduction in the issue and verify that AnchorMode.End initially displays the newest item, such as Message 100, while subsequent appends still follow the end.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, javascript
Domain
frontend, web-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.