WPF Prism episode: 8 ~ ReactiveProperty がバインドできないのはどう考えても Navigation が悪い! ~

前回記事「画面遷移のパラメータたちが INavigationAware から来るそうですよ?【episode: 7 WPF Prism】」

 

前回は Prism の RequestNavigate に渡したパラメータを INavigationAware インタフェース経由で受け取って Prism の BindableBase で View とバインドする方法を紹介しました。

今回は前回から引き続き INavigationAware.OnNavigatedTo イベントで受け取ったパラメータを ReactiveProperty を使用して View とバインドする方法を紹介します。

※ Google 等の検索エンジンから来た方へ
このエントリは 2019/6/8 をもってリニューアルしたため、元々 episode: 8 のタイトルで公開していた TreeView にコンテキストメニューを追加する方法の紹介は extra: 3 に分割しました。
お手数ですが『WPF Prism extra: 3 ~ とある TreeView の状況一覧 (Context menu) ~』へ移動してください。

尚、この記事は Visual Studio 2017 Community Edition で .NET Framework 4.7.2 以上 と C# + Prism 7.1 + ReactiveProperty を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。

Model でも ReactiveProperty を使用する

前回の episode: 7 では INavigationAware インタフェース経由で受け取ったパラメータを OnNavigatedTo イベントで VM へセットして、OnNavigatedFrom イベントで VM から Model へ書き戻す方法を紹介しました。
※ Prism の BindableBase を使用

まず episode: 7 で紹介した Prism の BindableBase を使用した部分を単純に ReactiveProperty に書き換えたのが src. 1 です。

src. 1 のような実装では fig. 1 のように一見正常に動作しているように見えますが、TreeView の他項目を選択して再度 View を表示すると入力した値が消えてしまいます。

fig.1 ReactiveProperty で編集データをバインド

これは src. 1 を見て分かる通り編集 View 用の personInfo フィールドが OnNavigatedTo イベントの度に上書きされるので値が保持されないのは当然です。

これでは使い物にならないので、使えるようにするにはどうするか順を追って紹介していきます。

Model と ViewModel をバインドする

episode: 7 でも紹介しましたが、サンプルアプリ内部のデータは fig. 2 のような構成(配置)になっています。

fig.2 サンプルアプリ内のデータ配置図

サンプルアプリでは fig. 2 のように NavigationTree で保持しているデータのインスタンスをそのまま各画面へ渡す設計にしているので、OnNavigatedTo イベントで受け取った Model をそのまま使い回せると NavigationTree モジュールと EditorViews モジュール(編集 View 用のモジュール)で同一のインスタンスを共有できそうです。
そして ReactiveProperty は View ⇔ VM 間のバインドだけでなく VM ⇔ Model 間もバインドできる機能を備えています
※ Prism の BindableBase も Model に使用することはできますが、継承が必要なのは微妙ですね…

ReactiveProperty で VM ⇔ Model 間をバインドするには何種類かの方法がありますが、使用頻度が高そうな双方向でバインディングする方法を紹介します。



ReactiveProperty で VM ⇔ Model 間を双方向でバインドする。

ReactiveProperty で VM ⇔ Model 間を双方向でバインドするには、まず Model に通知機能(INotifyPropertyChanged)が必要になります。
このサンプルアプリの Model は Prism の BindableBase を継承して作成しているので、既に通知機能を持っています。興味がある方は GitHub リポジトリ で見てください。

このサンプルアプリでは使用していませんが、Model のプロパティを ReactiveProperty(Slim)で定義して VM と双方向でバインドすることも可能です。
episode: 4.5 では ReactiveProperty を WPF で使用する場合はメモリリークする可能性があるので、親となるクラスが INotifyPropertyChanged を継承する必要があると書きましたが、Model のプロパティに使用する場合は INotifyPropertyChanged を継承する必要ありません。

ネットを検索してもはっきりと断言されている記述は見つかりませんでしたが、かずきさんの以下のツイートが根拠です。

ポイントは【DataContextに入れるやつ】の部分で、VM のプロパティを ReactiveProperty で定義する場合は VM 自体が INotioyPropertyChanged を実装している必要がありますが、Model は DataContext に入れる訳ではないので INotioyPropertyChanged を継承する必要はないと言えます。

もう 1 つ『【WPF】ViewModelがINotifyPropertyChangedを実装していないとメモリリークする件 – aridai.NET』に書かれている内容で PropertyDescriptor を通して変更通知を受け取る場合にメモリリークが発生するそうなので Model の場合は関係ないと言えます。

ですが、ReactiveProperty(Slim)自体は IDisposable を継承しているので Dispose するに越したことはないとは思いますが必須ではないと考えて構わないと思います。

前置きが長くなりましたが、ReactiveProperty で VM ⇔ Model 間を双方向でバインドするには src. 2 のように実装します。

ReactiveProperty に同梱されている ToReactivePropertyAsSynchronized 拡張メソッドを使用して初期化すると Model と VM を双方向でバインドできます

