Partilhar via


ASP.NET Renderização de componentes do Core Razor

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Importante

Estas informações referem-se a um produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado comercialmente. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

Para a versão atual, consulte a versão .NET 9 deste artigo.

Este artigo explica Razor renderização de componentes em aplicativos ASP.NET Core Blazor, incluindo quando chamar StateHasChanged para acionar manualmente um componente para renderizar.

Convenções de renderização para ComponentBase

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

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

Os componentes herdados do ComponentBase ignoram rerenderizações devido a atualizações de parâmetros se uma das seguintes opções for verdadeira:

  • Todos os parâmetros são de um conjunto de tipos conhecidos† ou qualquer tipo primitivo que não mudou desde que o conjunto anterior de parâmetros foi definido.

    †A estrutura Blazor usa um conjunto de regras internas e verificações explícitas de tipo de parâmetro para deteção de alterações. Estas regras e os tipos estão sujeitos a alterações a qualquer momento. Para obter mais informações, consulte a API ChangeDetection 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 a ramificação padrão do repositório, que representa o desenvolvimento atual para a próxima versão do .NET. Para selecionar uma tag para uma versão específica, use a lista suspensa ramificações ou tags. Para obter mais informações, consulte 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 de ComponentBase padrão sempre retorna true).

Controle o fluxo de renderização

Na maioria dos casos, as ComponentBase convenções resultam no subconjunto correto de rerenderizações de componentes após a ocorrência de um evento. Os desenvolvedores geralmente não são obrigados a fornecer lógica manual para dizer à estrutura quais componentes renderizar novamente e quando rerenderizá-los. O efeito geral das convenções da estrutura é que o componente que recebe um evento rerenderiza-se, o que, por sua vez, dispara recursivamente a rerenderização de componentes descendentes cujos parâmetros podem 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 de renderização de streaming com renderização estática do lado do servidor (SSR estático) ou pré-renderização para transmitir atualizações de conteúdo no fluxo de resposta e melhorar a experiência do usuário para componentes que executam tarefas assíncronas de longa duração para renderização completa.

Por exemplo, considere um componente que faz uma consulta de banco de dados de longa execução ou uma chamada de API da Web para renderizar dados quando a página é carregada. Normalmente, as 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 de utilizador, a renderização de streaming inicialmente renderiza a página inteira rapidamente com conteúdo temporário enquanto as operações assíncronas são executadas. Após a conclusão das operações, o conteúdo atualizado é enviado para o cliente na mesma conexão de resposta e integrado no DOM.

A renderização de streaming requer que o servidor evite o armazenamento em buffer da saída. Os dados de resposta devem fluir para o cliente à medida que os dados são gerados. Para hosts que impõem buffer, a renderização de streaming se degrada 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ático) ou a pré-renderização, aplique o atributo [StreamRendering] no .NET 9 ou posterior (use [StreamRendering(true)] no .NET 8) ao componente. A renderização de streaming deve ser explicitamente habilitada porque as atualizações transmitidas podem fazer com que o conteúdo da página mude. Os 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 desativar o recurso nesse ponto e mais abaixo na subárvore do componente. O atributo é funcional quando aplicado a componentes fornecidos por uma biblioteca de classes Razor.

O exemplo a seguir é baseado no componente Weather em um aplicativo criado a partir do modelo de projeto Blazor Web App. A chamada para Task.Delay simula a recuperação de dados meteorológicos de forma assíncrona. O componente inicialmente renderiza o conteúdo de espaço reservado ("Loading...") sem esperar que o atraso assíncrono seja concluído. Quando o atraso assíncrono é concluído e o conteúdo dos dados meteorológicos é gerado, o conteúdo é transmitido para a resposta e integrado na tabela de previsão do tempo.

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

Suprimir a atualização da interface de utilizador (ShouldRender)

ShouldRender é chamado cada vez 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() => 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++;
    }
}

Para obter mais informações sobre as práticas recomendadas de desempenho referentes ao ShouldRender, consulte as práticas recomendadas de desempenho de ASP.NET Core Blazor.

StateHasChanged

Chamar StateHasChanged enfileira uma nova renderização para ocorrer quando a thread principal da aplicação estiver livre.

Os componentes são enfileirados para renderização e não são enfileirados novamente se já houver uma nova renderização pendente. Se um componente chamar StateHasChanged cinco vezes seguidas em um loop, o componente só renderizará uma vez. Esse comportamento está codificado no ComponentBase, que verifica primeiro se o próprio já enfileirou uma rerenderização antes de adicionar outra.

Um componente pode renderizar várias vezes durante o mesmo ciclo, o que geralmente ocorre quando um componente tem filhos que interagem uns com os outros:

  • Um componente pai renderiza vários filhos.
  • Os componentes filho renderizam e acionam uma atualização no pai.
  • Um componente pai é renderizado novamente com um novo estado.

Este design permite que StateHasChanged seja chamado quando necessário, sem o risco de introduzir renderização desnecessária. Você pode sempre assumir o controle desse comportamento em componentes individuais implementando IComponent e manipulando direta e manualmente quando o componente é renderizado.

Considere o seguinte método IncrementCount que incrementa uma contagem, chama StateHasChangede incrementa a contagem novamente:

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

Ao percorrer o código no depurador, você pode pensar que a contagem é atualizada na interface do usuário para a primeira execução currentCount++ imediatamente após StateHasChanged ser chamada. No entanto, a interface do usuário não mostra uma contagem atualizada nesse ponto devido ao processamento síncrono que está ocorrendo para a execução desse método. Não há oportunidade para o renderizador renderizar o componente até que o manipulador de eventos seja concluído. A interface do usuário exibe aumentos para execuções currentCount++em uma única renderização.

Se você aguardar algo entre as currentCount++ linhas, a chamada aguardada dá ao renderizador a chance de renderizar. Isso levou alguns desenvolvedores a invocar Delay com um atraso de um milissegundo nos componentes deles para permitir que uma renderização ocorra, mas não recomendamos abrandar arbitrariamente uma aplicação para enfileirar uma renderização.

A melhor abordagem é aguardar Task.Yield, que força o componente a processar o código de forma assíncrona e a gerar imagem durante o lote atual, com uma segunda geração de imagem num lote separado após a tarefa gerada executar a continuação.

Considere o seguinte método IncrementCount revisado, que atualiza a interface do usuário duas vezes porque a renderização enfileirada por StateHasChanged é executada quando a tarefa é gerada com a chamada para Task.Yield:

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

Tenha cuidado para não chamar StateHasChanged desnecessariamente, o que é um erro comum que impõe custos de renderização desnecessários. O código não precisa chamar StateHasChanged quando:

  • Manipulação rotineira de eventos, seja de forma síncrona ou assíncrona, uma vez que ComponentBase aciona uma renderização para a maioria dos manipuladores de eventos de rotina.
  • Implementação de lógica de ciclo de vida típica, como OnInitialized ou OnParametersSetAsync, seja de forma síncrona ou assíncrona, uma vez que ComponentBase aciona 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 no .NET, um recetor de um Task só pode observar sua conclusão final, não estados assíncronos intermediários. Portanto, ComponentBase só pode acionar a rerenderização quando o Task é retornado pela primeira vez e quando o Task finalmente é concluído. A estrutura não pode saber quando voltar a renderizar um componente a outros pontos intermediários, como quando um IAsyncEnumerable<T>retorna dados numa série de Tasks intermediários. Se quiser renderizar novamente em pontos intermediários, ative 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 acionadas por chamadas para StateHasChanged quando a estrutura não aciona automaticamente rerenderizações em pontos de processamento intermediários onde currentCount é incrementada.

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

Receber uma chamada de algo externo ao sistema de renderização e manipulação de eventos Blazor

ComponentBase só conhece os seus próprios métodos de ciclo de vida e os eventos acionados por Blazor. ComponentBase não sabe sobre outros eventos que podem ocorrer no código. Por exemplo, quaisquer eventos C# gerados por um armazenamento de dados personalizado são desconhecidos para Blazor. Para que esses eventos acionem a rerenderização para exibir 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 gerenciado pelo Blazorou notificação de evento. Portanto, OnTimerCallback deve chamar StateHasChanged porque Blazor não está ciente das alterações em currentCount na chamada de retorno.
  • O componente implementa IDisposable, onde o Timer é descartado quando a estrutura chama o método Dispose. Para obter mais informações, consulte ciclo de vida do componente do ASP.NET Core Razor.

Como o retorno de chamada é invocado fora do contexto de sincronização do Blazor, o componente deve envolver a lógica do OnTimerCallback em ComponentBase.InvokeAsync, para movê-lo para o contexto de sincronização do renderizador. Isso é equivalente a encaminhar para o thread da UI em outros frameworks de UI. StateHasChanged só pode ser chamado a partir do contexto de sincronização do renderizador e, caso contrário, lança uma exceção.

System.InvalidOperationException: 'O thread atual não está associado ao Dispatcher. Use InvokeAsync() para alternar a execução para o Dispatcher ao ativar 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

<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 que é reprocessado por um evento específico

A interface do usuário pode envolver:

  1. Enviar um evento para um componente.
  2. Mudando algum estado.
  3. Rerenderizar um componente completamente diferente que não é descendente do componente que recebe o evento.

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

Para abordagens para gerenciar o estado, consulte os seguintes recursos:

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

A abordagem do gestor estatal é 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. Chamar InvokeAsync só é necessário se a lógica escapar do contexto de sincronização, como chamar ContinueWith num Task ou esperar uma Task com ConfigureAwait(false). Para obter mais informações, consulte a seção Receber uma chamada de algo externo ao sistema de Blazor renderização e manipulação de eventos.

Indicador de progresso de carregamento do WebAssembly para Blazor Web Apps

Um indicador de progresso de carregamento não está presente em um aplicativo criado a partir do modelo de projeto Blazor Web App. 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, consulte ASP.NET Core Blazor startup.