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

Примечание.

Это не последняя версия этой статьи. Сведения о текущем выпуске см. в ASP.NET версии Core 8.0 этой статьи.

В этой статье вы узнаете, как использовать виртуализацию компонентов в приложениях 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> выполняет отрисовку только видимых в данный момент рейсов.
  • Укажите контекст для каждого рейса с помощью параметра 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 только тот срез записей, который соответствует текущей видимой области, не скачивая все данные из коллекции.
  • извлекает универсальный интерфейс ICollection<T> для Virtualize<TItem>.Items. Если элементы предоставляет неуниверсальная коллекция (например, коллекция DataRow), следуйте указаниям в разделе Делегат поставщика элементов, чтобы предоставить элементы.

Содержимое элемента для компонента 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 (не показано):

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

Измените лямбда-метод, 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 по умолчанию некоторые элементы отображаются за пределами видимого в данный момент представления, запускается вторая повторная визуализация. Чтобы обеспечить правильное расположение элементов прокрутки в виртуализированном списке в браузере, начальная прорисовка должна быть правильной. В противном случае пользователи могут просматривать не те элементы.

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

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.

  • Как разделители, так и строки содержимого отрисовываются в одном вертикальном стеке, и каждый элемент заполняет всю ширину по горизонтали. Обычно это значение по умолчанию. В типичных случаях с элементами divVirtualize работает по умолчанию. Если вы используете 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. Дополнительные сведения см. на следующих ресурсах: