RazorRenderização de componentes do ASP.NET Core

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Este artigo explica a Razorrenderização de componentes em aplicativos ASP.NET CoreBlazor, inclusive quando chamar StateHasChanged para disparar manualmente um componente a ser renderizado.

Convenções de renderização para ComponentBase

Os componentes devem ser renderizados quando são adicionados pela primeira vez à hierarquia de componentes por um componente pai. Este é o único momento em que um componente deve ser renderizado. Os componentes podem ser renderizados em outros momentos, de acordo com sua própria lógica e convenções.

Por padrão, Razor os componentes herdam da ComponentBase classe base, que contém lógica para disparar a nova renderização nos seguintes momentos:

Componentes herdados de remetentes ignorados ComponentBase devido a atualizações de parâmetro, se uma das alternativas a seguir for verdadeira:

  • Todos os parâmetros são de um conjunto de tipos conhecidos† ou de qualquer tipo primitivo que não tenha sofrido alteração desde a definição do conjunto anterior de parâmetros.

    †A estrutura Blazor usa um conjunto de regras internas e verificações explícitas de tipo de parâmetro na detecção de alterações. Essas regras e os tipos estão sujeitos a alterações a qualquer momento. Para obter mais informações, consulte a ChangeDetection API na fonte de referência do ASP.NET Core.

    Observação

    Os links de documentação para a fonte de referência do .NET geralmente carregam o branch padrão do repositório, que representa o desenvolvimento atual da próxima versão do .NET. Para selecionar uma marca para uma versão específica, use a lista suspensa para Alternar branches ou marcas. Para saber mais, confira Como selecionar uma marca de versão do código-fonte do ASP.NET Core (dotnet/AspNetCore.Docs #26205).

  • A substituição do método ShouldRender do componente retorna false (a implementação padrão ComponentBase sempre retorna true).

Controle o fluxo de renderização

Na maioria dos casos, ComponentBase as convenções resultam no subconjunto correto de remetentes de componente após a ocorrência de um evento. Os desenvolvedores geralmente não são obrigados a fornecer lógica manual que informe à estrutura quais componentes renderizar novamente e quando renderizá-los novamente. O efeito geral das convenções da estrutura é que o componente que recebe um evento renderiza-se a si mesmo novamente, o que dispara recursivamente a nova renderização de componentes descendentes cujos valores de parâmetro possam ter sido alterados.

Para obter mais informações sobre as implicações de desempenho das convenções da estrutura e como otimizar a hierarquia de componentes de um aplicativo para renderização, consulte ASP.NET Core Blazor práticas recomendadas de desempenho.

Renderização de streaming

Use a renderização de streaming com renderização estática do lado do servidor (SSR estática) ou a pré-renderização para transmitir atualizações de conteúdo na transmissão de resposta e melhorar a experiência do usuário para componentes que executam tarefas assíncronas de execução longa para renderizar totalmente.

Por exemplo, considere um componente que faz uma consulta de banco de dados de execução longa ou uma chamada à API Web para renderizar dados quando a página é carregada. Normalmente, tarefas assíncronas executadas como parte da renderização de um componente do lado do servidor devem ser concluídas antes que a resposta renderizada seja enviada, o que pode atrasar o carregamento da página. Qualquer atraso significativo na renderização da página prejudica a experiência do usuário. Para melhorar a experiência do usuário, a renderização de streaming inicialmente renderiza a página inteira rapidamente com o conteúdo do espaço reservado enquanto as operações assíncronas são executadas. Depois que as operações forem concluídas, o conteúdo atualizado será enviado ao cliente na mesma conexão de resposta e corrigido no DOM.

A renderização de streaming requer que o servidor evite o buffer da saída. Os dados de resposta precisam fluir para o cliente à medida que os dados são gerados. Para hosts que impõem o buffer, a renderização de streaming é degradada normalmente e a página é carregada sem renderização de streaming.

Para transmitir atualizações de conteúdo ao usar a renderização estática do lado do servidor (SSR estática) ou pré-renderização, aplique o atributo [StreamRendering(true)] ao componente. A renderização de streaming deve ser habilitada explicitamente porque as atualizações transmitidas podem fazer com que o conteúdo na página mude. Componentes sem o atributo adotam automaticamente a renderização de streaming se o componente pai usar o recurso. Passe false para o atributo em um componente filho para desabilitar o recurso nesse ponto e mais abaixo da subárvore do componente. O atributo é funcional quando aplicado a componentes fornecidos por uma Razor biblioteca de classes.

O exemplo a seguir baseia-se no componente Weather em um aplicativo criado a partir do Blazormodelo de projeto do Aplicativo Web. A chamada Task.Delay para simular a recuperação de dados meteorológicos de forma assíncrona. O componente inicialmente renderiza o conteúdo do espaço reservado ("Loading...") sem aguardar a conclusão do atraso assíncrono. Quando o atraso assíncrono é concluído e o conteúdo dos dados meteorológicos é gerado, o conteúdo é transmitido para a resposta e corrigido na tabela de previsão do tempo.

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 = ...
    }
}

Supressão da atualização da interface do usuário (ShouldRender)

ShouldRender é chamada sempre que um componente é renderizado. Substitua ShouldRender para gerenciar a atualização da interface do usuário. Se a implementação retornar true, a interface do usuário será atualizada.

Mesmo que ShouldRender seja substituído, o componente é sempre renderizado inicialmente.

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

Para obter mais informações sobre as melhores práticas de desempenho relativas a ShouldRender, consulte ASP.NET Core Blazor melhores práticas de desempenho.

