Виртуализация компонентов ASP.NET Core Razor

Примечание

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 9 этой статьи.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 9 этой статьи.

Важно!

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см . версию .NET 9 этой статьи.

В этой статье вы узнаете, как использовать виртуализацию компонентов в приложениях ASP.NET Core Blazor.

Виртуализация

Повышение производительности отрисовки компонентов с помощью Blazor встроенной поддержки виртуализации платформы с компонентом Virtualize<TItem> . Виртуализация — это метод отображения только видимых в данный момент частей пользовательского интерфейса. Например, виртуализация удобна в случае, когда в приложении должен быть отрисован длинный список элементов и в любой конкретный момент времени должно быть видимым только подмножество элементов.

Используйте компонент Virtualize<TItem> в таких случаях:

  • при отрисовке набора элементов данных в цикле;
  • если большинство элементов не видны из-за настроек прокрутки;
  • если отображаемые элементы имеют одинаковый размер.

Когда пользователь прокручивает с произвольной точки в списке элементов компонента Virtualize<TItem>, компонент вычисляет видимые элементы для отображения. Невидимые элементы не отрисовываются.

Без виртуализации обычный список может использовать цикл C# foreach для отрисовки каждого элемента в списке. В следующем примере :

  • allFlights представляет собой коллекцию рейсов самолетов.
  • Компонент FlightSummary отображает сведения о каждом рейсе.
  • Атрибут директивы @key сохраняет связь каждого компонента FlightSummary с его отображаемым рейсом элемента FlightId рейса.
<div style="height:500px;overflow-y:scroll">
    @foreach (var flight in allFlights)
    {
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    }
</div>

Если коллекция содержит тысячи рейсов, отрисовка рейсов занимает много времени и пользователи сталкиваются с заметной задержкой отображения пользовательского интерфейса. Большинство полетов выходят за пределы высоты <div> элемента, поэтому большинство из них не видно.

Вместо отрисовки сразу всего списка рейсов замените цикл foreach в предыдущем примере на компонент Virtualize<TItem>:

  • Укажите allFlights как источник фиксированного элемента для Virtualize<TItem>.Items. Компонент Virtualize<TItem> выполняет отрисовку только видимых в данный момент рейсов.

    Если не универсальная коллекция предоставляет элементы, например коллекцию DataRow, следуйте указаниям в разделе делегата поставщика элементов, чтобы предоставить элементы.

  • Укажите контекст для каждого рейса с помощью параметра Context. В следующем примере элемент flight используется в качестве контекста, который обеспечивает доступ к каждому участнику рейса.

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

Если контекст не указан с помощью параметра Context, используйте значение context в шаблоне содержимого элемента, чтобы получить доступ к членам каждого рейса:

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

Компонент Virtualize<TItem>:

  • вычисляет количество подлежащих отрисовке элементов на основе высоты контейнера и размера отображаемых элементов;
  • пересчитывает и повторно отрисовывает элементы при прокрутке пользователем;
  • Извлекает только срез записей из внешнего API, соответствующий текущему видимому региону, включая overscan, когда ItemsProvider используется вместо Items (см . раздел делегата поставщика элементов).

Содержимое элемента для компонента Virtualize<TItem> может включать в себя следующее:

  • обычный код HTML и код Razor, как показано в предыдущем примере;
  • один или несколько компонентов Razor;
  • сочетание компонентов HTML/Razor и Razor.

Делегат поставщика элементов

Если вы не хотите загружать все элементы в память или коллекция не является универсальным интерфейсом ICollection<T>, можно указать метод делегата поставщика элементов для параметра Virtualize<TItem>.ItemsProvider компонента, который асинхронно извлекает запрошенные элементы по запросу. В следующем примере метод LoadEmployees предоставляет элементы компоненту Virtualize<TItem>.

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

Поставщик элементов получает ItemsProviderRequest, который указывает необходимое количество элементов, начиная с заданного начального индекса. Затем поставщик элементов извлекает запрошенные элементы из базы данных или другой службы и возвращает их в виде ItemsProviderResult<TItem> вместе с количеством всех элементов. Поставщик элементов может извлекать элементы с каждым запросом или кэшировать их, чтобы они были доступны.

Компонент Virtualize<TItem> может принимать только один источник элемента из параметров, поэтому не пытайтесь одновременно использовать поставщик элементов и назначить коллекцию для Items. Если назначаются оба значения, создается InvalidOperationException, когда параметры компонента задаются во время выполнения.

Следующий пример загружает сотрудников из EmployeeService (не показан). Обычно totalEmployees это поле назначается путем вызова метода в той же службе (например, в другом месте), EmployeesService.GetEmployeesCountAsyncнапример во время инициализации компонента.

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

В следующем примере коллекция DataRow не является универсальной, поэтому для виртуализации используется делегат поставщика элементов:

<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 указывает компоненту на необходимость повторного запроса данных из ItemsProvider. Это полезно в тех случаях, когда внешние данные изменяются. Вызывать метод RefreshDataAsync при использовании Items обычно не требуется.

RefreshDataAsync обновляет данные компонента Virtualize<TItem>, не приводя к повторной отрисовке. Если RefreshDataAsync вызывается из обработчика событий Blazor или метода жизненного цикла компонента, активация отрисовки не требуется, поскольку она автоматически активируется в конце обработчика событий или метода жизненного цикла. Если RefreshDataAsync запускается отдельно от фоновой задачи или события, например в следующем делегате ForecastUpdated, вызовите метод StateHasChanged, чтобы обновить пользовательский интерфейс в конце фоновой задачи или события:

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

...

private Virtualize<FetchData>? virtualizeComponent;

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

В предыдущем примере:

  • RefreshDataAsync вызывается первым, чтобы получить новые данные для компонента Virtualize<TItem>.
  • StateHasChanged вызывается для повторной отрисовки компонента.

Заполнитель

Поскольку запрос элементов из удаленного источника данных может занимать некоторое время, можно отрисовать заполнитель с содержимым элемента.

  • Используйте Placeholder (<Placeholder>...</Placeholder>) для отображения содержимого до тех пор, пока не будут доступны данные элемента.
  • Чтобы задать шаблон элемента для списка, используйте Virtualize<TItem>.ItemContent.
<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>

Пустое содержимое

EmptyContent Используйте параметр для предоставления содержимого при загрузке компонента и Items пуст или ItemsProviderResult<TItem>.TotalItemCount равен нулю.

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

Измените лямбда-метод, OnInitialized чтобы просмотреть строки отображения компонента:

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

Размер элемента

Высоту каждого элемента в пикселях можно задать с помощью Virtualize<TItem>.ItemSize (по умолчанию: 50). В следующем примере высота каждого элемента изменяется со стандартного значения 50 пикселей на 25 пикселей:

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

Компонент Virtualize<TItem> измеряет размер отрисовки (высота) отдельных элементов после первоначальной отрисовки. Используйте ItemSize, чтобы заранее предоставить точный размер элемента и обеспечить правильную первоначальную производительность отрисовки, а также убедиться в правильности позиции прокрутки для перегрузки страниц. Если по умолчанию ItemSize некоторые элементы будут отображаться вне видимого представления, активируется второй rerender. Чтобы обеспечить правильное расположение элементов прокрутки в виртуализированном списке в браузере, начальная прорисовка должна быть правильной. В противном случае пользователи могут просматривать не те элементы.

Количество элементов в нерабочей области

Virtualize<TItem>.OverscanCount определяет количество дополнительных элементов, отрисовываемых до и после видимой области. Этот параметр позволяет уменьшить частоту отрисовки во время прокрутки. Однако более высокие значения приводят к отображению большего числа элементов на странице (по умолчанию: 3). В следующем примере количество элементов в нерабочей области изменяется с трех элементов (стандартное значение) на четыре:

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

Изменения состояний

При внесении изменений в элементы, отображаемые компонентом Virtualize<TItem> , вызовите StateHasChanged повторное вычисление и повторное выполнение компонента. Дополнительные сведения см. в статье Отрисовка компонентов Razor ASP.NET Core.

Поддержка прокрутки с клавиатуры

Чтобы разрешить пользователям прокручивать виртуализированное содержимое с помощью клавиатуры, убедитесь, что виртуализованные элементы или сам контейнер прокрутки являются фокусируемыми. Если не выполнить этот шаг, прокрутка с клавиатуры не будет работать в браузерах на основе Chromium.

Например, атрибут tabindex можно использовать в контейнере прокрутки:

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

Дополнительные сведения о значении tabindex -1, 0 или других значениях см. в разделе tabindex (документация по MDN).

Расширенные стили и обнаружение прокрутки

Компонент Virtualize<TItem> предназначен только для поддержки конкретных механизмов макетов элементов. Чтобы можно было понять, какие макеты элементов работают правильно, далее объясняется, как Virtualize определяет, какие элементы должны быть видимы для отображения в правильном месте.

Если исходный код выглядит следующим образом:

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

В среде выполнения компонент Virtualize<TItem> отрисовывает структуру DOM следующим образом:

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

Фактическое число отображаемых строк и размер разделителей зависят от стиля и размера коллекции Items. Однако обратите внимание, что перед содержимым и после него есть элементы разделителей div. Они служат двум целям:

  • Чтобы обеспечить смещение до и после содержимого, в результате чего видимые элементы будут отображаться в правильном расположении в диапазоне прокрутки, а сам диапазон прокрутки будет представлять общий размер всего содержимого.
  • Чтобы определить, когда пользователь выполняет прокрутку за пределами текущего видимого диапазона, то есть должно быть отрисовано другое содержимое.

Примечание

Чтобы узнать, как управлять тегом HTML-элемента разделителя, см. раздел Управление именем тега элемента разделителя далее в этой статье.

Элементы разделителя внутренне используют наблюдатель пересечения, чтобы знать, когда они становятся видимыми. Virtualize зависит от получения этих событий.

Virtualize работает в следующих условиях:

  • Все отображаемые элементы содержимого, включая заполнитель, имеют одинаковую высоту. Это позволяет вычислить содержимое, соответствующее заданной позиции прокрутки, без предварительной выборки каждого элемента данных и отрисовки данных в элемент DOM.

  • Пробелы и строки содержимого отрисовываются в одном вертикальном стеке с каждым элементом, заполняя всю горизонтальную ширину. В типичных вариантах использования работает с div элементамиVirtualize. Если вы используете CSS для создания расширенного макета, учитывайте следующие требования:

    • Для стилизации контейнера прокрутки требуется display любой из следующих значений:
      • block (значение по умолчанию для div).
      • table-row-group (значение по умолчанию для tbody).
      • flex с параметром flex-direction со значением column. Убедитесь, что непосредственные дочерние элементы компонента Virtualize<TItem> не сжимаются в соответствии с правилами гибкого подхода. Например, добавьте .mycontainer > div { flex-shrink: 0 }.
    • Для стилизации строк содержимого требуется display одно из следующих значений:
      • block (значение по умолчанию для div).
      • table-row (значение по умолчанию для tr).
    • Не используйте CSS, чтобы изменить макет с элементами разделителей. Элементы пробелов имеют display значение block, за исключением того, если родительский элемент является группой строк таблицы, в этом случае они по умолчанию table-row. Не пытайтесь повлиять на ширину или высоту элемента разделителя, включая настройку границы или псевдо-элементов content.

Любой подход, который мешает элементам разделителей и содержимого отрисовываться в виде одного вертикального стека или приводит к различию в высоте элементов, нарушает функционирование компонента Virtualize<TItem>.

Виртуализация на корневом уровне

Компонент Virtualize<TItem> поддерживает использование самого документа в качестве корня прокрутки как альтернативу использованию другого элемента с overflow-y: scroll. В следующем примере для элементов <html> или <body> стиль настраивается в компоненте с помощью overflow-y: scroll:

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

Компонент Virtualize<TItem> поддерживает использование самого документа в качестве корня прокрутки как альтернативу использованию другого элемента с overflow-y: scroll. При использовании документа в качестве корня прокрутки не настраивайте стиль элементов <html> или <body> с помощью overflow-y: scroll, так как это приводит к тому, что наблюдатель пересечения обрабатывает всю прокручиваемую высоту страницы как видимую область, а не только окно просмотра окна.

Эту проблему можно воспроизвести, создав большой виртуализированный список (например, 100 000 элементов) и попытавшись использовать документ в качестве корня прокрутки с параметром html { overflow-y: scroll } на странице стилей CSS. Хотя иногда это может сработать, браузер пытается отрисовать все 100 000 элементов по крайней мере один раз в начале отрисовки, что может привести к блокировке вкладки браузера.

Чтобы обойти эту проблему до выпуска .NET 7, не используйте <html>/<body> overflow-y: scroll альтернативный подход. В следующем примере высота элемента <html> имеет значение чуть более 100 % высоты окна просмотра:

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

Компонент Virtualize<TItem> поддерживает использование самого документа в качестве корня прокрутки как альтернативу использованию другого элемента с overflow-y: scroll. При использовании документа в качестве корня прокрутки избегайте стилизации <html> элементов или <body> элементов overflow-y: scroll , так как это приводит к тому, что полная прокрутка высоты страницы будет рассматриваться как видимая область, а не только окно просмотра.

Эту проблему можно воспроизвести, создав большой виртуализированный список (например, 100 000 элементов) и попытавшись использовать документ в качестве корня прокрутки с параметром html { overflow-y: scroll } на странице стилей CSS. Хотя иногда это может сработать, браузер пытается отрисовать все 100 000 элементов по крайней мере один раз в начале отрисовки, что может привести к блокировке вкладки браузера.

Чтобы обойти эту проблему до выпуска .NET 7, не используйте <html>/<body> overflow-y: scroll альтернативный подход. В следующем примере высота элемента <html> имеет значение чуть более 100 % высоты окна просмотра:

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

Управление именем тега элемента разделителя

Если компонент Virtualize<TItem> помещается в элемент, которому требуется определенное имя дочернего тега, SpacerElement разрешает получить или задать имя тега разделителя в виртуализации. Значение по умолчанию — div. В следующем примере компонент Virtualize<TItem> отрисовывается внутри элемента тела таблицы (tbody), поэтому соответствующий дочерний элемент для строки таблицы (tr) задается в качестве разделителя.

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

В предыдущем примере корень документа используется в качестве контейнера прокрутки, поэтому для элементов html и body стиль задается с помощью overflow-y: scroll. Дополнительные сведения см. на следующих ресурсах: