12
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

BlazorでMVVMパターンを試す

Posted at

はじめに

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

・環境
.Net Core 3.1
ReactiveProperty 7.2.0

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

 striglength.gif

Viewの作成

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 データ バインディング

ViewModelの作成

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の作成

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

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 依存関係の挿入

補足: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遅延して結果が表示されるかと思いきや予想と違う挙動をします。正しく文字列をカウントしていません。

striglength2.gif

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

        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

12
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?