クラス中のReactivePropertyに自動でSubscribeを仕掛けてみよう(*'▽')ノ
ご挨拶
全国何人かの皆様、超お久しぶりです。
ずっとさぼってました( `ー´)ノ
仕事でもプライベートでもXamarinはあまり触ってなくて、もっぱらクラウドデザインパターンのお勉強とAzureのお勉強などをしておりました。
Prism.Formsはちょっとしたアイディアを試すために少々触っていましたがMVVMフレームワークの必要性に関して疑問符が付く結果となってしまい記事にはできませんでした。
昨日JXUGのカンファレンス
がありまして、地方在住の私はyoutubeから見ておりました。
最近はあまり面白いプログラミングのアイディアがなかったり仕事の方も人間関係や職場環境によりモチベーションが保てなくてウジウジな生活をしておりましたがJXUGCの登壇を見ていてmoq(もっきゅ(*'▽'))というキーワードからやってみたいことを思いつきまして早速プログラムをしてみたので記事にいたしました。
果てしなく一般向けでない内容となっておりまして私自身もしっかりと理解できていない点もありますので御指摘等ありましたらTwitterの方にお願いいたします。
成長の為にも是非ともお願いいたします。
なお本日は休日出勤しなければならないので早く寝なければなりません(^^)/
本題
MVVMのViewModelにて値の変化をログ出力したりデバッグ出力するような場合、ViewModel自体にINotifyPropertyChangedが実装されている時にはイベントを仕掛けて出力やその他処理を行えばよいです。
しかしプロパティにReactivePropertyを用いている場合にはプロパティ個別にSubscribeする必要があるので状況によってはコードを書くことが面倒だったりします。
なのでクラス中のReactivePropertyにリフレクションを使って汎用的にSubscribeとかする事ができたらいいなぁ というのが今回のアイディアでありました。
がしかし
ReactivePropertyってGenericなんですよね。
Genericの場合、Reflectionでメソッド実行したりする事が難しかったり無理だったりすることが多いんですよね。
しかもSubscribeはIObservable
当初はReflectionでサクッとできたらなぁと思っていたのですがどうやら無理そう・・・・
ReactiveProperty自体はINotifyPropertyChangedを持っているのでイベントを仕掛けてIReactivePropertyのValueプロパティからObject型の値をとるところくらいまではできるのですが、それでは発展性もなくてイマイチ(´・ω・`)
またアイディアを形にできないのかぁ・・・・なんて思っていました。
そんな中、ふと思い出したのがSystem.Expression 所謂式木です。
なんとなくはわかるのです。でもやっぱり難しい式木(´・ω・`)
以前O/Rマッパーのカスタマイズを少しやっていた時に式木に遭遇して多少は実コードを読んだことがあるのですが、やっぱり難しかったです。
でも、きっと、Genericなメソッドも式木を使えば呼び出せるのだろうと思い調べてみましたらこんな情報を見つけました。
なるほど、サンプルコードがわかりやすいのでなんとなく理解できた気がします。
このサイトを参考にして小一時間コネコネしてみましたらうまくできました。
完成コード
では実際にトライしてできたコードがこちらです。
実行結果がこちら
個別にReactivePropertyにSubsucribeを行わずに購読を行いコンソール出力を行うことができています。
解説します。
ReactivePropertyのみ抽出
vm.GetType().GetProperties() .Where( x => x.PropertyType.Name.StartsWith("ReactiveProperty") || x.PropertyType.Name.StartsWith("ReadOnlyReactiveProperty"))
ViewModelのインスタンスからTypeクラスを取得しPropertyInfo配列を取得しプロパティの名前の先頭から文字列比較しています。
もっとちゃんと書けると思いますが、今回の主題でないので割愛です。
ReactivePropertyにSubscribeを行うメソッドを定義
public static IDisposable MySubscribe<T>(IObservable<T> source, string name) => source.Subscribe(x => Console.WriteLine($"{name}:{x}"));
渡られたIObservableとnameを使用してSubscribeを仕掛けてOnNext時にはコンソール出力を行うようにします。
購読を追加する
.Select(x => Expression.Lambda( Expression.Call(typeof(Program), "MySubscribe" , new[] {x.PropertyType.GetGenericArguments().First()} , Expression.Constant(x.GetValue(vm)), Expression.Constant(x.Name))) .Compile().DynamicInvoke() as IDisposable)
PropertyInfoの配列に対してSelectを行ってその中でSubscribeをコールしています、
Expression.Lamda -> ラムダ式を作成するメソッドです。
引数には
メソッドをコールする式木を作成するExpression.Callを使用しています。
第1引数のtypeof(Program)はコンソールプログラムですのでメインプログラムクラスを指定しています。 第2引数にはコールするメソッド名を指定しますので先ほど作成しておいたMySubscribeの名前を指定します。 第3引数はメソッドがGenericの場合にTypeの配列を指定します。
今回はReactivePropertyのGenericの型を指定したいのでx(PropertyInfo)に対してPropertyType.GetGeneticArguments().First()とすることでGenericの型を指定します。
第4引数以降はparamsです。MySubscribeの引数を指定します。
MySubscribeの第1引数はIObservable
これでMySubscribeをコールするLambdaができますので .Compile()でラムダをコンパイルしDelegateを作成します。 DelegateのDynamicInvokeを行い実際に作成されたメソッドを実行します。 戻り値はIDisposableですので as で変換してselectの戻り値とします。
これでクラス中のReactivePropertyに対してSubScribeを仕掛けることができました。
テストしてみる
vm.StringTestProperty.Value = "test1"; vm.IntTestProperty.Value = 10; vm.StringTestProperty.Value = "test2"; vm.IntTestProperty.Value = 11; Console.WriteLine("SubscribeをDisposeします"); disposes.ForEach(x => x.Dispose()); vm.StringTestProperty.Value = "test3"; vm.IntTestProperty.Value = 12; Console.WriteLine("おしまい");
結果を見るとわかる通り
ReactivePropertyの値を変更するとコンソールに出力され式木で作成したメソッドの戻り値であるIDisposableをDisposeすると購読が解除されコンソール出力されなくなります。
今回は単純にコンソール出力を行っただけなのですが、Subscribeを仕掛けるMySubscribe内にて色々することができるので例えばZipとSkipを使って変更前の値との同時に出力したり色々できるので夢が広がります。