Virtualización de componentes de ASP.NET Core Razor

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

En este artículo se explica cómo usar la virtualización de componentes en aplicaciones de ASP.NET Core Blazor.

La virtualización

Mejore el rendimiento percibido de la representación de componentes usando la compatibilidad de virtualización integrada del marco Blazor con el componente Virtualize<TItem>. La virtualización es una técnica para limitar la representación de la interfaz de usuario a únicamente las partes visibles actualmente. Por ejemplo, la virtualización es útil cuando la aplicación debe representar una lista larga de elementos y solo es necesario que haya un subconjunto de elementos visible en un momento dado.

Use el componente Virtualize<TItem> en las siguientes situaciones:

  • La representación de un conjunto de elementos de datos en un bucle.
  • La mayoría de los elementos no están visibles debido al desplazamiento.
  • Los elementos representados tienen el mismo tamaño.

Cuando el usuario se desplaza a un punto arbitrario en la lista de elementos del componente Virtualize<TItem>, este calcula los elementos visibles que se van a mostrar. Los elementos no visibles no se representan.

Sin la virtualización, una lista típica podría usar un bucle foreach de C# para representar cada elemento de una lista. En el ejemplo siguiente:

  • allFlights es una colección de vuelos de avión.
  • El componente FlightSummary muestra detalles sobre cada vuelo.
  • El atributo de directiva @key conserva la relación de cada componente FlightSummary con el vuelo representado según el elemento FlightId del vuelo.
<div style="height:500px;overflow-y:scroll">
    @foreach (var flight in allFlights)
    {
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    }
</div>

Si la colección contiene miles de vuelos, la representación de los vuelos tardará mucho tiempo y los usuarios experimentarán un retraso apreciable en la interfaz de usuario. La mayoría de los vuelos no se ven porque quedan fuera del alto del elemento <div>.

En lugar de representar la lista de vuelos completa de una vez, reemplace el bucle foreach del ejemplo anterior por el componente Virtualize<TItem>:

  • Especifique allFlights como origen de elemento fijo en Virtualize<TItem>.Items. El componente Virtualize<TItem> representa únicamente los vuelos actualmente visibles.

    Si una colección no genérica proporciona los elementos (por ejemplo, una colección de DataRow), sigue las instrucciones de la sección Delegado del proveedor de elementos para proporcionar los elementos.

  • Especifique un contexto para cada vuelo con el parámetro Context. En el siguiente ejemplo se usa como contexto flight, que proporciona acceso a los miembros de cada vuelo.

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

Si no se especifica un contexto con el parámetro Context, use el valor de context en la plantilla de contenido del elemento para acceder a los miembros de cada vuelo:

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

El componente Virtualize<TItem>:

  • Calcula el número de elementos que se van a representar en función del alto del contenedor y del tamaño de los elementos representados.
  • Vuelve a calcular los elementos y los representa a medida que el usuario se desplaza.
  • Solo captura el segmento de registros de una API externa que se corresponde con la región visible actualmente, incluida la detección excesiva, cuando se usa ItemsProvider en lugar de Items (consulta la sección Delegado del proveedor de elementos).

El contenido del elemento para el componente Virtualize<TItem> puede incluir lo siguiente:

  • HTML sin formato y código Razor, tal como se muestra en el ejemplo anterior.
  • Uno o más componentes Razor.
  • Una combinación de componentes HTML/Razor y Razor.

Delegado de proveedor de elementos

Si no se quieren cargar todos los elementos en la memoria o la colección no es un ICollection<T> genérico, se puede especificar un método de delegado de proveedor de elementos en el parámetro Virtualize<TItem>.ItemsProvider del componente, que recupera de manera asincrónica los elementos solicitados a petición. En el ejemplo siguiente, el método LoadEmployees proporciona los elementos al componente Virtualize<TItem>:

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

El proveedor de elementos recibe un objeto ItemsProviderRequest, que especifica el número necesario de elementos empezando por un índice de inicio específico. Luego, el proveedor de elementos recupera los elementos solicitados de una base de datos u otro servicio y los devuelve como un objeto ItemsProviderResult<TItem> junto con un recuento total de elementos. El proveedor de elementos puede elegir entre recuperar los elementos con cada solicitud o almacenarlos en la memoria caché para que estén disponibles fácilmente.

Un componente Virtualize<TItem> solo puede aceptar un origen de elementos de sus parámetros, por lo que no intente usar simultáneamente un proveedor de elementos y asignar una colección a Items. Si se asignan ambos, se genera una clase InvalidOperationException cuando los parámetros del componente se establecen en tiempo de ejecución.

En el siguiente ejemplo se cargan empleados desde EmployeeService (no mostrado):

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

En el ejemplo siguiente, una colección de DataRow es una colección no genérica, por lo que se usa un delegado de proveedor de elementos para la virtualización:

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

@code{
    ...

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

Virtualize<TItem>.RefreshDataAsync indica al componente que vuelva a solicitar datos de su elemento ItemsProvider. Esto resulta útil cuando cambian los datos externos. Normalmente no es necesario llamar a RefreshDataAsync cuando se usa Items.

RefreshDataAsync actualiza los datos de un componente Virtualize<TItem> sin provocar que se vuelva a representar. Si RefreshDataAsync se invoca desde un método de ciclo de vida de componente o controlador de eventos Blazor, no es necesario desencadenar una representación porque una representación se desencadena automáticamente al final del controlador de eventos o del método de ciclo de vida. Si RefreshDataAsync se desencadena por separado de una tarea o evento en segundo plano, como en el delegado ForecastUpdated siguiente, llame a StateHasChanged para actualizar la interfaz de usuario al final de la tarea o evento en segundo plano:

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

...

private Virtualize<FetchData>? virtualizeComponent;

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

En el ejemplo anterior:

  • Se llama primero a RefreshDataAsync para obtener nuevos datos para el componente Virtualize<TItem>.
  • Se llama a StateHasChanged para volver a representar el componente.

Marcador de posición

Como la solicitud de elementos de un origen de datos remoto puede tardar tiempo, tiene la opción de representar un marcador de posición con contenido de los elementos:

  • Use una instancia de Placeholder (<Placeholder>...</Placeholder>) para mostrar el contenido hasta que los datos del elemento estén disponibles.
  • Use Virtualize<TItem>.ItemContent para establecer la plantilla de elemento de la lista.
<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>

Contenido vacío

Use el parámetro EmptyContent para proporcionar contenido cuando el componente se haya cargado y Items esté vacío o ItemsProviderResult<TItem>.TotalItemCount sea cero.

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 ??= new();
}

Cambie el método OnInitialized lambda para ver las cadenas de visualización de componentes:

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

Tamaño de elemento

El alto de cada elemento en píxeles se puede establecer con Virtualize<TItem>.ItemSize (valor predeterminado: 50). En el siguiente ejemplo, el alto de cada elemento se cambia del valor predeterminado de 50 píxeles a 25 píxeles:

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

De forma predeterminada, el componente Virtualize<TItem> mide el tamaño de representación (alto) de cada elemento después de que se produzca la representación inicial. Use ItemSize para proporcionar de antemano un tamaño de elemento exacto, con el fin de permitir un rendimiento de representación inicial preciso y garantizar la posición de desplazamiento correcta para las recargas de páginas. Si el valor predeterminado de ItemSize hace que algunos elementos se representen fuera de la vista actualmente visible, se desencadena una segunda representación. Para mantener correctamente la posición de desplazamiento del explorador en una lista virtualizada, la representación inicial debe ser correcta. De lo contrario, los usuarios podrían ver elementos incorrectos.

Recuento de sobrebarridos

