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


Отображение компонента 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 базового класса, который содержит логику для активации перерисовки в следующие моменты:

Компоненты, унаследованные от 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.

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

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

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

Weather.razor:

@page "/weather"
@attribute [StreamRendering]

...

@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, чтобы происходить, когда основной поток приложения свободен.

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

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

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

Этот дизайн позволяет вызывать 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 окончательно завершается. Платформа не может знать, когда перерисовать компонент на других промежуточных этапах, таких как при возврате данных в ряде промежуточных состояний. Если требуется повторная отрисовка в промежуточных точках, вызовите метод 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. Дополнительные сведения см. в разделе ASP.NET Core Razor удаления компонентов.

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

System.InvalidOperationException: 'Текущий поток не связан с диспетчером. Используйте 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.