ASP.NET Core Blazor 状態管理

注意

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、ユーザーがアプリを使用している間とブラウザー セッションが切り替わる間にユーザーのデータ (状態) を維持するための一般的な方法について説明します。

Note

この記事のコード例では、null 許容参照型 (NRT) と .NET コンパイラの null 状態スタティック分析を採用しています。これは、.NET 6 以降の ASP.NET Core でサポートされています。 ASP.NET Core 5.0 以前をターゲットとする場合は、記事の例の型から null 型の指定 (?) を削除してください。

ユーザーの状態を維持する

サーバー側 Blazor はステートフル アプリ フレームワークです。 アプリでは、ほとんど常に、サーバーとの接続が維持されています。 ユーザーの状態は、"回線" の中のサーバーのメモリに保持されます。

たとえば、回線には次のようなユーザー状態が保持されます。

  • レンダリングされた UI での、コンポーネント インスタンスの階層と最新のレンダリング出力。
  • コンポーネント インスタンスに含まれるフィールドとプロパティの値。
  • 回線に範囲が設定されている依存関係の挿入 (DI) サービス インスタンスに保存されているデータ。

ユーザー状態は、JavaScript 相互運用の呼び出しによって設定される、ブラウザーのメモリ内の JavaScript 変数にも存在する場合があります。

ユーザーが一時的にネットワークに接続できなくなった場合は、Blazor により、ユーザーを元の状態で元の回線に再接続することが試みられます。 ただし、サーバーのメモリにある元の回線にいつでもユーザーを再接続できるわけではありません。

  • サーバーでは、切断された回線を永久に保持することはできません。 サーバーでは、タイムアウト後、あるいはサーバーがメモリ不足になったとき、切断された回線を解放しなければなりません。
  • 複数のサーバーが存在する負荷分散された展開環境では、個々のサーバーは、それがなくても全体的な要求量を処理できるようになると、作動しなくなったり、自動的に削除されたりすることがあります。 ユーザーが再接続しようとしたときに、ユーザーの要求を処理していた元のサーバーを使用できなくなることがあります。
  • ユーザーはブラウザーを閉じてから再度開くか、ページを再読み込みできます。これにより、ブラウザーのメモリに保存されている状態が削除されます。 たとえば、JavaScript 相互運用の呼び出しによって設定された JavaScript 変数の値は失われます。

ユーザーが元の回線に再接続できないとき、そのユーザーには、状態が空の新しい回線が与えられます。 これはデスクトップ アプリを終了してから再度開くことと同じです。

回線間で状態を維持する

一般に、ユーザーが既存データを単に読み取るのではなく、活発にデータを作成している場合は、回線間で状態を保持します。

回線間で状態を維持するには、アプリで、サーバーのメモリとは異なる他の保存場所に、データを保持する必要があります。 状態は自動的に保存されません。 ステートフルなデータ保存を実装するアプリを開発するとき、措置を講じる必要があります。

データの保持は、通常、作成するためにユーザーが大きな労力を費やした、価値の高い状態の場合にのみ必要です。 次の例では、状態を維持することで、時間が節約され、商業活動の支援になります。

  • 複数ステップの Web フォーム: 複数ステップの Web フォームのいくつかのステップを完了した後で、状態が失われた場合、ユーザーがデータを再入力するのでは時間がかかります。 このシナリオでは、フォームからユーザーが離れて後で戻ってきた場合、状態が失われます。
  • ショッピング カート: 収益につながる可能性がある、アプリの中のビジネス上重要なコンポーネントを維持できます。 ユーザーが自分の状態を失い、そのため、自分のショッピング カートが消えると、後でサイトに戻ってきたとき、製品やサービスの購入数が減ることがあります。

アプリでは、"アプリの状態" のみが維持されます。 コンポーネント インスタンスやそのレンダー ツリーなど、UI は維持されません。 コンポーネントとレンダー ツリーは一般的にシリアル化されません。 アプリで、ツリー ビュー コントロールの展開されたノードなど、UI の状態を維持するには、カスタム コードを使用して、UI 状態の動作をシリアル化できるアプリ状態としてモデル化する必要があります。

状態を維持する場所

一般に、状態を維持するために、次のような場所があります。

サーバー側ストレージ

複数のユーザーとデバイスにまたがって永続的にデータを保持する場合は、サーバー側ストレージをアプリで使用できます。 次のオプションがあります。

  • BLOB ストレージ
  • キーと値のストレージ
  • リレーショナル データベース
  • テーブル ストレージ

データが保存された後は、ユーザーの状態は保持されていて、新しい回線で使用できます。

Azure のデータ ストレージ オプションの詳細については、以下を参照してください。

URL

ナビゲーションの状態を表わす一時的なデータについては、URL の一部としてデータをモデル化します。 たとえば、次のようなユーザー状態が URL でモデル化されます。

  • 表示されるエンティティの ID。
  • ページ付きグリッドでの現在のページ番号。

次の場合、ブラウザーのアドレス バーのコンテンツが保持されます。

  • ユーザーがページを手動で再読み込みした。
  • Web サーバーが利用できなくなり、別のサーバーに接続する目的で、ページの再読み込みがユーザーに強制される。

@page ディレクティブで URL パターンを定義する方法については、「ASP.NET Core の Blazor ルーティングとナビゲーション」をご覧ください。

ブラウザー ストレージ

ユーザーが頻繁に作成する一時的なデータの場合、一般的に使用されるストレージの場所は、ブラウザーの localStorage コレクションと sessionStorage コレクションです。

  • localStorage の対象範囲はブラウザーのウィンドウです。 ユーザーがページを再読み込みするか、ブラウザーを閉じてから再度開いた場合、状態は保持されます。 ユーザーが複数のブラウザー タブを開くと、状態はすべてのタブで同じになります。 データは直接消去されるまで localStorage に残ります。
  • sessionStorage の対象範囲はブラウザーのタブです。ユーザーがタブを再読み込みすると、状態は維持されます。 ユーザーがタブかブラウザーを閉じると、状態は失われます。 ユーザーが複数のブラウザー タブを開くと、それぞれのタブには、他に依存しないそのタブだけのバージョンのデータが保持されます。

一般に、sessionStorage を使用しておけば安全です。 sessionStorage の場合、ユーザーが複数のタブを開き、以下に遭遇するリスクが回避されます。

  • タブ間の状態保存に含まれるバグ。
  • タブで他のタブの状態が上書きされるときの紛らわしい動作。

ブラウザーを閉じてから再度開いてもアプリで状態が保持される必要がある場合、localStorage がより適切な選択肢です。

ブラウザー ストレージ使用時の注意事項:

  • サーバー側データベースの使用に似ていますが、データの読み込みと保存は非同期です。
  • サーバー側データベースとは異なり、プリレンダリング中はストレージを利用できません。プリレンダリング中は、要求されたページがブラウザーに存在しないためです。
  • 数キロバイトのデータのストレージは、サーバー側 Blazor アプリで保持するのが妥当です。 数キロバイトを超えると、パフォーマンスに影響が出ることを考慮する必要があります。ネットワーク中でデータが読み込まれ、保存されるためです。
  • ユーザーはデータを見たり、改ざんしたりするかもしれません。 ASP.NET Core のデータ保護で、このリスクを軽減できます。 たとえば、ASP.NET Core で保護されたブラウザー ストレージでは、ASP.NET Core のデータ保護が使用されます。

サードパーティ製 NuGet パッケージからは、localStoragesessionStorage を使用するための API が与えられます。 ASP.NET Core のデータ保護を透過的に使用するパッケージを選択してみることもお勧めします。 データ保護を使用すると、保存データが暗号化され、保存データが改ざんされる潜在的リスクが減ります。 JSON でシリアル化されたデータがプレーンテキストで保存されている場合、ユーザーはブラウザー開発者ツールを使用してデータを表示することに加え、保存データを変更することができます。 データが性質上、取るに足りないものであれば、データ セキュリティを問題にする必要はないかもしれません。 たとえば、UI 要素に保存されている色を読み取られたり、変更されたりしたところで、それはユーザーや組織にとって大きなセキュリティ リスクではありません。 "取り扱いに慎重を要するデータ" を見たり、改ざんしたりすることをユーザーに禁止します。

ASP.NET Core で保護されたブラウザー ストレージ

ASP.NET Core で保護されたブラウザー ストレージでは、localStoragesessionStorage に対して ASP.NET Core のデータ保護が使用されます。

Note

保護されたブラウザー ストレージは、ASP.NET Core のデータ保護に依存しており、サーバー側 Blazor アプリでのみサポートされます。

警告

Microsoft.AspNetCore.ProtectedBrowserStorage はサポートのない試験用パッケージであり、運用環境での使用を意図したものではありません。

パッケージは、ASP.NET Core 3.1 の アプリでのみ使用できます。

構成

  1. Microsoft.AspNetCore.ProtectedBrowserStorage へのパッケージ参照を追加します。

    Note

    .NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。

  2. _Host.cshtml ファイルで、終了 </body> タグの内部に、次のスクリプトを追加します。

    <script src="_content/Microsoft.AspNetCore.ProtectedBrowserStorage/protectedBrowserStorage.js"></script>
    
  3. Startup.ConfigureServicesAddProtectedBrowserStorage を呼び出し、localStorage サービスと sessionStorage サービスをサービス コレクションに追加します。

    services.AddProtectedBrowserStorage();
    

コンポーネント内でデータを保存し、読み込む

ブラウザー ストレージのデータの読み込みまたは保存が必要なすべてのコンポーネントで、@inject ディレクティブを使用して、次のいずれかのインスタンスを挿入します。

  • ProtectedLocalStorage
  • ProtectedSessionStorage

どれを選択するかは、使用するブラウザー ストレージの場所によって異なります。 次の例では、sessionStorage が使用されています。

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@using ディレクティブは、コンポーネントの代わりに、アプリの _Imports.razor ファイルに配置できます。 _Imports.razor ファイルを使用すると、アプリの中の大きなセグメントで、あるいはアプリ全体で名前空間を利用できます。

Blazor プロジェクト テンプレートに基づいてアプリの Counter コンポーネントに currentCount の値を保持するには、ProtectedSessionStore.SetAsync を使用するように IncrementCount メソッドを変更します。

private async Task IncrementCount()
{
    currentCount++;
    await ProtectedSessionStore.SetAsync("count", currentCount);
}

もっと大規模で現実に即したアプリの場合、個々のフィールドを保管するというのはありそうにないシナリオです。 アプリでは多くの場合、状態が複雑なモデル オブジェクト全体を保存します。 ProtectedSessionStore では、複雑な状態オブジェクトを格納するため、JSON データが自動的にシリアル化および逆シリアル化されます。

前のコード例では、currentCount データは、ユーザーのブラウザーに sessionStorage['count'] として保存されます。 データはプレーンテキストに保存されず、ASP.NET Core のデータ保護を使用して保護されます。 ブラウザーの開発者コンソールで sessionStorage['count'] が評価される場合、暗号化されたデータを調べることができます。

ユーザーが後で Counter コンポーネントに戻ったときに currentCount データを回復するには (ユーザーが新しい回線にいる場合も含め)、ProtectedSessionStore.GetAsync を使用します。

protected override async Task OnInitializedAsync()
{
    var result = await ProtectedSessionStore.GetAsync<int>("count");
    currentCount = result.Success ? result.Value : 0;
}
protected override async Task OnInitializedAsync()
{
    currentCount = await ProtectedSessionStore.GetAsync<int>("count");
}

コンポーネントのパラメーターにナビゲーションの状態が含まれている場合は、ProtectedSessionStore.GetAsync を呼び出して、OnInitializedAsync ではなく、OnParametersSetAsyncnull 以外の結果を割り当てます。 OnInitializedAsync は、コンポーネントが最初にインスタンス化されたときに 1 回だけ呼び出されます。 後で、その同じページにいるとき、ユーザーが別の URL に移動しても OnInitializedAsync が再び呼び出されることはありません。 詳しくは、「ASP.NET Core Razor コンポーネントのライフサイクル」をご覧ください。

警告

このセクションの例は、サーバーでプリレンダリングが有効になっていない場合に機能します。 プリレンダリングが有効になっていると、コンポーネントがプリレンダリングされているために JavaScript 相互運用の呼び出しを発行できないことを示すエラーが生成されます。

プリレンダリングを無効にするか、プリレンダリングで使用するコードを追加します。 プリレンダリングと連動するコードを記述する方法の詳細については、「プリレンダリングを処理する」を参照してください。

読み込み状態を処理する

ブラウザー ストレージはネットワーク接続経由で非同期にアクセスされるため、データが読み込まれ、コンポーネントで利用できるようになるまでに、常に一定の時間があります。 最良の結果を得るために、読み込みが進行中には空のデータや既定のデータを表示するのではなく、メッセージをレンダリングします。

1 つの手法は、データが null かどうかを追跡することです。これは、まだ読み込み中であることを意味します。 既定の Counter コンポーネントでは、カウントは int に保持されます。 型 (int) に疑問符 (?) を追加することで、currentCount を Null 許容にします

private int? currentCount;

カウントや Increment ボタンを無条件で表示するのではなく、HasValue を調べることで、データが読み込まれている場合にのみこれらの要素を表示します。

@if (currentCount.HasValue)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

プリレンダリングを処理する

プリレンダリング中:

  • ユーザーのブラウザーに対話式で接続することはありません。
  • ブラウザーには、JavaScript コードを実行できるページがまだありません。

localStorage または sessionStorage は、プリレンダリング中に使用できません。 コンポーネントがストレージとのやり取りを試みている場合、コンポーネントがプリレンダリングされているために JavaScript 相互運用の呼び出しを発行できないことを示すエラーが生成されます。

このエラーを解決する方法の 1 つは、プリレンダリングを無効にすることです。 これは通常、ブラウザーベースのストレージがアプリで頻繁に使用される場合、最良の選択肢となります。 プリレンダリングによってさらに複雑になり、アプリにとっては良いことがありません。アプリでは localStorage または sessionStorage が利用できなければ、役に立つコンテンツをプリレンダリングできないからです。

プリレンダリングを無効にするには、ルート コンポーネントではないアプリのコンポーネント階層の最上位コンポーネントで、prerender パラメーターを false に設定してレンダリング モードを指定します。

Note

ルート コンポーネントを対話型にすること (App コンポーネントなど) はサポートされていません。 そのため、プリレンダリングを App コンポーネントで直接無効にすることはできません。

Blazor Web アプリ プロジェクト テンプレートに基づくアプリの場合、プリレンダリングの無効化は通常、App コンポーネント内で Routes コンポーネントが使用されている場所 (Components/App.razor) で行います。

<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />

また、HeadOutlet コンポーネントのプリレンダリングを無効にしてください。

<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />

詳細については、「ASP.NET Core Blazor レンダリング モード」を参照してください。

プリレンダリングを無効にするには、_Host.cshtml ファイルを開き、コンポーネント タグ ヘルパーrender-mode 属性を、Server に変更します。

<component type="typeof(App)" render-mode="Server" />

事前レンダリングが無効になっている場合、<head> コンテンツの事前レンダリングは無効になります。

プリレンダリングは、localStoragesessionStorage を使用しない他のページでは役に立つかもしれません。 プリレンダリングを保持するには、ブラウザーが回線に接続されるまで読み込み操作を延期します。 次はカウンター値を格納する例です。

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount;
    private bool isConnected;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedLocalStore.GetAsync<int>("count");
        currentCount = result.Success ? result.Value : 0;
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount = 0;
    private bool isConnected = false;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        currentCount = await ProtectedLocalStore.GetAsync<int>("count");
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}

状態保存を取り出し、共通の場所に入れる

さまざまなコンポーネントがブラウザーベースのストレージに依存している場合、状態プロバイダー コードを何回も実装すると、コードが重複します。 コードの重複を回避する選択肢の 1 つは、状態プロバイダー ロジックをカプセル化する "状態プロバイダーの親コンポーネント" を作成することです。 状態保存メカニズムに関係なく、子コンポーネントは永続保存データとやりとりできます。

CounterStateProvider コンポーネントの次の例では、カウンター データは sessionStorage に保持されます。

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnInitializedAsync()
    {
        var result = await ProtectedSessionStore.GetAsync<int>("count");
        CurrentCount = result.Success ? result.Value : 0;
        isLoaded = true;
    }

    public async Task SaveChangesAsync()
    {
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnInitializedAsync()
    {
        CurrentCount = await ProtectedSessionStore.GetAsync<int>("count");
        isLoaded = true;
    }

    public async Task SaveChangesAsync()
    {
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}

注意

RenderFragment の詳細については、ASP.NET Core Razor コンポーネントに関する記事を参照してください。

CounterStateProvider コンポーネントによって読み込み段階が処理されます。状態の読み込みが完了するまで、その子コンテンツがレンダリングされることはありません。

アプリ内のすべてのコンポーネントが状態にアクセスできるようにするには、グローバル対話型サーバー側レンダリング (対話型 SSR) を使って、Routes コンポーネント内の Router (<Router>...</Router>) を CounterStateProvider コンポーネントで囲みます。

App コンポーネント (Components/App.razor) 内は、次のようになっています。

<Routes @rendermode="InteractiveServer" />

Routes コンポーネント (Components/Routes.razor) 内は、次のようになっています。

CounterStateProvider コンポーネントを使用するには、カウンター状態にアクセスする必要がある他のコンポーネントをコンポーネントのインスタンスでラップします。 アプリに含まれるすべてのコンポーネントが状態にアクセスできるようにするには、App コンポーネント (App.razor) で RouterCounterStateProvider コンポーネントでラップします。

<CounterStateProvider>
    <Router ...>
        ...
    </Router>
</CounterStateProvider>

Note

ASP.NET Core 5.0.1 のリリースと、その他の 5.x リリースでは、Router コンポーネントに @trueに設定された PreferExactMatches パラメーターが含まれています。 詳細については、「ASP.NET Core 3.1 から 5.0 への移行」を参照してください。

ラップされたコンポーネントの元に永続化されたカウンター状態が届くので、それを変更できます。 次の Counter コンポーネントはパターンが実装されています。

@page "/counter"

<p>Current count: <strong>@CounterStateProvider?.CurrentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>

@code {
    [CascadingParameter]
    private CounterStateProvider? CounterStateProvider { get; set; }

    private async Task IncrementCount()
    {
        if (CounterStateProvider is not null)
        {
            CounterStateProvider.CurrentCount++;
            await CounterStateProvider.SaveChangesAsync();
        }
    }
}

このコンポーネントは ProtectedBrowserStorage とやりとりするために必要ありませんし、"読み込み" 段階にも関係ありません。

前に説明したようにプリレンダリングを扱うには、カウンター データを利用するすべてのコンポーネントが自動的にプリレンダリングを使用するよう、CounterStateProvider を変更できます。 詳細については、「プリレンダリングを処理する」セクションを参照してください。

一般に、次の場合、"状態プロバイダーの親コンポーネント" パターンが推奨されます。

  • 多くのコンポーネントで状態を使用する。
  • 最上位の状態オブジェクトを 1 つだけ保持する場合。

さまざまな状態オブジェクトを保持し、さまざまな場所でさまざまなオブジェクト サブセットを使用するには、状態をグローバルに保持しないことをお勧めします。

Blazor WebAssembly アプリで作成されたユーザー状態は、ブラウザーのメモリに保持されます。

ブラウザーのメモリに保持されるユーザー状態の例としては、次のようなものがあります。

  • レンダリングされた UI での、コンポーネント インスタンスの階層と最新のレンダリング出力。
  • コンポーネント インスタンスに含まれるフィールドとプロパティの値。
  • 依存関係の挿入 (DI) サービス インスタンスに保持されているデータ。
  • JavaScript 相互運用の呼び出しによって設定された値。

ユーザーがブラウザーを閉じてから再度開いたり、ページを再度読み込んだりすると、ブラウザーのメモリに保持されているユーザー状態は失われます。

Note

保護されたブラウザー ストレージ (Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage 名前空間) は、ASP.NET Core のデータ保護に依存しており、サーバー側 Blazor アプリでのみサポートされます。

ブラウザー セッション間で状態を保持する

一般に、ユーザーが既存データを単に読み取るのではなく、活発にデータを作成している場合は、ブラウザー セッション間で状態を保持します。

ブラウザー セッション間で状態を維持するには、アプリで、ブラウザーのメモリとは異なる他の保存場所に、データを保持する必要があります。 状態は自動的に保存されません。 ステートフルなデータ保存を実装するアプリを開発するとき、措置を講じる必要があります。

データの保持は、通常、作成するためにユーザーが大きな労力を費やした、価値の高い状態の場合にのみ必要です。 次の例では、状態を維持することで、時間が節約され、商業活動の支援になります。

  • 複数ステップの Web フォーム: 複数ステップの Web フォームのいくつかのステップを完了した後で、状態が失われた場合、ユーザーがデータを再入力するのでは時間がかかります。 このシナリオでは、フォームからユーザーが離れて後で戻ってきた場合、状態が失われます。
  • ショッピング カート: 収益につながる可能性がある、アプリの中のビジネス上重要なコンポーネントを維持できます。 ユーザーが自分の状態を失い、そのため、自分のショッピング カートが消えると、後でサイトに戻ってきたとき、製品やサービスの購入数が減ることがあります。

アプリでは、"アプリの状態" のみが維持されます。 コンポーネント インスタンスやそのレンダー ツリーなど、UI は維持されません。 コンポーネントとレンダー ツリーは一般的にシリアル化されません。 アプリで、ツリー ビュー コントロールの展開されたノードなど、UI の状態を維持するには、カスタム コードを使用して、UI 状態の動作をシリアル化できるアプリ状態としてモデル化する必要があります。

状態を維持する場所

一般に、状態を維持するために、次のような場所があります。

サーバー側ストレージ

複数のユーザーとデバイスにまたがって永続的にデータを保持する場合は、Web API を使用してアクセスされる独立したサーバー側ストレージをアプリで使用できます。 次のオプションがあります。

  • BLOB ストレージ
  • キーと値のストレージ
  • リレーショナル データベース
  • テーブル ストレージ

データが保存された後は、ユーザーの状態は保持されていて、新しいブラウザー セッションで使用できます。

Blazor WebAssembly アプリはユーザーのブラウザー内で完全に実行されるため、ストレージ サービスやデータベースなど、セキュリティで保護された外部システムにアクセスするには、追加の手段が必要です。 Blazor WebAssembly アプリは、シングル ページ アプリケーション (SPA) と同じ方法でセキュリティ保護されます。 通常、アプリでは、OAuthOpenID Connect (OIDC) を使用してユーザーの認証を行った後、サーバー側アプリへの Web API 呼び出しを使用して、ストレージ サービスやデータベースとやり取りします。 Blazor WebAssembly アプリとストレージ サービスまたはデータベースの間のデータ転送は、サーバー側アプリによって仲介されます。 Blazor WebAssembly アプリではサーバー側アプリへの一時的な接続を維持し、サーバー側アプリではストレージへの永続的な接続が保持されます。

詳細については、次のリソースを参照してください。

Azure のデータ ストレージ オプションの詳細については、以下を参照してください。

URL

ナビゲーションの状態を表わす一時的なデータについては、URL の一部としてデータをモデル化します。 たとえば、次のようなユーザー状態が URL でモデル化されます。

  • 表示されるエンティティの ID。
  • ページ付きグリッドでの現在のページ番号。

ユーザーが手動でページを再度読み込むと、ブラウザーのアドレス バーの内容が保持されます。

@page ディレクティブで URL パターンを定義する方法については、「ASP.NET Core の Blazor ルーティングとナビゲーション」をご覧ください。

ブラウザー ストレージ

ユーザーが頻繁に作成する一時的なデータの場合、一般的に使用されるストレージの場所は、ブラウザーの localStorage コレクションと sessionStorage コレクションです。

  • localStorage の対象範囲はブラウザーのウィンドウです。 ユーザーがページを再読み込みするか、ブラウザーを閉じてから再度開いた場合、状態は保持されます。 ユーザーが複数のブラウザー タブを開くと、状態はすべてのタブで同じになります。 データは直接消去されるまで localStorage に残ります。
  • sessionStorage の対象範囲はブラウザーのタブです。ユーザーがタブを再読み込みすると、状態は維持されます。 ユーザーがタブかブラウザーを閉じると、状態は失われます。 ユーザーが複数のブラウザー タブを開くと、それぞれのタブには、他に依存しないそのタブだけのバージョンのデータが保持されます。

Note

localStoragesessionStorage は Blazor WebAssembly アプリで使用できますが、カスタム コードを記述するか、サードパーティのパッケージを使用する必要があります。

一般に、sessionStorage を使用しておけば安全です。 sessionStorage の場合、ユーザーが複数のタブを開き、以下に遭遇するリスクが回避されます。

  • タブ間の状態保存に含まれるバグ。
  • タブで他のタブの状態が上書きされるときの紛らわしい動作。

ブラウザーを閉じてから再度開いてもアプリで状態が保持される必要がある場合、localStorage がより適切な選択肢です。

警告

localStorage および sessionStorage に格納されているデータは、ユーザーによって表示または改ざんされる可能性があります。

メモリ内状態コンテナー サービス

入れ子になったコンポーネントは通常、「ASP.NET Core Blazor のデータ バインディング」で説明されているように、"チェーン バインド" を使用してデータをバインドします。 入れ子になったコンポーネントと入れ子になっていないコンポーネントでは、登録済みのメモリ内状態コンテナーを使用してデータへのアクセスを共有できます。 カスタムの状態コンテナー クラスでは、割り当て可能な Action を使用して、状態変更のアプリのさまざまな部分でコンポーネントに通知できます。 次に例を示します。

  • コンポーネントのペアでは、状態コンテナーを使用してプロパティを追跡します。
  • 次の例の 1 つのコンポーネントは、他のコンポーネントで入れ子になっていますが、この方法を使用するには入れ子である必要はありません。

重要

このセクションの例では、メモリ内状態コンテナー サービスを作成し、サービスを登録し、コンポーネントでサービスを使用する方法を示します。 この例では、それ以上開発しないとデータは保持されません。 データの永続ストレージの場合、状態コンテナーは、ブラウザー メモリがクリアされたときに存続する基になるストレージ メカニズムを採用する必要があります。 これは、localStorage/sessionStorage またはその他のテクノロジを使用して実現できます。

StateContainer.cs:

public class StateContainer
{
    private string? savedString;

    public string Property
    {
        get => savedString ?? string.Empty;
        set
        {
            savedString = value;
            NotifyStateChanged();
        }
    }

    public event Action? OnChange;

    private void NotifyStateChanged() => OnChange?.Invoke();
}

クライアント側 アプリ (Program ファイル):

builder.Services.AddSingleton<StateContainer>();

サーバー側アプリ (Program ファイル、.NET 6.0 以降の ASP.NET Core):

builder.Services.AddScoped<StateContainer>();

サーバー側アプリ (Startup.csStartup.ConfigureServices、ASP.NET Core 6.0 以前):

services.AddScoped<StateContainer>();

Shared/Nested.razor:

@implements IDisposable
@inject StateContainer StateContainer

<h2>Nested component</h2>

<p>Nested component Property: <b>@StateContainer.Property</b></p>

<p>
    <button @onclick="ChangePropertyValue">
        Change the Property from the Nested component
    </button>
</p>

@code {
    protected override void OnInitialized()
    {
        StateContainer.OnChange += StateHasChanged;
    }

    private void ChangePropertyValue()
    {
        StateContainer.Property = 
            $"New value set in the Nested component: {DateTime.Now}";
    }

    public void Dispose()
    {
        StateContainer.OnChange -= StateHasChanged;
    }
}

StateContainerExample.razor:

@page "/state-container-example"
@implements IDisposable
@inject StateContainer StateContainer

<h1>State Container Example component</h1>

<p>State Container component Property: <b>@StateContainer.Property</b></p>

<p>
    <button @onclick="ChangePropertyValue">
        Change the Property from the State Container Example component
    </button>
</p>

<Nested />

@code {
    protected override void OnInitialized()
    {
        StateContainer.OnChange += StateHasChanged;
    }

    private void ChangePropertyValue()
    {
        StateContainer.Property = "New value set in the State " +
            $"Container Example component: {DateTime.Now}";
    }

    public void Dispose()
    {
        StateContainer.OnChange -= StateHasChanged;
    }
}

前のコンポーネントによって IDisposable が実装され、Dispose メソッドで OnChange デリゲートがサブスクライブ解除されます。このメソッドは、コンポーネントが破棄されるときにフレームワークによって呼び出されます。 詳しくは、「ASP.NET Core Razor コンポーネントのライフサイクル」をご覧ください。

その他のアプローチ

カスタム状態ストレージを実装する場合、カスケード値とパラメーターを採用すると便利です。

  • 多くのコンポーネントで状態を使用する。
  • 最上位の状態オブジェクトを 1 つだけ保持する場合。

トラブルシューティング

カスタムの状態管理サービスにおいて、Blazor の同期コンテキスト外から呼び出されたコールバックは、コールバックのロジックを ComponentBase.InvokeAsync にラップして、レンダラーの同期コンテキストに移動する必要があります。

状態管理サービスが Blazor の同期コンテキストで StateHasChanged を呼び出さない場合は、次のエラーがスローされます。

System.InvalidOperationException: "現在のスレッドは Dispatcher に関連付けられていません。 レンダリングまたはコンポーネントの状態をトリガーするとき、InvokeAsync() を使用して Dispatcher に実行を切り替えます"

このエラーに対処する方法の詳しい情報と例については、「ASP.NET Core Razor コンポーネントのレンダリング」をご覧ください。

その他のリソース