ReactiveProperty ver 0.3.0.0 - MとVMのバインディングという捉え方
- 2011-11-20
今回の更新よりアイコンが付きました。専用のアイコンがあると、とっても本格的な感じがしますねー。色はRxにあわせて紫-赤紫。デザインは私の好みな幾何学的な感じです。@ocazucoさんに作って頂きました、ありがとうございます!色々ワガママ言ってお手数かけました。
ReactiveProperty - MVVM Extensions for Rx - ver 0.3.0.0
Rxとは何か、というとIObservable<T>と「見なせる」ものを合成するためのライブラリです。だから、見なせるものさえ見つかれば、活躍の幅は広がっていく。ReactivePropertyは色々なものを、そのように「見なして」いくことで、RxでOrchestrateできる幅をドラスティックに広げます。土台にさえ乗せてしまえば、あとはRxにお任せ。その場合に大切なのは、土台に乗せられるよう、閉じないことです。しかし、もし閉じているのなら、開くための鍵を提供します。
デフォルトモード変更
ReactivePropertyのデフォルトモードが DistinctUntilChanged|RaiseLatestValueOnSubscribe になりました。今まではRaise...が入ってなかったのですが、思うところあって変わりました。例えばCombineLatestは、全てが一度は発火していないと動き出しません。ReactiveCommandの条件に使うなどの場合にRaiseしてくれないと不都合極まりなく、かつ、Subscribeと同時にRaiseすることによる不都合なシーンは逆に少ない。ことを考えると、必然的にデフォルトをどちらに振るべきかは、分かりきった話でした。
そのことは0.1の時、サンプル作りながら思ってたんですが悩んだ末に、省いちゃったんですねえ。RaiseLatestValueOnSubscribeが入ると不便なシーンもある(initialValueを設定しないとまず最初にnullが飛んでいくとか)ので、どちらを取るかは悩ましいところではあるんですが、シチュエーションに応じて最適なほうを選んでください、としか言いようがないところです。
ToReactivePropertyAsSynchronized
長い。メソッド名が。
これは何かというとINotifyPropertyChanged->ReactiveProperty変換です。今までもObservePropertyメソッド経由で変換できましたが、それは一度IObservable<T>に変換するため、Model→ReactivePropertyという一方向のPushでしかありませんでした。Two-wayでのバインドで値の同期を取りたい場合は、今回から搭載されたToReactivePropertyAsSynchronizedを使ってください。
// こんな通知付きモデルがあるとして
public class ObservableObject : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set
{
name = value;
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
public event PropertyChangedEventHandler PropertyChanged = (_, __) => { };
}
// それを使ったViewModelを作るなら
public class TwoWayViewModel
{
public ReactiveProperty<string> OneWay { get; private set; }
public ReactiveProperty<string> TwoWay { get; private set; }
public TwoWayViewModel()
{
var inpc = new ObservableObject { Name = "ヤマダ" };
// ObservePropertyを使うとIObservable<T>に変換できます
// ラムダ式でプロパティを指定するので、完全にタイプセーフです
// それをToReactivePropertyすればOneWayで同期したReactivePropertyになります
OneWay = inpc.ObserveProperty(x => x.Name).ToReactiveProperty();
// ToReactivePropertyAsSynchronizedで双方向に同期することができます
TwoWay = inpc.ToReactivePropertyAsSynchronized(x => x.Name);
}
}
INotifyProeprtyChangedなModelをReactivePropertyなViewModelに持っていきたい時などに、使いやすいのではと思います。また、同期する型が異なっていても対応することができます。コンバーターのようにconvertとconvertBackを指定してください。
ReactiveProperty.FromObject
こちらもToReactivePropertyの亜種ですが、ReactiveProperty→Modelというソース方向への片方向の同期を取ります。ModelはINotifyPropertyChangedである必要はありません。
// こんなただのクラスがあったとして
public class PlainObject
{
public string Name { get; set; }
}
// それと同期させたいとき
public class OneWayToSourceViewModel
{
public ReactiveProperty<string> OneWayToSource { get; private set; }
public OneWayToSourceViewModel()
{
var poco = new PlainObject { Name = "ヤマダ" };
// ReactiveProperty.FromObjectで変換することができます
// この場合、ReactiveProperty -> Objectの方向のみ値が流れます
OneWayToSource = ReactiveProperty.FromObject(poco, x => x.Name);
}
}
片方向の同期が定型的な局面、例えば設定クラスなんかは通知は必要ないと思うのですが、それをUIから一方向で値を投影したい場合に、これを使うことで楽になると思います。
また、Sampleにこれら3つの解説を追加しましたので、実際にどう反映されるのか、動きを確認したい場合はそちらを見てください。
CombineLatestValuesAreAllTrue
長い。メソッド名が。これはReactive Extensionsお題 - かずきのBlog@Hatenaに書かれているもので、使うシーンよくありそうな頻出パターンになりそうだと思ったので、お借りすることにしました。ありがとうございます。使い方を見てもらったほうが速いので、まず例を。
<StackPanel>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsCheckedA.Value, Mode=TwoWay}">Check A</CheckBox>
<CheckBox IsChecked="{Binding IsCheckedB.Value, Mode=TwoWay}">Check B</CheckBox>
<CheckBox IsChecked="{Binding IsCheckedC.Value, Mode=TwoWay}">Check C</CheckBox>
</StackPanel>
<Button Command="{Binding ExecCommand}">全部チェックで押せる</Button>
</StackPanel>
// using Codeplex.Reactive.Extensions; (これを忘れないように)
public class MainPageViewModel
{
public ReactiveProperty<bool> IsCheckedA { get; private set; }
public ReactiveProperty<bool> IsCheckedB { get; private set; }
public ReactiveProperty<bool> IsCheckedC { get; private set; }
public ReactiveCommand ExecCommand { get; private set; }
public MainPageViewModel()
{
IsCheckedA = new ReactiveProperty<bool>();
IsCheckedB = new ReactiveProperty<bool>();
IsCheckedC = new ReactiveProperty<bool>();
ExecCommand = new[] { IsCheckedA, IsCheckedB, IsCheckedC }
.CombineLatestValuesAreAllTrue()
.ToReactiveCommand();
ExecCommand.Subscribe(_ => MessageBox.Show("しんぷる!"));
}
}
3つのチェックボックスが全てONなら実行可能なコマンドを作る、です。こんな風に、全てがtrueの時、といった集約をしたい場合に便利に使うことができます。プレゼンテーションロジック、に該当する部分だと思いますが、ここでもRxは十分以上に活躍できます。また、外部からCanExecuteChangedをぶっ叩くようなカオティックなこともしません、ReactiveCommandならね。
ReactiveTimer
Timerです。.NETはTimerが山のようにあります。Threading.Timer, Timers.Timer, Forms.Timer, DispatcherTimer, Observable.Timer。ここにまたReactiveTimerという新たなるTimerが誕生し、人類を混乱の淵に陥れようとしていた……。まさにカオス。
ちょっと整理しましょう。まず、Threading.Timerは一番ネイティブなTimerと捉えられます。そのままだと少しつかいづらいので、軽くラップしてイベントベースにしたのがTimers.Timer。Forms.TimerとDispatcherTimerは、それぞれのアプリケーション基盤で時間を計って伝達してくれるというもの、UI系でのInvokeが不要になるので便利。と、それなりに役割の違いはあります。微妙な差ですが。
最後のObservable.TimerはIObservableで通達してくれるのでRxと非常に相性が良いタイマー。また、タイマーを行う場所もISchedulerで任意に指定できるので、ThreadPoolでもDispatcherでもCurrentThread(この場合はSleepで止まるので固まりますけどね)でも、もしくは仮想スケジューラ(任意に時間を動かせるのでテストが簡単になる)でも良いという柔軟さが素敵で、Rx以降のプログラミングではタイマーなんてObseravble.Timer一択だろ常識的に考えて。という勢い。(精度は若干落ちるので、よほど精度を求める時はThreading.Timerを使いましょう)。だと思っていた時もありました。
一時停止出来ないんですよ、Observable.Timer。発動したらしっぱなし。Stopはできる(Disposeする)けど、そうしたら再開は出来ない。それじゃあ困る場合があります!はい。結構あります。そういう場合はTimers.TimerをFromEventでラップする。それはそれで良いのですが、Observable.TimerのISchedulerを指定可能という柔軟さを捨てるのは勿体無いなあ、と思ったのでした。
そこで、今回ReactiveTimerを作りました。機能は、Observable.TimerのStop/Start出来る版です。
[TestClass]
public class ReactiveTimerTest : ReactiveTest
{
[TestMethod]
public void TimerTest()
{
// テスト用の自由に時間を動かせるスケジューラ
var testScheduler = new TestScheduler();
var recorder = testScheduler.CreateObserver<long>();
// 作成時点では動き出さない
var timer = new ReactiveTimer(TimeSpan.FromSeconds(1), testScheduler);
timer.Subscribe(recorder); // Subscribeしても動き出さない
timer.Start(TimeSpan.FromSeconds(3)); // ここで開始。初期値を与えるとその時間後にスタート
// 時間を絶対時間10秒のポイントまで進める(AdvanceTo)
testScheduler.AdvanceTo(TimeSpan.FromSeconds(5).Ticks);
// MessagesにSubscribeに届いた時間と値が記録されているので、Assertする
recorder.Messages.Is(
OnNext(TimeSpan.FromSeconds(3).Ticks, 0L),
OnNext(TimeSpan.FromSeconds(4).Ticks, 1L),
OnNext(TimeSpan.FromSeconds(5).Ticks, 2L));
timer.Stop(); // timerを止める
recorder.Messages.Clear(); // 記録をクリア
// 時間を現在時間から5秒だけ進める(AdvanceBy)
testScheduler.AdvanceBy(TimeSpan.FromSeconds(5).Ticks);
// timerは止まっているので値は届いてないことが確認できる
recorder.Messages.Count.Is(0);
}
}
そう、単体テストしたい場合は、TestSchedulerに差し替えれば、AdvancedBy/Toによって、時間を自由に進めることが可能になります。Assertに使っているIs拡張メソッドはChaining Assertionです。Testing周りの詳しい解説はRx-Testingの使い方 - ZOETROPEの日記に書かれています。
CountNotifier/BooleanNotifier
SignalNotifierという名前はよく分からないので、今回よりCountNotifierに変更しました。また、名前空間をNotifiersに変更しました。更に、二値での通知を行うBooleanNotifierを新規追加しました。どちらも、IObservable経由での通知を行うフラグです。
// using Codeplex.Reactive.Notifiers;
// 通知可能(IObservable)なboolean flag
var boolFlag = new BooleanNotifier(initialValue: false);
boolFlag.Subscribe(b => Console.WriteLine(b));
boolFlag.TurnOn(); // trueにする, trueの状態だったら何もしない
boolFlag.Value = false; // .Valueで変更、既にfalseの状態でも通知する
boolFlag.SwitchValue(); // 値を反転させる
// 通知可能(IObservable)なcount flag
var countFlag = new CountNotifier();
countFlag.Subscribe(x => Console.WriteLine(x));
countFlag.Increment(); // incしたり
countFlag.Decrement(); // decしたりの状態が通知される
// Empty(0になった状態)という判定でフィルタして状態監視したりできる
countFlag.Where(x => x == CountChangedStatus.Empty);
例えば非同期処理を行う際などの、状態の管理に使うことができます。
Pairwise
neue cc - Reactive Extensionsで前後の値を利用するで書いた、前後の値をまとめる拡張メソッドです。
// { Old = 1, New = 2 }
// { Old = 2, New = 3 }
// { Old = 3, New = 4 }
// { Old = 4, New = 5 }
Observable.Range(1, 5)
.Pairwise()
.Subscribe(Console.WriteLine);
古い値と新しい値を使って何かしたい場合などにどうぞ。
CatchIgnore
例外処理用に、OnErrorRetryというものを用意していましたが、今回それ以外にCatchIgnoreを追加しました。
// 1, 2
Observable.Range(1, 5)
.Do(x => { if (x == 3) throw new Exception(); })
.CatchIgnore()
.Subscribe(Console.WriteLine);
ようするに、CatchしてEmptyを返す手間を省くためのものです。onErrorにe => {}と書くのと似てますが、シーケンスの途中で捕まえれるので、メソッドチェーンの繋ぎ方によっては全然異なる役割を持つ可能性があります。
その他の削除やバグ修正や見送ったものなど
RxのExperimental版が更新されてたので、それに合わせました。Rxの更新内容はZipとCombineLatestに大量のオーバーロード+配列を受け入れるようになったので、何でも結合できるようになりました。それにともないReactivePropertyでは独自拡張としてCombineLatestのオーバーロードを用意していたのですが、Experimental版のみ削除しました。パフォーマンスもExperimentalのもののほうがずっと良いので、早くStableにも降りてきて欲しいです。
WebRequestのUploadValuesで、値が&で連結されていないという致命的なバグがあったので修正しました。本当にすみません……。また、Silverlightでデザイン画面がプレビューできなくなる不具合を修正しました。デザインモード怖い。
バリデーション周りは、ちょっと大きめに(といっても内部だけの話で外部的には変わらない予定)変更入れようと思ってたのですが、それは次回で。あと、同期系メソッドもバリデーションの成否によって同期するかしないかを決定しようかなあ、とか思うんですが、ちょっと大変なので後になりそう。
まとめ
今回はデータリンクを主眼に置きました。デフォルトモードの変更もその一環です。直接的に意味を見るのなら、厚めのMをスマートにVMとシンクロナイズさせる、ということになります。冒頭の台詞、閉じた世界を開けるための道具です。ObserveProperty(OneWay)、ToReactivePropertyAsSynchronized(TwoWay)、ReactiveProperty.FromObject(OneWayToSource)。
OneWayとかTwoWayとかOneWayToSourceというとおり、VMとMの間のバインディングエンジンだと見ることができます。VとVMの間をWPFなりのフレームワークが担い吸収するように、ReactivePropertyはVMとMの間を吸収します。手書きでバインディングだと、ボイラープレートでは手間だし見通しも悪くなる。このほうが、ずっと、楽だし自然に書けます。
ReactivePropertyはV-VM間の接続も担うため、結果として全てがV-VM-M-VM-Vとして一つに繋がる。何をどう組もうと自然に一つに繋がっていく。わくわくしませんか?むしろカオスの予感がする?けれど、カオスの先に本当の光がある、……かもしれない。
ちなみに同期系のものはみんなプロパティ指定だけでGetとかSetとか自動でやっていますが、動的コード生成(&キャッシュ)によりハイパー高速化されているので、パフォーマンス上の問題はありません。そこは安心してください。というと何か凄そうなことやってる気がしますが、勿論そんなことはなくて、偉大なるExpressionTreeに全面的にお任せしているだけだったり。