ReactiveProperty : Rx + MVVMへの試み
Reactive Extensionsといったら非同期、じゃなくて、その前にイベントですよ!イベント!というわけで、随分手薄になっていたイベント周りの話を増強したいこの頃です。イベントと一口に言っても色々あります。UI(クリックやマウスムーブ)、センサー、変更通知(INotifyPropertyChanged)などなど。中でも一番よく使うのは、UI周りのイベントでしょう。
しかし、UIの持つTextChangedイベントだのから直接FromEventPatternで変換してしまったら、Viewと密接に結びついてしまってよろしくない。ここはMVVM的にやりましょう。でも、どうやって?
View(UI)が持つネイティブなイベントを、ViewModelの持つ更新通知付きのプロパティに変換します。これはバインディングにより可能です。そこはWPF/SLの仕組みに任せましょう。ということで、RxでUIに対してプログラミングするというのは、ViewModelの通知に対してプログラミングするという形になります。
テキストボックスの変更に反応して、1秒ディレイをかけた後に表示する、という簡単な例を(何の面白みもありません、すみません)
public class ToaruViewModel : INotifyPropertyChanged { private string input; public string Input { get { return input; } set { input = value; RaiseEvent("Input"); } } private string output; public string Output { get { return output; } set { output = value; RaiseEvent("Output"); } } public ToaruViewModel() { Observable.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>( h => (sender, e) => h(e), h => this.PropertyChanged += h, h => this.PropertyChanged -= h) .Where(e => e.PropertyName == "Input") // Inputが更新されたら .Select(_ => Input) // Inputの値を .Delay(TimeSpan.FromSeconds(1)) // 1秒遅らせて .ObserveOnDispatcher() // Dispatcherで(Silverlightではこれ必要・WPFでは不要) .Subscribe(s => Output = "入力が1秒後に表示される:" + s); // Outputへ代入 } // この辺は別途、ライブラリを使って持ってくるほうが良いかも public void RaiseEvent(string propertyName) { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged; } // xaml.csはInitializeだけ、xamlのバインディングは各プロパティへ当てるだけ。 // ただしSL/WP7はUpdateSourceTrigger=PropertyChangedに対応してないので別途Behaviorの適用が必要 // 詳しくは、最後にソース配布(WPF/SL/WP7全て含む)URLを置いているのでそちらを見てください
……実にダサい。はい。全くいけてないです。バインディング可能なのはプロパティなので、そういった中間レイヤへの中継が発生していて、冗長だし、美味しさがかなり損なわれています。わかりきったINotifyPropertyChangedのWhere, Selectは無駄そのもので。勿論、簡単にDelayを混ぜられるといった時間の扱いの容易さはRxならでは、ではあるのですけれど。
ReactiveProperty
中継が手間ならば、中間レイヤだけを抜き出してやればいい。通知処理を内包したIObservable<T>があれば解決する。というわけで、ReactivePropertyと名付けたものを作りました。それを使うと、こうなります。
public class SampleViewModel : INotifyPropertyChanged { public ReactiveProperty<string> ReactiveIn { get; private set; } public ReactiveProperty<string> ReactiveOut { get; private set; } public SampleViewModel() { // UIから入力されるものはnewで作成、デフォルト値も同時に指定出来る。 ReactiveIn = new ReactiveProperty<string>(_ => RaiseEvent("ReactiveIn"), "でふぉると"); // UIへ出力するIO<T>はToReactivePropertyで、初期値での発火も自動的にされます。 ReactiveOut = ReactiveIn .Delay(TimeSpan.FromSeconds(1)) .Select(s => "入力が1秒後に表示される:" + s) .ToReactiveProperty(_ => RaiseEvent("ReactiveOut")); } // 通常は、他のMVVMフレームワークなりを使い、それの更新通知システムを利用するといいでしょう // Rxを使ったからって、決してMVVMフレームワークと競合するわけではなく、むしろ協調すると考えてください public void RaiseEvent(string propertyName) { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged; }
// これはWPF版のもの <Window x:Class="ReactiveProperty.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:l="clr-namespace:ReactiveProperty" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <l:SampleViewModel /> </Window.DataContext> <StackPanel> <TextBox Text="{Binding ReactiveIn.Value, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Text="{Binding ReactiveOut.Value}" /> </StackPanel> </Window>
XAMLではPathに必ず.Valueまで指定します。これによりGetが求められれば最新の値を返し、値をSetされればPushするようになります。
今回はUI->ReactiveProperty->クエリ演算->ReactiveProperty->UIという風に戻してやりましたが、勿論、UIからの入力をModelに流してそれで止めてもいいし、Modelからの値をUIに流すだけでもいいし、トリガーはタイマーであってもいいし、その辺は完全に自由です。普通の通知プロパティと何も変わりません。また普通のプロパティとして使いたい時は.Valueで値を取り出す/セットできます。
かなりシンプルに仕上がります。通知付きプロパティは、本質的に値の変更毎に通知される無限長のIObservable<T>と見なせるので、そのことにより表現がより自然になっています。書き味も、リアクティブプログラミング(といわれてパッと浮かばれる値が自動更新されるという奴)にかなり近い感じの風合い。XAMLでのバインドも簡単ですし、VMの実装も自動実装プロパティだけで書けるので記述が楽チン。
そして、Rxを使うことによる最大の利点である、他のイベント(他の変更通知プロパティ)と合成しやすかったり、時間が扱いやすくなったり、非同期と混ぜても同じように扱えたり、スレッドの切り替えが簡単であったり、などを最大限に甘受できます。VMとして独立している、かつ全てがRxに乗っているため、単体テストも非常に作成しやすい状態です(時間軸を扱う処理のテストは通常難しいのですが、Rxの場合は自分で時間をコントロール可能なSchedulerを中間に挟むと、好きなように時間を進められるようになります、イベントのテストも、この状態ならばプロパティを変更するだけで生成されますし)。また、決して他のMVVMフレームワークと競合が起こるわけではない(多分……)のも見逃せない利点です。
単純な例なのでModelがありませんが、まあこんな感じ?(それと今はコマンドがないので単純なデータバインドのみの図です)。Modelへのアクセスは通常恐らくRx:Query内で行い、Modelの形態は色々だと思いますが、通信してデータを処理して返す、みたいなものはRxになっているとVMのReactiveProperty側での合成処理が容易なので、非同期にしてIObservable<T>で返すと良いのではかと思います。自身が通知を持つReactivePropertyになっていてもいいですね。そうなると、コードのほとんどがLINQになるという素敵な夢が見れる気がしますが気のせいです。
実装
ReactivePropertyの実装はこんな感じです。ご自由にコピペって使ってみてください。
using System; #if WINDOWS_PHONE using Microsoft.Phone.Reactive; #else using System.Reactive.Linq; using System.Reactive.Subjects; #endif public class ReactiveProperty<T> : IObservable<T>, IDisposable { T latestValue; IObservable<T> source; Subject<T> anotherTrigger = new Subject<T>(); IDisposable sourceDisposable; public ReactiveProperty(Action<T> propertyChanged, T initialValue = default(T)) : this(Observable.Never<T>(), propertyChanged, initialValue) { } public ReactiveProperty(IObservable<T> source, Action<T> propertyChanged, T initialValue = default(T)) { this.latestValue = initialValue; var merge = source.Merge(anotherTrigger) .DistinctUntilChanged() .Publish(initialValue); this.sourceDisposable = merge.Connect(); // PropertyChangedの発火はUIスレッドで行うことにする // UIへの反映の際に、WPFでは問題ないが、SL/WP7ではUIスレッドから発行しないと例外が出るため merge.ObserveOnDispatcher().Subscribe(x => { latestValue = x; propertyChanged(x); }); this.source = merge; } public T Value { get { return latestValue; } set { latestValue = value; anotherTrigger.OnNext(value); } } public IDisposable Subscribe(IObserver<T> observer) { return source.Subscribe(observer); } public void Dispose() { sourceDisposable.Dispose(); } } // 拡張メソッド public static class ObservableExtensions { public static ReactiveProperty<T> ToReactiveProperty<T>(this IObservable<T> source, Action<T> propertyChanged, T initialValue = default(T)) { return new ReactiveProperty<T>(source, propertyChanged, initialValue); } }
Valueで値の中継をしているという、それだけです。Publish(value)はBehaviorSubjectというものを使った分配で、必ず最新の値一つをキャッシュとして持っていて、Subscribeされると同時に、まずその値で通知してくれます。これにより「初期値での自動発火」が自然に行える、という仕組みになっています。また、プロパティの変更時に同値の場合は変更通知をしない、というよくあるほぼ必須処理も、ここでDistinctUntilChangedを挟んで行っています(オプションで選択制にしてもいいかもしれない)。
それReactiveUI?
ReactiveUIというRxを前提にしたMVVMフレームワークがあって、それに用意されているObservableAsPropertyHelperと、ReactivePropertyはかなり近いです(ということにプロトタイプ作ってから気づいた、ReactiveUIはこれまで名前は知ってたけど中身完全ノーチェックだったので)。ただ、機能的にはOAPHは双方向バインディングに対応していないので、ReactivePropertyのほうが上です。また、OAPHは使い勝手もあまり良くないし、名前がダサい(ObservableAsPropertyHelperは長すぎるし型名として宣言させるにはイマイチに思える……)などなどで、あまり気に入るものではなかったです。
ReactiveUIは全体的には軽く眺めた程度なのですが、今ひとつ私には合わない。ちょっと、いや、かなり気にいらない。なので、私としてはそのうち他のMVVMライブラリをベースに置いた上での拡張として、Rx用のUI周りライブラリを作りたい。独自に上から下まで面倒を見るフレームワーク、という指針は今一つに思えるので、Rxならではの特異な部分だけを、最初から他のMVVMフレームワークの拡張として用意していく、という方向性のほうが良いものが作れると思っています。素のままのRxでは辛いので、何かしらの中間層が必要なのは間違いないので。
次は、ReactiveCommandを!あー、あとReactiveCollectionも必要かしら。Validationとかも……。まあ、そういうところは普通に書けばいいんですよ、何も全部Rxでやる必要はないですからね。
まとめ
WPFのバインディングの美味しさをRxで更に美味しくする、ということでした。世の中的には弱参照が~などなどというお話もありますが、それには全然追いついてませんので、おいおいちかぢかそのうち。
今回のコードの全体(WPF/SL/WP7)はneuecc / ReactiveProperty /Bitbucketに置いてありますので、好きに見てください。例が単純すぎると美味しさもよくわからないので、もう少し複雑な例で、サンプル準備中なのでしばしお待ちを。
ところで9/15にいよいよRx本が出ます。
オライリーで出ているProgramming C#の著者と、ReactiveUIの作者(元Microsoft Office Labs、つい最近Githubに転職した模様)の共著です。私も買いますので、うーん、読書会とかやったら来てくれる方います?