ReactivePropertyのPropertyChanged発生タイミングについて

初めてReactivePropertyを使っていて盛大にハマったのでメモ。

(.Net4.0なのでReactivePropertyは2.9を使用しています)

 

ReactivePropertyのOnNext実行と、PropertyChangedイベント発生タイミングって必ずしも同じではないんですね。

 

WPFでLivetを使ってVliewModelクラス内で以下のようなコードを書いた。

(IDisposable関連の処理は省いてるのと、外部からInitParam???とか色々あると思いますがテストなので)

public class HogeViewModel : Livet.ViewModel {
    public ReactiveProperty<Int32> Num;

public HogeViewModel(){ Num = new ReactiveProperty<Int32>(0); } public void InitParam(Int32 num){ //前に登録したイベントの解除とか Num.Value = num; // Value代入後にPropertyChangedイベントリスナ登録(CompositeDisposableに入れてね) new Livet.EventListeners.PropertyChangedEventListener(Num, (s, e) => RaisePropertyChanged(e.PropertyName)); } }

 

Num.Valueの代入後にEventListenerを登録しているので、

イベントは飛んでこないと思ったのですが、なぜか飛んでくる・・・。

しかもよくわからないタイミングで・・・。

System.Reactive.Core.dll!System.Reactive.Concurrency.Scheduler.Invoke(System.Reactive.Concurrency.IScheduler scheduler, System.Action action) 不明 

( ^ω^)・・・???

 

色々試したところ以下で解決。

  1. PropertyChangedイベントを捕まえるのではなくReactivePropertyのSubscribe(OnNext)で処理する
  2. ReactivePropertyのSchedulerをImmidiateSchedulerかCurrentThredSchedulerを設定する(本当にこれで良いのか怪しい)

1.はあたりまえですね。というかReactivePropertyを使いながらなぜEventListenerを使っているのか・・・。

(あーその時はSubscribeだとPropertyNameが解らないじゃん!とか思ったからかもです。後で考えたら別に解らなくてもそこまで困らないですね。)

多分これが正解なはず。

 

2.ですが、ReactivePropertyのSetValueの処理は以下のようになっています。

this.Source.OnNext(value);
this.RaiseEventScheduler.Schedule(() => this.PropertyChanged?.Invoke(this, SingletonPropertyChangedEventArgs.Value));

 

OnNextはSetValueが呼び出された時に即時呼び出されますが、

PropertyChangedはRaiseEventSchedulerなるものに投げ込まれています。

これがデフォルトではImmidiateではないっぽいです。

作者の方々の説明ページもあったのですが、Rxに詳しくないのでいまいちピンときてません。

かずきさんにコメントいただきまして、UIスレッド外からのPropertyChangedイベント実行でエラーになるプラットフォームがあるためのこと。

ReactivePropertyで自動でUIスレッドにイベント発行を変えるのを抑止する - かずきのBlog@hatena

Reactive Extensions再入門 その45「Scheduler」 - かずきのBlog@hatena

Schedulerに関しては以下のページを参考に。

Rx入門 (15) - スケジューラの利用 - xin9le.net

Reactive Extensions再入門 その45「Scheduler」 - かずきのBlog@hatena

 

とりあえずReactiveProperty生成時に下記のようにしてやればこちらの望んでいた処理になりました。(元に戻す必要あるのかしら?)

var scheduler = Reactive.Bindings.ReactivePropertyScheduler.Default;
Reactive.Bindings.ReactivePropertyScheduler.SetDefault(System.Reactive.Concurrency.Scheduler.Immediate);
Num = new ReactiveProperty<Int32>(0);
Reactive.Bindings.ReactivePropertyScheduler.SetDefault(scheduler);

もしくは

Num = new ReactiveProperty<Int32>(0, System.Reactive.Concurrency.Scheduler.Immediate);  

 

ImmidiateはCurrentThreadに変えても挙動は同じでした。

ただ、アプリの初期化でSetDefaultすればよいとあったので、MainWindowのコンストラクタでSetDefaultしたのですが挙動がなぜか変わらず・・・。ReactivePropertyの生成時に設定するようにしたらうまくいきました。

アプリの初期化なのでApp.xaml.csのOnStartupで処理するのが良いようです。

(そういえばWPFで初期化処理書いたことなかった・・・)

 

とりあえずSubscribeで処理するのが良いとして、デフォルトでImmidiateやCurrentThreadでないのってなぜなんでしょうね。

OnNext実行とPropertyChanged発生が同一タイミングでないことのメリットってあるのだろうか・・・。

うーん。難しい。