Virtualize<TItem>.OverscanCount determina el número de elementos adicionales que se representan antes y después del área visible. Esta configuración ayuda a reducir la frecuencia de representación durante el desplazamiento. Pese a esto, unos valores superiores hacen que se representen más elementos en la página (valor predeterminado: 3). En el siguiente ejemplo se cambia el recuento de sobrebarridos del valor predeterminado de tres elementos a cuatro elementos:

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

Cambios de estado

Al realizar cambios en los elementos representados por el componente Virtualize<TItem>, llame a StateHasChanged para forzar que el componente vuelva a evaluarse y representarse. Para más información, consulte Representación de componentes de Razor de ASP.NET Core.

Compatibilidad con el desplazamiento del teclado

Para permitir que los usuarios desplacen contenido virtualizado con su teclado, asegúrese de que los elementos virtualizados o el contenedor de desplazamiento en sí se puedan centrar. Si no realiza este paso, el desplazamiento con teclado no funcionará en exploradores basados en Chromium.

Por ejemplo, puede usar un atributo tabindex en el contenedor de desplazamiento:

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

Para obtener más información sobre el significado del valor tabindex, -1, 0 u otros valores, consulte tabindex (documentación de MDN).

Estilos avanzados y detección de desplazamiento

El componente Virtualize<TItem> solo está diseñado para admitir mecanismos de diseño de elementos específicos. Para comprender qué diseños de elementos funcionan correctamente, a continuación se explica cómo Virtualize detecta qué elementos deben estar visibles para mostrarse en el lugar correcto.

Si el código fuente es similar al siguiente:

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

En tiempo de ejecución, el componente Virtualize<TItem> representa una estructura DOM similar a la siguiente:

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

El número real de filas que se representan y el tamaño de los espaciadores varían según el estilo y el tamaño de la colección Items. Sin embargo, tenga en cuenta que hay elementos div espaciadores que se insertan antes y después del contenido. Sirven para dos propósitos:

  • Para proporcionar un desplazamiento antes y después del contenido, haciendo que los elementos visibles actualmente aparezcan en la ubicación correcta del intervalo de desplazamiento y el propio intervalo de desplazamiento para representar el tamaño total de todo el contenido.
  • Para detectar cuándo el usuario se desplaza más allá del intervalo visible actual, lo que significa que se debe representar contenido diferente.

Nota

Para obtener información sobre cómo controlar la etiqueta de elemento HTML del espaciador, consulte la sección Control del nombre de etiqueta del elemento espaciador más adelante en este artículo.

Los elementos espaciadores usan internamente un observador de intersección para recibir una notificación cuando se vuelven visibles. Virtualize depende de la recepción de estos eventos.

Virtualize se manifiesta en las siguientes condiciones:

  • Todos los elementos de contenido representados, incluido el contenido del marcador de posición, tienen una altura idéntica. Esto permite calcular qué contenido corresponde a una posición de desplazamiento determinada sin capturar primero todos los elementos de datos y representar los datos en un elemento DOM.

  • Tanto los espaciadores como las filas de contenido se representan en una sola pila vertical con cada elemento que rellena todo el ancho horizontal. Suele ser el valor predeterminado. En casos con elementos div, Virtualize funciona de forma predeterminada. Si usa CSS para crear un diseño más avanzado, tenga en cuenta los siguientes requisitos:

    • El estilo de contenedor de desplazamiento requiere un display con cualquiera de los valores siguientes:
      • block (el valor predeterminado para un div).
      • table-row-group (el valor predeterminado para un tbody).
      • flex con flex-direction establecido en column. Asegúrese de que los elementos secundarios inmediatos del componente Virtualize<TItem> no se reduzcan con reglas flexibles. Por ejemplo, agregue .mycontainer > div { flex-shrink: 0 }.
    • El estilo de fila de contenedor requiere un display con cualquiera de los valores siguientes:
      • block (el valor predeterminado para un div).
      • table-row (el valor predeterminado para un tr).
    • No use CSS para interferir con el diseño de los elementos espaciadores. De forma predeterminada, los elementos espaciadores tienen un valor display de block, excepto si el elemento primario es un grupo de filas de tabla, en cuyo caso tienen table-row como valor predeterminado. No intente influir en el ancho o alto de los elementos espaciadores, incluso haciendo que tengan un borde o pseudo-elementos de content.

Cualquier enfoque que detenga la representación de los espaciadores y los elementos de contenido como una sola pila vertical, o que los elementos de contenido variarán en alto, impide el funcionamiento correcto del componente Virtualize<TItem>.

Virtualización de nivel raíz

El componente Virtualize<TItem> admite el uso del propio documento como raíz de desplazamiento, como alternativa a tener algún otro elemento con overflow-y: scroll. En el ejemplo siguiente, se aplica estilo a los elementos <html> o <body> en un componente con overflow-y: scroll:

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

El componente Virtualize<TItem> admite el uso del propio documento como raíz de desplazamiento, como alternativa a tener algún otro elemento con overflow-y: scroll. Al usar el documento como raíz de desplazamiento, evite aplicar estilos a los elementos <html> o <body> con overflow-y: scroll porque hace que el observador de intersección trate el alto desplazable completo de la página como región visible, en lugar de simplemente la ventanilla de la ventana.

Para reproducir este problema, cree una lista virtualizada grande (por ejemplo, 100 000 elementos) e intente usar el documento como raíz de desplazamiento con html { overflow-y: scroll } en los estilos CSS de la página. Aunque a veces puede funcionar correctamente, el explorador intenta representar los 100 000 elementos al menos una vez al principio de la representación, lo que puede provocar un bloqueo de pestañas del explorador.

Para solucionar este problema antes de la versión .NET 7, evite aplicar estilos a los elementos <html>/<body> con overflow-y: scroll o adopte un enfoque alternativo. En el ejemplo siguiente, el alto del elemento <html> se establece en un poco más del 100 % del alto de la ventanilla:

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

El componente Virtualize<TItem> admite el uso del propio documento como raíz de desplazamiento, como alternativa a tener algún otro elemento con overflow-y: scroll. Al usar el documento como raíz de desplazamiento, evite aplicar estilos a los elementos <html> o <body> con overflow-y: scroll porque hace que el alto desplazable completo de la página se trate como la región visible, en lugar de simplemente la ventanilla de la ventana.

Para reproducir este problema, cree una lista virtualizada grande (por ejemplo, 100 000 elementos) e intente usar el documento como raíz de desplazamiento con html { overflow-y: scroll } en los estilos CSS de la página. Aunque a veces puede funcionar correctamente, el explorador intenta representar los 100 000 elementos al menos una vez al principio de la representación, lo que puede provocar un bloqueo de pestañas del explorador.

Para solucionar este problema antes de la versión .NET 7, evite aplicar estilos a los elementos <html>/<body> con overflow-y: scroll o adopte un enfoque alternativo. En el ejemplo siguiente, el alto del elemento <html> se establece en un poco más del 100 % del alto de la ventanilla:

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

Control del nombre de la etiqueta del elemento espaciador

Si el componente Virtualize<TItem> se coloca dentro de un elemento que requiere un nombre de etiqueta secundario específico, SpacerElement permite obtener o establecer el nombre de etiqueta del espaciador de virtualización. El valor predeterminado es div. Para el ejemplo siguiente, el componente Virtualize<TItem> se representa dentro de un elemento de cuerpo de tabla (tbody), por lo que el elemento secundario adecuado para una fila de tabla (tr) se establece como espaciador.

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

En el ejemplo anterior, la raíz del documento se usa como contenedor de desplazamiento, por lo que se aplica estilo a los elementos html y body con overflow-y: scroll. Para obtener más información, vea los siguientes recursos: