次の方法で共有


ASP.NET Core Blazor プリレンダリングされた状態の永続化

コンポーネントの状態を保持しないと、プリレンダリング中に使用される状態は失われ、アプリが完全に読み込まれるときに再作成する必要があります。 状態が非同期的に作成される場合、コンポーネントの再レンダリングによってプリレンダリングされたUIが置き換えられる際に、UIが一時的にちらつくことがあります。

次の PrerenderedCounter1 カウンター コンポーネントについて検討します。 コンポーネントは、OnInitializedライフサイクル メソッドでのプリレンダリング中に初期ランダム カウンター値を設定します。 その後、コンポーネントが対話形式でレンダリングされると、 OnInitialized が 2 回目に実行されたときに初期カウント値が置き換えられます。

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

アプリが対話型ルーティング 採用し、ページに内部 拡張ナビゲーション経由で到達した場合、プリレンダリングは行われません。 したがって、次の出力を表示するには、PrerenderedCounter1 コンポーネントのページ全体の再読み込みを行う必要があります。 詳細については、「対話型ルーティングとプリレンダリングの」セクションを参照してください。

アプリを実行し、コンポーネントのログを調査します。 以下に、出力の例を示します。

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

最初のログ カウントはプリレンダリング中に生じます。 コンポーネントが再レンダリングされると、プリレンダリング後にカウントが再び設定されます。 カウントが 41 から 92 に更新されるときにも、UI にちらつきが生じます。

プリレンダリング中にカウンターの初期値を保持するために、Blazor では、PersistentComponentState サービス (さらに、Razor Pages または MVC アプリのページまたはビューに埋め込まれたコンポーネントの場合は、コンポーネントの状態保持タグ ヘルパー) を使用してプリレンダリングされたページ内の状態の永続化をサポートしています。

プリレンダリング中に使用されたのと同じ状態でコンポーネントを初期化することにより、負荷の高い初期化ステップが 1 回だけ実行されます。 レンダリングされた UI もプリレンダリングされた UI に一致するので、ブラウザーでちらつきは発生しません。

永続化されたプリレンダリングされた状態はクライアントに転送され、そこでコンポーネントの状態を復元するために使用されます。 クライアント側のレンダリング (CSR、 InteractiveWebAssembly) 中に、データはブラウザーに公開され、機密情報を含めてはなりません。 対話型のサーバー側レンダリング (対話型 SSR、 InteractiveServer) 中に、コア データ保護 ASP.NET データが安全に転送されるようにします。 InteractiveAutoレンダリング モードは WebAssembly とサーバーの対話機能を組み合わせたものなので、CSR の場合と同様に、ブラウザーへのデータ公開を考慮する必要があります。

プリレンダリングされた状態を保持するには、 [PersistentState] 属性 を使用してプロパティの状態を保持します。 この属性を持つプロパティは、プリレンダリング中に PersistentComponentState サービスを使用して自動的に永続化されます。 状態は、コンポーネントが対話形式でレンダリングされるか、サービスがインスタンス化されるときに取得されます。

既定では、プロパティは既定の設定で System.Text.Json シリアライザーを使用してシリアル化され、プリレンダリングされた HTML に保持されます。 シリアル化はトリマー使用に対する安全性がなく、使用される型の保存が必要です。 詳しくは、「ASP.NET Core Blazor 用のトリマーを構成する」をご覧ください。

次のカウンター コンポーネントは、プリレンダリング中にカウンター状態を保持し、コンポーネントを初期化する状態を取得します。

  • [PersistentState]属性は、null 許容int型 (CurrentCount) に適用されます。
  • カウンターの状態は、nullOnInitializedする際に割り当てられ、コンポーネントがインタラクティブにレンダリングされる際に自動的に復元されます。

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

コンポーネントが実行されると、プリレンダリング中に CurrentCount が 1 回だけ設定されます。 この値は、コンポーネントが再レンダリングされると復元されます。 以下に、出力の例を示します。

アプリが対話型ルーティング 採用し、ページに内部 拡張ナビゲーション経由で到達した場合、プリレンダリングは行われません。 したがって、次の出力を表示するには、コンポーネントのページ全体の再読み込みを実行する必要があります。 詳細については、「対話型ルーティングとプリレンダリングの」セクションを参照してください。

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

次の例では、同じ型の複数のコンポーネントの状態をシリアル化します。

  • [PersistentState]属性で注釈が付けられたプロパティは、プリレンダリング中にシリアル化されます。
  • @key ディレクティブ属性は、状態がコンポーネント インスタンスに正しく関連付けられていることを確認するために使用されます。
  • Element プロパティは、クエリ パラメーターやフォーム データに対する null 参照の回避方法と同様に、null 参照の例外を回避するために、OnInitialized ライフサイクル メソッドで初期化されます。

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

サービスの状態をシリアル化する

依存関係挿入サービスの状態をシリアル化する次の例では、

  • [PersistentState]属性で注釈が付けられたプロパティは、プリレンダリング中にシリアル化され、アプリが対話型になったときに逆シリアル化されます。
  • RegisterPersistentService拡張メソッドは、永続化のためにサービスを登録するために使用されます。 レンダリング モードはサービスの種類から推論できないため、レンダリング モードが必要です。 次のいずれかの値を使用します。
    • RenderMode.Server: サービスは、対話型サーバー レンダリング モードで使用できます。
    • RenderMode.Webassembly: サービスは対話型 Webassembly レンダリング モードで使用できます。
    • RenderMode.InteractiveAuto: コンポーネントがいずれかのモードでレンダリングされる場合、サービスは Interactive Server レンダリング モードと Interactive Webassembly レンダリング モードの両方で使用できます。
  • 対話型レンダリング モードの初期化中にサービスが解決され、 [PersistentState] 属性 で注釈が付けられたプロパティが逆シリアル化されます。

スコープ付きサービスの永続化のみがサポートされています。

シリアル化されたプロパティは、実際のサービス インスタンスから識別されます。

  • この方法では、抽象化を永続的なサービスとしてマークできます。
  • 実際の実装を内部型または異なる型にできます。
  • さまざまなアセンブリの共有コードをサポートします。
  • 結果として、各インスタンスで同じプロパティが公開されます。

次のカウンター サービスCounterTrackerは、現在の count プロパティをCurrentCount[PersistentState]属性でマークします。 プロパティは、プリレンダリング中にシリアル化され、サービスが挿入された場所でアプリが対話型になったときに逆シリアル化されます。

CounterTracker.cs:

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

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

Program ファイルで、スコープ付きサービスを登録し、RegisterPersistentServiceで永続化するためにサービスを登録します。 次の例では、コンポーネントが CounterTracker に登録されているため、これらのモードのいずれかでレンダリングされる場合、RenderMode.InteractiveAuto サービスは Interactive Server レンダリング モードと Interactive WebAssembly レンダリング モードの両方で使用できます。

Program ファイルで Microsoft.AspNetCore.Components.Web 名前空間がまだ使用されていない場合は、ファイルの先頭に次の using ステートメントを追加します。

using Microsoft.AspNetCore.Components.Web;

サービスが Program ファイルに登録されている場所:

builder.Services.AddScoped<CounterTracker>();

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

CounterTracker サービスをコンポーネントに挿入し、それを使用してカウンターをインクリメントします。 次の例のデモンストレーションでは、サービスの CurrentCount プロパティの値は、プリレンダリング中にのみ 10 に設定されます。

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

前のコンポーネントを使用して、 CounterTracker.CurrentCountで 10 のカウントを保持する方法を示すには、コンポーネントに移動し、ブラウザーを更新します。これによってプリレンダリングがトリガーされます。 プリレンダリングが行われると、最終的なレンダリング後に "RendererInfo.Name" を表示する前に、"Static" を示すServerが簡単に表示されます。 カウンターは 10 から始まります。

宣言型モデルではなく、 PersistentComponentState サービスを直接使用する

[PersistentState]属性を使用して状態を永続化するために宣言型モデルを使用する代わりに、PersistentComponentState サービスを直接使用することもできます。これによって、複雑な状態永続化シナリオの柔軟性が向上します。 プリレンダリング中にコンポーネントの状態を保持するコールバックを登録する PersistentComponentState.RegisterOnPersisting を呼び出します。 状態は、コンポーネントが対話形式でレンダリングされるときに取得されます。 アプリのシャットダウン中に競合状態になる可能性を回避するために、初期化コードの最後に呼び出しを行います。

次のカウンター コンポーネントでは、プリレンダリング中にカウンターの状態を保持し、コンポーネントを初期化するためにその状態を取得します。

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

コンポーネントが実行されると、プリレンダリング中に currentCount が 1 回だけ設定されます。 この値は、コンポーネントが再レンダリングされると復元されます。 以下に、出力の例を示します。

アプリが対話型ルーティング 採用し、ページに内部 拡張ナビゲーション経由で到達した場合、プリレンダリングは行われません。 したがって、次の出力を表示するには、コンポーネントのページ全体の再読み込みを実行する必要があります。 詳細については、「対話型ルーティングとプリレンダリングの」セクションを参照してください。

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

プリレンダリングされた状態を保持するには、PersistentComponentState サービスを使用して、どの状態を永続化するかを決定します。 PersistentComponentState.RegisterOnPersisting は、プリレンダリング中にコンポーネントの状態を保持するコールバックを登録します。 状態は、コンポーネントが対話形式でレンダリングされるときに取得されます。 アプリのシャットダウン中に競合状態になる可能性を回避するために、初期化コードの最後に呼び出しを行います。

次のカウンター コンポーネントでは、プリレンダリング中にカウンターの状態を保持し、コンポーネントを初期化するためにその状態を取得します。

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

コンポーネントが実行されると、プリレンダリング中に currentCount が 1 回だけ設定されます。 この値は、コンポーネントが再レンダリングされると復元されます。 以下に、出力の例を示します。

アプリが対話型ルーティング 採用し、ページに内部 拡張ナビゲーション経由で到達した場合、プリレンダリングは行われません。 したがって、次の出力を表示するには、コンポーネントのページ全体の再読み込みを実行する必要があります。 詳細については、「対話型ルーティングとプリレンダリングの」セクションを参照してください。

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

永続的なコンポーネントの状態に対するシリアル化の拡張性

PersistentComponentStateSerializer<T>を使用してカスタム シリアライザーを実装します。 登録済みのカスタム シリアライザーがない場合、シリアル化は既存の JSON シリアル化にフォールバックします。

カスタム シリアライザーは、アプリの Program ファイルに登録されます。 次の例では、 CustomUserSerializerTUser 型に登録されています。

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

型は、カスタム シリアライザーを使用して自動的に永続化および復元されます。

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

ページとビューに埋め込まれたコンポーネント (Razor Pages/MVC)

Razor Pages または MVC アプリのページまたはビューに埋め込まれたコンポーネントに対しては、アプリのレイアウトの終了 タグ内に、<persist-component-state /> HTML タグを含む</body>を追加する必要があります。 これは、Razor Pages アプリと MVC アプリでのみ必要です。 詳細については、「ASP.NET Core でのコンポーネントの状態保持タグ ヘルパー」を参照してください。

Pages/Shared/_Layout.cshtml:

<body>
    ...

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

対話型ルーティングとプリレンダリング

Routes コンポーネントでレンダリング モードが定義されていない場合、アプリはページ単位/コンポーネントごとの対話機能とナビゲーションを使用します。 ページ/コンポーネントごとのナビゲーションを使用すると、アプリが対話型になった後、 拡張ルーティング によって内部ナビゲーションが処理されます。 このコンテキストの "内部ナビゲーション" は、ナビゲーション イベントの URL 宛先がアプリ内の Blazor エンドポイントであることを意味します。

Blazor は、 拡張ナビゲーション中の永続的なコンポーネント状態の処理をサポートします。 拡張ナビゲーション中に保持された状態は、ページ上の対話型コンポーネントで読み取ることができます。

既定では、永続的なコンポーネントの状態は、対話型コンポーネントが最初にページに読み込まれるときにのみ読み込まれます。 これにより、コンポーネントの読み込み後に同じページへの追加の拡張ナビゲーション イベントが発生した場合に、編集された Web フォーム内のデータなどの重要な状態が上書きされなくなります。

データが読み取り専用で頻繁に変更されない場合は、AllowUpdates = true属性に[PersistentState]を設定して、拡張ナビゲーション中に更新を許可するようにオプトインします。 これは、フェッチにコストがかかるが頻繁に変更されないキャッシュされたデータを表示するシナリオに役立ちます。 次の例では、天気予報データに AllowUpdates を使用する方法を示します。

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

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

プリレンダリング中に状態の復元をスキップするには、 RestoreBehaviorSkipInitialValueに設定します。

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

再接続中の復元状態をスキップするには、 RestoreBehaviorSkipLastSnapshot に設定します。 これは、再接続後にデータを最新の状態に保つのに役立ちます。

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

PersistentComponentState.RegisterOnRestoringを呼び出して、状態の復元方法を命令的に制御するためのコールバックを登録します。これは、PersistentComponentState.RegisterOnPersistingが状態の永続化方法を完全に制御する方法と同様です。

PersistentComponentState サービスは、初期ページ読み込みでのみ機能し、内部拡張ページ ナビゲーション イベントでは機能しません。

永続的なコンポーネントの状態を利用するページへの完全な (拡張されていない) ナビゲーションをアプリが実行すると、永続化された状態が対話型になったときにアプリで使用できるようになります。

対話型回線が既に確立されていて、永続的なコンポーネントの状態を利用するページへの拡張ナビゲーションが実行されている場合、コンポーネントがを使用するために、既存の回線では状態 使用できなくなります。 内部ページ要求のプリレンダリングはなく、PersistentComponentState サービスは拡張ナビゲーションが発生したことを認識していません。 既存の回線で既に実行されているコンポーネントに状態更新を配信するメカニズムはありません。 その理由は、Blazor はランタイムの初期化時にサーバーからクライアントへの状態の受け渡しのみをサポートし、ランタイムの開始後にはサポートされないためです。

拡張ナビゲーションを無効にするとパフォーマンスが低下しますが、内部ページ要求の PersistentComponentState での状態読み込みの問題を回避することもできます。このことについては、ASP.NET Core Blazor ナビゲーションで説明されています。 または、アプリを .NET 10 以降に更新します。 Blazor では、拡張ナビゲーション中の永続的なコンポーネント状態の処理がサポートされます。