1. WLAN Profile Viewer
先に簡単にアプリの紹介を。無線LANプロファイルを管理するためのWindowsデスクトップアプリです。ストレートにWLAN Profile Viewerという名前にしました。
Windows 8以降、OS標準の無線LANのUIは現在電波の入っている無線LANを入口にしたものになりました。これは初めて接続するときだけ接続先の無線LANを選んで、後は自動的に接続するようOSにお任せにしてしまう前提であればシンプルでいいですが、ユーザーが自分でコントロールしようとすると必ずしも使い勝手のいいものではないです。今時コーヒーショップで無線LANを見ると20も30も電波が飛んでますし。
このアプリは既に作成された無線LANプロファイルを電波状態を含めて一覧表示し、そこから接続と切断ができます。これにプロファイルの順番の変更(ただしWindows 10では意味なし)と削除も併せて最低限一通りの管理ができるので、OS標準のUIよりは多少使い勝手がいいと思います。
正直いえばReactivePropertyの練習用に作っていたものですが、意外と実用性がありそうなのでアプリに仕立てました。詳しくはレポジトリで。
2. ReactivePropertyのプロパティ
ReactiveProperty自体については開発者の@neueccさん、@xin9leさん、@okazukiさんがたくさん書かれてますので、そちらを読んでいただければいいのですが、「そもそもReactivePropertyのプロパティって何?」という点について、「そこからか?」と言われそうですが、自分は何だろうと思ったので簡単に書きます。
これは最初に@neueccさんが書かれてます。正確にはそちらを。
ざっくりと自分のイメージでいえば以下のような感じです。
まずプロパティはデータを入れておく入れ物といっていいと思いますが、そのためには内部に値を保持していて、それに随時アクセスできる必要があります。一方、IObservableのチェーンではイベントなどで値が流れていくわけですが、常に値があるとは限らないので、そのままではデータの入れ物にはなりません。
そこで内部のlatestValueフィールドに値を常に保持するようにし、それにValueプロパティを通してアクセスできるようにしたのがReactiveProperty、のベースなのだと思います。したがって、プロパティを利用する立場からは、相手は一義的にはValueプロパティで、latestValueフィールドはそのバッキングストア、それらを提供するコンテナがReactivePropertyと見なすことができ、それをIObservableのチェーンに挟み込むことで(発端でも終端でも構いませんが)、両者をうまく融合させたと。
というわけで、ReactiveProperty自体はValueプロパティのコンテナなので、基本的にReadonlyなプロパティとして生成すればいいわけです。なお、ReadOnlyReactivePropertyはそのValueプロパティがReadonlyという意味で、Readonlyの対象が違うのですが、初めは混同してました。
3. ObserveElementXXXXX
ObserveElementXXXXXはコレクション要素のプロパティ変化をIObservableにする拡張メソッド群です。自分がコレクション要素のプロパティ変化をReactivePropertyで捉えるにはどうすればいいかと書いてたら、@xin9leさんと@okazukiさんがあれよあれよという間に作り上げて、すげーと思いました。
この使用例として、要素になるものとして以下のMemberViewModelがあるとします。この中のIsLongとIsSelectedはModelと同期、あるいはViewにバインドされて随時変わるものと考えてください。
これを要素とするObservableCollectionを持つMainWindowViewModelは以下のとおり。
この中でCLRプロパティであるIsLongにObserveElementPropertyを、ReactivePropertyであるIsSelectedにObserveElementObservablePropertyをそれぞれ繋げています。これらの戻り値の型はPropertyPackで、このValueに元のプロパティの値、Instanceにその要素のインスタンスが入っているので、まずValueを見てtrueのものだけ通し、その後で要素のインスタンスをメソッドに渡しています。
コレクション要素の変化を追うのは割と面倒ですが、それがこれだけでできます。なお、同じ結果は、適当なタイミングでコレクションをループして走査することでも得られますが、そうすると当然ループのコストがかかります。
また、プロパティ変化をある条件でフィルターした要素を集計したいときは、FilteredReadOnlyObservableCollectionが利用できます。この例は以下のようなものです。
ReactivePropertyであるIsAllLongを生成するのに、上の方法ではIsLongにObserveElementPropertyを繋げ、その後で元のコレクションをループさせて判定しているので、当然ループのコストがかかります。対して、下の方法ではIsLongがfalseである要素のコレクションをFilteredReadOnlyObservableCollectionで生成し、このCollectionChangedイベントを捉えて、元のコレクションとFilteredReadOnlyObservableCollectionのCountから判定しています。
ただ、これが使えるのは対象の、変化するプロパティがCLRプロパティの場合で、ReactivePropertyを対象とするときは別の方法を考える必要があります。
単純には、ObserveElementObservablePropertyを繋げ、そこから拾い出した要素を外のコレクションに保持し、その要素数を使えばいい気がしますが、これだけだと元のコレクションから要素が削除された場合に追い切れません(ObserveElementObservablePropertyは現在コレクションにある要素を対象とするので)。これを考慮した場合は以下のようなものが考えられます。
ReactivePropertyであるIsAnySelectedを生成するのに、上の方法では元のコレクションをループさせて判定しています。対して、下の方法では、いきなり膨らみましたが、条件を満たす要素をListに保持するようにし、現在コレクションにある(新たに追加された場合も含む)要素をObserveElementObservablePropertyで、要素が削除された場合とコレクションがクリアされた場合をCollectionChangedAsObservableで追うようにした上で、ListのCountで判定しています。
一応これを汎用の拡張メソッドにしてみたものが、他の例と併せてReactivePropertyTestプロジェクトにあるので、興味があれば見てください。ちなみに、NewItemsをチェックしているルートは実際はObserveElementObservablePropertyが先に捉えるので不要ということに後から気づきました。
しかし、変化の頻度が少なく要素数も特に多くなければループさせる方法でも十分なので、実際ここまでやることはないかもしれません。
4. まとめ
このアプリではM-V-VM間を専らRxとReactivePropertyで結ぶようにした結果、その部分のコードがとても少なく、すっきりとなりました。
したがって、Rxをガンガン使うようなアプリであれば、ReactivePropertyも併用すればその威力はさらに増すと思います。Rx自体の学習コストが決して低くない問題はありますが。ReactivePropertyのソースにはRxの高等テクニックが詰まっているので、時々見ると発見があります。
最後に一つ。RxとReactivePropertyを駆使すればアプリ内を融通無碍に繋ぐことができますが、これと非同期が組み合わさると実行コンテキスト(スレッド)がよく分からない状態になることがあります。何か動作が妙……というときは実行コンテキストを確かめてみるのも手です。