BlazorでMVVMパターンを試す

## はじめに
　Blazorでアプリケーションを開発していて、WPF開発の際に行われているようなMVVMパターンを適用できれば開発が楽になるのではないかと思い試しました。
　サンプルはBlazor Serverアプリとして作成しています。
　事前準備としてMVVMパターンの開発を補助してくれるライブラリであるReactivePropertyをnuget経由でインストールしています。

・環境
.Net Core 3.1
ReactiveProperty 7.2.0

　今回はテキストボックスに文字を入力すると文字数を表示してくれるアプリを作ります。

　![striglength.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135472/770fa7a3-518b-2381-568f-3fb2231b9f1f.gif)

## Viewの作成

```html:StringLengthCounter.razor
@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 データ バインディング](https://docs.microsoft.com/ja-jp/aspnet/core/blazor/components/data-binding?view=aspnetcore-3.1)

## ViewModelの作成

```csharp:StringLengthCounterViewModel.cs
    /// <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の作成

```csharp
    /// <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コンテナの登録

```csharp:Startup.cs
        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 依存関係の挿入](https://docs.microsoft.com/ja-jp/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-3.1)

## 補足：ViewModelからViewへの通知

　上記の要領で作成したアプリケーションのModelを`Delay`メソッドを使ってText2への通知を100ms遅延させるように変更を加えます。

```csharp
        public StringLengthCounterModel()
        {
            this.Text2 =
                Text1
                .Select(x => string.IsNullOrEmpty(x) ? "0文字" : x.Length + "文字")
                .Delay(TimeSpan.FromMilliseconds(100))
                .ToReadOnlyReactivePropertySlim();
        }
```

　100ms遅延して結果が表示されるかと思いきや予想と違う挙動をします。正しく文字列をカウントしていません。

![striglength2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135472/95fec220-bd01-33c5-bf5c-bf5d89407c62.gif)

　更に先程の処理を書き換えます。

```csharp
        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に記載することで意図した動作をさせることができます。

```html
@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