Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the Virtualization docs to clarify that placeholder content must be the same size as item content #44430

Closed
1 task done
Alerinos opened this issue Oct 9, 2022 · 42 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components Docs This issue tracks updating documentation feature-blazor-server Pillar: Technical Debt

Comments

@Alerinos
Copy link

Alerinos commented Oct 9, 2022

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

@javiercn
Copy link
Member

javiercn commented Oct 10, 2022

@Alerinos thanks for contacting us.

When you are scrolling fast, LoadEmployees is being called multiple times without the calls being properly serialized, which is causing multiple EF queries to be sent in parallel to the database (hence your error).

You have a cancellation token in the ItemsProviderRequest that you can flow to stop the ongoing queries before starting a new one to avoid sending multiple queries in parallel.

@javiercn javiercn added question area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. feature-blazor-server labels Oct 10, 2022
@ghost ghost added the Status: Resolved label Oct 10, 2022
@Alerinos
Copy link
Author

Alerinos commented Oct 10, 2022

@javiercn
Scrolling slowly also causes a problem. I used something like that but it doesn't work:

      var count = await _search._context.Article.AsNoTracking().Where(x => x.Status == Models.Article.StatusType.Show).CountAsync(request.CancellationToken);  <--- here
        var numEmployees = Math.Min(request.Count, count - request.StartIndex);
        
        var dto2 = await _search._context.Article
            .AsNoTracking()
            .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(request.Count)
            .ToListAsync(request.CancellationToken); <--- here

Are you able to see how it should be done? I think it is also worth updating the documentation, there may be many people like me.

Edit: The same error
System.NullReferenceException: „Object reference not set to an instance of an object.”
image

[11:12:44 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()
[11:12:44 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)
[11:12:44 ERR] Unhandled exception in circuit 'o1s8tybE1NTMJdx1xMJHuxxRCjkFr8vVvLo6YW9dGK4'.
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)
[11:12:44 INF] Executed endpoint '/_blazor'

@javiercn
Copy link
Member

javiercn commented Oct 10, 2022

@Alerinos thanks for the additional details.

We already have documentation covering this. You can check it here. The suggestion would be to instantiate a new DbContext per operation as described in the article.

I am not 100% sure why passing the cancellation token approach did not work. That would be a question for the EF folks.

@Alerinos
Copy link
Author

Alerinos commented Oct 10, 2022

I found a similar problem.
dotnet/efcore#26884
Unfortunately, nothing is explained in it, also marking it as a duplicate does not say anything.

@javiercn
https://gyazo.com/ad72b054b5605326dd15a83cb2cf6df2
or:
https://user-images.githubusercontent.com/42449535/194839172-438966e8-d905-4199-908b-fb24353c65f0.mp4

I think it is also Blazor's fault, see for yourself that instead of loading a new element once, it refreshes it many times until the exception.

my code:

    bool IsRun = false;

    private async ValueTask<ItemsProviderResult<DTO.Article>> LoadEmployees(ItemsProviderRequest request)
    {
        if (IsRun is true)
            await Task.Delay(TimeSpan.FromSeconds(1));

        IsRun = true;

...

        IsRun = false;

        return new ItemsProviderResult<DTO.Article>(dto3, count);

Edit:
This happens with OverscanCount="1"
In the case of the default "3", the number of flashes is 3 times greater.

@javiercn
Copy link
Member

@Alerinos to be more concrete, use a DbContextFactory to create a new DbContext inside LoadEmployees. I thought the documentation above mentioned that (One context per operation).

@Alerinos
Copy link
Author

In the case of Cancel Token:

[12:27:12 ERR] An error occurred using the connection to database 'Stand' on server '(localdb)\mssqllocaldb'.
[12:27:12 WRN] Unhandled exception rendering component: A task was canceled.
System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize`1.BuildRenderTree(RenderTreeBuilder builder)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)
[12:27:12 ERR] Unhandled exception in circuit '-hj7HmI-Jd_Mt8oxwAQwPL_s3B8LSC4EDqUgaZb06xI'.
System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize`1.BuildRenderTree(RenderTreeBuilder builder)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)

