はじめに
Blazorでアプリケーションを開発していて、WPF開発の際に行われているようなMVVMパターンを適用できれば開発が楽になるのではないかと思い試しました。
サンプルはBlazor Serverアプリとして作成しています。
事前準備としてMVVMパターンの開発を補助してくれるライブラリであるReactivePropertyをnuget経由でインストールしています。
・環境
.Net Core 3.1
ReactiveProperty 7.2.0
今回はテキストボックスに文字を入力すると文字数を表示してくれるアプリを作ります。
Viewの作成
@page "/stringLengthCounter"
@inject StringLengthCounterViewModel ViewModel
<h1>文字列カウンタ</h1>
<input id="text1" @bind="ViewModel.Text1.Value" @bind:event="oninput" />
<p>@ViewModel.Text2.Value</p>
Viewに相当するRazor コンポーネントを作成します。文字入力用のテキストボックスと結果の文字数表示部分が存在しています。
@inject
でDIコンテナに登録してあるViewModelのインスタンスを取得します。Viewではコーディングは最小限にしてViewModelのパラメータをバインドすることのみに留めるという方針で作っています。
Razorコンポーネントにおけるデータバインディングは以下のページを参考にしました。
ASP.NET Core Blazor データ バインディング
ViewModelの作成
/// <summary>
/// 文字列カウンタのビューモデル
/// </summary>
public class StringLengthCounterViewModel
{
public ReactivePropertySlim<string> Text1 { get; set; } = new ReactivePropertySlim<string>();
public ReadOnlyReactivePropertySlim<string> Text2 { get; set; }
public StringLengthCounterViewModel(StringLengthCounterModel model)
{
this.Text1 = model.Text1;
this.Text2 = model.Text2;
}
}
Viewで扱う項目を持つViewModelを作成します。今回はModelの値をそのままプロパティに代入しているだけの単純なものになっています。
ModelはコンストラクタインジェクションによってDIコンテナから取得されます。
Modelの作成
/// <summary>
/// 文字列カウンタのモデル
/// </summary>
public class StringLengthCounterModel
{
public ReactivePropertySlim<string> Text1 { get; set; } = new ReactivePropertySlim<string>();
public ReadOnlyReactivePropertySlim<string> Text2 { get; set; }
public StringLengthCounterModel()
{
this.Text2 =
Text1
.Select(x => string.IsNullOrEmpty(x) ? "0文字" : x.Length + "文字")
.ToReadOnlyReactivePropertySlim();
}
}
Text1をインプットに文字数を調べて結果をText2に出力するModelを作成します。
DIコンテナの登録
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddScoped<StringLengthCounterModel>();
services.AddTransient<StringLengthCounterViewModel>();
}
作成したViewModelとModelをDIコンテナに登録します。既存のStartup.csにあるConfigureServicesメソッドに追記する形で行います。
AddScoped``AddTransient
というメソッドが出てきていますがそれぞれでオブジェクトの寿命が異なります。またBlazor ServerとBlazor WebAssemblyで仕様が異なります。
以上でテキストボックスに入力した文字数を表示してくれるアプリケーションを作ることができました。
DIコンテナの詳細は公式サイトに記述してあります。
ASP.NET Core Blazor 依存関係の挿入
補足:ViewModelからViewへの通知
上記の要領で作成したアプリケーションのModelをDelay
メソッドを使ってText2への通知を100ms遅延させるように変更を加えます。
public StringLengthCounterModel()
{
this.Text2 =
Text1
.Select(x => string.IsNullOrEmpty(x) ? "0文字" : x.Length + "文字")
.Delay(TimeSpan.FromMilliseconds(100))
.ToReadOnlyReactivePropertySlim();
}
100ms遅延して結果が表示されるかと思いきや予想と違う挙動をします。正しく文字列をカウントしていません。
更に先程の処理を書き換えます。
public StringLengthCounterModel()
{
this.Text2 =
Text1
.Select(x => string.IsNullOrEmpty(x) ? "0文字" : x.Length + "文字")
.Delay(TimeSpan.FromMilliseconds(100), Scheduler.Immediate)
.ToReadOnlyReactivePropertySlim();
}
変更した箇所はDelay
メソッドの第二引数です。これで予想していたとおり100ms遅延してText2の変更がViewに反映されるようになりました。今回はScheduler.Immediate
を指定しましたがDelayメソッドのデフォルトのスケジューラはThreadPoolSchedulerです。どうやらModelでスレッドプールを利用するとViewに変更が自動的に反映されないようです。
Scheduler.Immediate
を指定しなくてもViewModelからの変更を通知する処理をViewに記載することで意図した動作をさせることができます。
@page "/stringLengthCounter"
@inject StringLengthCounterViewModel ViewModel
<h1>文字列カウンタ</h1>
<input id="text1" @bind="ViewModel.Text1.Value" @bind:event="oninput" />
<p>@ViewModel.Text2.Value</p>
@code {
protected override void OnInitialized()
{
ViewModel.Text2.Subscribe(_ => this.InvokeAsync(() => this.StateHasChanged()));
}
}
Subscribe
メソッドにText2で変更が行われたときの処理を記載します。StateHasChanged
メソッドを呼び出すとコンポーネントが再レンダリングされます。これによりText2が変更されると再レンダリングが行われるようになります。InvokeAsync
メソッドはStateHasChanged
メソッドをコンポーネントが動作しているスレッドから呼び出すために使用しています。コンポーネントが動作しているスレッドとは違うスレッドでStateHasChanged
メソッドを呼び出すと実行時エラーになります。
さいごに
Blazorを元に簡単なMVVMパターンのアプリケーションの作成を行いました。
ViewModelからViewへの値の反映を行う場合に工夫する必要があることが分かりました。
今回動作させたソースコード
https://github.com/ttlatex/BlazorMvvmTiny