ReactiveProperty : Rx + MVVMへの試み
- 2011-08-26
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に転職した模様)の共著です。私も買いますので、うーん、読書会とかやったら来てくれる方います?