Without:
https://user-images.githubusercontent.com/42449535/194846863-ffa547a3-b8e6-41a1-b97b-693d82127f1a.mp4

    [Inject] IDbContextFactory<Context> DbFactory { get; set; } = null!;

    private async ValueTask<ItemsProviderResult<DTO.Article>> LoadEmployees(ItemsProviderRequest request)
    {
        using var context = DbFactory.CreateDbContext();
        
        var count = await context.Article.AsNoTracking().Where(x => x.Status == Models.Article.StatusType.Show).CountAsync(); // request.CancellationToken
        var numEmployees = Math.Min(request.Count, count - request.StartIndex);
        
        var dto2 = await context.Article
            .AsNoTracking()
            .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(request.Count)
            .ToListAsync(); // request.CancellationToken

@javiercn I understand that this is correct behavior and is it supposed to be?

@javiercn
Copy link
Member

@Alerinos it is not obvious what is going on in your app.

Could you provide a minimal repro project so that we can try and determine if there is an issue here?

At this point I do not think I have the expertise in this area to be accurate, so someone more familiar with the code will help here.

@javiercn javiercn added the Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. label Oct 10, 2022
@ghost
Copy link

ghost commented Oct 10, 2022

Hi @Alerinos. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

@Alerinos
Copy link
Author

Alerinos commented Oct 10, 2022

@javiercn
Virtualize-bug.zip
Just run, the database will create itself.

edit:
Sometimes it works, sometimes it doesn't.
Often the video below shows:
https://gyazo.com/9fd18aa68464bebe9ff1292889bbce8e

@ghost ghost added Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. and removed Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. labels Oct 10, 2022
@Alerinos
Copy link
Author

@javiercn
I understand that if there is a bug, the patch will not be created soon.
Do you have an idea how I can solve this problem now?
I need to lazily load items, unfortunately I wouldn't want to do that in javascript.

Can you also open this topic? It's closed and I don't want it to be forgotten.

@javiercn javiercn added investigate and removed question ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. Status: Resolved labels Oct 10, 2022
@javiercn javiercn added this to the .NET 8 Planning milestone Oct 10, 2022
@ghost
Copy link

ghost commented Oct 10, 2022

Thanks for contacting us.
We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. Because it's not immediately obvious that this is a bug in our framework, we would like to keep this around to collect more feedback, which can later help us determine the impact of it. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@javiercn javiercn removed the Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. label Oct 10, 2022
@javiercn
Copy link
Member

@Alerinos I think this will likely come down to an issue in the sample app (once we get a chance to look at it) or a more complete doc, I have peek through the framework code, and I don't see anything obviously wrong.

@Alerinos
Copy link
Author

Alerinos commented Oct 10, 2022

@javiercn

 await Task.Delay(TimeSpan.FromSeconds(1));

I used something like that. It seems that the component does not wait for return to be executed, but creates a new instance.
I don't think it works properly asynchronously.

I will try to test with rest api.

A whole class of site
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore;

namespace Virtualize_bug.Pages;

[Route("Articles")]
public partial class Articles : ComponentBase
{
    [Inject] IDbContextFactory<Context> DbFactory { get; set; } = null!;

    private async ValueTask<ItemsProviderResult<DTO.Article>> LoadEmployees(ItemsProviderRequest request)
    {
        await Task.Delay(TimeSpan.FromSeconds(1));

        using var context = DbFactory.CreateDbContext();

        var count = await context.Article.AsNoTracking().CountAsync(); // request.CancellationToken
        var numEmployees = Math.Min(request.Count, count - request.StartIndex);

        var dto2 = await context.Article
            .AsNoTracking()
            .Include(x => x.Details)
            .OrderByDescending(x => x.Details.Created)
            .Skip(request.StartIndex)
            .Take(request.Count)
            .ToListAsync(); // request.CancellationToken

        var dto3 = dto2.Select(x => new DTO.Article
        {
            Title = x.Details.Title,
            Description = x.Details.Description,
            Created = x.Details.Created
        }).ToList();

        return new ItemsProviderResult<DTO.Article>(dto3, count);
    }
}

Edit:
Sometimes there should be Task instead of ValueTask?

@mkArtakMSFT mkArtakMSFT added Docs This issue tracks updating documentation triaged labels Oct 27, 2022
@mkArtakMSFT
Copy link
Member

@Alerinos can you please share a minimum repro project hosted as a public GitHub repo, so that we can investigate this further. Thanks!

@MackinnonBuck
Copy link
Member

@Alerinos, we have an issue tracking that as well: #28821

@Alerinos
Copy link
Author

Alerinos commented Nov 4, 2022

@MackinnonBuck
Now I think so, there should be some protection against a large amount of reordering in a virtual component. What if someone erases the height in the browser (or the browser does it itself)? There will be a large number of requests to the server and ddos can be created.
It is worth discussing this topic and thinking about some sensible safeguard. Maybe the number of requests within e.g. 1 second?

@MackinnonBuck
Copy link
Member

@Alerinos That seems like a reasonable suggestion. We currently have #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!

@Alerinos
Copy link
Author

Alerinos commented Nov 4, 2022

Thanks, I created a new topic.

@MackinnonBuck I found another problem, the component does not "cache" HTML elements but cleans them, so there may be another security problem. See in the attached video how I load the database by playing only with the scroll. You should also consider not disposing of the loaded item or keeping it somewhere in memory.

https://gyazo.com/1cf98d0ad8c29323ba843bd790e0725a

@MackinnonBuck
Copy link
Member

Thanks, @Alerinos.

You should also consider not disposing of the loaded item or keeping it somewhere in memory.

This can already be done by manually caching results and returning them from the ItemsProvider.

@Alerinos
Copy link
Author

Alerinos commented Nov 4, 2022

This can already be done by manually caching results and returning them from the ItemsProvider.

I meant more on the browser side, thanks to which we will save transfer between the server and the client. For large pages, this will make a big difference.

Edit:
image
@MackinnonBuck I think you can hold already visible objects in the HTML DOM element. Currently, it requires constant communication with the server, also in the case of poor internet, the elements will be constantly loaded. So if we are watching something down and suddenly we want to go back, we still have to wait for the server to load our items.

@MackinnonBuck
Copy link
Member

@Alerinos If I'm understanding correctly, you're suggesting we cache the removed DOM elements on the JavaScript side and reinsert them (still via JavaScript) when they get scrolled back into view? I'm inclined to think that's not a direction we would take for the following reasons:

  • This only works if the content in the list never changes beyond the first render. If we were to instead modify this suggestion to only use the previously rendered elements as placeholders while waiting for the server to send updates, it would make the UI slightly more responsive, but it wouldn't reduce network bandwidth.
  • Implementing this would require that we duplicate a lot of the .NET virtualization logic on the JavaScript side and make the two implementations compatible.
  • There would likely be a lot of complexity around having the DOM mutations dictated by the new JavaScript logic play nicely with the DOM updates sent by the server (generally speaking, you have to be really careful about having JavaScript mutate any part of the DOM generated by Blazor).

So, this might end up resulting in a lot of added complexity for marginal gain, but please correct me if I'm misinterpreting the suggestion 🙂

@Alerinos
Copy link
Author

Alerinos commented Nov 5, 2022

@MackinnonBuck

Currently loaded items
image
When moving, old elements are deleted and new ones are added:
image

Instead of deleting them, we can leave them and possibly give an ID so that blazor knows which elements are already loaded.
I think that we do not need to delete HTML that has already been downloaded

So in theory we don't need javascript integration.

https://gyazo.com/bbed2cdd93a7adfe88721ec6516f3854
The movie shows how old elements are deleted and new ones added, so there is always a fixed amount of HTML.

Also thanks to this, we solve the problem when we lose the Internet connection. We have offline content that we just watched. Unfortunately, blazor server is problematic, in wasm you can add everything to the browser's storage and display data from it.

Another thing I would improve is:

return new ItemsProviderResult<DTO.Article>(dto3, count);

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

We have to make an additional query to the server how many elements are there. It is inconvenient and resource-consuming.
How would I go about it?

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

<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>
    <Missing>
        <div style="height: 50px;">
           Sorry, no content, please continue or refresh the page.
        </div>
    </Missing>
</Virtualize>

I think that something like this would make the work of developers easier and save resources. Let me know what you think.

@Alerinos
Copy link
Author

Alerinos commented Nov 5, 2022

Another problem I see is when we have 200 articles. Someone adds 20 items while browsing, the cout will change. How will the component react? We will not have duplicate data? At the time of viewing the data, there were fewer items to display than there is now, the new items are higher.

It seems to me that this is a difficult logistic topic that is worth considering and discussing.

@Alerinos
Copy link
Author

Alerinos commented Nov 5, 2022

I did another test.

If we have a high screen, for example, we are shown 30 records, each downward movement causes reloading of 30 elements. (Why load the same if it already exists)

What's the problem? The same data is downloaded every time you move up or down.

How should it be done in my opinion?
Each move should charge +3 items, i.e. moving down will charge, +3 then +3 etc.

@MackinnonBuck
Copy link
Member

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...

#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!

@MackinnonBuck MackinnonBuck changed the title Problem with Virtualize and EF core Improve the Virtualization docs to clarify that placeholder content must be the same size as item content Nov 7, 2022
@ghost
Copy link

ghost commented Oct 6, 2023

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@szalapski
Copy link

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.

I like this idea. @MackinnonBuck , are you saying that this is possible now? I don't see a property on Virtualize with the number of items to keep in DOM, nor do I see a way to get the number of items currently in the DOM.

@ghost
Copy link

ghost commented Dec 13, 2023

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@MackinnonBuck
Copy link
Member

MackinnonBuck commented Jan 17, 2024

@szalapski Sorry for the late reply - just saw your question.

Here's a basic demonstration of how this could work:

<Virtualize ItemsProvider="LoadItemsAsync">
    <ItemContent>
        <p @key="@context">@context</p>
    </ItemContent>
</Virtualize>

@code {
    private const int PageSize = 40;

    private int totalItemCount = 20;

    private async ValueTask<ItemsProviderResult<int>> LoadItemsAsync(ItemsProviderRequest request)
    {
        var items = Enumerable.Range(request.StartIndex, request.Count);
        if (request.StartIndex + request.Count >= totalItemCount)
        {
            // Simulate an artificial delay when loading new content
            await Task.Delay(250, request.CancellationToken);
            totalItemCount += PageSize;
        }

        return new(items, totalItemCount);
    }
}

@MackinnonBuck
Copy link
Member

@guardrex Would you be able to clarify the virtualization docs, as suggested in this comment?

(I know there's a lot going on in this issue, but we're just using it to track improving this area of the docs)

@guardrex
Copy link
Contributor

Addressed on dotnet/AspNetCore.Docs#31499.

@MackinnonBuck
Copy link
Member

Thanks @guardrex! Closing this out.

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 Docs This issue tracks updating documentation feature-blazor-server Pillar: Technical Debt
Projects
None yet
Development

No branches or pull requests

6 participants