Condividi tramite


Virtualizzazione dei componenti di ASP.NET Core Razor

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 10 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Questo articolo illustra come usare la virtualizzazione dei componenti nelle app ASP.NET Core Blazor .

Virtualizzazione

Migliorare le prestazioni percepite del rendering dei componenti con il componente Blazor, usando il supporto di virtualizzazione predefinito del framework Virtualize<TItem>. La virtualizzazione è una tecnica per limitare il rendering dell'interfaccia utente solo alle parti attualmente visibili. Ad esempio, la virtualizzazione è utile quando l'app deve eseguire il rendering di un lungo elenco di elementi e deve essere visibile solo un sottoinsieme di elementi in un determinato momento.

Usare il componente Virtualize<TItem> quando:

  • Rendering di un insieme di dati in un ciclo.
  • La maggior parte degli elementi non è visibile a causa dello scorrimento.
  • Gli elementi di cui è stato eseguito il rendering hanno le stesse dimensioni.

Quando l'utente scorre fino a un punto arbitrario nell'elenco Virtualize<TItem> di elementi del componente, il componente calcola gli elementi visibili da visualizzare. Non viene eseguito il rendering degli elementi non visualizzati.

Senza virtualizzazione, un elenco tipico potrebbe usare un ciclo C# foreach per eseguire il rendering di ogni elemento in un elenco. Nell'esempio seguente :

  • allFlights è una raccolta di voli aerei.
  • Il componente FlightSummary visualizza i dettagli relativi a ogni volo.
  • L'attributo @key della direttiva preserva la relazione di ogni componente FlightSummary con il relativo volo reso dal FlightId.
<div style="height:500px;overflow-y:scroll">
    @foreach (var flight in allFlights)
    {
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    }
</div>

Se la raccolta contiene migliaia di voli, il rendering dei voli richiede molto tempo e gli utenti riscontrano un notevole ritardo dell'interfaccia utente. La maggior parte dei voli si trova all'esterno dell'altezza dell'elemento <div> , quindi la maggior parte dei voli non viene visualizzata.

Anziché eseguire il rendering dell'intero elenco dei voli contemporaneamente, sostituire il ciclo foreach nell'esempio precedente con il componente Virtualize<TItem>.

  • Specificare allFlights come origine di un elemento fisso in Virtualize<TItem>.Items. Solo i voli attualmente visibili vengono visualizzati dal Virtualize<TItem> componente.

    Se una raccolta non generica fornisce gli elementi, ad esempio una raccolta di DataRow, seguire le indicazioni nella sezione Provider delegato degli elementi per fornire gli elementi.

  • Specificare un contesto per ogni volo con il parametro Context. Nell'esempio seguente, flight è usato come contesto, che fornisce l'accesso ai membri di ogni volo.

<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights" Context="flight">
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    </Virtualize>
</div>

Se un contesto non viene specificato con il parametro Context, usare il valore di context nel modello di contenuto dell'elemento per accedere ai membri di ogni volo.

<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights">
        <FlightSummary @key="context.FlightId" Details="@context.Summary" />
    </Virtualize>
</div>

Componente Virtualize<TItem> :

  • Calcola il numero di elementi di cui eseguire il rendering in base all'altezza del contenitore e alle dimensioni degli elementi di cui è stato eseguito il rendering.
  • Ricalcola e ricompila gli elementi quando l'utente scorre.
  • Recupera solo la sezione di record da un'API esterna che corrisponde all'area attualmente visibile, inclusa l'overscan, quando ItemsProvider viene usato invece di Items (vedere la sezione Item provider delegate).

Il contenuto dell'elemento per il Virtualize<TItem> componente può includere:

  • HTML normale e codice Razor, come illustrato nell'esempio precedente.
  • Uno o più Razor componenti.
  • Una combinazione di HTML/Razor e componenti Razor.

Delegato del fornitore di elementi

Se non si desidera caricare tutti gli elementi in memoria o la raccolta non è un generico ICollection<T>, è possibile specificare un metodo delegato del provider di elementi nel parametro del Virtualize<TItem>.ItemsProvider componente che recupera in modo asincrono gli elementi richiesti su richiesta. Nell'esempio seguente il LoadEmployees metodo fornisce gli elementi al Virtualize<TItem> componente:

<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <p>
        @employee.FirstName @employee.LastName has the 
        job title of @employee.JobTitle.
    </p>
</Virtualize>

Il provider di elementi riceve un ItemsProviderRequest, che specifica il numero necessario di elementi a partire da un indice di inizio specifico. Il fornitore di elementi recupera quindi gli elementi richiesti da un database o da un altro servizio e li restituisce come ItemsProviderResult<TItem> insieme a un conteggio degli elementi totali. Il provider di elementi può scegliere di recuperare gli elementi con ogni richiesta o memorizzarli nella cache in modo che siano prontamente disponibili.

Un Virtualize<TItem> componente può accettare solo un'unica origine di elementi dai suoi parametri, quindi non tentare di usare contemporaneamente un provider di elementi ed assegnare una raccolta a Items. Se entrambi vengono assegnati, viene generata un'eccezione InvalidOperationException quando i parametri del componente vengono impostati in fase di esecuzione.

L'esempio seguente carica i dipendenti da EmployeeService (non visualizzato). Il totalEmployees campo viene in genere assegnato chiamando un metodo nello stesso servizio (ad esempio , EmployeesService.GetEmployeesCountAsync) altrove, ad esempio durante l'inizializzazione del componente.

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);
}

Nell'esempio seguente, una raccolta di DataRow è una raccolta non generica, quindi viene utilizzato un delegato del provider di elementi per la virtualizzazione.

<Virtualize Context="row" ItemsProvider="GetRows">
    ...
</Virtualize>

@code{
    ...

    private ValueTask<ItemsProviderResult<DataRow>> GetRows(ItemsProviderRequest request) => 
        new(new ItemsProviderResult<DataRow>(
            dataTable.Rows.OfType<DataRow>().Skip(request.StartIndex).Take(request.Count),
            dataTable.Rows.Count));
}

Virtualize<TItem>.RefreshDataAsync ordina al componente di richiedere nuovamente i dati dal suo ItemsProvider. Ciò è utile quando cambiano i dati esterni. In genere non è necessario chiamare RefreshDataAsync quando si usa Items.

RefreshDataAsync aggiorna i dati di un Virtualize<TItem> componente senza causare un rerender. Se RefreshDataAsync viene richiamato da un gestore eventi o da un Blazor metodo del ciclo di vita del componente, l'attivazione di un rendering non è necessaria perché un rendering viene attivato automaticamente alla fine del gestore eventi o del metodo del ciclo di vita. Se RefreshDataAsync viene attivato separatamente da un'attività o un evento in background, ad esempio nel delegato seguente ForecastUpdated , chiamare StateHasChanged per aggiornare l'interfaccia utente alla fine dell'attività o dell'evento in background:

<Virtualize ... @ref="virtualizeComponent">
    ...
</Virtualize>

...

private Virtualize<FetchData>? virtualizeComponent;

protected override void OnInitialized()
{
    WeatherForecastSource.ForecastUpdated += async () => 
    {
        await InvokeAsync(async () =>
        {
            await virtualizeComponent?.RefreshDataAsync();
            StateHasChanged();
        });
    });
}

Nell'esempio precedente:

  • RefreshDataAsync viene chiamato prima per ottenere nuovi dati per il Virtualize<TItem> componente.
  • StateHasChanged viene chiamato per rendere nuovamente il componente.

Segnaposto

Poiché la richiesta di elementi da un'origine dati remota potrebbe richiedere del tempo, è possibile eseguire il rendering di un segnaposto con contenuto dell'elemento:

  • Usare (Placeholder<Placeholder>...</Placeholder>) per visualizzare il contenuto fino a quando non sono disponibili i dati dell'elemento.
  • Usare Virtualize<TItem>.ItemContent per impostare il modello di elemento per l'elenco.
<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <ItemContent>
        <p>
            @employee.FirstName @employee.LastName has the 
            job title of @employee.JobTitle.
        </p>
    </ItemContent>
    <Placeholder>
        <p>
            Loading&hellip;
        </p>
    </Placeholder>
</Virtualize>

Contenuto vuoto

