Model のインタフェースの上に ReactiveCommand は立っている【step: 8 .NET Core WPF MVVM ReactiveCommand 入門 2020】

前回記事「ReactiveProperty を編む【step: 7 .NET Core WPF MVVM ReactiveProperty 入門 2020】」

 

新作アニメの一覧作成に手を出してしまったので思っていた以上に間が空きましたが、前回は ReactiveProperty と ReactivePropertySlim の基本的な使い方と、Model ⇔ VM 間を双方向でバインドする方法を紹介しました。

今回は ReactiveProperty に含まれる ICommand を実装した ReactiveCommand と AsyncReactiveCommand の基本的な使用方法と Command から呼び出す Model のインタフェースを紹介します。

尚、この記事は Visual Studio 2019 Community Edition で .NET Core 3.1 以上 と C# + Prism + ReactiveProperty + MahApps.Metro + Material Design In XAML Toolkit + AutoMapper を使用して、WPF アプリケーションを MVVM パターンで作成するのが目的なので、C# の文法や基本的なコーディング知識を持っている人が対象です。

ReactiveCommand

step: 6 では Prism の DelegateCommand を利用してボタンの Command とバインドする例を紹介しましたが、ReactiveProperty にも同じように ICommand を実装した ReactiveCommand と AsyncReactiveCommand が含まれています。

サンプルに使用する画面は前回も使用した fig. 1 の画面を流用します。

fig.1 サンプル画面

src. 1 は fig. 1 の『読込ボタン』とバインドする LoadClick コマンドを追加した VM です。

Command で実行する処理は前回紹介した ReactiveProperty の場合と同じく Subscribe メソッドを使用してラムダ式を指定するか別メソッドに delegate します

src. 1 で delegate している onLoadClick は未実装なので『読込ボタン』を Click しても何も起こりませんが、コントロールからの Command は受け取れています。src. 1 のように ReactiveCommand の初期化と Subscribe を別々に記述することもできますが、src. 2 のように WithSubscribe を使用すると初期化と同時に処理を指定することもできます。

src. 1 のように別々に記述しても特にメリットがある訳ではないので src. 2 のように WithSubscribe を使用すれば良いと思います。

ReactiveCommand のコードスニペット

前回紹介した通りに ReactiveProperty のコードスニペットをインストールしていれば ReactiveCommand もコードスニペットで入力できます。

fig. 2 は ReactiveCommand を入力するための『rcomm コードスニペット』AsyncReactiveCommand を入力するための『arcomm コードスニペット』です。

fig.2 ReactiveCommand のコードスニペット

又、fig. 2 では確認できませんが、パラメータを受け取る『rcommg と arcommg コードスニペット』も含まれています。但し、ReactiveCommand のコードスニペットは定義部分しか入力されないので、そこまで便利だと感じないかもしれません。

ReactiveCommand の CanExecute

ReactiveCommand は ICommand を継承しているので CanExecute を設定すると CommandSource の IsEnabled に反映されますが、常に実行可能な Command は上の src. 1、2 のように new ReactiveCommand() で作成できます。step: 6 で紹介した Prism の DelegateCommand に設定する CanExecute は bool でしたが、ReactiveCommand の場合は IObservable<bool> を指定する必要があります。

ReactiveProperty(Slim)<T> は IObservable<T> も継承しているので、ReactiveProperty(Slim)<bool> 型の変数(private プロパティ or フィールド)を用意すれば CanExecute のソースに指定できます。

例えば、fig. 1 の『ID TextBox』に何か入力されている場合のみ『読込ボタン』を有効にするには src. 3 のように ReactivePropertySlim<bool> 型の hasId フィールドを追加して CanExecute に設定します。

前回紹介した通り ReactiveProperty(Slim)で定義したプロパティを Subscribe すると値変更時に処理が追加できるので『ID TextBox』とバインドしている『Id プロパティを』Subscribe して hasId フィールドを更新しています。

そして hasId(IObservable<bool>)から ToReactiveCommand すると hasId フィールドの値が CanExecute に渡されるようになり、実行すると fig. 3 のようになります。

fig.3 CanExecute のサンプル

『ID TextBox』への入力有無で『読込ボタン』の IsEnabled が変わるのが確認できます。CanExecute は src. 3 のように ToReactiveCommand で指定する事もできますが、new ReactiveCommand(hasId) のようにコンストラクタの第 1 パラメータに指定する事もできます。

IObservable<bool> を継承する ReactiveCommand

かずきさんが書かれた『MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 中編 – Qiita』からのパクリですが ReactiveCommand は IObservable<bool> も継承しています

そのため src. 4 のように Select(Where や Concat 等も利用可能)で処理した結果を ReactiveProperty(Slim)等に変換できます。

Select 拡張メソッドを呼び出すと Command 呼び出しの(ボタンを Click する)度、指定したラムダ式や delegate 先の処理が呼び出されるので、実行すると fig. 4 のようになります。

fig.4 ReactiveCommand を ReactiveProperty に変換

確かに src. 4 のように書くとスマートだとは思いますが、管理人的には無理に Reactive Extensions が提供する拡張メソッドを使用しなくても、今まで紹介した WithSubscribe 等で別途用意したプロパティへ値を設定するような方法でも構わないと思っています。どちらの方法を採用しても見た目の動作は同じなので、使い始めの時はあまり Reactive Extensions に拘らず徐々に覚えていけば良いと思います。

AsyncReactiveCommand

WPF のボタンで 2 度押しを防止したい場合、Windows Form のように Click イベントの先頭でボタンの IsEnabled を false に設定するような処理を追加しても WPF では最初の Click 処理完了後に 2 度目の Click 処理が開始されてしまうので同じ方法では防止できません

そのため以前は『ReactivePropertyで2度押し防止 – かずきのBlog@hatena』で紹介されているような方法で防いでいたようですが、ReactiveProperty Ver. 3.0.0 で追加された AsyncReactiveCommand を使用すると非同期処理が終了するまで押せないボタンになるので、結果的に 2 度押し防止が実現できます。

今は async/await のおかげで非同期処理も書き易くなったと思うので、この連載で使用する Command は基本的に AsyncReactiveCommand を使用します。(管理人が非同期処理の書き方を試したいだけとも言います)

但し、AsyncReactiveCommand は IObservable<T> を継承していないので前章の最後で紹介した Select 等の Reactive Extensions が提供する拡張メソッド類は使用できず WithSubscribe か Subscribe するしかないと言う違いはあります。ですが、基本的な使い方は ReactiveCommand と同じなので、AsyncReactiveCommand で書いたサンプルも ReactiveCommand で読み替える事ができます。

Command のパラメータ

Command をバインドする時、src. 5 のようにパラメータを指定する事もできます。

XAML に設定したパラメータの受け取りには src. 6 のように型パラメータ付きの(Async)ReactiveCommand で受け取る事ができます。

AsyncReactiveCommand で宣言しているので意味の無いスレッドを起こしていますが、XAML に設定した CommandParameter は 56 ~ 58 行目のように WithSubscribe の第 1 パラメータへセットされます。一応、AsyncReactiveCommand をラムダ式で実行する例もコメントアウトして残しています。

src. 5 のように XAML へ CommandParameter を直接書いた場合(Async)ReactiveCommand の型パラメータには string しか指定できませんが、CommandParameter はバインドを設定することもできます。ですが、CommandParameter をバインドすると言う事は VM 側でもその時設定する値は分かっているので意味がありません。そのため、CommandParameter をバインドする必要は無さそうです。

つまり、CommandParameter はバインドするのではなく XAML に直接値を記述するための項目だと言えるので、主な用途は複数コントロールの CommandSource を 1 つの(Async)ReactiveCommand にバインドするような場合に、どのコントロールからの Command なのかを判別するような場合に限られると思います。

ちなみに、複数コントロールの CommandSource を 1 つの(Async)ReactiveCommand にバインドする場合でも特別な設定は不要で、今までのサンプルと同じくバインド先を指定するだけです。
VM から通知する CanExecute は 1 つなので片方は true、もう一方は false 等と言う事はできません)



コントロールのイベントを VM で受け取る

これまでは Button.Command のようなコントロール側に用意されている Command にバインドする例を紹介してきました。Windows Form ではコードビハインドのイベントハンドラに処理を書く手法が当たり前だったので WPF でもイベントを VM で処理したい場合もあると思います。

WPF でもコードビハインドにイベントハンドラを作成する事はできますが、MVVM パターンで作成する場合はコードビハインドに処理を書くことは躊躇うでしょうし、イベントも VM で処理したいと考えると思いますが、Windows Form で使用していたイベントが全て Command として定義されている訳ではありません。

イベントは Command に変換すれば VM で受け取れるようになるので、ReactiveProperty ではイベントを ReactiveCommand や ReactiveProperty に変換するための EventToReactiveCommand や EventToReactiveProperty が用意されていますが、最初にインストールした ReactiveProperty のパッケージには含まれていないので、WPF 用の別パッケージを追加インストールする必要があります。

EventToReactiveCommand や EventToReactiveProperty を使用するための【ReactiveProperty.WPF パッケージ】も、fig. 5 の『ソリューションの Nuget パッケージの管理画面』で『reactiveproperty』又は『reactive』を検索してインストールします。

fig.5 ReactiveProperty.WPF パッケージのインストール

【ReactiveProperty.WPF パッケージ】をインストールするとイベントを ReactiveProperty や ReactiveCommand に変換する事はできますが、XAML では EventToReactiveCommand や EventToReactiveProperty を呼び出すためのトリガーを指定する必要があるため『System.Windows.Interactivity.dll から Xaml.Behaviors.Wpf へ』で紹介した Xaml.Behaviors.Wpf も必要になります。

但し、Xaml.Behaviors.Wpf は上でインストールした ReactiveProperty.WPF パッケージにも含まれているので、別途インストールする必要はありません。

イベントを Command に変換する

例として部分 View(UserControl)の Loaded イベントを Command で受け取る方法を紹介します。src. 7 のように XAML へハイライト部分を追加します。

System.Windows.Interactivity.dll から Xaml.Behaviors.Wpf へ』で紹介した通り XAML でイベントをハンドルするには Interaction.Triggers の EventTrigger を使用します。

EventTrigger.EventName は正式なイベント名を Microsoft Docs 等で調べて指定します。

後は Command に VM へ定義した(Async)ReactiveCommand を指定すると VM でイベントを受け取れるようになります

Prism にも EventToReactiveCommand と同じくイベントを Command に変換するための InvokeCommandAction が含まれていますが、System.Windows.Interactivity.dll に依存しているため、現時点では .NET Core、Framework のどちらを選択した場合でも Prism の InvokeCommandAction は使用できないと考えた方が良いと思います。

ReactiveProperty を使用しない場合、Xaml.Behaviors.Wpf に含まれる Prism に含まれる同名の InvokeCommandAction クラスを使用すれば EventToReactiveCommand と同じようにイベントを Command に変換できます。この Xaml.Behaviors.Wpf に含まれる InvokeCommandAction をサラっと見た所 Prism の InvokeCommandAction より高機能っぽいのでReactiveProperty を使用しない場合は Xaml.Behaviors.Wpf に含まれる InvokeCommandAction を使用した方が良いかもしれません。

src. 8 のように Command に変換したイベントを受け取ることができますが、通常の Command の場合と変わりません

実行するとイミディエイトウィンドウに『Loaded!』が出力されるので Loaded イベントが受け取れている事が確認できます。

本章ではイベントを Command に変換してバインドする方法を紹介しましたが、基本的に EventToReactiveCommand や EventToReactiveProperty の使用は極力避けるべきだと管理人は考えています。イベント以外に選択肢が無い場面が存在するのも確かですが、本来上のような Loaded イベントは VM のコンストラクタに書けば済みます(書くべきです)。

Prism 等のフレームワークが持つ機能を把握しきれなくて代わりにイベントを使ってしまう等、多少しょうがない場合がある(管理人にもありました)事も理解していますが、WPF アプリを MVVM パターンで作成するならまず、プロパティの変更通知で処理できないかを探す方が重要だと思います。

Xaml.Behaviors.Wpf + EventToReactiveCommand でイベントを処理するのは確かにお手軽かもしれませんが、プロパティの変更をトリガーに処理を書くのが MVVM パターンの基本だと思うので、イベントに処理を書こうと考えた時には、その前に ReactiveProperty の Subscribe 等で対応できないかを考える事は重要です。

EventArgs を取得する

イベントが受け取れると EventArgs も取得したくなる場合もあると思いますが、EventToReactiveCommand、EventToReactiveProperty のどちらを使った場合でも取得できます。かずきさんが書かれた『MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 後編』では EventArgs を Converter で変換する例を紹介されていますが、Converter を通さず EventArgs 自体を受け取る事もできます

src. 9 は上のかずきさんの記事を真似て MouseMove をハンドルした XAML です。

XAML 側は Loaded イベントをハンドルした場合と変わりません。以降は EventToReactiveCommand の例を紹介しますが、一応 EventToReactiveProperty のバインド例もコメントアウトして残しています。

コメントアウトしている EventToReactiveProperty.ReactiveProperty は今までと同じくバインディング先のプロパティ名を指定しますが【.Value】を付けるとバインドされないので気を付けてください。

src. 9 とバインドする VM が src. 10 です。

VM で EventArgs を受け取ると Source や OriginalSource プロパティ等から Command 通知元のコントロールや Window まで取得できてしまうので、ガッツリ View に依存した処理を書くこともできてしまいます。そのため本来は、上で紹介した Qiita のかずきさんの記事 のように Converter を通して渡す値を絞るべきだと思うので、このサンプルは EventArgs を手軽に受け取ることができる事を示すためのサンプルと考えてください。

src. 10 の MousePoint コンストラクタに指定した mode パラメータは同一座標でも変更通知を受け取るために none を指定しています。パラメータの受け取りは型パラメータの指定が増えるだけで今までの記述と変わりません。

実行すると fig. 6 のように『読込ボタン』の右に座標が表示されます。

fig.6 MouseMove イベントで座標を表示

新しい画面を用意するのが面倒だったので、src. 9 の通り UserControl の MouseMove を拾っていますが、どうもコントロールを配置した所しかイベントが発生しないようなので、動作を確認するだけのサンプルと考えてください。



Model ⇔ ViewModel ⇔ View をそれぞれ双方向でバインドした場合の Model のインタフェース

ここまでで ReactiveCommand の基本的な使い方は紹介できたと思うので、いよいよ本題の Model ⇔ VM ⇔ View 間をそれぞれ双方向でバインドした場合の Model のインタフェースについて紹介します。

尾上さんが書かれた Model のインタフェースについての記述をここでも再度引用します。

ViewModel に公開する Model のインタフェースは以下の二つしかありません。

  • Model のステートの公開とその変更通知
  • Model の操作のための戻り値のないメソッド

ステートの公開とその変更通知を行うのは簡単な話でしょう。リッチクライアントの Model はステートフルです。そのステートを公開しないと ViewModel と View は表示すべき情報がありません。そしてその変化を ViewModel と Viewに伝えるために変更通知を行うのも当然の事です。

Model の操作のためのメソッドには戻り値がない・・これにはひっかかる方も多いかもしれません。しかしこれも難しい話ではありません。

Model のメソッド呼び出しは何をもたらすのでしょうか?。それは Model 内状態の変化(あるいは外部サービス呼び出しとそれに伴う Model 内状態の変化)と、なんらかのイベント発生(通信エラー発生とか)しかないのです。ViewModel が Model の影であれば当然それしかないのです。ViewModel が Model を呼び出して Model から戻り値を受け取って何になるんでしょう?それは Model 内のステートの不完全な意味のないコピーでしかありません

ViewModel に対する Model のインターフェース

step: 6 でも紹介した通り、Model のインタフェースは上の引用を参考に設計します。

まず、本エントリの最初で紹介して未実装のまま放置していた fig. 7 の『読込ボタン』を実装します。

fig.7 サンプル画面

色々な動作紹介のため fig. 1 からコントロール類が増えていますが、赤枠で囲んだ『読込ボタン』の処理は src. 11 のようになります。

上の引用の通り Model のインタフェースはデータを取得する場合でも戻り値を返さず、設定対象のエンティティ系モデルをメソッドのパラメータに渡すようにします。Model ⇔ VM 間を双方向でバインドする場合、VM 初期化(コンストラクタ)時にエンティティ系モデル(PersonSlim)のインスタンスが必要なので、Model からの戻り値を受け取らないのは当然と考えられます。

step: 4 でも書きましたが、Model のインタフェースにしている IDataAgent は内部で使用する PersonRepository と生存期間が等しい前提のため止む無く ServiceLocator パターンでインスタンスを取得していますが、推奨される方法ではないので気を付けてください。

大事な事なので何度も書いていますが、本来 IDataAgent はコンストラクタへインジェクションされても問題無い構造で作成すべきです。

Model 側のインタフェース

src. 12 は『読込ボタン』Command から呼び出す IDataAgent.UpdatePersonSlimAsync の中身です。

UpdatePersonSlimAsync の第 1 パラメータは『ID項目』の値を渡すためですが、第 2 パラメータの person は View の入力内容がリアルタイムで反映されているため、fig. 7 のように person から ID の値も取得できるので本来は不要です。

fig.7 person パラメータ

src. 12 の 19 行目にブレークポイントを張って person パラメータの内容を確認すると、fig. 7 の通り『Id』だけでなく他のプロパティも View で入力した値を保持している事が分かると思います。つまり View の入力値が即座に Model へ伝播するので VM で値を移し替える必要がありません。そのため、VM は単なる中継役に徹する事ができるようになり薄い層として作成できます。

又、person パラメータの内容確認後に再度実行すると src. 12 最下段の GetPersonSlimAsync から返す値に変わることも確認できます。



AutoMapper の利用

src. 12 の UpdatePersonSlimAsync メソッド内で、データアクセス層から受け取った値を UI 層から受け取った PersonSlim クラスに移し替えています。データの保存先が RDBMS の場合は WPF MVVM L@bo #5 で紹介したように Dapper 等の ORM を使用して値を取得する事も多いと思います。

又、前回も紹介した通り ReactivePropertySlim 型のプロパティを含むクラスはシリアライズ・デシリアライズできないので、データアクセス層では POCO なクラスを使う方が都合が良い場合が多いと思います。

そんな場合は『AutoMapper で ReactiveProperty にマッピング』で紹介した AutoMapper を使用して値の入れ替えを自動化することもできます。

AutoMapper を使用する場合は、src. 13 のようにデータアクセス層から POCO な PersonDto クラスを取得するように変更します。

SampleUtilities から呼び出している 2 つのメソッドは XmlSerializer で XML ファイルへ保存・読込しているだけの単純なメソッドです。AutoMapper を使用すると値の移し替え部分は src. 14 のようになります。

src. 14 のようにデータアクセス層で POCO なクラスを使用すればシリアライズもできますし、WPF MVVM L@bo #5 で紹介した SqlMapper を書く必要もありません

ですが『AutoMapper で ReactiveProperty にマッピング』でも書いていますが、ここで紹介しているようなエンティティ系モデルの数が少ないアプリでは AutoMapper を導入しても省力化は見込めません

逆に、エンティティ系モデルの数が多い場合や、プロパティ数の多いエンティティ系モデルをいくつも扱うアプリを作成する場合は導入を検討する価値はあると思います。

尚、このエントリで紹介しているサンプルのソリューションと『AutoMapper で ReactiveProperty にマッピング』で紹介しているサンプルのソリューションは共通なので、マッピング設定を書いた MapperConfiguration のソースコードは上の記事か GitHub リポジトリ を直接見てください。

AutoMapper は MVVM パターンのアプリを作成するのに必須ではなくあれば便利なユーティリティですが、この連載で紹介するサンプルアプリには使用します。

Model 用に Adapter を追加する

ここまで紹介した通り、Model ⇔ VM 間が双方向でバインドされていれば、Model のインタフェースには戻り値を返さないメソッドを用意すれば済みます。(非同期処理の Task 等は除く)
ですが、ここで紹介しているような単純なサンプルならともかく、複数のエンティティ系モデルをバインドするような画面の場合は Model と VM の間にもう 1 つクラスを用意した方が見通しが良くなる場合もあると思います。

Adapter を追加した場合の Model ⇔ VM 間

管理人の個人的な考えですが、Adapter パターンを使って各画面用の Adapter を作成する方法を考えてみました。例えば、src. 15 のような ReactiveSamplePanel 用の Adapter を Model(アプリケーションロジック層のプロジェクト)に作成します。

これまで VM に書いていた処理をまとめただけのクラスです。ポイントは Model に相当するアプリケーションロジック層のプロジェクトに置くクラスである点と、Viewとバインドするエンティティ系モデルをプロパティとして明示できる点です。つまり、Viewとバインドするエンティティ系モデルが増えた場合は Adapter にプロパティとして公開する形をとります。

Adapter を使用した VM

src. 15 の Adapter は src. 16 のように VM へインジェクションして Model ⇔ VM 間の双方向バインド先も変更します。

基本的な流れは今まで紹介してきた内容と変わりませんが、VM にインジェクションするのは Adapter だけで済むようになり、メソッドもバインドするプロパティも全て Adapter を参照するように変わるので VM は Adapter に用意されるインタフェースだけ見れば良く、シンプルな構造になると思います。

とは言え、このサンプルアプリの場合では Adapter を作成すると手間が増えるだけであまり有用ではないと思いますが、View とバインドするエンティティ系モデルクラスが 2 つ、3 つ又はそれ以上になる場合は、Adapter にインジェクションするだけで済みます(VM 側のインジェクションは変更不要)。

加えて、Model ⇔ VM 間が双方向でバインドされていれば View の入力内容が Model に伝搬されるので Adapter に定義するメソッドにはパラメータも必要もありません

Adapter を使用した場合でもサンプルアプリは fig. 8 のように動作します。

fig.8 Adapter を追加したサンプルアプリの動作

デモ用にデータの保存先・読込先になる XML を別ファイルにしたので、保存後でも以前のデータが読み込めるようになっていますが、Adapter を追加しても動作は変わりません。

fig. 8 で保存した内容は src. 17 のように XML ファイルに保存されます。

管理人的に Adapter が有用だと思うのは特定の画面専用 Adapter として作成する点です。MVVM パターンで設計する場合の Model(アプリケーションロジック層)は View(UI 層)を意識しない設計にするのが基本だと思いますが、ユーザ要望等の都合からエンティティ系モデルに含めたくない画面固有の情報を取得したい場合もあると思います。

そのような場合に Adapter が間にあれば Adapter にバインド用にプロパティを追加する事もできる等、それなりに便利な場合も多いと思いますが、src. 15 のような Adapter は必須ではありませんし、あくまでも方法の 1 つです。
Adapter を作成する手間が増えるのは間違いないのでデメリットもあると思いますが、この連載では Model へ本章で紹介したような各 VM 用の Adapter を作成する方法で進めます。

まとめ的な

ここまで紹介した通り、Prism + ReactiveProperty を使用して MVVM パターンを適用した WPF アプリを作成するためのポイントは以下の 3 つです。

  • Prism では View と VM は自動的に DI コンテナへ登録されるので、自分で作成する Model も DI コンテナへ登録してインジェクションされる構造で作成する
  • Model 内のエンティティ系モデルのプロパティにも ReactivePropertySlim を使用して、Model ⇔ VM ⇔ View 間をそれぞれ双方向でバインドしてデータが連環する構造で作成する
  • Model 内のコントローラー、サービス等の VM から呼び出すインタフェースになるメソッドは、戻り値を返すのではなく VM と双方向でバインドするエンティティ系モデルをパラメータに渡して Model 内部で値を設定する

上記 3 点に加えて補助的に Adapter パターンを適用する方法も紹介したので MVVM パターンで WPF アプリを作成するための Model のインタフェースは紹介できたと思います。後は ListBox 等のリスト系コントロールとバインドする方法の紹介が残っていますが、次回で紹介します。

次回紹介する ReactiveCollection は今まで紹介してきた単一値のバインドではなくリストとのバインドですが、基本的な考え方は今まで紹介してきた Model ⇔ VM 間の双方向バインドや Model のインタフェース等は変わらず、VM をどのように薄く作るかと言う点に注目して紹介したいと思っています。

尚、今回紹介したサンプルコードもいつもの通り GitHub リポジトリ に上げています。

 

 

 

おすすめ

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

%d人のブロガーが「いいね」をつけました。