src. 2 はコンストラクタで ReactiveProperty を初期化していますが、バインド元になる personInfo フィールドのインスタンスはコンストラクタ実行時点では null なので src. 2 のような実装はできません。
※ personInfo フィールドは OnNavigatedTo イベントで渡されるインスタンスをセットする予定

VM ⇔ Model 間を双方向でバインドすると fig. 3 のように V ⇔ VM ⇔ M の中で値がシームレスに連環するようになります。

fig.3 MVVM 内で値を連環する

値がシームレスに連環するようになると Model → VM や VM → Model のような値の積み替えが不要になるので MVVM パターンで構築したアプリ内でのデータの取り扱いが非常に楽になります。
今回のエントリでは最終的に fig. 3 のような構造を実現することを目的に進めていきます。

又、ReactiveProperty には VM ⇔ Model 間を双方向でバインドするだけでなく Model → VM 間を単一バインドする FromObject メソッドも用意されていますがここでは紹介しません。
興味がある方はかずきさんの『MVVMとリアクティブプログラミングを支援するライブラリ「ReactiveProperty v2.0」オーバービュー – かずきのBlog@hatena』を見てください。

尚、今回のエントリは情報紹介と言うより管理人が試行錯誤した過程を調査レポート的に書いているので、かなり勿体付けた文章構成になっているのでご了承ください。(結構苦労したのでその過程を書きたいんですw)
結論だけ知りたい方は最後の章まで飛ばしてください。

OnNavigatedTo イベントで ReactiveProperty を初期化 ~ 試行錯誤編 ~

まず、コンストラクタに書いていた ReactiveProperty の初期化処理を src. 3 のように OnNavigatedTo イベントへ単純に移動してみます。

src. 3 を実行しても fig. 4 のように編集 View へデータは表示されません

fig.4 OnNavigatedTo イベントで ReactiveProperty を初期化

ネットを検索しても Prism の Navigation で受け取ったパラメータを ReactiveProperty に設定するような例は見つからないので【ReactiveProperty】のキーワードで個別に調べるしかなく、何とか辿り着いたかずきさんのブログには以下のように書かれていました。

ReactivePropertyは、コンストラクタで組み立てるのが一番スムーズなので出来るだけ外部からDIするものはプロパティでインジェクションするのではなくて、コンストラクタでインジェクションしましょう。そうすると、コンストラクタで準備万端状態になるので、ReactivePropertyの組み立てがスムーズに行えます。
<span class="su-quote-cite"><a href="https://blog.okazuki.jp/entry/2014/05/08/122454" target="_blank">ReactivePropertyとPrism for WinRTとの連携 - かずきのBlog@hatena</a></span>

つまり、src. 3 のような実装だとコンストラクタでバインドプロパティ(ReactiveProperty)が初期化されていないため View にデータが表示されないと考えられます。

早速手詰まりになったので、方向性を変えて DI コンテナ(ここでは Unity)から編集したいデータ(ここでは個人情報データ)をコンストラクタへインジェクションしてもらう方法を考えて試すと fig. 4 の個人情報編集画面であれば正常に表示されました。

データオブジェクト 自体をコンストラクタへインジェクションすることはできますが、『身体測定情報』と『試験結果情報』の 2 つは List 内のいずれか 1 つのインスタンスを表示する必要があるため TreeView で項目を選択するまで対象のデータは特定できません
やはり OnNavigatedTo イベントで受け取るインスタンスを表示する方法を模索するしかなさそうです。



コンストラクタで初期化した ReactiveProperty にバインドしているオブジェクトを差し替え

上で引用したかずきさんの記事内容から ReactiveProperty をコンストラクタで初期化する必要があると言う事なら、コンストラクタでは仮のインスタンスをバインドしておいて、OnNavigatedTo メソッドで受け取ったインスタンスに差し替える方法を試したのが src. 5 です。

結果は fig. 4 と同じで何も表示されません… orz
気を取り直して再度検索すると該当しそうなのはお馴染みのかずきさんのブログから 2 件。

上記 2 パターンとも実際に書いて試してみましたが、fig. 4 と変わらず View にデータは表示されませんでした… orz
(新規のプロジェクトに丸コピして試した訳ではないので、管理人が書き間違えたのかもしれません)
この時点でかなり諦めムードになりましたが、XAML をデバッグモードで確認すると DataContext の中も見れると言う情報を見つけたので、とりあえず実装を src. 3 に戻して確認してみました。

XAML のデバッグ

まず、fig. 5 の赤枠で囲った部分【選択を有効にする】を選択します。

fig.5 XAML デバッグ:選択を有効にする

そして、fig. 6 のように値を確認したいコントロール(ここでは生徒氏名)を選択します。

fig.6 XAML デバッグ:コントロールを選択

対象のコントロールを選択すると fig. 7 のように IDE の【ライブ ビジュアル ツリー ウィンドウ】で対象のコントロールが選択された状態で開きます。

fig.7 XAML デバッグ:ライブ ビジュアル ツリー

そして、対象のコントロールを右クリックして、[プロパティの表示]を選択すると fig. 8 のような【ライブ プロパティ エクスプローラー】が開きます。

fig.8 XAML デバッグ:ライブ プロパティ エクスプローラー

DataContext 内の VM に設定された値が View から見えてる !?
これって実際はちゃんとバインドされているが、View に表示されていないだけじゃないかと予想しました。
その線でもう少し粘ることにして、とりあえず、現状を整理します。

OnNavigatedTo イベントで ReactiveProperty を初期化 ~ 解決編 ~

OnNavigatedTo イベントで ReactiveProperty を初期化してもちゃんとバインドされるのであれば、後はコントロールの表示を更新するだけで良さそうです。
コードは src. 3 に戻して確認しています

Windows From の経験が長いと「コントロールの Refresh メソッド呼べば更新されんじゃね?」等とついつい考えてしまいますが、MVVM パターンではコントロールのメソッドを呼ばない(呼べない)ので却下。

「WPF でコントロールの更新ってどうやるんだろ…?」と考えましたが、どうと言う事はなく WPF の場合は VM から INotifyPropertyChanged.PropertyChanged イベントを呼び出せば(つまり VM へ定義しているプロパティの Setterを呼び出せば)PropertyChanged イベントが発行されて View 側のコントロールが更新されます

では、INotifyPropertyChanged.PropertyChanged イベントを発行するために全プロパティへ値を再セットすれば更新される!… src. 1 に逆戻りです…

最悪、全プロパティへ値を再設定すれば望む結果は得られそうな気はしますが、再セットが必要なプロパティの数が増えるとそんなことはやってられません。
別の方法が無いか探すと ReactiveProperty.ForceNotify と言うメソッドで強制的に通知を発行することができるそうですが、メソッドを呼んでみても相変わらず fig. 4 のまま… orz

万策尽きたかと諦めかけた所、VM は Prism の BindableBase を継承しているのを思い出しました。
BindableBase も ReactiveProperty のように強制変更通知メソッドも持ってそうだと思って調べたら…ありました!BindableBase.RaisePropertyChanged メソッド
これを src. 6 のように全プロパティ分呼び出してみます。

src. 6 を実行すると、ようやく fig. 9 のように編集用データが表示されるようになりました。

fig.9 バインディング成功例

src. 6 では PropertyChanged メソッドをプロパティの数だけ呼び出していますが src. 7 のように RaisePropertyChanged のパラメータに null か string.Empty をセットすると全プロパティを一括で呼び出してくれるようです。(実行結果は当然同じです)

又、OnNavigatedTo イベントは TreeView で項目を選択する度に呼び出されるので ReactiveProperty の初期化が何度も実行されてしまいます。
それを防ぐためフィールド変数が null の場合のみ ReactiveProperty の初期化が実行されるようにしています。
(ReactiveProperty をコンストラクタで初期化する場合はこの辺りを考慮する必要が無いと言う違いがあります)

こんな感じで回り道をしましたが、ようやく TreeView から渡したインスタンスを VM へバインドできるようになり、TreeView の表示に使用しているインスタンスと編集 View へ表示しているインスタンスが同一のインスタンスを参照するようになります。
その結果 fig. 10 のように Model ⇔ VM ⇔ View がシームレスに連環するようになりました。

fig.10 MVVM でデータをシームレスに連環

XAML へ UpdateSourceTrigger を追加

又、src. 8のように編集画面の XAML へ UpdateSourceTrigger = “PropertyChanged” を追加します。

そしてサプルアプリを実行すると、TreeViewItem と 編集画面 View で同一のインスタンスを共有している為、fig. 11 のように編集画面で生徒氏名を編集すると TreeViewItem にも値が即時に反映されるようになり、View を切り替えてもちゃんと値が保持されるようになります。

fig.11 変更内容の同時反映

src. 6 の実装に辿り着いた事で ReactiveProperty を OnNavigatedTo イベントで初期化できるようになり、VM と Model も双方向でバインドされるようになりました。

これで fig. 11 のようなアプリ全体で同一のインスタンスを参照するような構成にできるので、データをどのタイミングで保存しても入力値が反映されたデータが保存されることになります。

fig. 11 サンプルアプリ内部のデータ構成

他の View も src. 6 のような実装にすれば同じ動作になりますが、他の VM についてはリポジトリ で確認してください。

今年の年初からサーバ契約を変更して PHP モジュールが実行できる環境に移ったので去年までと比べると表示が速くなったと思いますが、サーバ移行のごたごたもあって今年初のエントリが既に 1 月も終わりかけのこんな時期になってしまいました。

次回は WPF での Validation について紹介する予定です。
ただ、少々検証に手間取っているので、次回エントリの公開はひと月程先になるかもしれません。

 

今回作成したコードもいつも通り GitHub リポジトリへ上げておきます。

 

 

次回記事「ReactiveProperty の Validation は DataAnnotation じゃないと思った?【episode: 9 WPF Prism】」

 

 

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

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