Отрисовка компонента Razor ASP.NET Core

Примечание.

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

Внимание

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

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

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

Соглашения об отрисовке ComponentBase

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

По умолчанию компоненты Razor наследуются от базового класса ComponentBase, который содержит логику, вызывающую повторную отрисовку в следующие моменты времени:

Компоненты, унаследованные от ComponentBase пропускают повторную отрисовку при обновлении параметров в том случае, если выполняется одно из следующих условий:

  • Все параметры относятся к набору известных типов* или любому примитивному типу, который не изменился с момента установки предыдущего набора параметров.

    *Платформа Blazor использует набор встроенных правил и явным образом проверяет тип параметров для обнаружения изменений. Эти правила и типы могут быть изменены в любое время. Дополнительные сведения см. разделе API ChangeDetection в справочных материалах по ASP.NET Core.

    Примечание.

    По ссылкам в документации на справочные материалы по .NET обычно загружается ветвь репозитория по умолчанию, которая представляет текущую разработку для следующего выпуска .NET. Чтобы выбрать тег для определенного выпуска, используйте раскрывающийся список Switch branches or tags (Переключение ветвей или тегов). Дополнительные сведения см. в статье Выбор тега версии исходного кода ASP.NET Core (dotnet/AspNetCore.Docs #26205).

  • Переопределение метода компонента ShouldRender возвращается false (реализация по умолчанию ComponentBase всегда возвращает).true

Управление потоком отрисовки

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

Дополнительные сведения о том, как соглашения для платформы влияют на производительность и как оптимизировать иерархию компонентов приложения для отрисовки, см. в статье Рекомендации по повышению производительности ASP.NET Core Blazor.

Потоковая отрисовка

Используйте потоковую отрисовку с отрисовкой на стороне статического сервера (статический SSR) или предварительной отрисовкой для потоковой передачи содержимого в потоке отклика и улучшения пользовательского интерфейса для компонентов, выполняющих длительные асинхронные задачи для полной отрисовки.

Например, рассмотрим компонент, который делает длительный запрос базы данных или вызов веб-API для отрисовки данных при загрузке страницы. Как правило, асинхронные задачи, выполняемые в процессе отрисовки компонента на стороне сервера, должны выполняться до отправки отрисованного ответа, что может отложить загрузку страницы. Любая существенная задержка при отрисовке страницы вредит пользовательскому интерфейсу. Чтобы улучшить взаимодействие с пользователем, потоковая отрисовка изначально отображает всю страницу с содержимым заполнителя во время асинхронных операций. После завершения операций обновленное содержимое отправляется клиенту в том же подключении ответа и исправлено в DOM.

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

Чтобы передавать обновления содержимого при использовании статической отрисовки на стороне сервера (статический SSR) или предварительной подготовки, примените [StreamRendering(true)] атрибут к компоненту. Потоковая отрисовка должна быть явно включена, так как потоковые обновления могут привести к перемещению содержимого на странице. Компоненты без атрибута автоматически принимают потоковую отрисовку, если родительский компонент использует эту функцию. Передайте false атрибут в дочернем компоненте, чтобы отключить функцию в этом моменте и далее вниз по поддереву компонента. Атрибут работает при применении к компонентам, предоставляемым библиотекой Razorклассов.

Следующий пример основан на Weather компоненте в приложении, созданном Blazor из шаблона проекта веб-приложения. Task.Delay Вызов имитации данных погоды асинхронно. Компонент изначально отрисовывает содержимое заполнителя ("Loading..."), не ожидая завершения асинхронной задержки. После завершения асинхронной задержки и создания содержимого данных о погоде содержимое передается в ответ и исправлено в таблицу прогноза погоды.

Weather.razor:

@page "/weather"
@attribute [StreamRendering(true)]

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        ...
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    ...

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(500);

        ...

        forecasts = ...
    }
}

Подавление обновления пользовательского интерфейса (ShouldRender)

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

Даже при переопределении ShouldRender компонент всегда проходит первоначальную отрисовку.

ControlRender.razor:

@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

Дополнительные рекомендации, связанные с ShouldRender, см. в статье Рекомендации по повышению производительности ASP.NET Core Blazor.

Когда следует вызывать метод StateHasChanged

Вызов метода StateHasChanged позволяет запускать отрисовку в любое время. Тем не менее, метод StateHasChanged не следует вызывать без необходимости. Это распространенная ошибка, которая влечет за собой ненужные затраты ресурсов на отрисовку.

Код не должен вызывать метод StateHasChanged в следующих случаях:

  • Стандартная синхронная или асинхронная обработка событий, поскольку ComponentBase запускает отрисовку для большинства стандартных обработчиков событий.
  • Реализация типовой синхронной или асинхронной логики жизненного цикла, например OnInitialized или OnParametersSetAsync, поскольку ComponentBase запускает отрисовку для типовых событий жизненного цикла.

Тем не менее, вызов метода StateHasChanged может требоваться в случаях, которые описываются в следующих разделах этой статьи:

Использование нескольких асинхронных стадий в асинхронном обработчике

Из-за особенностей определения задач в .NET получатель Task может наблюдать только за его окончательным завершением, а не за промежуточными асинхронными состояниями. Таким образом, ComponentBase может активировать повторную отрисовку только при первом возвращении Task и при завершении Task. Платформа не может знать, когда необходимо выполнить повторную отрисовку компонента в других промежуточных точках, например, когда IAsyncEnumerable<T> возвращает данные в виде последовательности промежуточных Task. Если требуется повторная отрисовка в промежуточных точках, вызовите в них метод StateHasChanged.

Рассмотрим следующий CounterState1 компонент, который обновляет число четыре раза каждый раз при выполнении IncrementCount метода:

  • Автоматическая отрисовка выполняется после первого и последнего приращений currentCount.
  • Отрисовка вручную активируется вызовами метода StateHasChanged, когда платформа не запускает повторные отрисовки автоматически в промежуточных точках обработки, где увеличивается значение currentCount.

CounterState1.razor:

@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

Получение вызова извне к системе обработки событий Blazor

ComponentBase имеет сведения только о собственных методах жизненного цикла и событиях, вызываемых Blazor. ComponentBase не располагает информацией о других событиях, которые могут возникать в коде. Например, Blazor не имеет сведений о любых событиях C#, вызываемых пользовательским хранилищем данных. Чтобы такие события вызывали повторную отрисовку для отображения обновленных значений в пользовательском интерфейсе, вызовите метод StateHasChanged.

Рассмотрим следующий компонент CounterState2, который использует System.Timers.Timer для обновления счетчика с установленным интервалом и вызывает метод StateHasChanged для обновления пользовательского интерфейса:

  • OnTimerCallback выполняется за пределами управляемого потока отрисовки или уведомления о событии Blazor. Следовательно, OnTimerCallback должен вызвать метод StateHasChanged, поскольку Blazor не знает об изменениях currentCount в обратном вызове.
  • Компонент реализует IDisposable, где Timer удаляется, когда платформа вызывает метод Dispose. Дополнительные сведения см. в статье Жизненный цикл компонентов Razor ASP.NET Core.

Поскольку обратный вызов выполняется вне контекста синхронизации Blazor, компонент должен упаковать логику OnTimerCallback в ComponentBase.InvokeAsync, чтобы переместить ее в контекст синхронизации модуля отрисовки. Это поведение эквивалентно маршалингу потока пользовательского интерфейса на других платформах пользовательского интерфейса. Метод StateHasChanged может вызываться только из контекста синхронизации модуля отрисовки, иначе это приводит к возникновению исключения:

System.InvalidOperationException: 'The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.' (Текущий поток не связан с Dispatcher. Используйте InvokeAsync() для переключения выполнения на Dispatcher при активации отрисовки или состояния компонента).

CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

Отрисовка компонента за пределами поддерева, которое повторно отрисовывается в связи с определенным событием

Пользовательский интерфейс может использоваться для выполнения следующих задач:

  1. Отправка события в один компонент.
  2. Изменение состояния.
  3. Повторная отрисовка совершенно другого компонента, который не является потомком компонента, получающего событие.

Одним из способов работы в таких сценариях является предоставление класса управления состоянием, часто в виде службы внедрения зависимостей, внедренной в несколько компонентов. Когда один компонент вызывает метод в диспетчере состояний, диспетчер состояний вызывает событие C#, которое затем получается независимым компонентом.

Сведения о подходах к управлению состоянием см. в следующих ресурсах:

Для подхода диспетчера состояний события C# находятся вне конвейера Blazor отрисовки. Вызовите StateHasChanged другие компоненты, которые вы хотите перенаправить в ответ на события диспетчера состояний.

Подход диспетчера состояний аналогичен предыдущему варианту с System.Timers.Timer предыдущим разделом. Поскольку стек вызовов выполнения, как правило, остается в контексте синхронизации модуля отрисовки, вызывать InvokeAsync обычно не требуется. Вызывать InvokeAsync требуется только в том случае, если логика выходит из контекста синхронизации, например в результате вызова ContinueWith для Task или ожидания Task с ConfigureAwait(false). Дополнительные сведения см. в разделе Получение вызова извне к системе обработки событий Blazor.

Индикатор выполнения загрузки WebAssembly для Blazor веб-приложения

Индикатор хода загрузки отсутствует в приложении, созданном Blazor из шаблона проекта веб-приложения. Для будущего выпуска .NET планируется новая функция индикатора хода загрузки. В то же время приложение может внедрить пользовательский код для создания индикатора хода загрузки. Дополнительные сведения см. в статье Запуск ASP.NET Core Blazor.