[.NET 8]MVVMで作るBlazor Web App

# 忙しい人向けの結論

1. `ReactiveProperty.Blazor` を使います。
1. Blazor Web Appテンプレートの `Counter` ページ向けに `ViewModels/CounterViewModel.cs` を作ります。
1. `ReactivePropertySlim<int>` と `ReactiveCommandSlim` を使って、プロパティと、値をインクリメントするコマンドを作ります。
1. `CounterViewModel` をDIして、 `Counter.razor` にインジェクションします。
1. `Counter` ページの　`@onclick` に `CounterViewModel` のコマンドを紐づけます。
1. おわり

以下で解説していきます。なお、この記事ではMVVMとは何か？という話はしません。BlazorでMVVMをするにはどうするのか？にフォーカスして書いていきます。

# `ReactiveProperty.Blazor` って何？

Blazor向けに作られた[ReactiveProperty](https://github.com/runceel/ReactiveProperty)のNuGetパッケージです。Blazorはボタンなどのクリックイベント `@onclick` を実行すると、UI（DOM）が自動で再描画されるため、嬉しさ半減なのですが、例えば実行する処理が、タイマー実行系の処理であったり、非同期で少し重めの処理であったりする場合は、明示的に `StateHasChanged()` を呼ぶ必要があるため、このパッケージを使用するメリットが出てきます。

どちらかと言うと、処理に対するメリットよりも、 **「MVVMで書くぞ！」** という強い意気込み（制約）がカオスになりやすい `.razor` ファイル内の `@code{ }` や `.razor.cs` ファイル自体をあるべき姿に保ってくれるというメリットの方が大きいかもしれません。

詳しいドキュメントは[こちら](https://okazuki.jp/ReactiveProperty/)になりますので、興味があればご参照ください。

# Counterページ向けにViewModelを作る

おなじみの `Blazor Web App` プロジェクトテンプレートを使用します。
まずは、クライアント側に `ViewModels/CounterViewModel.cs` を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/160980/e359eb3e-23e3-15d6-3114-309261e67d46.png)

続いて、以下のコードを参考に `CounterViewModel.cs` に実装していきます。

```CounterViewModel.cs
using Reactive.Bindings;
using Reactive.Bindings.Disposables;
using Reactive.Bindings.Extensions;

namespace BlazorApp1.Client.ViewModels
{
    public class CounterViewModel : IDisposable
    {
        private readonly CompositeDisposable _disposable = [];

        // ボタンを押されたときの動作を定義する
        public ReactiveCommandSlim IncrementCommand { get; }
        // 実際のカウンターの値を保持するプロパティ
        public ReactivePropertySlim<int> Count { get; }

        public CounterViewModel()
        {
            // 初期化
            Count = new ReactivePropertySlim<int>().AddTo(_disposable);
            IncrementCommand = new ReactiveCommandSlim();

            // コマンドが実行された時にCountの値をインクリメントする
            IncrementCommand.Subscribe(_ => Count.Value++).AddTo(_disposable);
        }

        // Viewの@onclickに紐付ける用
        public void ExecuteIncrementCommand()
        {
            IncrementCommand.Execute();
        }

        public void Dispose()
        {
            _disposable.Dispose();
            GC.SuppressFinalize(this);
        }
    }
}
```

## `ReactiveCommandSlim IncrementCommand`

MVVMのコマンドを定義するプロパティです。コンストラクタ内の以下のコードで具体的な処理を定義しています。

```cs
// コマンドが実行された時にCountの値をインクリメントする
IncrementCommand.Subscribe(_ => Count.Value++).AddTo(_disposable);
```

`.Subscribe()` の引数にLINQで処理を渡してあげることで、 `IncrementCommand.Execute()` が呼ばれた時にその処理が実行されるようになります。今回の例ですと、 `Count.Value++` が実行され、 `Count` の値が1増えます。

## `ReactivePropertySlim<int> Count`

値が変更されてことを自動的にView側へ通知してくれるプロパティです。 `IncrementCommand` によって `Count` の値が変更された時にViewへ通知が飛び、UIが更新され最新の値が画面に表示されます。

# `CounterViewModel` をDIする

クライアント側、サーバー側ともに編集が必要です。
まずはクライアント側の `Program.cs` を以下のように修正します。

```diff_c_sharp
+ using BlazorApp1.Client.ViewModels;
  using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

  var builder = WebAssemblyHostBuilder.CreateDefault(args);
+ builder.Services.AddSingleton<CounterViewModel>();

  await builder.Build().RunAsync();
```

次にサーバ側の `Program.cs` も同様に修正します。

```diff_c_sharp
  using BlazorApp1.Client.Pages;
+ using BlazorApp1.Client.ViewModels;
  using BlazorApp1.Components;

  namespace BlazorApp1
  {
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddRazorComponents()
                .AddInteractiveServerComponents()
                .AddInteractiveWebAssemblyComponents();

+           builder.Services.AddScoped<CounterViewModel>();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseWebAssemblyDebugging();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseStaticFiles();
            app.UseAntiforgery();

            app.MapRazorComponents<App>()
                .AddInteractiveServerRenderMode()
                .AddInteractiveWebAssemblyRenderMode()
                .AddAdditionalAssemblies(typeof(Counter).Assembly);

            app.Run();
        }
    }
  }
```

これで、 `Countr.razor` で `CounterViewModel` をインジェクションできるようになりました。

# `Counter.razor` でViewModelを使う

それでは、 `Counter.razor` を以下のように修正します。

```diff_html
  @page "/counter"
+ @using BlazorApp1.Client.ViewModels
  @rendermode InteractiveAuto
+ @inject CounterViewModel ViewModel

  <PageTitle>Counter</PageTitle>

  <h1>Counter</h1>

- <p role="status">Current count: @currentCount</p>
+ <p role="status">Current count: @ViewModel.Count.Value</p>

- <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
+ <button class="btn btn-primary" @onclick="ViewModel.ExecuteIncrementCommand">Click me</button>

- @code {
-     private int currentCount = 0;

-     private void IncrementCount()
-     {
-         currentCount++;
-     }
- }
```

まず、ソース上部で ViewModelをインジェクションしています。 `@inject TypeName VariableName` とすることで、フレームワーク側で自動的にViewModelを割り当ててくれます。

```cs
@inject CounterViewModel ViewModel
```

今回のスコープですと、ページ遷移してもViewModelの状態は保たれているため、`Count`の値もブラウザを閉じるまで保持し続けます。

----
続いてUI側を見ていきます。
```html
<p role="status">Current count: @ViewModel.Count.Value</p>

<button class="btn btn-primary" @onclick="ViewModel.ExecuteIncrementCommand">Click me</button>
```

ReactivePropertyで定義したプロパティの値を参照する場合は、 `.Value` を使用して参照する必要があるため注意が必要です。今回ですと、 `@ViewModel.Count.Value` の部分です。
そして、`ViewModel.ExecuteIncrementCommand` を `@onclick` に紐付けることで、ボタンがクリックされる度に `IncrementCommand` が実行され、 `Count` の値がインクリメントしていきます。

## おわりに

今回の例示では、ReactivePropertyの良さが活かしきれませんでしたが、例えばPull型で定期的にサーバに問い合わせて情報を取得、そして反映のようなクライアントをBlazorで実装する場合には恩恵を受けることでしょう。

また、既に `INotifyPropertyChanged` インターフェースを使用してオレオレViewModelを作っている場合は、簡単に乗り換えることができるため、ぜひ使ってみてはいかがでしょうか？

最後になりますが、Blazorにはかっちりはまるソフトウェアアーキテクチャが無い（もしかしたら知らないだけかも・・・）です。なので、こうしてMVVMの型にはめて開発を進めるのも、クリーンなソリューションを保つための1つの手かもしれません。

もし、Blazorでの開発でいい方法があるよ！と手を挙げられる方がおりましたら、ぜひ [**Blazor Advent Calendar**](https://qiita.com/advent-calendar/2023/blazor) に寄稿をお待ちしております！！（それか、コソっと [@nr_ck](https://twitter.com/nr_ck) に教えてください。 ）

----

この記事で伝えたいことは以上です。
これを参考にBlazorでMVVMやるぜ！って言う人が増えてくれたら筆者は幸いです。