あなたが世界最長のフォームを記入していると想像してください。住所から誕生日、最近訪れた7か国のリストまで詳細を入力するのに30分かかりました。「送信」ボタンをクリックした瞬間、「接続が失われました」というメッセージが表示されます。大丈夫ですよね?「戻る」ボタンをクリックすれば… ああ、ダメだ!フォームは空っぽです。これはひどい体験で、あなたは二度とそのサイトを訪れないと誓うでしょう。
これがあなたのウェブサイト訪問者に与えたい体験ではありません。したがって、Blazor アプリケーションで状態を管理する方法を理解することが重要です。状態を管理しつつ、そのために記述しなければならないコード量を最小限に抑えるには?「ぜひそうしたい!」
関連動画をご覧ください:Blazor Apps における状態管理
1 Blazor 状態の定義
まず、Blazor アプリにおける「状態」の意味を明確にしましょう。最適なユーザー体験を実現するには、エンドユーザーの接続が一時的に切れたり、ページをリフレッシュしたり、戻ってきたりしたときに、一貫した体験を提供することが重要です。体験の構成要素は次のとおりです。
- ユーザーインターフェース(UI)を表す HTML ドキュメントオブジェクトモデル(DOM)
- ページ上で入力・出力されるデータを表すフィールドとプロパティ
- ページのコードの一部として実行される登録済みサービスの状態
特別なコードがない場合、状態は Blazor ホスティングモデル に応じて 2 つの場所に保存されます。Blazor WebAssembly(クライアント)アプリケーションの場合、状態はユーザーがページをリフレッシュするか移動するまでブラウザのメモリに保持されます。Blazor Server アプリケーションの場合、状態は各クライアントセッションに割り当てられた「サーキット」と呼ばれる特別な「バケツ」に保持されます。これらのサーキットは、切断後にタイムアウトすると状態を失う可能性があり、さらにはサーバーがメモリ不足の状態にあるアクティブな接続中に消えることもあります。
2. 参考アプリケーション
状態のニュアンスを説明するために、Blazor Health App から始めます:
Angular から Blazor へ:The Health App

Blazor でサンプルアプリケーションを構築します。Blazor は .NET ベースのフレームワークで、ブラウザで実行できる Web アプリケーションを構築でき、C# と Razor テンプレートを利用してクロスプラットフォームで HTML5 互換の WebAssembly コードを生成します。
これを拡張して 2 つのページを含め、ナビゲーションの微妙な違いを説明します。関連する GitHub リポジトリ:
いくつかのサンプルプロジェクトがあります。問題は Blazor WebAssembly プロジェクトと Blazor Server プロジェクトで異なる形で現れます。
3. Blazor WebAssembly における状態
Blazor WebAssembly(クライアントプロジェクト)では、状態はメモリに保持されます。つまり、リフレッシュや強制ナビゲーションは状態を破壊します。実際に確認してみましょう。
BlazorState.Wasmをスタートアッププロジェクトに設定して実行します。- フォーム情報を更新します。
- 「結果」に移動し、同じ結果が表示されることを確認します。
- 「ホーム」に戻り、強制リフレッシュ(通常は
CTRL+F5)を実行します。フォームがデフォルト値に戻ることに注意してください。 - フォーム情報を更新し、ブラウザのアドレスバーに
/resultsを追加して手動でナビゲーションし、ENTERを押します。これもデフォルト値を使用することに注意してください。
よくない体験です!Blazor Server と比較すると少し異なります。
4. Blazor Server における状態
スタートアッププロジェクトを BlazorState.Server に変更して実行します。クライアントバージョンと同じ手順を試してみてください。状態がサーバーメモリに保持されているため、状態が維持されていることに注意してください。アプリケーションを開いた後、Web サーバーを停止して再起動します。切断メッセージが表示されるはずです。サーバーが再起動したら、「再読み込み」オプションをクリックします。アプリケーションは復元されますが、すべての状態を失うことに注意してください。
これで問題が発生しました。解決策を研究しましょう!
5. ソリューションアーキテクチャ
以下のソリューションは、再利用性を最大化するように設計されたアーキテクチャアプローチを使用しています。Blazor.ViewModel プロジェクトは、アプリケーションのインターフェース、プロパティ、ビジネスロジックをホストします。これは Model-View-ViewModel(MVVM)パターン の .NET Standard ライブラリ実装であり、WPF、Xamarin、さらには Blazor といったあらゆるタイプの .NET Core プロジェクトから簡単に参照できます。再利用性を最大化!
UI とユーザーエクスペリエンスロジック、および共有アセット(画像、スタイルシート、JavaScript コード、Razor ビューコンポーネントなど)については、Blazor.Shared が Razor クラスライブラリ を利用しています。このソリューションは、MVVM コードの重複を避けるために HealthModelBase を実装しています。また、ここで説明するすべての状態管理ソリューションを、Blazor WebAssembly プロジェクトと Blazor Server プロジェクトの両方に簡単に適用できるサービスやコンポーネントとして実装しています。「ホスト」プロジェクトは、共有コンポーネントとリソースを参照するためのいくつかの構造を提供するだけなので、これによりコードの再利用性がさらに最大化されます。

これで、問題と解決策のアプローチが明らかになりました。それでは、Blazor アプリケーションでの状態管理に進みましょう!
6. サービス登録
最初のステップはあまり明白ではないかもしれませんが、完全を期すためにサービスを紹介したいと思います。実際に確認するには、新しい Blazor クライアントアプリケーションを作成して実行してください。組み込みテンプレートは、いくつかのページの簡単なナビゲーションを提供します。Counter ページに移動し、カウンターをインクリメントします。ここで、ページを離れて再度移動すると、カウンターはゼロにリセットされます!これは、カウンターの状態がコンポーネント内に保持されているため、コンポーネントが初期化されるたびにリセットされるからです。
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
「メモリ内」(または Blazor Server の「サーキット内」)状態を維持するには、カウンター「サービス」を作成します。
public class CounterService
{
public int Count { get; private set; }
public void Increment()
{
Count += 1;
}
}
Startup.cs でサービスを登録します。
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<CounterService, CounterService>();
}
次に、Counter.razor の @code ブロック内のコードを削除し、カウンターサービスを注入して直接データバインディングを行います。
@inject CounterService Svc
<h1>Counter</h1>
<p>Current count: @Svc.Count</p>
<button class="btn btn-primary" @onclick="Svc.Increment">Click me</button>
コンポーネントが破棄・再作成されても、サービスはメモリ内に保持され、ナビゲーション中も一貫したカウントを維持します。これが状態を維持するための最初のステップです。参考アプリケーションは、この方法でメインビューモデルを登録しています。
7. ブラウザキャッシュ
状態を維持する方法の1つは、HTML5 Web Storage を使用してブラウザキャッシュを利用することです。API は非常にシンプルです。BlazorState.Shared の stateManagement.js ファイルは、シンプルでグローバルにアクセス可能なインターフェースを定義しています。localStorage JavaScript API を使用していますが、sessionStorage を選択することもできます。
window.stateManager = {
save: function (key, str) {
localStorage[key] = str;
},
load: function (key) {
return localStorage[key];
}
};
これは、Blazor WebAssembly プロジェクトの index.html と Blazor Server プロジェクトの _Host.cshtml のルートに含まれています。共有アセットを含めるには、パスを使用するだけです。
<script src="_content/BlazorState.Shared/stateManagement.js"></script>
Blazor のコンポーネントモデルにより、状態変更を管理する「ラッパー」コンポーネントを簡単に作成できます。これは StorageHelper.razor で実装されています。まず、using ステートメントがビューモデル、JavaScript 相互運用、JSON シリアライザを参照します。実装が注入されます。
@using Microsoft.JSInterop; @using System.Text.Json; @inject IJSRuntime
JsRuntime @inject IHealthModel Model
テンプレートは単に子コンポーネントをラップし、読み込みが完了するとそれらをレンダリングします。
@if (hasLoaded) { @ChildContent } else {
<p>Loading...</p>
}
コンポーネントが初期化されると、コードはキャッシュからビューモデルを読み込もうとします。
string vm;
try
{
vm = await JsRuntime.InvokeAsync<string>("stateManager.load", nameof(HealthModel));
}
catch(InvalidOperationException)
{
return;
}
Blazor Server では、コンポーネントはサーバー上であらかじめレンダリングされています。JavaScript は利用できないため、相互運用呼び出しは InvalidOperationException をスローします。これは最初のパスでキャッチされます。2 回目の呼び出しはクライアントから行われ、ビューモデルがキャッシュされていれば成功します。キャッシュからビューモデルの JSON が読み込まれると、それはデシリアライズされ、プロパティがグローバルビューモデルインスタンスに移動されます。
var viewModel = JsonSerializer.Deserialize<HealthModel>(vm);
if (viewModel != null)
{
isDeserializing = true;
Model.AgeYears = viewModel.AgeYears;
Model.HeightInches = viewModel.HeightInches;
Model.IsFemale = viewModel.IsFemale;
Model.IsImperial = viewModel.IsImperial;
Model.WeightPounds = viewModel.WeightPounds;
isDeserializing = false;
}
isDeserializing フラグは、次のプロパティ変更通知を登録するコードで見られるように、無限ループを避けるために重要です。
Model.PropertyChanged += async (o, e) =>
{
if (isDeserializing)
{
return;
}
var vmStr = JsonSerializer.Serialize(((HealthModel)Model));
await JsRuntime.InvokeAsync<object>(
"stateManager.save", nameof(HealthModel), vmStr);
};
hasLoaded = true;
ビューモデルのプロパティが変更されると、ビューモデルはシリアライズされ、キャッシュに保存されます。初期読み込みによってプロパティ変更がトリガーされた場合(つまり、isDeserializing フラグがあるため、デシリアライズ中にシリアライズしようとするのを避けるため)、この処理はスキップされます。これでコンポーネントが使用可能になります!Blazor.ServerLocal と Blazor.WasmLocal はヘルパークラスで、App.razor で同じように実装されています。
<BlazorState.Shared.StorageHelper>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</BlazorState.Shared.StorageHelper>
ルーターをラップすることで、状態管理は追加のコードを書くことなく、アプリケーション内のすべてのページとコンポーネントを処理します。ブラウザの開発者ツールを開き、「ローカルストレージ」に移動して、フォームを更新するたびに値が変化するのを観察できます。

重要なのは、ユーザーはローカルキャッシュにアクセスできるため、機密性の高い値を保存する場合は暗号化する必要があることです。Microsoft.AspNetCore.ProtectedBrowserStorage パッケージがその例を提供しています。
8. サーバーサイド管理
状態を処理する別の方法は、API を呼び出してサーバー上に保存することです。その永続性はあなた次第です。SQL、NoSQL、Redis のような単純なキャッシュまで選択肢はさまざまです。BlazorState.WasmRemote.Server は ASP.NET ホスト型 Blazor WebAssembly アプリケーションです。StateController は、リモート IP アドレスをキーとして使用してビューモデルを保存および取得する API を公開します。これはデモをシンプルにするためです。認証を備えた本番アプリケーションは、ユーザーやセッションにロックする可能性があります。
Blazor.Shared の StateService は API 呼び出しを処理します。コンストラクターは、グローバルビューモデルインスタンス、API エンドポイント URL を提供する IStateServiceConfig インスタンス、および HttpClient インスタンスを受け取ります。新しいインスタンスを作成するのではなく HttpClient を注入することが重要です。なぜなら、Blazor WebAssembly はブラウザのサンドボックス内で実行されるように特別に構成されたバージョンを必要とするからです。コンストラクターは、ビューモデルからプロパティ変更通知を登録します。
初期化中にページコンポーネントによって InitAsync が呼び出され、ビューモデルの状態が読み込まれます。
public async Task InitAsync()
{
_initializing = true;
var vmJson = await _client.GetStringAsync(_config.Url);
var vm = JsonSerializer.Deserialize<HealthModel>(vmJson, _options);
_model.AgeYears = vm.AgeYears;
_model.HeightInches = vm.HeightInches;
_model.IsFemale = vm.IsFemale;
_model.IsMetric = vm.IsMetric;
_model.WeightPounds = vm.WeightPounds;
_initializing = false;
}
このコードはクライアントのキャッシュ方法と非常によく似ていますが、ローカルキャッシュではなく API 呼び出しからモデルを取得します。プロパティ変更ハンドラーはモデルをシリアライズし、サーバーに POST します。
private async void Model_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{
if (_initializing || _config == null)
{
return;
}
var vm = JsonSerializer.Serialize(_model);
var content = new StringContent(vm);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
await _client.PostAsync(_config.Url, content);
}
BlazorState.WasmRemote.Server をスタートアッププロジェクトに設定して実行し、実際の動作を確認してください。.Client プロジェクトの Startup.cs で IStateServiceConfig の実装にある正しい URL を更新する必要があるかもしれません(ポートが異なる可能性があるため)。ソリューションを実行した状態で、「ネットワーク」タブを開き、フォームを更新するたびに呼び出しが行われるのを確認してください。

このサービスは Blazor WebAssembly 向けにデモされていますが、Blazor Server でも同じです。
9. 結論
Blazor は状態管理の方法について意見を持っていません。サービスとコンポーネントモデルにより、プロジェクト全体にわたるソリューションを簡単に実装できます。この記事では、Model-View-ViewModel パターンの実装と、プロパティ変更通知を登録してローカルまたは API 経由で状態をシリアライズする方法に焦点を当てました。Redux のような異なるアプローチを使用する場合でも、同じ方法が機能します。重要なステップは、プロパティが変更されたときにストアを更新し、コンポーネントの初期化時に状態管理ソリューションから読み込むことです。残りはブラウザの履歴だけです!
ASP.NET Core Blazor 状態管理 の公式ドキュメントもご覧ください。