ReactiveProperty を編む【step: 7 .NET Core WPF MVVM ReactiveProperty 入門 2020】

前回記事「Prism に Model ⇔ VM の双方向バインドは難しい【step: 6 .NET Core WPF Prism MVVM 入門 2020】」

 

前回は Prism だけで Model ⇔ VM 間を疑似的に双方向バインドする方法等を紹介しましたが、結果的に Prism だけで実現するのは難しい事も分かりました。そのため、今回は Prism の MVVM サポートクラス等は使用せず ReactiveProperty を使用してデータを Model ⇔ VM ⇔ View 間をそれぞれ双方向でバインドする方法を紹介します。

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

Prism と ReactiveProperty

前回は Prism だけで Model ⇔ VM 間を疑似的に双方向バインドする方法を紹介しましたが、Prism だけでは値変更の通知はできても変更の通知を受け取ることができず Model ⇔ VM 間を双方向でバインドするメリットが感じられるサンプルも紹介できませんでした。

ですが、前回紹介した通り『Prism だけでできなければ ReactiveProperty を使えばいいじゃない』的な感じで今回は ReactiveProperty を使用したデータバインディングについて紹介します。

とは言え、紹介するサンプルは今まで通り Prism Template Pack から作成したプロジェクトを使用しますし、部分 View の表示や Ioc を実現するための DI コンテナ等も今までと変わらず使用します。

Prism は元々疎結合に作成された部品を動的に組み合わせる複合アプリケーションを作成するためのフレームワークなので MVVM サポートクラスが何かに依存していることはありませんし、ReactiveProperty も各種フレームワークと併用する前提で設計されているため Prism と組み合わせて使用する事に問題はありません。

ReactiveProperty とは

ReactiveProperty は元々 neuecc さん双方向にバインド可能な IObservable<T> を拡張して作成された MVVM 拡張ライブラリです。

ReactiveProperty はゲームエンジンの Unity 用に最適化されている UniRx もあり、現状(2020/9 現在)では UniRx は neuecc さん、WPF 用の ReactiveProperty は『かずきのBlog@hatena』を主宰されているかずきさんがメンテナーをされています。

最近(2020/9 現在)のかずきさんは上で紹介したブログではなく Qiita に記事を投稿されることが多く、以前は自身のブログに書かれていた ReactiveProperty のオーバービューも最新版は Qiita で公開されています。

このエントリはかずきさんが Qiita に投稿された『MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 前編 – Qiita』に近い範囲の内容を扱っています。

範囲的には近い内容を扱っていますが、このエントリは WPF アプリのデータバインディングに重点を置いて、より初心者向けの内容にしたつもりです。



MVVM 拡張ライブラリ と MVVM フレームワーク

いきなりの余談で申し訳ありませんが、最近(2020/9 現在)『ReactiveProperty はフレームワークだ』と書かれたブログを見かけましたが、管理人的に ReactiveProperty はフレームワークではなく上に書いた通りライブラリだと思っています。まあ、ブログへのリンクを張って真っ向から反論したい訳ではなく、管理人が考える用語の定義を紹介するのが目的です。

以下に Livet 作者の尾上さんのツイートを引用します。

上のツイートを見て、管理人もフレームワークとは Prism のように想定したシナリオに合わせてアプリを構成する機能を提供するものだと思うようになりました。

そして ReactiveProperty は View ⇔ VM のみに使用する事も、後で詳しく紹介しますが View ⇔ VM ⇔ Model 間のバインディングに使用する事もできるので『様々な用途に使える』ライブラリに分類されると思います。一応、当サイト内の記事は上で引用した尾上さんのツイートの内容を踏まえて用語を使い分けているつもりです。

ReactiveProperty のインストール

ReactiveProperty のインストールも fig. 1 のように【ソリューションの Nuget パッケージの管理画面】から『reactiveproperty』又は『reactive』を検索します。

fig.1 Nuget で ReactiveProperty を検索

ReactiveProperty は Ver. 7.0.0 から分割したパッケージでリリースされるようになりましたが、とりあえず fig. 1 の通り【ReactiveProperty】をインストールすれば OK です。

Reactive プログラミングについて

ReactiveProperty は有名なライブラリなので Qiita 等を始めとして紹介記事も多く見かけますが、Reactive Extensions が提供する拡張メソッドに馴染めず ReactiveProperty 自体を諦めた人も居ると思います。確かに Reactive Extensions を一口で説明するのは難しいと思いますし、馴染めそうにないからと言う理由で手出しを拒む人が居るのも理解できますが、実は管理人も Reactive Extensions が提供する拡張メソッドは大して理解できていないので恐れることはありません。

本来であれば『連載:Reactive Extensions(Rx)入門 「第1回 Reactive Extensions の概要と利用方法」- @IT』等で紹介されている fig. 2 のような概念の理解は必須だと思っています。

fig.2 イベント/非同期/配列が同じ時間軸に乗るイメージ図

第1回 Reactive Extensionsの概要と利用方法

管理人の個人的意見ですが fig. 2 の図が理解できていなくても特に問題はありません。と書いてしまうと反発があるかもしれませんが、WPF のデータバインディングに使う程度であれば Reactive Extensions が提供する拡張メソッドを使わなければならない機会は大して多い訳ではなく ReactiveProperty に含まれる拡張メソッドだけで事足りる場合も多いと思います。

Reactive Extensions 特有の拡張メソッドをどう適用するか分からないとしても、Linq にも存在する Select や Where くらいは分かると思いますし、ぶっちゃけデータバインディングに限って言えば Reactive Extensions が提供する拡張メソッドを使用しなくてもどうにかなる場合も多いと思うのでとりあえず読み進めてみてください。

ですが、Reactive Extensions を本格的に使いこなしたいと思っている場合は当然、概念から理解できている必要があり、上で紹介した ReactiveProperty の作者である neuecc さんこと河合 宜文さんが書かれた『連載:Reactive Extensions(Rx)入門 – @IT』やじんぐるさんが書かれた『Rx入門 – xin9le.net』等で 1 から勉強すべきだと思います。

ちなみに管理人は両サイトとも読むのを途中で断念していますが、こんなエントリが書ける程度は理解できているつもりですし、そこまで困ったことはありません



ReactiveProperty の基本的な使い方

ReactiveProperty には step: 2 で紹介した WPF のデータバインディング用インタフェースの実装クラスが全て含まれています。

ReactiveProperty には以下のクラスを始めとして他にも数多くのクラスが含まれています。

  • ReactiveProperty<T>
  • ReadOnlyReactiveProperty<T>
  • ReactivePropertySlim<T>
  • ReadOnlyReactivePropertySlim<T>
  • ReactiveCollection<T>
  • ReadOnlyReactiveCollection<T>
  • ReactiveCommand
  • AsyncReactiveCommand

主要クラスだけでも数が多いので記事を複数回に分けますが、少なくとも上のクラスは全てこの連載内で紹介する予定です。

ReactiveProperty と ReactivePropertySlim

【ReactiveProperty】の中心的存在と言える ReactiveProperty と ReactivePropertySlim は INotifyPropertyChanged を継承したクラスなので VM のプロパティに利用できます。前回 Prism だけで作成した fig. 3 の画面を ReactiveProperty で実装し直します。

fig. 3 データバインディングサンプル画面

fig. 3 画面の VM を ReactiveProperty で実装し直すと src. 1 のようになります。

前回紹介した Prism で作成した場合と比べて驚くほどシンプルになるのが一目で分かると思います。

ReactiveProperty を導入する最大の目的は Model ⇔ VM 間を双方向でバインドする事ですが、Prism の BindableBase と比べてコード量も減らせて見易くなるのも大きな利点の 1 つです。

そして、src. 1 では全てのプロパティを【ReactiveProperty】ではなく【ReactivePropertySlim】で定義しているのは以下の『Slim で非サポートの機能』を使用しないからです。

  • タイマー機能
  • Validation サポート
  • INotifyPropertyChanged を継承したエンティティ系モデルとの双方向バインディング
  • イベントを UI スレッドへ自動ディスパッチ

ReactiveProperty 作者の neuecc さんはゲーム開発を多く手掛ける人なのでタイマーを使う機会も多いのかもしれませんが、業務系のようなアプリでタイマーを使用する事は多くないと思うので、管理人にはタイマーを使用する良いサンプルが思い付きません。管理人的に馴染み深い業務系アプリに重要なのは【Validation サポートの有無】だと思います。

とりあえずこのエントリでは Validation まで紹介できないので次回以降に繰り越しますが、Validation を設定しないプロパティの場合は無印の ReactiveProperty より高速に動作する【ReactivePropertySlim】で充分カバーできると考えて良いと思います。尚、ReactivePropertySlim の内部構造等は neuecc さんが『ReactivePropertySlim詳解 – neue cc』で詳しく紹介されています。

又、かずきさんも Qiita で ReactivePropertySlim のベンチマーク を紹介されています。

実際にアプリを作成していて『Slim 有り・無し』の速度差を体感した事はありませんが、比較数値を見ると Slim 付を使用したくなるのは開発者としての性なのかもしれません。

ReactiveProperty の初期化

又、src. 1 は ReactiveProperty の宣言と初期化のオーソドックスな記述サンプルです。14 行目でコメントアウトした箇所のように定義部で初期化する事もできますが、Model とバインドしたり Dispose 処理を追加したい場合がほとんどなので src. 1 のようにコンストラクタで初期化する方が良いと思います。

ReactiveProperty(Slim) のコンストラクタには以下の 3 パラメータが定義されています。全て省略可能なパラメータなので指定する機会はあまり多くないかもしれませんが『initialValue や mode』の設定頻度は高いと思います。

パラメータ名内容
initialValueプロパティの初期値を指定します。省略した場合は default(T) が設定されます。
mode以下の ReactivePropertyMode 列挙型から 1 つ又は 複数を組み合わせて指定します。

  • Defult
    • 省略した場合に設定される値。DistinctUntilChanged と RaiseLatestValueOnSubscribe を指定した場合と同じ
  • DistinctUntilChanged
    • Value に同じ値を設定した場合、変更通知を発行しません。DistinctUntilChanged を含まない Mode を設定すると同じ値を設定しても変更通知が発行されます
  • RaiseLatestValueOnSubscribe
    • Subscribe 時に現在の値で OnNext を呼び出す
  • IgnoreInitialValidationError
    • 初回のバリデーションエラーを無視する。Slim に設定しても意味が無い
  • None
    • 指定無し

上記の値は『| 演算子』で複数の値が指定可能です。

equalityComparer値を比較する IEqualityComparer<T> を指定します。省略した場合は null が設定されます。

Value に同一の値を設定した時でも変更通知を発行したい場合、無印の ReactiveProperty では mode: RaiseLatestValueOnSubscribe | IgnoreInitialValidationError のように設定すると 同一値を設定した場合でも変更通知が発行され、初回(画面の初回表示時等)のバリデーションエラーが無視されます。

Slim 付(ReactivePropertySlim)の場合は mode: RaiseLatestValueOnSubscribe で同一値を設定した場合でも変更通知が発行されます。Slim でも IgnoreInitialValidationError は設定可能だと思いますが、Validation をサポートしていないので指定する意味はありません

ReactiveProperty のコードスニペット

前回は Prism Template Pack に付属するコードスニペットを紹介しましたが、ReactiveProperty にもコードスニペットは用意されています。但し、ReactiveProperty の GitHub リポジトリ から直接ファイルを取得して手動でインストールする必要があります

GitHub からファイルを取得する方法はいくつかありますが、fig. 4 のように zip で取得する方法が楽だと思います。

fig.4 リポジトリのダウンロード

インストールは以下の手順で行います。

  1. 以下の手順実行前に Visual Studio が起動中であれば終了します。
  2. fig. 4 のように GitHub から zip をダウンロードします。
  3. 『[マイドキュメント]\Visual Studio 2019\Code Snippets\Visual C#\』配下へ『ReactivePropertySnippets』的な任意の名前のフォルダを作成します。
  4. ダウンロードした zip 内の『ReactiveProperty-master\Snippet\csharp6\』に格納されている全ファイルを上で作成したフォルダへコピーします。

コピー後に Visual Studio 2019 を起動して [ツール] – [コードスニペットマネージャー] から表示する fig. 5 で追加したコードスニペットが確認できます。

fig.5 コードスニペットマネージャー

コードスニペットを追加すると fig. 6 のように【rprops コードスニペット】、【rprop コードスニペット】でプロパティが入力できるようになります。

fig.6 ReactiveProperty(Slim) のコードスニペット

WPF Prism episode: 5 では Nuget のパッケージにコードスニペットが含まれると書いていますが、現時点の最新バージョン(Ver. 7.2.0)のパッケージ内には見当たらなかったので上で紹介したように GitHub リポジトリ から取得するしかないと思います。

普段『prop コードスニペット』等を使ってプロパティを入力している人には便利だと思います。

ReactiveProperty とバインドする XAML

src. 1 で紹介したようにプロパティを ReactiveProperty で定義した VM はコード量がかなり少なくなりますが、XAML 側は src. 2 のようにほんの少しだけ記述が増えます。

src. 2 9 行目のようにバインド先のプロパティが ReactiveProperty の場合はプロパティ名に【.Value】を追加する必要があります前回の『子 VM の章』でも紹介しましたがバインド先のプロパティはクラスのメンバも指定可能なので、ReactiveProperty の場合は値を入出力する【Value プロパティ】を指定します。

管理人の場合、最近でこそ ReactiveProperty に慣れたので XAML へ【.Value】を追加し忘れる事は減りましたが、当初は .Value を追加し忘れて値が表示されない事も多かったので、項目に値が表示されないような場合は、まず最初に XAML へ【.Value】が記述されているかを確認する癖を付けると良いと思います。

ReactiveProperty のメモリリーク

ここで src. 1 を再掲します。

ReactiveProperty は INotifyPropertyChanged を継承しているので、VM 自体が INotifyPropertyChanged を継承しなくても ReactiveProperty 自体が変更通知を発行するため本来は必要ありませんが、src. 1 の 8行目は前回と変わらず BindableBase を継承しています。

これは継承元を削除し忘れたわけではなく『【WPF】ViewModelがINotifyPropertyChangedを実装していないとメモリリークする件 – aridai.NET』等で紹介されている通り、DataContext に設定するクラス(通常は ViewModel)が INotifyPropertyChanged を継承していない場合、メモリリークする可能性があるからです。

これは ReactiveProperty の問題ではなく View が VM のプロパティを探す際に使用する PropertyDescriptor が原因なので、ReactiveProperty の使用・不使用に関わらず VM は INotifyPropertyChanged(BindableBase)を継承するのが基本だと思ってください。



ReactiveProperty の Dispose

又、上で紹介した PropertyDescriptor でメモリリークする事とは無関係に ReactiveProperty で定義したプロパティは Dispose しないとメモリリークする可能性があります。

src. 1 のように自身が値を保持するだけの ReactiveProperty の場合は Dispose する必要は無いようですが、後述する Subscribe 等で値の変更を監視しているような場合は Dispose しないとメモリリークする可能性があるそうなので、VM を破棄するタイミングで Dispose すべきです。

step: 5 で紹介した通り、Prism の部分 View の VM は IDestructible を継承すると破棄できます

ReactiveProperty は IDisposable を継承しているので、IDestructible.Destroy メソッド内で ReactiveProperty で定義した全プロパティを Dispose するだけですが、プロパティの数が多いと面倒です。そんな場合、src. 3 のように CompositeDisposable と ReactiveProperty に用意されている AddTo 拡張メソッドを利用すると一括 Dispose できます

ReactiveProperty(Slim) 初期化時に AddTo 拡張メソッド呼び出すように変更しました。src. 3 のように ReactiveProperty(Slim)を CompositeDisposable へ追加しておくと CompositeDisposable を Dispose するだけで追加したクラスを一括で破棄できます

又、Shell の VM も同様に CompositeDisposable を用意しておくと step: 5 で紹介した DataContextDisposeAction 等を利用して Shell(VM)で定義している ReactiveProperty も併せて Dispose できます

尚、CompositeDisposable は ReactiveProperty 専用ではなく IDisposable を継承したクラスであれば何でも追加できますし、AddTo 拡張メソッドも IDisposable を継承したクラスであれば src. 3 と同じように呼び出せるのでフィールドに定義した IDisposable なクラスは全て CompositeDisposable に追加して破棄できます。

ReactiveProperty で Model ⇔ ViewModel 間も双方向でバインドする

ここまでは ReactiveProperty で View ⇔ VM 間をバインドする方法を紹介して来ましたが、ここからは本題の Model ⇔ VM 間の双方向バインドについて紹介します。VM と双方向でバインドする Model は前回紹介した BindableBase を継承したクラスと、プロパティを ReactiveProperty(Slim)で定義したクラスの 2 種類が考えられます。

まずは、前回紹介した BindableBase を継承した Person クラスと VM を双方向でバインドする例を紹介します。

INotifyPropertyChanged を継承したエンティティ系モデルクラスと双方向でバインドする

前章までは VM のプロパティを ReactivePropertySlim で定義していましたが、Slim 付は BindableBase(INotifyPropertyChanged)を継承したクラスとの双方向バインディングに対応していないので、VM のプロパティを src. 4 のように Slim 無しの ReactiveProperty に書き換えました。

BindableBase(INotifyPropertyChanged)を継承したクラスと双方向でバインドするには ReactiveProperty に含まれる INotifyPropertyChanged.ToReactivePropertyAsSynchronized 拡張メソッドを使用します。src. 4 で person  フィールドは BindableBase(INotifyPropertyChanged)を継承したクラスなので、ToReactivePropertyAsSynchronized 拡張メソッドを呼び出す事ができます。ToReactivePropertyAsSynchronized 拡張メソッドには以下のパラメータが定義されています。

パラメータ名内容
propertySelector左辺で指定した ReactiveProperty とバインドするクラス(src. 4 では this.person)のプロパティを選択します。ここで指定するプロパティと左辺の ReactiveProperty に設定した型パラメータは同一の型で定義されている必要があります。
modeReactiveProperty(Slim) のコンストラクタ第 2 パラメータと同じく ReactivePropertyMode を指定します。省略した場合は Defult が設定されます。
ignoreValidationErrorValueVM に指定した Validation がエラーになった場合、バインド先の Model のプロパティに値を反映するかを bool で指定します。Validation でエラーになった値を Model 側に反映したくない場合は true を設定します。

省略した場合のデフォルト値は false なので Validation を指定したプロパティと Model を双方向でバインドする場合は、true に設定すべきパラメータです。

convertModel を ReactiveProperty(VM 側プロパティ)に値を変換する場合の変換式を Linq で指定します。
convertBackReactiveProperty(VM 側プロパティ)を Model に変換する場合の変換式を Linq で指定します。

propertySelector は指定必須ですが、それ以外に設定頻度が高いのは ignoreValidationErrorValue と mode だと思います。どちらも Validation が関係するパラメータなので Validation を紹介する際に再度紹介する予定です。

BindableBase(INotifyPropertyChanged)を継承したクラスと VM を双方向でバインドする場合の方法は以上です。src. 4 のように VM と双方向でバインドした状態で実行すると View で変更した値が person フィールドに反映されることが確認できます。逆に person フィールドの値を更新すると View の表示が更新されることも確認できます。

プロパティを ReactivePropertySlim で定義した Model と双方向でバインドする

本項ではプロパティを ReactiveProperty(Slim)で定義した Model と VM を双方向でバインドする例を紹介します。

Person クラス自体は BindableBase(INotifyPropertyChanged)を継承したクラスの例として残しておきたいので、新たに src. 5 のような PersonSlim クラスを作成しました。

Model のプロパティを ReactivePropertySlim で定義する最大の利点は BindableBase のようなクラスの継承が必要無い点です。又、BindableBase を継承して作成したクラスと比べてコード量が大幅に減るのもメリットの 1 つです。

加えて、ReactivePropertySlim は Ver. 7.0.0 以降から【ReactiveProperty.Core パッケージ】に分割され、Reactive Extensions への依存も削除されたので採用する理由が更に増えました。(後述しますが、ReactivePropertySlim.Subscribe 等の使用には Core パッケージではなく ReactiveProperty のフルパッケージが必要です)

Model 側のプロパティが Slim、VM 側が無印の ReactiveProperty の場合に Model ⇔ VM 間を双方向でバインドするは src. 6 のようにします。

2020/9/17 追記
かずきさんから指摘を頂いて気が付きましたが、変更前は src. ex-1 のように書いていました。

src. ex-1 のように【ToReactiveProperty】すると View で入力した内容が反映されません。動作するはずと思い込んでいて確認せず載せてしまい申し訳ありませんでした。

BindableBase を継承した Model と双方向でバインドする場合と同じく【ToReactivePropertyAsSynchronized】で Model のプロパティ.Value を指定すると双方向でバインドできます

又、Model、VM 両方のプロパティを ReactivePropertySlim で定義した場合は src. 7 のようにします。

2020/9/17 追記
src. 6 と同じくかずきさんからの指摘で気が付きましたが、変更前は src. ex-2 のように書いていました。

管理人的には VM 側のプロパティを disposable に追加するつもりで書いていましたが、ex-2 のように書いてしまうと Model 側(ここでは personSlim)のプロパティが AddTo されてしまうので意図した動作になりません。
申し訳ありませんでした。

src. 7 の【ToReactivePropertySlimAsSynchronized】はかずきさんから指摘頂いた後に ReactiveProperty Ver. 7.3.0 で新規に追加された拡張メソッドなので、古いバージョンを使用している場合は最新版に更新してください。
かずきさん、追加ありがとうございました

ReactiveProperty Ver. 7.3.0 から追加された【ToReactivePropertySlimAsSynchronized】は src. 6 で紹介した Slim ⇔ 無印間を双方向でバインドする【ToReactivePropertyAsSynchronized】に『Slim』が付いただけで使い方は変わらず【.Value】を指定すると双方向でバインドできます。

今まで紹介したソースコードは全てコメントアウトして残しているので見にくいかもしれませんが、コメントを切り替えて動作が確認できます。前回紹介した Prism の MVVM サポートクラスで疑似的に双方向バインドする実装と比べて View 側を更新する呼び出しが無く直感的な実装になっていると思います。

ここで 1 つ補足します。上で VM は BindableBase(INotifyPropertyChanged)を継承しないとメモリリークする可能性があると書きましたが、Model の場合は必要ありません。VM の場合は View がバインド先のプロパティを検索する過程で PropertyDescriptor 使用する可能性がある事が原因でしたが、Model ⇔ VM の場合はプロパティを直接指定しているので PropertyDescriptor 等は使用されません

Model の場合、BindableBase(INotifyPropertyChanged)の継承は必要ないと考えて良いと思います。

読み取り専用の ReadOnlyReactiveProperty

PersonSlim に BirthDay から算出する読み取り専用の年齢(Age)プロパティを追加したのが src. 8 です。

ReadOnly ありと ReadOnly 無しの最大の違いはソースになる IObserbable<T> 型の変数が別途必要になる点です。『ReadOnly 無し』の場合は ReactiveProperty 自身が値を持ちますが、『ReadOnly 有り』の ReadOnlyReactiveProperty(Slim)の場合は自身で値を保持できず、src. 8 23 行目の calcAge private プロパティのような別の変数をソースに指定する必要があります。src. 8 では private なプロパティとして定義していますが、通常のフィールドで定義しても構いません。とりあえず何かしらのデータソースが必要になります。

そして、ReadOnlyReactiveProperty(Slim) のソースに指定できるのは IObserbable<T> や ReactiveProperty のように変更通知可能な型で定義した変数を指定する必要があります。

旧連載の懺悔

ここで 1 つ懺悔があります。WPF Prism episode シリーズでも上のような ReactiveProperty を使ったサンプルコードを紹介していますが、プロパティ定義を『public ReactivePropertySlim<T> Hoge { get; set; }』のように間違えて Setter も書いていました…

上記のように ReactiveProperty で定義したプロパティに Setter があるのは間違いです。ReactiveProperty は Value プロパティを読み書きするので ReactiveProperty 型の Setter は必要ありません。状況によって Setter が必要になる場面も無くは無いですが、サンプルコードとして紹介する場合は書くべきでないと思います。

『public ReactivePropertySlim<T> Hoge { get; }』で問題なく読み書きできます。管理人の完全な勘違いで WPF Prism episode シリーズが終了してしばらく経つまで全く気が付きませんでした。『ReadOnly 有り』の ReactiveProperty は Getter だけで定義していますが、『ReadOnly 無し』の ReactiveProperty はおそらく全て Setter も定義しているはずです。

間違いに気が付いても流石に GitHub リポジトリ と記事内の全サンプルコードまで修正する気にならずそのまま放置しています。WPF Prism episode シリーズで紹介したサンプルをそのまま使用された方にはご迷惑をおかけしました
Setter があっても深刻な実害等は無いと思いますが、申し訳ありませんでした

ReactiveProperty.Subscribe で値変更時に処理を追加する

さて、本題に戻って src. 8 25 ~ 29 行目のように Subscribe メソッドを使用すると値変更時に実行する処理を追加できます。Subscribe は通常プロパティの Setter に処理を追加する事と同じですが、src. 8 のようにラムダ式で書くことも、別メソッドに delegate もできます

又、わざわざ書く必要は無いかもしれませんが、ReactiveProperty(Slim) に値を設定する場合は、28 行目のように【Value プロパティ】に対して行います。

上でも少し書きましたが、Subscribe の使用には注意があります【ReactiveProperty.Core パッケージ】しかインストールしていないプロジェクトで Subscribe を使用すると『ラムダ式はデリゲート型ではないため、「IObserbable…」型に変換できません』と言うコンパイルエラーが表示されます。

通常の【ReactiveProperty フルパッケージ】に入れ替え又は追加すればコンパイルエラーが消えるので、やはり Model のプロジェクトでも【ReactiveProperty フルパッケージ】をインストールすべきなのかもしれません。ちなみに、【ReactiveProperty フルパッケージ】をインストールすると Reactive Extensions が利用できるようになるので、src. 8 25 ~ 29 行目は src. 9 のように処理実行前に Linq でフィルタリングしたり Select した結果を ReadOnlyReactivePropertySlim に変換する事もできます。

又、Subscribe で値の変更時に処理を追加した ReactiveProperty(Slim)は Dispose しないとメモリリークの可能性があるので、上のサンプルには見えていませんが、src. 10 のように Dispose 処理を含んだ Model 用のベースクラスを作成して継承しています。

Subscribe の使用頻度はそこまで高くないかもしれませんが、管理人的に IDisposable を継承したクラスを破棄しないのは何となく気持ち悪いので Model 用のベースクラスを継承して破棄する派です。

ReactivePropertySlim でプロパティを定義したエンティティ系モデルのシリアライズ

ここまでエンティティ系モデルのプロパティを ReactiveProperty(Slim)で定義する例を紹介してきましたが、エンティティ系モデルはシリアライズしたい場合もあると思います。結論から先に言うと、残念ながらプロパティを ReactivePropertySlim で定義したクラスはシリアライズできません。おそらく ReactiveProperty.Core パッケージに分割されたことが原因だと思われるため Ver. 7.0.0 以降のパッケージを使用している場合はシリアライズできません

XmlSerializer、DataContractSerializer ではシリアライズ時に例外、JsonSerializer はシリアライズはできましたが、デシリアライズで例外になりました。『ReactivePropertyをMessagePackとJSONにシリアライズ/デシリアライズする – Qiita』と言う記事もある MessagePack for C# が最後の頼みの綱でしたがシリアライズ時に例外になりました。

上の Qiita の記事 は ReactiveProperty 6.2.0 の頃に書かれた記事なのでシリアライズできたようですが、2020/9 現在、上記 Qiita 記事 のサンプルコードを GitHub から落としてきても同じくシリアライズ時に例外になりました。

上のサンプルコード でシリアライズが失敗するのは ReactiveProperty を Ver. 7.0.0 に上げたことが原因だと思います。MessagePack for C# で ReactiveProperty をシリアライズ/デシリアライズする際に使用する MessagePack.ReactiveProperty が ReactiveProperty.Core パッケージを見に行かない事が原因だと例外メッセージから予想していますが、英語の Issue を書くことに二の足を踏んでいます…

そのため、現時点ではプロパティを ReactivePropertySlim で定義したクラスをシリアライズ/デシリアライズする方法が無いため、シリアライズ/デシリアライズする必要があるクラスは BindableBase(INotifyPropertyChanged)を継承するしか選択肢が無さそうです。

恐らく、全プロパティを Slim 付ではなく無印の ReactiveProperty で定義したクラスであればシリアライズ/デシリアライズする方法はあると予想しています(最低でも MessagePack for C# では可能なはず)が、Model に無印の ReactiveProperty を使用する事に抵抗があるので確認していません。

2020/9/18 訂正・追記
上で紹介した Qiita の記事を書かれた soi さんから情報頂きました。ReactiveProperty で定義してもシリアライズできないそうです…

一応、MessagePack for C# の動向はウォッチしていますが、Issue にすら上がっていないので、しばらく対応されないと予想しています。何か動きがあればこのエントリを更新しようと思っています。

まとめ的な

ここまで紹介した方法を適用すれば Model ⇔ VM ⇔ View 間がそれぞれ双方向でバインドされるので、 View の更新内容が Model まで、Model を更新した内容が View まで反映される事はデバッグモード等で確認できますが、Model ⇔ VM ⇔ View の中で値が連環する事のメリットは Command の処理まで含めたサンプルで紹介したいので、続きは次回の ReactiveCommand を紹介するエントリに繰り越します。

このエントリは最初に紹介したかずきさんの『MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 前編 – Qiita』より、どちらかと言うと初心者向けの内容にしたつもりですし、アプリ内の使用に重点を置いているので、このエントリの後に上記かずきさんのオーバービューを読むと Reactive Extensions が提供する拡張メソッドの使用例等は理解し易いかもしれません。

又、このエントリは ReactiveProperty を WPF で使用するための初歩の初歩を紹介する事も目的ですが、Model ⇔ VM 間を双方向でバインドして MVVM パターンを有効的に適用する事が目的なので、Reactive Extensions が提供する拡張メソッドにはあまり触れていません。そのため、値をフィルタしたり変換したり合成したり等の方法はかずきさんのオーバービューを読んで頂く方が良いと思います。

さて次回は上に書いた通り、Command とバインドする(Async)ReactiveCommandを紹介する予定です。そして今回紹介したコードもいつものように GitHub リポジトリ へ上げています。

 

 

 

おすすめ

コメントを残す

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

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

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