Usare il EmptyContent parametro per fornire contenuto quando il componente è stato caricato ed Items è vuoto o ItemsProviderResult<TItem>.TotalItemCount è zero.

EmptyContent.razor:

@page "/empty-content"

<PageTitle>Empty Content</PageTitle>

<h1>Empty Content Example</h1>

<Virtualize Items="stringList">
    <ItemContent>
        <p>
            @context
        </p>
    </ItemContent>
    <EmptyContent>
        <p>
            There are no strings to display.
        </p>
    </EmptyContent>
</Virtualize>

@code {
    private List<string>? stringList;

    protected override void OnInitialized() => stringList ??= [];
}
@page "/empty-content"

<PageTitle>Empty Content</PageTitle>

<h1>Empty Content Example</h1>

<Virtualize Items="stringList">
    <ItemContent>
        <p>
            @context
        </p>
    </ItemContent>
    <EmptyContent>
        <p>
            There are no strings to display.
        </p>
    </EmptyContent>
</Virtualize>

@code {
    private List<string>? stringList;

    protected override void OnInitialized() => stringList ??= [];
}

Modificare l'espressione lambda del OnInitialized metodo per visualizzare le stringhe di visualizzazione del componente:

protected override void OnInitialized() =>
    stringList ??= [ "Here's a string!", "Here's another string!" ];

Dimensioni dell'elemento

L'altezza di ogni elemento in pixel può essere impostata con Virtualize<TItem>.ItemSize (impostazione predefinita: 50). L'esempio seguente modifica l'altezza di ogni elemento dal valore predefinito di 50 pixel a 25 pixel:

<Virtualize Context="employee" Items="employees" ItemSize="25">
    ...
</Virtualize>

Il Virtualize<TItem> componente misura le dimensioni di rendering (altezza) dei singoli elementi dopo il rendering iniziale. Usare ItemSize per fornire in anticipo una dimensione esatta dell'elemento per facilitare le prestazioni di rendering iniziali accurate e garantire la posizione di scorrimento corretta per i ricaricamenti della pagina. Se il valore predefinito ItemSize causa il rendering di alcuni elementi all'esterno della visualizzazione attualmente visibile, viene attivato un secondo rerender. Per mantenere correttamente la posizione di scorrimento del browser in un elenco virtualizzato, il rendering iniziale deve essere corretto. In caso contrario, gli utenti potrebbero visualizzare gli elementi errati.

Conteggio dell'overscan

Virtualize<TItem>.OverscanCount determina il numero di elementi aggiuntivi di cui viene eseguito il rendering prima e dopo l'area visibile. Questa impostazione consente di ridurre la frequenza di rendering durante lo scorrimento. Tuttavia, i valori più elevati comportano il rendering di più elementi nella pagina (impostazione predefinita: 3). Nell'esempio seguente il numero di overscan viene modificato dal valore predefinito di tre elementi a quattro elementi:

<Virtualize Context="employee" Items="employees" OverscanCount="4">
    ...
</Virtualize>

Modifiche dello stato

Quando si apportano modifiche agli elementi di cui viene eseguito il rendering dal componente Virtualize<TItem>, chiamare StateHasChanged per accodare nuovamente la valutazione e il rerendering del componente. Per altre informazioni, vedere Rendering dei componenti di ASP.NET CoreRazor.

Supporto per lo scorrimento della tastiera

Per consentire agli utenti di scorrere il contenuto virtualizzato usando la tastiera, assicurarsi che gli elementi virtualizzati o lo stesso contenitore di scorrimento siano focalizzabili. Se non si esegue questo passaggio, lo scorrimento della tastiera non funziona nei browser basati su Chromium.

Ad esempio, è possibile usare un tabindex attributo nel contenitore di scorrimento:

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights">
        <div class="flight-info">...</div>
    </Virtualize>
</div>

Per altre informazioni sul significato del tabindex valore -1, 0o di altri valori, vedere tabindex.

Stili avanzati e rilevamento dello scorrimento

Il Virtualize<TItem> componente è progettato solo per supportare meccanismi di layout di elementi specifici. Per comprendere il corretto funzionamento dei layout degli elementi, di seguito viene illustrato come Virtualize rilevare quali elementi devono essere visibili per la visualizzazione nella posizione corretta.

