Compartilhar via


Persistência de estado pré-renderizado do ASP.NET Core Blazor

Sem persistir o estado do componente, o estado usado durante a pré-renderização é perdido e deve ser recriado quando o aplicativo está completamente carregado. Se qualquer estado for criado de maneira assíncrona, a interface do usuário poderá piscar à medida que a interface do usuário pré-renderizada for substituída quando o componente for renderizado novamente.

Considere o componente de contador PrerenderedCounter1 a seguir. O componente define um valor inicial de contador aleatório durante a pré-renderização no método do ciclo de vida . Quando o componente é renderizado interativamente, o valor de contagem inicial é substituído quando OnInitialized é executado uma segunda vez.

PrerenderedCounter1.razor:

@page "/prerendered-counter-1"
@inject ILogger<PrerenderedCounter1> Logger

<PageTitle>Prerendered Counter 1</PageTitle>

<h1>Prerendered Counter 1</h1>

<p role="status">Current count: @currentCount</p>

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

@code {
    private int currentCount;

    protected override void OnInitialized()
    {
        currentCount = Random.Shared.Next(100);
        Logger.LogInformation("currentCount set to {Count}", currentCount);
    }

    private void IncrementCount() => currentCount++;
}

Observação

Se o aplicativo adotar o roteamento interativo e a página for acessada por meio de uma navegação aprimorada interna, a pré-geração não ocorrerá. Portanto, você deve executar um recarregamento completo de página para que o componente PrerenderedCounter1 veja a saída a seguir. Para mais informações, consulte a seção Roteamento interativo e pré-renderização.

Execute o aplicativo e inspecione o log no componente. A seguir está a saída de exemplo:

info: BlazorSample.Components.Pages.PrerenderedCounter1[0]
currentCount set to 41
info: BlazorSample.Components.Pages.PrerenderedCounter1[0]
currentCount set to 92

A primeira contagem registrada ocorre durante a pré-renderização. A contagem é definida novamente após a pré-renderização quando o componente é renderizado novamente. Há também uma cintilação na interface do usuário quando a contagem é atualizada de 41 para 92.

Para manter o valor inicial do contador durante a pré-geração, Blazor dá suporte ao estado persistente em uma página pré-gerada usando o PersistentComponentState serviço (e para componentes inseridos em páginas ou exibições de Razor páginas ou aplicativos MVC, o Auxiliar de Marca de Estado do Componente Persistente).

Ao inicializar componentes com o mesmo estado usado durante a pré-renderização, todas as etapas de inicialização dispendiosa só são executadas uma vez. A interface do usuário renderizada também corresponde à interface do usuário pré-renderizada, portanto, nenhuma cintilação ocorre no navegador.

O estado pré-renderizado persistente é transferido para o cliente, onde é usado para restaurar o estado do componente. Durante a renderização do lado do cliente (CSR, InteractiveWebAssembly), os dados são expostos ao navegador e não devem conter informações confidenciais e privadas. Durante a renderização interativa do lado do servidor (SSR interativo, InteractiveServer), ASP.NET Core Data Protection garante que os dados sejam transferidos com segurança. O modo de renderização InteractiveAuto combina interatividade WebAssembly e Server, exigindo atenção à exposição de dados ao navegador, como no caso de CSR.

Para persistir o estado pré-gerado usando o PersistentComponentState serviço, aplique o [PersistentState] atributo às public propriedades. O estado é recuperado quando o componente é renderizado interativamente ou o PersistentComponentState serviço é instanciado.

Use as propriedades public porque o framework utiliza reflexão para tarefas como eliminação de código não utilizado e geração de código-fonte.

Por padrão, as propriedades são serializadas usando o System.Text.Json serializador com configurações padrão e persistidas no HTML pré-gerado. A serialização não é segura para operações de corte e requer a preservação dos tipos usados. Para obter mais informações, consulte Configure the Trimmer for ASP.NET Core Blazor.

O seguinte componente de contador persiste o estado do contador durante a pré-renderização e recupera o estado para inicializar o componente.

  • O [PersistentState] atributo é aplicado à propriedade pública anulável CurrentCount do tipo int?.
  • O estado do contador é atribuído quando null em OnInitialized e restaurado automaticamente quando o componente é renderizado de forma interativa.

PrerenderedCounter2.razor:

@page "/prerendered-counter-2"
@inject ILogger<PrerenderedCounter2> Logger

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @CurrentCount</p>

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

@code {
    [PersistentState]
    public int? CurrentCount { get; set; }

    protected override void OnInitialized()
    {
        if (CurrentCount is null)
        {
            CurrentCount = Random.Shared.Next(100);
            Logger.LogInformation("CurrentCount set to {Count}", CurrentCount);
        }
        else
        {
            Logger.LogInformation("CurrentCount restored to {Count}", CurrentCount);
        }
    }

    private void IncrementCount() => CurrentCount++;
}

Quando o componente é executado, a CurrentCount é definida apenas uma vez durante a pré-renderização. O valor é restaurado quando o componente é renderizado novamente. A seguir está a saída de exemplo:

Observação

Se o aplicativo adotar o roteamento interativo e a página for acessada por meio de uma navegação aprimorada interna, a pré-geração não ocorrerá. Portanto, você deve executar um recarregamento de página inteira para que o componente exiba a saída a seguir. Para mais informações, consulte a seção Roteamento interativo e pré-renderização.

info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
CurrentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
CurrentCount restored to 96

No exemplo a seguir, que serializa o estado para vários componentes do mesmo tipo:

  • As propriedades públicas anotadas com o [PersistentState] atributo são serializadas durante o processo de pré-renderização.
  • O @key atributo de diretiva é usado para garantir que o estado esteja corretamente associado à instância do componente.
  • A Element propriedade é inicializada no método do OnInitialized ciclo de vida para evitar exceções de referências nulas, da mesma forma como as referências nulas são evitadas para parâmetros de consulta e dados de formulário.

PersistentChild.razor:

<div>
    <p>Current count: @Element.CurrentCount</p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</div>

@code {
    [PersistentState]
    public State Element { get; set; }

    protected override void OnInitialized()
    {
        Element ??= new State();
    }

    private void IncrementCount()
    {
        Element.CurrentCount++;
    }

    private class State
    {
        public int CurrentCount { get; set; }
    }
}

Parent.razor:

@page "/parent"

@foreach (var element in elements)
{
    <PersistentChild @key="element.Name" />
}

Serializar o estado para serviços

No exemplo que se segue, que serializa o estado de um serviço de injeção de dependência:

  • As propriedades anotadas com o [PersistentState] atributo são serializadas durante a pré-geração e desserializadas quando o aplicativo se torna interativo.
  • O RegisterPersistentService método de extensão é usado para registrar o serviço para persistência. O modo de renderização é necessário porque o modo de renderização não pode ser inferido do tipo de serviço. Use qualquer um dos seguintes valores:
    • RenderMode.Server: o serviço está disponível para o modo de renderização do Servidor Interativo.
    • RenderMode.Webassembly: O serviço está disponível para o modo de renderização Interativo Webassembly.
    • RenderMode.InteractiveAuto: O serviço estará disponível para os modos de renderização do Servidor Interativo e WebAssembly Interativo se um componente for renderizado em qualquer um desses modos.
  • O serviço é resolvido durante a inicialização de um modo de renderização interativo e as propriedades anotadas com o [PersistentState] atributo são desserializadas.

Observação

Apenas a persistência de serviços com escopo definido é suportada.

As propriedades serializadas são identificadas pela instância real do serviço.

  • Essa abordagem permite marcar uma abstração como um serviço persistente.
  • Permite que as implementações reais sejam de tipos internos ou diferentes.
  • Dá suporte a código compartilhado em assemblies diferentes.
  • Os resultados em cada instância revelam as mesmas propriedades.

O serviço de contador a seguir, CounterTracker, marca sua propriedade de contagem atual, CurrentCount, com o atributo [PersistentState]. A propriedade pública é serializada durante a pré-geração e desserializada quando o aplicativo se torna interativo onde quer que o serviço seja injetado.

CounterTracker.cs:

public class CounterTracker
{
    [PersistentState]
    public int CurrentCount { get; set; }

    public void IncrementCount()
    {
        CurrentCount++;
    }
}

No arquivo Program, registre o serviço com escopo e registre o serviço para persistência com RegisterPersistentService. No exemplo a seguir, o serviço CounterTracker estará disponível tanto para os modos de renderização do Servidor Interativo quanto para o WebAssembly Interativo, se um componente for renderizado em qualquer um desses modos, porque está registrado com RenderMode.InteractiveAuto.

Se o arquivo Program ainda não usar o namespace Microsoft.AspNetCore.Components.Web, adicione a seguinte instrução using à parte superior do arquivo:

using Microsoft.AspNetCore.Components.Web;

Onde os serviços são registrados no Program arquivo:

builder.Services.AddScoped<CounterTracker>();

builder.Services.AddRazorComponents()
    .RegisterPersistentService<CounterTracker>(RenderMode.InteractiveAuto);

Injete o serviço CounterTracker em um componente e use-o para incrementar um contador. Para fins de demonstração no exemplo a seguir, o valor da propriedade do serviço CurrentCount é definido como 10 somente durante a pré-renderização.

Pages/Counter.razor:

@page "/counter"
@inject CounterTracker CounterTracker

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p>Rendering: @RendererInfo.Name</p>

<p role="status">Current count: @CounterTracker.CurrentCount</p>

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

@code {
    protected override void OnInitialized()
    {
        if (!RendererInfo.IsInteractive)
        {
            CounterTracker.CurrentCount = 10;
        }
    }

    private void IncrementCount()
    {
        CounterTracker.IncrementCount();
    }
}

Para usar o componente anterior para demonstrar a persistência da contagem de 10 em CounterTracker.CurrentCount, navegue até o componente e atualize o navegador, o que dispara a pré-renderização. Quando ocorrer a pré-renderização, você verá brevemente RendererInfo.Name indicar "Static" antes de exibir "Server" após a renderização final. O contador começa em 10.

Use o PersistentComponentState serviço diretamente em vez do modelo declarativo

Como alternativa ao uso do modelo declarativo para manter o estado com o [PersistentState] atributo, você pode usar diretamente o PersistentComponentState serviço, que oferece maior flexibilidade para cenários complexos de persistência de estado. Chame PersistentComponentState.RegisterOnPersisting para registrar um callback para persistir o estado do componente durante a prerenderização. O estado é recuperado quando o componente é renderizado interativamente. Faça a chamada ao final do código de inicialização para evitar uma possível condição de corrida durante o desligamento do aplicativo.

O exemplo de componente de contador a seguir persiste o estado do contador durante a pré-renderização e recupera o estado para inicializar o componente.

PrerenderedCounter3.razor:

@page "/prerendered-counter-3"
@implements IDisposable
@inject ILogger<PrerenderedCounter3> Logger
@inject PersistentComponentState ApplicationState

<PageTitle>Prerendered Counter 3</PageTitle>

<h1>Prerendered Counter 3</h1>

<p role="status">Current count: @currentCount</p>

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

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override void OnInitialized()
    {
        if (!ApplicationState.TryTakeFromJson<int>(
            nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
            Logger.LogInformation("currentCount set to {Count}", currentCount);
        }
        else
        {
            currentCount = restoredCount!;
            Logger.LogInformation("currentCount restored to {Count}", currentCount);
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount);
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }

    private void IncrementCount() => currentCount++;

    void IDisposable.Dispose() => persistingSubscription.Dispose();
}

Quando o componente é executado, a currentCount é definida apenas uma vez durante a pré-renderização. O valor é restaurado quando o componente é renderizado novamente. A seguir está a saída de exemplo:

Observação

Se o aplicativo adotar o roteamento interativo e a página for acessada por meio de uma navegação aprimorada interna, a pré-geração não ocorrerá. Portanto, você deve executar um recarregamento de página inteira para que o componente exiba a saída a seguir. Para mais informações, consulte a seção Roteamento interativo e pré-renderização.

info: BlazorSample.Components.Pages.PrerenderedCounter3[0]
currentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter3[0]
currentCount restored to 96

Para preservar o estado pré-renderizado, decida qual estado será persistido por meio do serviço PersistentComponentState. PersistentComponentState.RegisterOnPersisting registra um retorno de chamada para persistir o estado do componente durante a pré-renderização. O estado é recuperado quando o componente é renderizado interativamente. Faça a chamada ao final do código de inicialização para evitar uma possível condição de corrida durante o desligamento do aplicativo.

O exemplo de componente de contador a seguir persiste o estado do contador durante a pré-renderização e recupera o estado para inicializar o componente.

PrerenderedCounter2.razor:

@page "/prerendered-counter-2"
@implements IDisposable
@inject ILogger<PrerenderedCounter2> Logger
@inject PersistentComponentState ApplicationState

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @currentCount</p>

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

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override void OnInitialized()
    {
        if (!ApplicationState.TryTakeFromJson<int>(
            nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
            Logger.LogInformation("currentCount set to {Count}", currentCount);
        }
        else
        {
            currentCount = restoredCount!;
            Logger.LogInformation("currentCount restored to {Count}", currentCount);
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount);
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose() => persistingSubscription.Dispose();

    private void IncrementCount() => currentCount++;
}

Quando o componente é executado, a currentCount é definida apenas uma vez durante a pré-renderização. O valor é restaurado quando o componente é renderizado novamente. A seguir está a saída de exemplo:

Observação

Se o aplicativo adotar o roteamento interativo e a página for acessada por meio de uma navegação aprimorada interna, a pré-geração não ocorrerá. Portanto, você deve executar um recarregamento de página inteira para que o componente exiba a saída a seguir. Para mais informações, consulte a seção Roteamento interativo e pré-renderização.

info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
currentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
currentCount restored to 96

Extensibilidade de serialização para o estado do componente persistente

Implementar um serializador personalizado com PersistentComponentStateSerializer<T>. Sem um serializador personalizado registrado, a serialização volta para a serialização JSON existente.

O serializador personalizado é registrado no arquivo do Program aplicativo. No exemplo a seguir, o registro CustomUserSerializer é para o tipo TUser.

builder.Services.AddSingleton<PersistentComponentStateSerializer<TUser>, 
    CustomUserSerializer>();

O tipo é mantido e restaurado automaticamente com o serializador personalizado:

[PersistentState] 
public User? CurrentUser { get; set; } = new();

Componentes inseridos em páginas e exibições (Páginas/MVC Razor)

Para componentes inseridos em uma página ou exibição de um Razor aplicativo Pages ou MVC, você deve adicionar o Auxiliar de Marca de Estado do Componente Persist com a <persist-component-state /> marca HTML dentro da marca de fechamento </body> do layout do aplicativo. Isso só é necessário em aplicativos MVC e Razor Pages. Para obter mais informações, consulte Persist Component State Tag Helper in ASP.NET Core.

Pages/Shared/_Layout.cshtml:

<body>
    ...

    <persist-component-state />
</body>

Roteamento interativo e pré-renderização

Quando o componente Routes não define um modo de renderização, o aplicativo está usando interatividade e navegação por página/componente. Usando a navegação por página/componente, a navegação interna é tratada pelo roteamento aprimorado depois que o aplicativo se torna interativo. "Navegação interna", nesse contexto, significa que o destino da URL do evento de navegação é um endpoint dentro do aplicativo.

Blazor dá suporte ao tratamento do estado persistente de componentes durante a navegação aprimorada. O estado persistente durante a navegação aprimorada pode ser lido por componentes interativos na página.

Por padrão, o estado do componente persistente só é carregado por componentes interativos quando eles são carregados inicialmente na página. Isso impede que estado importante, como dados de um formulário da web editado, seja substituído se eventos de navegação aprimorados adicionais para a mesma página ocorrerem após o carregamento do componente.

Se os dados forem somente leitura e não forem alterados com frequência, opte por permitir atualizações durante a navegação aprimorada definindo o atributo [PersistentState] em AllowUpdates = true. Isso é útil para cenários como exibir dados armazenados em cache que são caros de buscar, mas não mudam com frequência. O exemplo a seguir demonstra o uso de AllowUpdates para dados de previsão do tempo.

[PersistentState(AllowUpdates = true)]
public WeatherForecast[]? Forecasts { get; set; }

protected override async Task OnInitializedAsync()
{
    Forecasts ??= await ForecastService.GetForecastAsync();
}

Para ignorar o estado de restauração durante a pré-geração, defina RestoreBehavior como SkipInitialValue:

[PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)]
public string NoPrerenderedData { get; set; }

Para ignorar o estado de restauração durante a reconexão, defina RestoreBehavior como SkipLastSnapshot. Isso pode ser útil para garantir novos dados após a reconexão:

[PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
public int CounterNotRestoredOnReconnect { get; set; }

Chame PersistentComponentState.RegisterOnRestoring para registrar um retorno de chamada para controlar imperativamente como o estado é restaurado, de maneira semelhante a PersistentComponentState.RegisterOnPersisting, que fornece controle total de como o estado é persistido.

O serviço PersistentComponentState funciona apenas na carga inicial da página e não em eventos de navegação de página avançados internos.

Se o aplicativo executar uma navegação completa (não aprimorada) para uma página que utiliza estado do componente persistente, o estado persistido fica disponível para o aplicativo usar quando ele se tornar interativo.

Se um circuito interativo já tiver sido estabelecido e uma navegação aprimorada for executada em uma página utilizando o estado do componente persistente, o estado não será disponibilizado no circuito existente para uso do componente. Não é feita nenhuma pré-renderização para a solicitação de página interna, e o serviço PersistentComponentState não está ciente de que uma navegação aprimorada tenha ocorrido. Não há mecanismo para fornecer atualizações de estado para componentes que já estão em execução em um circuito existente. O motivo disso é que Blazor só dá suporte à passagem de estado do servidor para o cliente no momento em que o runtime é inicializado, não após o runtime ter sido iniciado.

A desabilitação da navegação aprimorada, que reduz o desempenho, mas também evita o problema de carregar o estado com PersistentComponentState para solicitações de página internas, é abordada em ASP.NET Core Blazor navegação. Como alternativa, atualize o aplicativo para .NET 10 ou posterior, em que Blazor dá suporte ao tratamento do estado do componente persistente durante a navegação aprimorada.