Quando chamar StateHasChanged

Chamar StateHasChanged permite disparar uma renderização a qualquer momento. No entanto, tenha o cuidado de não chamar StateHasChanged desnecessariamente: um erro comum que impõe custos de renderização desnecessários.

O código não deve precisar chamar StateHasChanged ao:

  • Fazer a manipulação de rotina de eventos, seja de forma síncrona ou assíncrona, uma vez que ComponentBase dispara uma renderização para a maioria dos manipuladores de eventos de rotina.
  • Implementar a lógica típica do ciclo de vida, como OnInitialized ou OnParametersSetAsync, seja de forma síncrona ou assíncrona, uma vez ComponentBase que dispara uma renderização para eventos típicos do ciclo de vida.

No entanto, pode fazer sentido chamar StateHasChanged nos casos descritos nas seguintes seções deste artigo:

Um manipulador assíncrono envolve várias fases assíncronas

Devido à maneira como as tarefas são definidas em .NET, um receptor de um Task só pode observar sua conclusão final, mas não os estados assíncronos intermediários. Portanto, ComponentBase só poderá disparar a nova renderização quando o Task estiver retornado pela primeira vez e quando o Task finalmente concluir. A estrutura não sabe como renderizar novamente um componente em outros pontos intermediários, como por exemplo, quando um IAsyncEnumerable<T>retorna dados em uma série de Tasks.intermediários. Se você quiser renderizar novamente em pontos intermediários, chame StateHasChanged nesses pontos.

Considere o seguinte componente CounterState1, que atualiza a contagem quatro vezes cada vez que o método IncrementCount é executado:

  • As renderizações automáticas ocorrem após o primeiro e o último incremento de currentCount.
  • As renderizações manuais são disparadas por chamadas para StateHasChanged quando a estrutura não dispara remetentes automaticamente em pontos de processamento intermediários, nos quais currentCount é incrementado.

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

O recebimento de chamada de algo externo ao Blazor sistema de renderização e manipulação de eventos

ComponentBase sabe somente sobre seus próprios métodos de ciclo de vida e sobre Blazoreventos disparados por . ComponentBase não sabe sobre outros eventos que podem ocorrer em código. Por exemplo, todos os eventos C# gerados por um armazenamento de dados personalizado são desconhecidos para Blazor. Para que esses eventos disparem uma nova renderização para a exibição de valores atualizados na interface do usuário, chame StateHasChanged.

Considere o seguinte componente CounterState2,que usa System.Timers.Timer para atualizar uma contagem em um intervalo regular e chama StateHasChanged para atualizar a interface do usuário:

  • OnTimerCallback é executado fora de qualquer fluxo de renderização ou notificação de evento gerenciado por Blazor. Portanto, OnTimerCallback deve chamar StateHasChanged porque Blazor não está ciente das alterações em currentCount no retorno de chamada.
  • O componente implementa IDisposable, em que o Timer é descartado quando a estrutura chama o método Dispose. Para saber mais, consulte Ciclo de vida de renderização de Razor no ASP.NET Core.

Como o retorno de chamada é invocado fora do contexto de sincronização de Blazor, o componente deve encapsular a lógica de OnTimerCallback em ComponentBase.InvokeAsync para a mover para o contexto de sincronização do renderizador. Isso equivale à realização de marshaling para o thread da interface do usuário em outras estruturas de interface do usuário. StateHasChanged só pode ser chamado do contexto de sincronização do renderizador e gera uma exceção, caso contrário:

System.InvalidOperationException: 'O thread atual não está associado ao Dispatcher. Use InvokeAsync() para alternar a execução para o Dispatcher ao disparar a renderização ou o estado do componente.'

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

Para renderizar um componente fora da subárvore, o qual é novamente renderizado por um evento específico

A interface do usuário pode implicar:

  1. A expedição de um evento para um componente.
  2. A alteração de algum estado.
  3. A nova renderização de um componente completamente diferente que não seja um descendente do componente que recebe o evento.

Uma maneira de lidar com esse cenário é fornecer uma classe de gerenciamento de estado , geralmente como um serviço de DI (injeção de dependência), injetada em vários componentes. Quando um componente chama um método no gerenciador de estado, o gerenciador de estado gera um evento C#, que é recebido por um componente independente.

Para obter abordagens para o gerenciamento do estado, consulte os seguintes recursos:

Para a abordagem do gerenciador de estado, os eventos C# estão fora do Blazor pipeline de renderização. Chame StateHasChanged outros componentes que você deseja renderizar novamente em resposta aos eventos do gerenciador de estado.

A abordagem do gerenciador de estado é semelhante ao caso anterior com System.Timers.Timer na seção anterior. Como a pilha de chamadas de execução normalmente permanece no contexto de sincronização do renderizador, a chamada InvokeAsync normalmente não é necessária. A chamada de InvokeAsync só será necessária se a lógica escapar do contexto de sincronização, como chamar ContinueWith em um Task ou aguardar um Task com ConfigureAwait(false). Para obter mais informações, consulte a seção Recebendo uma chamada de algo externo para o Blazor sistema de renderização e manipulação de eventos .

Indicador de progresso de carregamento do WebAssembly para aplicativos Web Blazor

Um indicador de progresso de carregamento não está presente em um aplicativo criado a partir do modelo de projeto de Aplicativo Web Blazor. Um novo recurso de indicador de progresso de carregamento está planejado para uma versão futura do .NET. Enquanto isso, um aplicativo pode adotar código personalizado para criar um indicador de progresso de carregamento. Para obter mais informações, confira Inicialização Blazor do ASP.NET Core.