aspnetcore: Improve the Virtualization docs to clarify that placeholder content must be the same size as item content

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I used this example from the documentation: https://learn.microsoft.com/pl-pl/aspnet/core/blazor/components/virtualization?view=aspnetcore-7.0

private async ValueTask<ItemsProviderResult<Employee>> LoadEmployees(
    ItemsProviderRequest request)
{
    var numEmployees = Math.Min(request.Count, totalEmployees - request.StartIndex);
    var employees = await EmployeesService.GetEmployeesAsync(request.StartIndex, 
        numEmployees, request.CancellationToken);

    return new ItemsProviderResult<Employee>(employees, totalEmployees);
}

My code looks like this:

    private async ValueTask<ItemsProviderResult<DTO.Article>> LoadEmployees(ItemsProviderRequest request)
    {
// Count = 336
        var count = await _search._context.Article.CountAsync(x => x.Status == Models.Article.StatusType.Show);
        var numEmployees = Math.Min(request.Count, count - request.StartIndex);
        
        var dto2 = await _search._context.Article
            .Include(x => x.Details)
            .Include(x => x.Names)
            .Include(x => x.Images)
            .Where(x => x.Status == Models.Article.StatusType.Show)
            .OrderByDescending(x => x.Details.Created)
            .Skip(request.StartIndex)
            .Take(numEmployees)
            .ToListAsync();

        var dto3 = dto2.Select(x => new DTO.Article
        {
            Name = x.Names.OrderByDescending(x => x.Created).Select(x => x.Name).FirstOrDefault() ?? string.Empty,
            Title = x.Details.Title,
            Description = x.Details.Description,
            Created = x.Details.Created,
            Tags = x.HashTags.Select(x => x.Tag).Take(3).ToList(),
            ImageHead = x.Images.LastOrDefault(x => x.Type == Image.StatusType.Header).Url ?? string.Empty,
        }).ToList();
        
// dto3 = 35 object
// count = 336 int
        return new ItemsProviderResult<DTO.Article>(dto3, count);
    }

My model:

internal class Article
{
    public Guid Id { get; set; }
    public string Status { get; set; } = string.Empty;
    public string Culture { get; set; } = string.Empty;
    public List<string> Cultures { get; set; } = null!;
    public string Name { get; set; } = string.Empty;
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public bool Nsfw { get; set; } = false;
    public bool Sponsor { get; set; } = false;
    public bool Logged { get; set; } = false;
    public bool Premium { get; set; } = false;
    public decimal ReadingTime { get; set; }
    public DateTime Created { get; set; } = DateTime.UtcNow;
    public DateTime Updated { get; set; } = DateTime.UtcNow;
    public List<string> Tags { get; set; } = new();
    public string ImageHead { get; set; } = string.Empty;
    public List<Article> Articles { get; set; } = null!;
}

Razor:

<Row Class="gap-y-[1rem] gap-x-[1%]">

    <Virtualize Context="article" ItemsProvider="@LoadEmployees">
        <ItemContent>
            <Column Class="basis-full">
                <Card Title="@article.Title" Link="@($"Article/{article.Name}")" Hover=true>
                    <CardPicture Url="@article.ImageHead" Class="h-[15rem] xl:h-[20rem]" Sizes=@(new List<CardPicture.Size>{ new () { Width = 512, Height = 288 }})/>
                    <CardTitle Class="text-left text-xl"/>
                    <CardContent>
                        <HashTag Tag="@article.Tags" Class="inline"/>
                    <p class="inline text-md text-date dark:text-date_dark float-right">@article.Created.ToString("dd/MM/yy")</p>
                    </CardContent>   
                </Card>
            </Column>
        </ItemContent>
        <Placeholder>
            <Card>
                <CardTitle Title="Loading..."/>
                <CardContent>
                    In progres...
                </CardContent>
            </Card>
        </Placeholder>
    </Virtualize>

</Row>

The first results are correct. The problem starts with scrolling.

First loading, its ok: image

1 break point scrool: image

The next one is right away, I don’t have to do anything. image

Scrolls on, 2 break point scroll: image

Scrolls on, 3 brak point scroll: image

Still ok, now takes the breakpoint off.