Se il codice sorgente è simile al seguente:

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights" ItemSize="100">
        <div class="flight-info">Flight @context.Id</div>
    </Virtualize>
</div>

In fase di esecuzione, il componente Virtualize<TItem> renderizza una struttura DOM simile alla seguente:

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <div style="height:1100px"></div>
    <div class="flight-info">Flight 12</div>
    <div class="flight-info">Flight 13</div>
    <div class="flight-info">Flight 14</div>
    <div class="flight-info">Flight 15</div>
    <div class="flight-info">Flight 16</div>
    <div style="height:3400px"></div>
</div>

Il numero effettivo di righe di cui è stato eseguito il rendering e le dimensioni degli spaziatori variano in base allo stile e Items alle dimensioni della raccolta. Si noti tuttavia che sono presenti elementi spacer div inseriti prima e dopo il contenuto. Questi servono due scopi:

  • Per fornire un offset prima e dopo il contenuto, causando la visualizzazione degli elementi attualmente visibili nella posizione corretta nell'intervallo di scorrimento e l'intervallo di scorrimento stesso per rappresentare le dimensioni totali di tutto il contenuto.
  • Per rilevare quando l'utente scorre oltre l'intervallo visibile corrente, il che significa che è necessario eseguire il rendering di contenuto diverso.

Nota

Per informazioni su come controllare il tag di elemento HTML dello spacer, vedere la sezione Controllare il nome del tag dell'elemento spacer più avanti in questo articolo.

Gli elementi dello spacer usano internamente un Intersection Observer per ricevere notifiche quando diventano visibili. Virtualize dipende dalla ricezione di questi eventi.

Virtualize funziona in base alle condizioni seguenti:

  • Tutti gli elementi di contenuto renderizzati, incluso il contenuto segnaposto, hanno un'altezza identica. In questo modo è possibile calcolare il contenuto corrispondente a una determinata posizione di scorrimento senza prima recuperare ogni elemento di dati e eseguire il rendering dei dati in un elemento DOM.

  • Sia gli spaziatori che le righe di contenuto sono renderizzati in un singolo stack verticale, con ogni elemento che riempie l'intera larghezza orizzontale. Nei casi d'uso tipici, Virtualize funziona con elementi div. Se si usa CSS per creare un layout più avanzato, tenere presenti i requisiti seguenti:

    • Lo stile del contenitore di scorrimento richiede uno display con uno dei valori seguenti:
      • block (impostazione predefinita per un oggetto div).
      • table-row-group (impostazione predefinita per un oggetto tbody).
      • flex con flex-direction impostato su column. Assicurarsi che gli elementi figlio immediati del componente Virtualize<TItem> non si riducano secondo le regole di flessibilità. Ad esempio, aggiungere .mycontainer > div { flex-shrink: 0 }.
    • Lo stile delle righe del contenuto richiede un display con uno dei seguenti valori:
      • block (impostazione predefinita per un oggetto div).
      • table-row (impostazione predefinita per un oggetto tr).
    • Non usare CSS per interferire con il layout per gli elementi dello spacer. Gli elementi spacer hanno un valore di display, block, tranne se l'elemento padre è un gruppo di righe di tabella, nel qual caso l'impostazione predefinita è table-row. Non tentare di influenzare la larghezza o l'altezza degli elementi spaziatori, compresa l'aggiunta di un bordo o di pseudoelementi.

Qualsiasi approccio che impedisce agli spazi e agli elementi di contenuto di essere renderizzati come un unico stack verticale o che causa la variazione dell'altezza degli elementi di contenuto impedisce il corretto funzionamento del componente Virtualize<TItem>.

Virtualizzazione a livello radice

Il Virtualize<TItem> componente supporta l'uso del documento stesso come radice di scorrimento, in alternativa alla presenza di un altro elemento con overflow-y: scroll. Nell'esempio seguente, i tag <html> o <body> sono stilizzati nel componente overflow-y: scroll.

<HeadContent>
    <style>
        html, body { overflow-y: scroll }
    </style>
</HeadContent>

Il Virtualize<TItem> componente supporta l'uso del documento stesso come radice di scorrimento, in alternativa alla presenza di un altro elemento con overflow-y: scroll. Quando si utilizza il documento come radice di scorrimento, evitare di applicare stile agli elementi <html> o <body> con overflow-y: scroll, poiché causa all'osservatore di intersezione di considerare l'intera altezza scorrevole della pagina come area visibile, invece del solo viewport della finestra.

È possibile riprodurre questo problema creando un elenco virtualizzato di grandi dimensioni (ad esempio 100.000 elementi) e tentando di usare il documento come radice di scorrimento con html { overflow-y: scroll } negli stili CSS della pagina. Anche se può funzionare correttamente a volte, il browser tenta di eseguire il rendering di tutti i 100.000 elementi almeno una volta all'inizio del rendering, che può causare un blocco delle schede del browser.

Per risolvere questo problema prima del rilascio di .NET 7, evitare di applicare stili <html>/<body> agli elementi con overflow-y: scroll o adottare un approccio alternativo. Nell'esempio seguente l'altezza dell'elemento <html> è impostata su oltre il 100% dell'altezza del riquadro di visualizzazione:

<HeadContent>
    <style>
        html { min-height: calc(100vh + 0.3px) }
    </style>
</HeadContent>

Il Virtualize<TItem> componente supporta l'uso del documento stesso come radice di scorrimento, in alternativa alla presenza di un altro elemento con overflow-y: scroll. Quando si utilizza il documento come radice di scorrimento, evitare di applicare lo stile tramite <html> agli elementi <body> o overflow-y: scroll, poiché ciò fa sì che l'intera altezza scorrevole della pagina venga considerata l'area visibile, anziché limitarsi al solo riquadro di visualizzazione della finestra.

È possibile riprodurre questo problema creando un elenco virtualizzato di grandi dimensioni (ad esempio 100.000 elementi) e tentando di usare il documento come radice di scorrimento con html { overflow-y: scroll } negli stili CSS della pagina. Anche se può funzionare correttamente a volte, il browser tenta di eseguire il rendering di tutti i 100.000 elementi almeno una volta all'inizio del rendering, che può causare un blocco delle schede del browser.

Per risolvere questo problema prima del rilascio di .NET 7, evitare di applicare stili <html>/<body> agli elementi con overflow-y: scroll o adottare un approccio alternativo. Nell'esempio seguente l'altezza dell'elemento <html> è impostata su oltre il 100% dell'altezza del riquadro di visualizzazione:

<style>
    html { min-height: calc(100vh + 0.3px) }
</style>

Controllare il nome del tag dell'elemento distanziatore

Se il Virtualize<TItem> componente viene inserito all'interno di un elemento che richiede un nome di tag figlio specifico, SpacerElement consente di ottenere o impostare il nome del tag di spacer di virtualizzazione. Il valore predefinito è div. Per il seguente esempio, il componente Virtualize<TItem> viene renderizzato all'interno di un elemento del corpo della tabella (tbody), quindi l'elemento figlio appropriato per una riga di tabella (tr) è impostato come spaziatore.

VirtualizedTable.razor:

@page "/virtualized-table"

<PageTitle>Virtualized Table</PageTitle>

<HeadContent>
    <style>
        html, body {
            overflow-y: scroll
        }
    </style>
</HeadContent>

<h1>Virtualized Table Example</h1>

<table id="virtualized-table">
    <thead style="position: sticky; top: 0; background-color: silver">
        <tr>
            <th>Item</th>
            <th>Another column</th>
        </tr>
    </thead>
    <tbody>
        <Virtualize Items="fixedItems" ItemSize="30" SpacerElement="tr">
            <tr @key="context" style="height: 30px;" id="row-@context">
                <td>Item @context</td>
                <td>Another value</td>
            </tr>
        </Virtualize>
    </tbody>
</table>

@code {
    private List<int> fixedItems = Enumerable.Range(0, 1000).ToList();
}

Nell'esempio precedente, la radice del documento viene usata come contenitore di scorrimento, quindi gli html elementi e body vengono stiliti con overflow-y: scroll. Per ulteriori informazioni, vedi le seguenti risorse: