Поделиться через


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

Примечание.

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

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

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

Внимание

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

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

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

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

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

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

Компоненты, унаследованные от 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 Web App из шаблона проекта. 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() => shouldRender;

    private void IncrementCount() => currentCount++;
}
@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() => 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 вызывает rerender, чтобы происходить, когда основной поток приложения свободен.

Компоненты заквещаются для отрисовки, и они не заквещаются снова, если уже есть ожидающий rerender. Если компонент вызывает StateHasChanged пять раз в строке в цикле, компонент выполняет отрисовку только один раз. Это поведение закодировано в ComponentBaseкоде, которое сначала проверяет, задается ли он в очередь rerender перед перечислением дополнительного.

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

  • Родительский компонент отображает несколько дочерних элементов.
  • Дочерние компоненты отрисовывают и активируют обновление родительского элемента.
  • Родительский компонент rerenders с новым состоянием.

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

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

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

Пошаговое выполнение кода в отладчике может подумать, что количество обновлений в пользовательском интерфейсе для первого currentCount++ выполнения сразу после StateHasChanged вызова. Однако в пользовательском интерфейсе не отображается обновленное число на этом этапе из-за синхронной обработки для выполнения этого метода. Не существует возможности отрисовщика отрисовки компонента до завершения обработки обработчика событий. Пользовательский интерфейс отображает увеличение обоих currentCount++ выполнений в одном отрисовке.

Если вы ожидаете что-то между currentCount++ строками, ожидающий вызов дает отрисовщику возможность отрисовки. Это привело к тому, что некоторые разработчики Delay звонят с одной миллисекунд задержкой в своих компонентах, чтобы обеспечить отрисовку, но мы не рекомендуем произвольно замедлять приложение, чтобы заставить отрисовку.

Оптимальным подходом является ожидание Task.Yield, которое заставляет компонент обрабатывать код асинхронно и отрисовывать во время текущего пакета с второй отрисовкой в отдельном пакете после запуска продолжения.

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

private async Task IncrementCount()
{
    currentCount++;
    StateHasChanged();
    await Task.Yield();
    currentCount++;
}

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

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

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

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

Из-за особенностей определения задач в .NET получатель Task может наблюдать только за его окончательным завершением, а не за промежуточными асинхронными состояниями. Таким образом, ComponentBase может активировать повторную отрисовку только при первом возвращении Task и при завершении Task. Платформа не может знать, чтобы rerender a component на других промежуточных точках, таких как при 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"

<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

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

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