ReactiveProperty : WPF/SL/WP7のためのRxとMVVMを繋ぐ拡張ライブラリ
- 2011-10-07
MVVM拡張、という言い方が適切かは不明ですが、ともあれ、RxでXAMLによるUIシステムとの親和性を高めるライブラリを作成し、リリースしました。
中身は大きく分けて二つで、一つはReactivePropertyというXAMLと双方向にバインド可能なIObservable<T>、ReactiveCommandというIObservable<bool>からCanExecuteの条件を宣言的に生成するコマンドなど、MVVM的なUI絡みのクラス群。もう一つはWebClientやWebRequestなど、非同期処理のための拡張メソッド群になります。
名前はUI中心に見えますが、UI絡みはいらないよ、という人は非同期周りだけを使ってくれても問題ありません。それと、機能紹介の前に一つ。決して既存のMVVMフレームワークを置き換えたり、同等の機能を提供するものではありません。ViewModelBaseやMessengerなどに相当するものはないので、その辺は適宜、既存のMVVMフレームワークを使えばいいと思います。というか、併用することを推奨します。だから「拡張ライブラリ」と名乗っています。
UIへのバインディング
ReactivePropertyとは何か。というと、双方向にバインド可能なIObservable<T>です。まず、ViewModel(Model)->Viewという片方向のバインドを見てみましょう。時計のようなものを作ります。
<Grid>
<TextBlock Text="{Binding DisplayText.Value}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
public class SimpleClockViewModel
{
// 双方向にバインド可能なIObservable<T>
public ReactiveProperty<string> DisplayText { get; private set; }
public SimpleClockViewModel()
{
// 1秒毎に値を発行、Selectで現在時刻に変換してToReactivePropertyでバインド可能にする
DisplayText = Observable.Interval(TimeSpan.FromSeconds(1))
.Select(_ => DateTime.Now.ToString())
.ToReactiveProperty();
}
}
XAML側ではDisplayText.Valueというように、.Valueまで指定してバインドします。実に簡単にIObservableがバインドできると分かるのではないでしょうか?
IObservableとは、時間軸に沿って値が変わるものです。Intervalはx秒置きに等間隔で変わるので、まさに「時間」といったものですが、それ以外のものも全て時間軸に乗っていると考えることが可能です。例えばイベント、クリックやマウスムーブ、ジェスチャーやセンサーイベントで考えると、タッチした、x秒後にまたタッチした、x秒後にまたタッチした…… 発行される時間が不定期なだけで、時間軸に沿って次の値が出力されるという図式は同じです。
非同期処理もそうで、x秒後に一回だけ値が来る。Rangeや配列のToObservableは0.0001秒刻みに値が来る。Rxで、IObservableで表現することが出来る値というのは、時間軸に乗って変わる/発行する値ということになります。そして、見渡してみると、IObservableになる、時間によって変わるという表現がマッチするものは意外と多い。特にリッチクライアントでは。UI自身の値の変化(バインディング/イベントによる通知)もそうだし、ModelのINotifyPropertyChangedもそう。INotifyPropertyChangedとは、或るプロパティの値が変化したという通知を行うオブジェクト。そのプロパティだけに着目してみれば、時間軸上で連続的に変化する値とみなせる、つまりIObservableで表現できます。
UIからのバインディング
では、UIからのバインディングもしてみましょう。これは、空のReactivePropertyを作成してバインディングします。これにより、UIからの入力をIObservableとして他へと中継することができます。
<StackPanel>
<!-- このTriggerは入力と同時に発火させるために(SL4では)必要なもの -->
<TextBox Text="{Binding CurrentText.Value, Mode=TwoWay}">
<i:Interaction.Behaviors>
<prism:UpdateTextBindingOnPropertyChanged />
</i:Interaction.Behaviors>
</TextBox>
<TextBlock Text="{Binding DisplayText.Value}" />
</StackPanel>
public class FromUIViewModel
{
public ReactiveProperty<string> CurrentText { get; private set; }
public ReactiveProperty<string> DisplayText { get; private set; }
public FromUIViewModel()
{
// UIからのテキスト入力の受け口
CurrentText = new ReactiveProperty<string>();
// そして、それを元にして加工してUIへ返してみたり
DisplayText = CurrentText
.Select(x => x.ToUpper()) // 全て大文字にして
.Delay(TimeSpan.FromSeconds(3)) // 3秒後に値を流す
.ToReactiveProperty();
}
}
Interaction.Behaviorは本題とは関係なくて、値の更新通知のタイミングを、値の変更と同時にするためのものです(デフォルトだとフォーカスが移ったときかな)。WPFでは、こういった小細工がなくてもいいのですが、SL4,WP7では必要なので已むを得ず。詳しくは Silverlight 4のTextBoxのTextプロパティの変更のタイミングでBindingのSourceを更新したい - MSDN Samples Gallery に。というわけで、このUpdateTextBindingOnPropertyChangedはPrismからコピペってきたものです。
さて、入力の受け付けをベースにするものは、newで空の物を作ります。出力中心のものはToReactivePropertyなわけですね。あとは、文字が非連続的に、(同じスレッド上の)非同期でやってくるので、LINQで加工します。Selectで大文字にして、そして、Rxなので時間系のものも使えるので、Delayを使ってみたりしながら、UIに戻しました。なお、Delayの時点で値の実行スレッドはUIスレッドからスレッドプールに移りますが、ReactivePropertyを使う限りは、ReactiveProperty内部でスレッド間の値の通知を解決するため、Dispatcher.BeginInvokeも、ObserveOnDispatcherも不必要です。実行スレッドを意識するなんて原始的ですよね、ReactivePropertyなら、全く意識する必要がなくなります。非同期は自然のまま非同期で扱える。だって、そもそも全てが非同期なのだもの。
ReactiveCommand
ReactivePropertyのもう一つの大事な機構が、ReactiveCommandです。これは、IObservable<bool>という、実行可否の変化のストリームからICommandを生成します。一般的なMVVMフレームワークで使われるRelayCommand, DelegateCommandとは発想が異なるのですが、私はこのReactiveCommandのアプローチこそがベストだと考えます。まずは例を。
<StackPanel>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked1.Value, Mode=TwoWay}">CheckBox1</CheckBox>
<CheckBox IsChecked="{Binding IsChecked2.Value, Mode=TwoWay}">CheckBox2</CheckBox>
<CheckBox IsChecked="{Binding IsChecked3.Value, Mode=TwoWay}">CheckBox3</CheckBox>
<CheckBox IsChecked="{Binding IsChecked4.Value, Mode=TwoWay}">CheckBox4</CheckBox>
</StackPanel>
<TextBox Text="{Binding CurrentText.Value, Mode=TwoWay}" />
<Button Command="{Binding ExecCommand}">Execute?</Button>
</StackPanel>
using Codeplex.Reactive.Extensions; // 拡張メソッドを使う場合はこれを忘れず。
public class CommmandDemoViewModel
{
public ReactiveProperty<bool> IsChecked1 { get; private set; }
public ReactiveProperty<bool> IsChecked2 { get; private set; }
public ReactiveProperty<bool> IsChecked3 { get; private set; }
public ReactiveProperty<bool> IsChecked4 { get; private set; }
public ReactiveProperty<string> CurrentText { get; private set; }
public ReactiveCommand ExecCommand { get; private set; }
public CommmandDemoViewModel()
{
var mode = ReactivePropertyMode.RaiseLatestValueOnSubscribe | ReactivePropertyMode.DistinctUntilChanged;
IsChecked1 = new ReactiveProperty<bool>(mode: mode);
IsChecked2 = new ReactiveProperty<bool>(mode: mode);
IsChecked3 = new ReactiveProperty<bool>(mode: mode);
IsChecked4 = new ReactiveProperty<bool>(mode: mode);
CurrentText = new ReactiveProperty<string>(
initialValue: "テキストが空の時はボタン押せないよ",
mode: mode);
ExecCommand = IsChecked1.CombineLatest(IsChecked2, IsChecked3, IsChecked4, CurrentText,
(a, b, c, d, txt) => a && b && c && d && txt != "")
.ToReactiveCommand();
ExecCommand.Subscribe(_ => MessageBox.Show("Execute!"));
}
}
全てのチェックがONで、かつ、テキストボックスに文字が含まれていないとボタンを押せません(テキストボックスの値の判定はフォーカスが外れてからになります)。CombineLatestというのは、二つの値のうち、どちらか一つが更新されると、片方は新しい値、片方はキャッシュから値を返して、イベントを合成するものです。もし片方にキャッシュがない場合はイベントは起こしません。「二つの値」というように、標準では二つの合成しか出来ないのですが、ReactivePropertyではこれを拡張して7つの値まで同時に合成できるようにしました(それ以上合成したい場合は、匿名型にまとめて、再度CombineLatestを繋げればよいでしょう)。この拡張はCodeplex.Reactive.Extensionsをusingすることで使えるようになります。Extensions名前空間には、他にも有益な拡張メソッドが大量に定義されていますので、是非覗いてみてください。
さて、つまりCombineLatestの結果というのは、ボタンが押せるか否かの、条件のストリームです。どういうことかというと、連続的なCanExecuteです。なので、そのままICommandに変換してしまいましょう、というのがToReactiveCommandになります。CanExecuteに変更があることを、条件のほうからPushして伝えるので、従来使われてきたコマンドを集中管理するCommandManager.RequerySuggestedや、(イベントなので)本来外から叩けないCanExecuteChangedを外から叩けるようにする、などの手立ては不要です。
常にtrueのコマンドならば、new ReactiveCommand()で生成できます。
ところで、mode: ReactivePropertyMode.RaiseLatestValueOnSubscribe というのは、Subscribeされる時に(ToReactivePropertyやToReactiveCommandは内部でSubscribeしています)、同時に最新の値を返します(最新の値がない場合は初期値を返します。初期値は指定することもできますし、もし指定していない場合はdefault(T)になります)。どういうことかというと、判定のタイミングの問題があります。CombineLatestは全てに一度は値の通知が来ている(キャッシュが存在する)状態じゃないと、イベントを起こしてくれません。なので、CanExecuteを初回時から判定させるために、これの指定が必要です。なお、ReactivePropertyModeのデフォルトはDistinctUntilChangedのみで、RaiseLatestValueOnSubscribeは明示的に指定しなければなりません。一件便利そうに見えるRaiseLatestValueOnSubscribeですが、問題も抱えていまして、後でも詳しく説明しますがバリデーションの時。バリデーションの場合は初回実行はして欲しくない(画面を表示したら、いきなり真っ赤っかだと嫌でしょう?)ケースがほとんどのはずです。RaiseLatestValueOnSubscribeを指定すると、初回実行してしまうので、そういう場合にとても都合が悪いのです。これは、良し悪しなので、適宜判断して、最適な方をお選びください。
宣言的であるということ
ReactiveCommandは、条件を宣言的に記述しました。そして、外部から叩くことは禁じられているので、その宣言以外のことが絡む可能性はありません。また、状態を外部変数から取得する(RelayCommandなどはそうなりますね)わけではないので、CanExecuteの変化のスコープはToReactiveCommandをする一連のシーケンスを読むだけですみます。変数を返す場合は、変数を使う範囲、つまりオブジェクト全体から変更可能性が混ざるという、大きなスコープの観察を余儀なくされます。読む場合だけでなく、書く場合でも、集中的に変化の条件を記述することができるので、ずっと楽でしょう。
ReactivePropertyもまた、同じです。値の変化の条件を宣言的に記述しました(こちらはReactiveCommandと違い、(Two-wayでバインド可能にするという都合上外部から叩くことが可能なのでスコープは閉じていませんが)。大事なのは、スコープを小さくすること。大きなスコープは往々に管理しきれないものです。リッチクライアントはステートを持つ。その通りだ。プロパティが、オブジェクトが、絡みあう。それは実に複雑なのは間違いない。でも複雑だから難しくて当然、複雑だからテストできない、複雑さを複雑なまま放っておいたら、それはただの新世代のスパゲティにすぎない。
ステートを捨てようじゃあなくて、宣言的にやろう。それがReactivePropertyの解決策、提案です。
INotifyPropertyChangedと一緒に。
全てReactivePropertyで相互作用を記述する、というのは理想的ですが過激派です。それに、既存のModelや自動生成のModelなど、様々なところにINotifyPropertyChangedはあります。理想だけじゃ生きていけません。それに、私もプレーンなModelはPOCO(+INotifyPropertyChanged)のほうが嬉しい。でも、IObservableになっていないと、関係の合成が不可能で困るので、INotifyPropertyChanged -> ReactivePropery変換を可能にしました。ここでは説明しませんが、その逆のReactiveProperty -> INotifyPropertyChanged変換も可能です。
using Codeplex.Reactive.Extensions; // ObservePropertyもこれをusingで。
public class ObserveViewModel
{
public ReactiveProperty<string> ModelText { get; private set; }
public ObserveViewModel()
{
ModelText = new ToaruModel()
.ObserveProperty(x => x.Text) // タイプセーフにIObservable<T>に変換
.ToReactiveProperty();
}
}
// WCFからだったりEntity Frameworkだったり既存のModelだったり他のViewModelだったり
// ともかく、INotifyPropertyChangedは至る所に存在します
public class ToaruModel : INotifyPropertyChanged
{
private string text;
public string Text
{
get { return text; }
set { text = value; PropertyChanged(this, new PropertyChangedEventArgs("Text")); }
}
public event PropertyChangedEventHandler PropertyChanged = (_, __) => { };
}
INotifyPropertyChangedの仕組みって、とあるプロパティが変更された、と名前でPushして、変更通知を受けた方はその名前を元にPullで取り出す。描画フレームワークが面倒を見ているなら、それでいいのですが、通常使うには、とてもまどろっこしい。だから、そうしたオブジェクトという大きな土台から名前ベースのPush & Pull通知を、プロパティ単位の小さなPush通知に変換してやりました。指定はExpressionで行うのでタイプセーフですしね。
ReactivePropertyのほうがINotifyPropertyChangedよりも細かいハンドリングが効くのは当たり前の話で、単位が小さいから。逆に、だから、INotifyPropertyChangedという大きい単位で関係を作り込んでいくのは、非常に複雑でスパゲティの元だと言わざるを得ない。勿論、Reactive Extensionsという、プロパティ単位でのPushを自在に扱う仕組みが背後にあってこそのやり方ではあるのですが。
MとVMの境界
が、曖昧にみえるのはその通りかもしれません。けれど、処理がVMに偏りすぎるように見えるのなら、それは素直にMに移せばいい。細かいMを束ねるMを導入すればいい。名前は、サービスでもファサードでもプロキシーでもアプリケーションでもコントローラーでも、なんでもいい(わけではないけれど)。移せばいいなんて簡単にいいますが、それは簡単にできるからです。VM-M-VMが一気通貫してループを描いているなら、ローカル変数への依存もなくメソッドチェーンを切った貼ったするだけなので、どこに置くのも移すのは楽です。
そもそも、最初から明確に分けようとしたってどうせうまくいかないもの。インターフェイスだって、具象型から抽象を見出したほうが簡単だし、ずっとうまくいくでしょう?ネーミングだってリファクタリングで徐々に洗練させる。そもそもVMがヘヴィになりがちなのは、目で見える境界がないから、なせいでしょう。VはXAMLで線引きされるけれど、それ以外はコードで地続き。理想論だけで線を引こうとしたって空疎だし、そもそも、無理な話。境界を見出すには具体的に積み重なった後じゃないと無理でしょう(勿論、境界の敷き方を常日頃考える、研究することは有意義だと思います。そもそも考えていないと、いざ境界を見出そうとしても見えませんから)
そもそもMVVMなのか
UIに対するReactive Programmingなのは間違いないと思ってます。Reactive ProgrammingはUI向きだ。よく聞くその話は、実際その通りだと思うのですが、しかし同時にUI(というか、WPF/SL/WP7などXAML)とRxってどうもイマイチフィットしないなぁ、と悩んでいました。その理由は、最終的に描画を司るフレームワーク(XAML)とミスマッチなせいにあるのだと、気づきました。フレームワークの要求(INotifyPropertyChangedなオブジェクトであったりICommandであったり)と異なるものを、そのまま使おうとしたところで、良い結果は得られない。ゴリ押ししてもXAMLの旨みが生かせないし、Reactive Programmingを大前提に置いた描画フレームワークを構築すれば、もっと違う形になるでしょうが、そんなものは非現実的な話です。膨大な投資のされた、現在のXAML中心のシステムより良いもの……。やはり、絵空事にしか見えません。それに、XAMLは何だかんだ言って、良いものです。
それを認識したならば、必要なのは、境界を繋ぐシステムだと導ける。そのことを念頭においてReactivePropertyとReactiveCommandをデザインしました。MVVMライクなのは描画フレームワークに合わせた結果です、だから、MVVMでもあり、そうでもないようでもある。ただ、それによってパラダイムがミックスされてどちらの長所も活かせるし、世界最高峰のシステムであるXAMLアプリケーションに乗っかれるので今すぐ実用的という面もあるわけなので、これでいいと思うんです。いや、これがいいんです。マルチパラダイムは悪いことではない。あとは、ミックス故に生まれる新しい悩みをどう解消していくか、です。
マルチパラダイムといえば、ReactivePropertyは描画フレームワークからの言語への要求が変化(吸収)しているので、F#でも美味しくXAMLアプリケーションを書くことが可能になるでしょう。多分。
非同期拡張メソッド群
Rxは非同期の苦痛を癒す。とはいっても、実のところ素の状態だと罠が多くて、意外と使いづらかったりします。WebClientは、実行順序の問題があり、そのままではRxで扱いにくい。WebRequestはWebRequestでプリミティブすぎて機能が乏しいし、そのままではリソース処理に問題を抱えたりします。どちらも、ただFromEvent, FromAsyncするだけでは足りなくて、もう一手間かけたRx化が必要です。そのため、WebClient, WebRequestに対して拡張メソッドを用意し、簡単に実行出来るようにしました。
ReactivePropertyと合わせてのインクリメンタルサーチの例を。これは、ReactivePropertyのダウンロードファイルに含む非同期サンプルですので、是非ダウンロードして、サンプルを実際に実行してみてください。
using Codeplex.Reactive.Asynchronous; // 非同期系の拡張メソッド群を格納
using Codeplex.Reactive.Extensions; // OnErrorRetryはこちら
public class AsynchronousViewModel
{
public ReactiveProperty<string> SearchTerm { get; private set; }
public ReactiveProperty<string> SearchingStatus { get; private set; }
public ReactiveProperty<string> ProgressStatus { get; private set; }
public ReactiveProperty<string[]> SearchResults { get; private set; }
public AsynchronousViewModel()
{
// IncrementしたりDecrementしたりすることでイベント(Empty ,Inc, Dec, Max)が発生する
// それはネットワークの状態を管理するのに都合が良い(IObservable<SignalChangedStatus>)
var connect = new SignalNotifier();
// 指定したスケジューラ(デフォルトはUIDispatcherScheduler)上で任意にイベントを起こせる
// 主にProgressと併用して進捗報告に利用する
var progress = new ScheduledNotifier<DownloadProgressChangedEventArgs>();
SearchTerm = new ReactiveProperty<string>();
// 検索は当然非同期で行い、それをダイレクトにバインドしてしまう
SearchResults = SearchTerm
.Select(term =>
{
connect.Increment(); // 非同期なのでリクエストは一つじゃなく並列になるので、これで管理
return WikipediaModel.SearchTermAsync(term, progress)
.Finally(() => connect.Decrement()); // リクエストが終了したら、確実にカウントを下げる
})
.Switch()
.OnErrorRetry((WebException ex) => ProgressStatus.Value = "error occured")
.Select(w => w.Select(x => x.ToString()).ToArray())
.ToReactiveProperty();
// SignalChangedStatus : Increment(network open), Decrement(network close), Empty(all complete)
SearchingStatus = connect
.Select(x => (x != SignalChangedStatus.Empty) ? "loading..." : "complete")
.ToReactiveProperty();
ProgressStatus = progress
.Select(x => string.Format("{0}/{1} {2}%", x.BytesReceived, x.TotalBytesToReceive, x.ProgressPercentage))
.ToReactiveProperty();
}
}
// 非同期リクエストとデータ。単純ですが、Modelということで。
public class WikipediaModel
{
const string ApiFormat = "http://en.wikipedia.org/w/api.php?action=opensearch&search={0}&format=xml";
public string Text { get; set; }
public string Description { get; set; }
public WikipediaModel(XElement item)
{
var ns = item.Name.Namespace;
Text = (string)item.Element(ns + "Text");
Description = (string)item.Element(ns + "Description");
}
// WebClientの他に、WebRequestやWebResponseへの非同期拡張メソッドも多数用意されています
// また、ほとんど全ての非同期拡張メソッドにはプログレス通知を受け付けるオーバーロードがあります
public static IObservable<WikipediaModel[]> SearchTermAsync(string term, IProgress<DownloadProgressChangedEventArgs> progress)
{
var clinet = new WebClient();
return clinet.DownloadStringObservableAsync(new Uri(string.Format(ApiFormat, term)), progress)
.Select(Parse);
}
static WikipediaModel[] Parse(string rawXmlText)
{
var xml = XElement.Parse(rawXmlText);
var ns = xml.Name.Namespace;
return xml.Descendants(ns + "Item")
.Select(x => new WikipediaModel(x))
.ToArray();
}
public override string ToString()
{
return Text + ":" + Description;
}
}
色々な機能を一度に説明しようとしているので、些か複雑かもしれません。まず、非同期リクエストは並列になります。例えばボタンを、通信中はDisabledにするのに、単純にbooleanで管理してもうまくいきません。どれか一つのアクセスが始まったらDisabledにしてどれか一つのアクセスが終わったらEnabledにする。それではダメです。どれか一つのアクセスが終わったところで、他のリクエストが通信中かもしれないケースに対応できませんから。
そこで、ReactivePropertyではSignalNotifierというものを用意しました。これは、IncrementかDecrementの操作によって、「ゼロになった」「インクリメントされた」「デクリメントされた」「Max(初期値で指定した場合)になった」というイベントを発行します。イベントといっても、自身がIObservable<SignalStatus>になっているので、直接Rxで扱えます。これのネットワークリクエストへの適用はシンプルで、通信開始されたらインクリメント。通信終了したらデクリメントする。そして、ゼロになったか否かを見れば、それが通信中か否かの判定になります。
非同期拡張メソッドはキャンセルに対しても強く考慮されています。WebClient(のSubscribeの戻り値)にDisposeするとCancelAsyncを、WebRequest(のSubscribeの戻り値)にDisposeするとAbortを呼ぶようになっています。このような挙動は、単純にFromEvent, FromAsyncしただけでは実現できないので、大きくて間を省けることでしょう。ネットワークリクエストを自身でキャンセルすることは少ないかもしれませんが、上の例であげたSwitchは内部でDisposeを呼びまくる仕組みになっていますので、しっかり対応している、というのは実行上、大きなアドバンテージとなります。
Switchは複数の非同期リクエストが確認された場合に、前のリクエストをキャンセル+キャンセルが遅れた場合でも遮断して結果を後続に返さないことで、最新のリクエストの結果のみを返します。そのため、非同期リクエストが抱える結果が前後してしまう可能性、例えばインクリメンタルサーチではLINQと検索したのに、LINQの結果よりも後にLIの結果が返ってきたために、表示されるのがLIの結果になってしまう。などという自体が防げます。
また、OnErroRetryに注目してください。これはReactivePropertyが独自に定義している拡張メソッドで、例外発生時の処理をすると同時に、Retry(ここでいうとSearchTermの再購読なので、つまりチェーンの状態が維持される、ということになる)します。ToReactivePropertyを使い、ダイレクトに結び付けている場合は、例外が発生するとチェーンが終了して困るのですが、例外処理にこのOnErrorRetryを使うことで、そのような悩みは不要になります。なお、このOnErrorRetryは勿論ReactiveProperty専用というわけでもなく汎用的に使えます。例えば、もしネットワークからのダウンロードに失敗したら、一定間隔を置いて再度ダウンロードをする、但しリトライの挑戦は指定回数まで。というよくありそうな処理が、引数で回数とTimeSpanが指定できるので、簡単に記述できます。
進捗レポートも非同期処理では欠かせませんが、これは非同期拡張メソッドとScheduledNotifierを組み合わせることで、簡単に実現出来ます。これら非同期周りのサポートはReactivePropertyの重要な柱だと考えているので、UI周りの機能は必要ない、という人も、是非試してみて欲しいです。
同期 vs 非同期
SLやWP7はともかく、WPFでこのように強烈に非同期サポートする意味はあるのでしょうか。というと、あります(ただたんにコード共有しているから、というだけではなく)。まず、WinRTがそうなように、時間のかかる処理は時間のかかる処理なわけなので、強制的に非同期になっていたほうが、ViewModelなり束ねるModelなりで、そこら中に、明示的にスレッド管理(ただたんにTaskに投げるのも含む)をしないで済みます。本質的に非同期(CPU依存ではない形で時間がかかる)なものは非同期として扱ったほうが易しいのです。
もう一つは、Switchのような、キャンセルを多用した処理が書きやすいこと。それに、自然な形でプログレス処理もサポートできます。更には、ReactivePropertyを全面に使うのなら、全てがReactiveに通知しあう世界、つまり全てが非同期で回っているので、非同期のほうが圧倒的に相性が良いです。同期プログラミングさようなら。大丈夫です、何も問題ありません。
C#5.0 Async vs Rx
従来通りに書く。シンプルに同期のように。のならば、async/awaitのほうがずっと良い。そういう使い方をする場合は、Rxを非同期に使う必要性というのは、今後はなくなるでしょう。ではRxでの非同期に価値はなくなってしまうのか?というと、それに関しては明確にNOと答えます。
Rxの場合はLINQということで、宣言的なスタイル、演算子という形に汎用的な処理を閉じ込められる高いモジュール性。というのがあります。上で見たきたように、Switchのようなこと、OnErrorRetryのようなこと、これらを演算子という形で定義して、メソッド一発で適用出来るのはRxならではの利点です。もし自分でそれらの処理を書くとしたら…… あまり考えたくはないし、もしメソッドの形でまとめあげるとしても、Rxのように綺麗に適用させるのは不可能でしょう。どこか歪んだシグネチャを抱えることになります。
ReactivePropertyと親和性が高いのでViewModelへの伝達に使いやすいというのもポイントですね(TaskとIObservableは相互に変換可能なので、ToTaskしたりToObservableしたりするだけなので、別段問題でもないですけど)
使い分けというのは実際のところ幻想みたいなことなので、人によりどちらか主体のスタイルには落ち着くでしょう。私は、とりあえずRx主体で行きたいかなあと思ってますが、ライブラリ的な部分ではasync/awaitを使って書くでしょう(演算子の組み合わせでやろうとすると書くのも難しいし、パフォーマンスも出ないので)。現在のシーケンスに対する、yield returnで汎用的な演算子を作って、通常使うシーンではLINQ to Objectsで、定義した演算子を含めて使っていく。というのと同じスタイルが良さそうだと想像します。async/awaitの書きやすさ・パフォーマンスと、Rxのモジュール性の両立はその辺かなあ、って。
あと、連続的な非同期処理を一纏めにするというのが(今のところ)async/awaitだと出来ない(Task<T>の戻り値は一つだけだから)ので、その辺をやりたい場合にもRx(IObservable<T>は元より複数の戻り値を内包する)頼みになります。ここは将来的にどういう形になるのか、まだ不明瞭なところなので断言はしませんが。
Validation
ReactivePropertyでは、三種類のバリデーションに対応しています。DataAnnotationsによる属性ベース、IDataErrorInfoによるPull型のエラー確認、INotifyDataErrorInfoによるPush型/非同期のエラー確認。ただし、WPFではINotifyDataErrorInfoは使えなく(.NET4.5からは入るようですが)、WP7ではDataAnnotationsが使えません。これはクラスライブラリの問題なので私の方では如何ともしがたくで。
// XAMLは省略。詳しくはSample/Validationを見てください
public class ValidationViewModel
{
[Required]
[Range(0, 100)]
public ReactiveProperty<string> ValidationAttr { get; private set; }
public ReactiveProperty<string> ValidationData { get; private set; }
[StringLength(5)]
public ReactiveProperty<string> ValidationBoth { get; private set; }
public ReactiveProperty<string> ValidationNotify { get; private set; }
public ReactiveProperty<string> ErrorInfo { get; private set; }
public ReactiveCommand NextCommand { get; private set; }
public ValidationViewModel()
{
// 属性ベースのバリデーションは、自身のプロパティをExpressionで指定することで適用できます
// 通常属性ベースの場合、例外経由ですが、ReactivePropertyではIDataErrroInfo経由になります
// そのため、XAML側ではValidatesOnDataErrors=Trueにしてください
ValidationAttr = new ReactiveProperty<string>()
.SetValidateAttribute(() => ValidationAttr);
// IDataErrorInfoではエラー時のメッセージを渡します、nullの場合は成功の判定になります
ValidationData = new ReactiveProperty<string>()
.SetValidateError(s => s.All(Char.IsUpper) ? null : "not all uppercase");
// 三種類の指定は、重ねることが可能です(但し同じ種類のものを複数指定するのは不可能)
ValidationBoth = new ReactiveProperty<string>()
.SetValidateAttribute(() => ValidationBoth)
.SetValidateError(s => s.All(Char.IsLower) ? null : "not all lowercase");
// INotifyDataErrorInfoの場合は、IObservable<IEnumerable>を返してください
// 第一引数はself、つまりIObservable<T>になっていて、最終的にSelectでIEnumerableに変換します
// IObservableということで、非同期での検証が可能になっているのがポイントです
// これもIDataErrorInfoと同じく、nullの場合は成功という判定になります
ValidationNotify = new ReactiveProperty<string>("foo!", ReactivePropertyMode.RaiseLatestValueOnSubscribe)
.SetValidateNotifyError(self => self
.Delay(TimeSpan.FromSeconds(3)) // DB問い合わせなど非同期なバリデーション(が可能)
.Select(s => string.IsNullOrEmpty(s) ? null : new[] { "not empty string" }));
// バリデーションの結果は、三種類全てまとめられてObserveErrorChangedから購読できます
var errors = Observable.Merge(
ValidationAttr.ObserveErrorChanged,
ValidationData.ObserveErrorChanged,
ValidationBoth.ObserveErrorChanged,
ValidationNotify.ObserveErrorChanged);
// もし、それらを分類したいときは、OfTypeを使うといいでしょう
ErrorInfo = Observable.Merge(
errors.Where(o => o == null).Select(_ => ""), // 成功はnull
errors.OfType<Exception>().Select(e => e.Message), // 属性からはException
errors.OfType<string>(), // IDataErrorInfoからはstring
errors.OfType<string[]>().Select(xs => xs[0])) // INotifyDataErrorInfoからは、IEnumerableの何か
.ToReactiveProperty();
// 検証が全部通ったら実行可能にするコマンド、などもこうやって書けますね!
NextCommand = errors.Select(x => x == null).ToReactiveCommand(initialValue: false);
NextCommand.Subscribe(_ => MessageBox.Show("Can go to next!"));
}
}
一点だけ通常と異なるのは、属性ベースのものを例外ではなくてIDataErrorInfoとして扱います(この辺はRxのパイプラインを通す都合上、例外を出すという形での実現が不可能だったので)
Event to Observable
イベントをXAML側で指定して、ReactivePropetyにバインドすることが可能です。
<Grid>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseMove">
<r:EventToReactive ReactiveProperty="{Binding MouseMove}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBlock Text="{Binding CurrentPoint.Value}" />
</Grid>
public class EventToReactiveViewModel
{
public ReactiveProperty<MouseEventArgs> MouseMove { get; private set; }
public ReactiveProperty<string> CurrentPoint { get; private set; }
public EventToReactiveViewModel()
{
// UIからのイベントストリームを受信
MouseMove = new ReactiveProperty<MouseEventArgs>();
// とりあえず座標を表示する、というもの
CurrentPoint = MouseMove
.Select(m => m.GetPosition(null))
.Select(p => string.Format("X:{0} Y:{1}", p.X, p.Y))
.ToReactiveProperty("MouseDown and drag move");
}
}
お手軽なので、結構便利だと思います。あの長大なFromEventPatternを書くよりかは(笑)
シリアライズ
特にWP7では頻繁な休止と復帰で、シリアライズ/デシリアライズによる状態の回復が重要です。そこで、値の回復を可能にするシリアライズ用のヘルパーを用意しました。
// こんなビューモデルがあるとして
public class SerializationViewModel
{
// なにもつけてないと普通にシリアライズ対象
public ReactiveProperty<bool> IsChecked { get; private set; }
[IgnoreDataMember] // Ignoreつけたら無視
public ReactiveProperty<int> SelectedIndex { get; private set; }
[DataMember(Order = 3)] // Orderつけたら、デシリアライズの順序を規程
public ReactiveProperty<int> SliderPosition { get; private set; }
}
// 例えばWindows Phone 7のトゥームストーンなシチュエーションを考えてみると
private SerializationViewModel viewmodel = new SerializationViewModel();
private string viewmodelData = null;
protected override void OnNavigatingFrom(System.Windows.Navigation.NavigationEventArgs e)
{
viewmodelData = SerializeHelper.PackReactivePropertyValue(viewmodel);
}
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
SerializeHelper.UnpackReactivePropertyValue(viewmodel, viewmodelData);
}
デシリアライズの順序はDataMember属性のOrderに従います。詳しくはデータ メンバーの順序を参照のこと。Pushしあう関係の都合上、デシリアライズの順序によって正確な復元ができないこともあるでしょうから、その場合は、Orderをつけると、ある程度制御できます。また、IgnoreDataMember属性をつけておくと、シリアライズ対象から除外することが可能です。
スニペットとサンプル
NuGetから入れてもらってもいいのですが、ダウンロードしてもらえるとコードスニペットとサンプルがついてきますので、最初はダウンロードのほうがいいかもです。コードスニペットはrpropでReactiveProperty<T> PropertyName{ get; private set; }という頻繁に書くことになる宣言が展開されます。他にはrcomm(ReactiveCommand)など。
サンプルはWPF/SL4/WP7全てで用意しました。サンプルを割としっかり用意した最大の理由は、ただ渡されても、もしかしなくてもどう書けばいいのかさっぱり分からないのでは、と思ったのがあります。決して複雑ではなく、むしろシンプルだし記述量は大幅に減るわけです、が、従来のやり方からするとあまりにも突飛なのは否めないので、いきなりスイスイ書くというのは無理ですよねぇ、と。
その他紹介していない、サンプルに載ってない機能は、まだいっぱい。こんなに記事がなくなっちゃってもまだ全然足りない。でも、いきなりてんこ盛りだと引いてしまうので、基本的にはReactivePropertyとReactiveCommandが主体で、慣れたら徐々に周囲を見てもらえばな、ぐらいに思っています。
まとめ
仕上がりはかなり良いと、興奮しています。この長い記事!興奮を伝えたいという気持ちでいっぱいだからです。今後も、利用シーンの模索と合わせて、どんどん進化させていくつもりです。初回リリースですし、というのもありますが、コアコンセプトの実現と、使い勝手としてのAPIの錬成に力を注いだので、それ以外の部分の研究が疎かになっているというのは否めませんので、そこのところの強化も行なっていきます。また、JavaScriptへの移植もノリ気なので、まずKnockout.jsを試して、その上に構築させたいなあ、とか考えています。
ところで、10/8土曜日、明日のSilverlightを囲む会in東京#4の一番最後に、少し、デモ中心でお話をするつもりなので(最後のオマケなのでほんの少しだけですけどね)良ければ見に来てください。ギリギリではありますが、まだ申し込みも出来ると思います。また、もしよければ会場/懇親会でつかまえて聞いてくれたりすると泣いて喜びます。会場に来れなくてもIIJさんのSmooth Streamingで超高画質な配信が行われると思われますので、そちらでも是非是非。