この記事は 2020 年の ReactiveProperty のオーバービューの全 3 編からなる記事の 3 つ目の記事です。
他の記事はこちらです。
- MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 前編
- MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 中編
- MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 後編 (この記事)
イベントから ReactiveProperty や ReactiveCommand を呼ぶ
WPF と UWP 限定の機能としてボタンのクリックなどのイベントが発生したら ReactiveProperty の値を更新したり、ReactiveCommand を呼び出すといった機能を提供しています。EventToReactiveProperty と EventToReactiveCommand を使用して、この機能が利用可能です。
この機能を利用するにはプラットフォーム固有のパッケージをインストールする必要があります。
- ReactiveProperty.WPF (WPF 用)
- ReactiveProperty.UWP (UWP 用)
上記パッケージをインストールすると Microsoft.Xaml.Behaviors.Wpf (WPF 用)、
Microsoft.Xaml.Behaviors.Uwp.Managed (UWP 用) パッケージもインストールされます。このパッケージ内にある EventTrigger と EventToReactiveProprety/EventToReactiveCommand を組み合わせて使うことでイベントをハンドリングして ReactiveProperty/ReactiveCommand に伝搬することが出来ます。
また、イベント発生時のイベント引数を変換するための変換レイヤーも提供しています。DelegateConverter<T, U>
と ReactiveConverter<T, U>
を継承して作成します。型引数の T が変換元(普通は XxxEventArgs)で U が変換先 (ReactiveProperty の値の型やコマンドパラメーターの型) です。
例えば WPF でマウスを動かしたときのイベント引数の MouseEventArgs を表示用メッセージに加工するコンバーターは以下のようになります。
usingReactive.Bindings.Interactivity;usingSystem;usingSystem.Reactive.Linq;usingSystem.Windows;usingSystem.Windows.Input;namespaceRxPropLabWpf{publicclassMouseEventToStringReactiveConverter:ReactiveConverter<MouseEventArgs,string>{protectedoverrideIObservable<string>OnConvert(IObservable<MouseEventArgs>source)=>source// MouseEventArgs から GetPosition でマウスポインターの位置を取得(AssociateObject で EventTrigger を設定している要素が取得できる).Select(x=>x.GetPosition(AssociateObjectasIInputElement))// ReactiveProperty に設定する文字列に加工.Select(x=>$"({x.X}, {x.Y})");}publicclassMouseEventToStringDelegateConverter:DelegateConverter<MouseEventArgs,string>{protectedoverridestringOnConvert(MouseEventArgssource){// MouseEventArgs から ReactiveProperty に設定する文字列に加工varpos=source.GetPosition(AssociateObjectasIInputElement);return$"({pos.X}, {pos.Y})";}}}
2 つのクラスは同じ処理をしています。ReactiveConverter は変換処理を Rx のメソッドチェーンで書けます。DelegateConverter は変換処理を普通の C# のメソッドとして書けます。
このコンバーターを使って View のイベントを ReactiveProperty や ReactiveCommand に伝搬させる先の ViewModel を作成します。今回は確認ようにシンプルに受け取ったメッセージを格納するための ReactiveProperty と、ReactiveCommand を用意しました。
usingReactive.Bindings;usingSystem.ComponentModel;usingSystem.Reactive.Linq;namespaceRxPropLabWpf{// WPF のメモリリーク対策で INotifyPropertyChanged は実装しておくpublicclassMainWindowViewModel:INotifyPropertyChanged{publiceventPropertyChangedEventHandlerPropertyChanged;publicReactivePropertySlim<string>Message{get;}publicReactiveCommand<string>CommandFromViewEvents{get;}publicReadOnlyReactivePropertySlim<string>MessageFromCommand{get;}publicMainWindowViewModel(){Message=newReactivePropertySlim<string>();CommandFromViewEvents=newReactiveCommand<string>();MessageFromCommand=CommandFromViewEvents.Select(x=>$"Command: {x}").ToReadOnlyReactivePropertySlim();}}}
ReactiveCommand は実行されると受け取った文字を加工して MessageFromCommand という名前の ReadOnlyReactiveProperty に流しています。これを XAML にバインドします。EventToReactiveProperty と EventToReactiveCommand は EventTrigger の子要素として配置します。そして EventToReactiveProperty と EventToReactiveCommand の子要素としてコンバーターを指定します。
<Windowx:Class="RxPropLabWpf.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:RxPropLabWpf"xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"xmlns:rp="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"mc:Ignorable="d"Title="MainWindow"Height="450"Width="800"><Window.DataContext><!-- ViewModel を設定して --><local:MainWindowViewModel/></Window.DataContext><Grid><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition/></Grid.ColumnDefinitions><Grid.RowDefinitions><RowDefinitionHeight="Auto"/><RowDefinition/></Grid.RowDefinitions><TextBlockText="ToReactiveProperty"/><BorderGrid.Row="1"Background="Blue"Margin="10"><!-- MouseMove イベントを MouseEventToStringReactiveConverter で変換して ReactiveProperty に設定する --><behaviors:Interaction.Triggers><behaviors:EventTriggerEventName="MouseMove"><rp:EventToReactivePropertyReactiveProperty="{Binding Message}"><local:MouseEventToStringReactiveConverter/></rp:EventToReactiveProperty></behaviors:EventTrigger></behaviors:Interaction.Triggers><TextBlockText="{Binding Message.Value}"Foreground="White"/></Border><TextBlockText="ToReactiveCommand"Grid.Column="1"/><BorderGrid.Row="1"Grid.Column="1"Background="Red"Margin="10"><!-- MouseMove イベントを MouseEventToStringReactiveConverter で変換して ReactiveCommand を実行する --><behaviors:Interaction.Triggers><behaviors:EventTriggerEventName="MouseMove"><rp:EventToReactiveCommandCommand="{Binding CommandFromViewEvents}"><local:MouseEventToStringReactiveConverter/></rp:EventToReactiveCommand></behaviors:EventTrigger></behaviors:Interaction.Triggers><TextBlockText="{Binding MessageFromCommand.Value}"Foreground="White"/></Border></Grid></Window>
実行すると以下のようになります。EventToReactiveProperty はコンバーターで変換した結果がそのまま表示されています。EventToReactiveCommand のほうは、コマンドで加工したメッセージが表示されていることが確認できます。
Notifiers
Reactive.Bindings.Notifiers 名前空間には、いくつかの IObservable を拡張したクラスがあります。
- BooleanNotifier
- CountNotifier
- ScheduledNotifier
- BusyNotifier
- MessageBroker
- AsyncMessageBroker
単品で見ると大したこと無いクラスですが、これらも IObservable なので ReactiveProperty や RreactiveCommand や ReactiveCollection とつないで使うことが出来ます。
とはいっても使用頻度は少なめなので、Notifier 関連の詳細はドキュメントを参照してください。
Notifiers | ReactiveProperty document
ここでは MessageBroker と AsyncMessageBroker を紹介します。
MessageBroker / AsyncMessageBroker
この 2 つのクラスはグローバルにメッセージを配信して購読するための機能を提供します。Prism でいう IEventAggregator が近い機能を提供しています。他にはメッセンジャー パターンなどと言われている機能を Rx フレンドリーに実装したものになります。
MessageBroker と AsyncMessageBroker は MessageBroker.Default
と AsyncMessageBroker.Default
でシングルトンのインスタンスを取得できます。ただ、これはグローバルにメッセージを配信するユースケースが多いので利便性のために提供しているもので独自に new を使ってインスタンスを生成して使うことも可能です。
MessageBroker は ToObservable を呼ぶことで IObservable に変換できます。AsyncMessageBroker クラスは非同期処理に対応しています。AsyncMessageBroker は IObservable には変換できません。
使用方法を以下に示します。
usingReactive.Bindings.Notifiers;usingSystem;usingSystem.Reactive.Linq;usingSystem.Threading.Tasks;publicclassMyClass{publicintMyProperty{get;set;}publicoverridestringToString(){return"MP:"+MyProperty;}}classProgram{staticvoidRunMessageBroker(){// global scope pub-sub messagingMessageBroker.Default.Subscribe<MyClass>(x=>{Console.WriteLine("A:"+x);});vard=MessageBroker.Default.Subscribe<MyClass>(x=>{Console.WriteLine("B:"+x);});// support convert to IObservable<T>MessageBroker.Default.ToObservable<MyClass>().Subscribe(x=>{Console.WriteLine("C:"+x);});MessageBroker.Default.Publish(newMyClass{MyProperty=100});MessageBroker.Default.Publish(newMyClass{MyProperty=200});MessageBroker.Default.Publish(newMyClass{MyProperty=300});d.Dispose();// unsubscribeMessageBroker.Default.Publish(newMyClass{MyProperty=400});}staticasyncTaskRunAsyncMessageBroker(){// asynchronous message pub-subAsyncMessageBroker.Default.Subscribe<MyClass>(asyncx=>{Console.WriteLine($"{DateTime.Now} A:"+x);awaitTask.Delay(TimeSpan.FromSeconds(1));});vard=AsyncMessageBroker.Default.Subscribe<MyClass>(asyncx=>{Console.WriteLine($"{DateTime.Now} B:"+x);awaitTask.Delay(TimeSpan.FromSeconds(2));});// await all subscriber completeawaitAsyncMessageBroker.Default.PublishAsync(newMyClass{MyProperty=100});awaitAsyncMessageBroker.Default.PublishAsync(newMyClass{MyProperty=200});awaitAsyncMessageBroker.Default.PublishAsync(newMyClass{MyProperty=300});d.Dispose();// unsubscribeawaitAsyncMessageBroker.Default.PublishAsync(newMyClass{MyProperty=400});}staticvoidMain(string[]args){Console.WriteLine("MessageBroker");RunMessageBroker();Console.WriteLine("AsyncMessageBroker");RunAsyncMessageBroker().Wait();}}
実行結果は以下のようになります。
MessageBroker
A:MP:100
B:MP:100
C:MP:100
A:MP:200
B:MP:200
C:MP:200
A:MP:300
B:MP:300
C:MP:300
A:MP:400
C:MP:400
AsyncMessageBroker
2020/08/02 10:59:39 A:MP:100
2020/08/02 10:59:39 B:MP:100
2020/08/02 10:59:41 A:MP:200
2020/08/02 10:59:41 B:MP:200
2020/08/02 10:59:43 A:MP:300
2020/08/02 10:59:43 B:MP:300
2020/08/02 10:59:45 A:MP:400
AsyncMessageBroker のほうは、2 秒ごとにログが出ているので await 出来ていることが確認できます。
各種拡張メソッド
IObservable 向けの便利な拡張メソッドをいくつか用意しています。使う場合は Reactive.Bindings.Extensions
名前空間を using してください。
ここでは特に使用頻度が高いと思うものだけを紹介します。完全なリストは以下のドキュメントを参照してください。
Extension methods | ReactiveProperty document
CombineLatestValuesAreAllTrue/CombineLatestValuesAreAllFalse
IEnumerable<IObservable<bool>>
に対して最後の値がすべて true かどうか、もしくは false かどうかを表す bool を後続に流す IObservable<bool>
に変換します。
例えば複数の ReactiveProperty の ObserveHasErros が全て false (エラーなし) になったら実行できるコマンドの生成などで便利です。以下のようになります。
// rp1, rp2, rp3 は ReactiveProperty SomeCommand=new[]// ReactiveProperty の ObserveHasErrors が{rp1.ObserveHasErrors,rp2.ObserveHasErrors,rp3.ObserveHasErrors,}.CombineLatestValuesAreAllFalse()// 全て false の場合に.ToReactiveCommand();// 実行可能なコマンド
ObserveElementProperty
ObservableCollection<T>
の型引数 T
が INotifyPropertyChanged の場合に利用できる拡張メソッドです。ObservableCollection<T>
の全ての要素の PropertyChanged イベントを監視できます。
コード例を以下に示します。
usingReactive.Bindings.Extensions;usingSystem;usingSystem.Collections.ObjectModel;usingSystem.ComponentModel;namespaceReactivePropertyEduApp{publicclassPerson:INotifyPropertyChanged{publiceventPropertyChangedEventHandlerPropertyChanged;privatestring_name;publicstringName{get=>_name;set{_name=value;PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(nameof(Name)));}}}classProgram{staticvoidMain(string[]args){varc=newObservableCollection<Person>();c.ObserveElementProperty(x=>x.Name).Subscribe(x=>Console.WriteLine($"Subscribe: {x.Instance}, {x.Property.Name}, {x.Value}"));varneuecc=newPerson{Name="neuecc"};varxin9le=newPerson{Name="xin9le"};varokazuki=newPerson{Name="okazuki"};Console.WriteLine("Add items");c.Add(neuecc);c.Add(xin9le);c.Add(okazuki);Console.WriteLine("Change okazuki name to Kazuki Ota");okazuki.Name="Kazuki Ota";Console.WriteLine("Remove okazuki from collection");c.Remove(okazuki);Console.WriteLine("Change okazuki name to okazuki");okazuki.Name="okazuki";}}}
実行すると以下のようになります。
AdditemsSubscribe:ReactivePropertyEduApp.Person,Name,neueccSubscribe:ReactivePropertyEduApp.Person,Name,xin9leSubscribe:ReactivePropertyEduApp.Person,Name,okazukiChangeokazukinametoKazukiOtaSubscribe:ReactivePropertyEduApp.Person,Name,KazukiOtaRemoveokazukifromcollectionChangeokazukinametookazuki
コレクションにある要素のプロパティの変更が監視できていることがわかります。またコレクションから削除した要素(この場合は okazuki 変数)は削除後は変更してもコールバックが呼ばれていないことも確認できます。
コレクションの要素が POCO ではなく、ReactiveProperty を持つクラスの場合も ObserveElementObservableProperty 拡張メソッドを使うとコレクション内のオブジェクトの ReactiveProperty の監視を行えます。
usingReactive.Bindings;usingReactive.Bindings.Extensions;usingSystem;usingSystem.Collections.ObjectModel;usingSystem.ComponentModel;namespaceReactivePropertyEduApp{publicclassPerson{publicReactiveProperty<string>Name{get;}publicPerson(stringname){Name=newReactiveProperty<string>(name);}}classProgram{staticvoidMain(string[]args){varc=newObservableCollection<Person>();c.ObserveElementObservableProperty(x=>x.Name).Subscribe(x=>Console.WriteLine($"Subscribe: {x.Instance}, {x.Property.Name}, {x.Value}"));varneuecc=newPerson("neuecc");varxin9le=newPerson("xin9le");varokazuki=newPerson("okazuki");Console.WriteLine("Add items");c.Add(neuecc);c.Add(xin9le);c.Add(okazuki);Console.WriteLine("Change okazuki name to Kazuki Ota");okazuki.Name.Value="Kazuki Ota";Console.WriteLine("Remove okazuki from collection");c.Remove(okazuki);Console.WriteLine("Change okazuki name to okazuki");okazuki.Name.Value="okazuki";}}}
実行すると以下のようになります。
Add items
Subscribe: ReactivePropertyEduApp.Person, Name, neuecc
Subscribe: ReactivePropertyEduApp.Person, Name, xin9le
Subscribe: ReactivePropertyEduApp.Person, Name, okazuki
Change okazuki name to Kazuki Ota
Subscribe: ReactivePropertyEduApp.Person, Name, Kazuki Ota
Remove okazuki from collection
Change okazuki name to okazuki
IObservable<bool>
の反転
Inverse 拡張メソッドを使うと ox.Select(x => !x)
を ox.Inverse()
のように書けます。それだけ。
await と使いたい
ReactiveProperty は Rx の機能を使ってメソッドチェーンが綺麗にきまると気持ちいいですが、やりすぎると可読性の低下や、知らない人にはトリッキーなコードになってしまうといった問題があります。
ReactiveProperty や async/await にも対応しているので、そちらを使って値の変更があったタイミングで処理を書くということも出来ます。ただ、現状まだちょっと非力です。
値が変わるまで await したい
WaitUntilValueChangedAsync
メソッドで await で待つことが出来ます。コード例を以下に示します。
usingReactive.Bindings;usingReactive.Bindings.Notifiers;usingSystem;usingSystem.Reactive.Linq;usingSystem.Threading.Tasks;classProgram{staticvoidMain(string[]args){varrp=newReactivePropertySlim<string>();_=WaitAndOutputAsync(rp);rp.Value="Hello world";}staticasyncValueTaskWaitAndOutputAsync(IReactiveProperty<string>rp){varvalue=awaitrp.WaitUntilValueChangedAsync();Console.WriteLine($"await してゲットした値: {value}");}}
実行すると以下のような結果になります。
await してゲットした値: Hello world
コマンドも同様に await が可能です。
まとめ
ということで前編・中編・後編終わりました。
適当機能強化とかがあったら、ここを更新していこうと思います。