忙しい人向けの結論
-
ReactiveProperty.Blazor
を使います。 - Blazor Web Appテンプレートの
Counter
ページ向けにViewModels/CounterViewModel.cs
を作ります。 -
ReactivePropertySlim<int>
とReactiveCommandSlim
を使って、プロパティと、値をインクリメントするコマンドを作ります。 -
CounterViewModel
をDIして、Counter.razor
にインジェクションします。 -
Counter
ページの@onclick
にCounterViewModel
のコマンドを紐づけます。 - おわり
以下で解説していきます。なお、この記事ではMVVMとは何か?という話はしません。BlazorでMVVMをするにはどうするのか?にフォーカスして書いていきます。
ReactiveProperty.Blazor
って何?
Blazor向けに作られたReactivePropertyのNuGetパッケージです。Blazorはボタンなどのクリックイベント @onclick
を実行すると、UI(DOM)が自動で再描画されるため、嬉しさ半減なのですが、例えば実行する処理が、タイマー実行系の処理であったり、非同期で少し重めの処理であったりする場合は、明示的に StateHasChanged()
を呼ぶ必要があるため、このパッケージを使用するメリットが出てきます。
どちらかと言うと、処理に対するメリットよりも、 「MVVMで書くぞ!」 という強い意気込み(制約)がカオスになりやすい .razor
ファイル内の @code{ }
や .razor.cs
ファイル自体をあるべき姿に保ってくれるというメリットの方が大きいかもしれません。
詳しいドキュメントはこちらになりますので、興味があればご参照ください。
Counterページ向けにViewModelを作る
おなじみの Blazor Web App
プロジェクトテンプレートを使用します。
まずは、クライアント側に ViewModels/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のコマンドを定義するプロパティです。コンストラクタ内の以下のコードで具体的な処理を定義しています。
// コマンドが実行された時に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
を以下のように修正します。
+ 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
も同様に修正します。
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
を以下のように修正します。
@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を割り当ててくれます。
@inject CounterViewModel ViewModel
今回のスコープですと、ページ遷移してもViewModelの状態は保たれているため、Count
の値もブラウザを閉じるまで保持し続けます。
続いてUI側を見ていきます。
<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 に寄稿をお待ちしております!!(それか、コソっと @nr_ck に教えてください。 )
この記事で伝えたいことは以上です。
これを参考にBlazorでMVVMやるぜ!って言う人が増えてくれたら筆者は幸いです。