[02:07:20 ERR] An exception occurred while iterating over the results of a query for context type 'Stand.Plugins.Articles.Context'.
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
[02:07:20 WRN] Unhandled exception rendering component: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize`1.BuildRenderTree(RenderTreeBuilder builder)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)
[02:07:20 ERR] Unhandled exception in circuit '7S3gOiTMV1OjU6v35dYZcg1i2Sb-MZExeXvGsHDWF2Q'.
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize`1.BuildRenderTree(RenderTreeBuilder builder)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)

It turns out that everything is happening fast … I’m sitting for this half day today. I do not understand what’s going on… There is a documentation error or I don’t understand how to do this. Can I ask for help?

Expected Behavior

No response

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

.net 7 core rc1

Anything else?

No response

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Comments: 38 (20 by maintainers)

Most upvoted comments

I think that we do not need to delete HTML that has already been downloaded

If you want to preserve already-rendered HTML, then Virtualize probably isn’t the component you’re looking for. The Virtualize component was designed to be capable of handling quantities of items that would be too large to reasonably render all at once on the page without performance problems. If this capability isn’t a requirement for your use case, and if frequent communication with the server caused by scrolling is a legitimate concern, then it’s probably better to use a custom component.

Return total quantity of items. I believe that it is unnecessary. Why?

It’s necessary because Virtualize needs to calculate how large the scrollable area should be, and it needs the total number of items to do so. If you can’t compute the actual total number of items, you can specify it as (number of currently-loaded items + 1) and you’ll get a sort of infinite scroll effect where the size of the scrollable area expands as more items are loaded.

If the first parameter is zero, it means that there are no elements, you can additionally handle this exception like this…

https://github.com/dotnet/aspnetcore/issues/28770


It seems you have a lot of ideas and concerns, and that’s awesome. However, it’s best that we keep comments on this issue related to this issue’s topic, which we’ve decided should be improving the Virtualize docs to make the “all item content should be the same size” requirement clearer.

Going forward, would you please use the following process when making feature requests?

  1. See if there is already an issue logged for the feature you’re asking about, and if there is, upvote it so it shows up as higher on the list for our planning process.
  2. If there is not an issue tracking the feature you want addressed, file a new one. This way, we can track and address each item separately.

Thanks for your feedback and enthusiasm!

@Alerinos That seems like a reasonable suggestion. We currently have https://github.com/dotnet/aspnetcore/issues/10522 tracking the general idea of throttling/debouncing events, but the Virtualize case might require special attention. It doesn’t look like we have an issue for throttling Virtualize specifically, so feel free to open a separate issue for it 🙂 Thanks!

The page goes up to the top instead of accepting the URL and staying put.

Yeah, unfortunately NavigationManager does this and currently there isn’t API to disable that behavior. But you can work around it today by doing something like this: https://github.com/dotnet/aspnetcore/issues/40190#issuecomment-1203857906

Also, regarding:

You need to complete the documentation.

The docs do state:

Virtualize works under the following conditions:

  • All content items are of identical height. This makes it possible to calculate which content corresponds to a given scroll position without first fetching every data item and rendering the data into a DOM element. …

However, the phrase “content item” isn’t really defined in that document, and it’s not made explicit elsewhere that <Placeholder> content is included in the “all items must have identical height” constraint. So, I agree the docs can be improved.

Since there isn’t a bug in the framework, let’s use this issue to track improving the wording of the docs to make the placeholder content requirements more obvious.

@Alerinos sorry, but it’s hard to tell what the issue is in that video. What’s going wrong, exactly?

In the case of refreshing the page, the user will return to the element he saw, not from the beginning

This is something we don’t support yet, but there is an issue tracking it: https://github.com/dotnet/aspnetcore/issues/26943

@Alerinos, could you apply that same styling to the <Placeholder> as well? For example:

<Virtualize Context="article" ItemsProvider="@LoadEmployees">
    <ItemContent>
        <div style="height: 50px;">
            <h3>@article.Title</h3>
            <p>@article.Content</p>
        </div>
    </ItemContent>
    <Placeholder>
        <div style="height: 50px;">
            In progress...
        </div>
    </Placeholder>
</Virtualize>