私は最近、VBのWinFormsアプリでスパゲティをほぐしたりおかわりさせたりする作業からC#でのWPFアプリケーションのシステム開発と業務内容が大きく変わりました。当然MVVMなのでRxを活用しているのですが、ちょっと調べた程度ではいまいちReactiveProperty
たちの違いがピンと来なかったので、備忘録と将来の初学者の方たちのために本稿にまとめます。
2024/08/23 編集
だいぶ遅れましたが、v 9.0で追加された内容について追記しました。
2023/02/28 追記
02/12にv 9.0がリリースされました。
更新事項は@okazukiさんのZennnの記事を御覧ください。
私はまだアップデートできておりませんが、記事を読んで重要かな~と思ったのは以下の点です。
- コマンドのスリム版
ReactiveCommandSlim
の追加 - バリデーション機能を持つ
ValidatableReactiveProperty<T>
の追加 - Notifier 系クラスの
BooleanNotifier, BusyNotifier, CountNotifier, MessageBroker, AsyncMessageBroker
がスレッドセーフではなくなった(従来と同じくスレッドセーフ版の**Legacy
クラスもある
前提知識
- オブザーバーパターンをある程度理解していること
- こちらの記事がとてもわかりやすいです
-
ReactiveProperty<T>
を使ってMVVMアプリケーションの実装を少しでもしたことがあること- 全く経験のない方は初学者ですらないため本稿の対象外です、まずはHelloWorldアプリでも作ってみてください
Reactiveたち
Reactive
と名のつくクラスは以下の一覧になります。
クラス | 簡単な説明 |
---|---|
ReactiveProperty<T> |
基本となるクラス。Value プロパティで値の書き換えが可能 |
ReadOnlyReactiveProperty<T> |
Value プロパティで値の取得のみ行える版。データはソースとなるIObservable<T> から流れてくる |
ReactivePropertySlim<T> |
ReactiveProperty<T> の軽量版。理由がなければこれを使う
|
ValidatableReactiveProperty<T> |
入力されたT に対するバリデーション機能を持つ軽量なクラス。 |
ReadOnlyReactivePropertySlim<T> |
ReadOnlyReactiveProperty<T> の軽量版。理由がなければこれを使う |
ReactiveCollection<T> |
コレクションの追加・変更・削除操作を行う場合に使うクラス。 |
ReadOnlyReactiveCollection<T> |
コレクションの内容がソースとなるIObservable<T> または各操作のCollectionChanged<T> によって決まるクラス |
ReactiveCommand |
ボタン等のコマンドのクラス。Subscribe メソッドには値が流れてこないためAction を設定する |
ReactiveCommand<T> |
イベント引数を使いたいときなどに使う。Subscribe メソッドにはT が流れてくるためAction<T> を設定する |
ReactiveCommandSlim |
ReactiveCommand の軽量版。理由がなければこれを使う
|
ReactiveCommandSlim<T> |
ReactiveCommand<T> の軽量版。理由がなければこれを使う
|
AsyncReactiveCommand |
コマンドに設定した非同期メソッドが完了するまでExecute できなくなるコマンド。Subscribe メソッドにはFunc<Task> (またはasync void )を設定する |
AsyncReactiveCommand<T> |
上記の引数が流れてくる版。Func<T, Task> を設定する |
ReactiveTimer |
タイマーのReacitve 版。スタート、ストップができる。タイマー到達時にSubscribe に流れてくるのはタイマー到達回数 |
これ以外の機能はこちらの記事によくまとまっているので参照してください。
ReactiveProperty<T>
とReadOnlyReactiveProperty<T>
の使い分け
2つの違いは単純に「値をValue
プロパティ経由で直接変えるかどうか」です。以下2パターンのどちらかをするならReadOnly
でない方を使います。
- ViewModelのコードから
Rp.Value = 1;
のように値を代入する場合 - xamlで
{Binding Rp.Value, Mode=TwoWay}
のように画面からVMのプロパティへ値が設定される方向にバインドする場合(TextBox
など)
IObservable<T>
から流れてきた値や、他のReactiveProperty<T>
の内容を元に値が一意に決まるような場合はReadOnlyReactiveProperty<T>
を使いましょう。これはReactivePropertySlim<T>
とReadOnlyReactivePropertySlim<T>
の場合も同じです。
ReactiveProperty<T>
とReactivePropertySlim<T>
の使い分け
すごく雑にまとめるとTextBox
のText
プロパティをBindingMode=TwoWay
で受ける場合はSlim
でないReactiveProperty<T>
を使うということが言えます。
v 9.0で事情が変わりました。基本的にReactivePropertySlim<T>
を使用します。ただし、入力検証が必要な場合はValidatableReactiveProperty<T>
を使用することになります。 あえてSlim
でないほうを使うのは、下記にあるように「別スレッドで発生したPropertyChanged
イベントを必ずUIスレッドで処理したいケース」のみとなります。
以下、ReactivePropertySlim<T>
にできないことを記述します。以下のことをやりたい場合でなければ、Slim
のほうを基本的に使いましょう。以下のドキュメントの紹介では、インスタンスを作る時間は5倍、基本的なユースケースでは36倍の速度差があるそうです。本節では一部の説明及びコードを以下から引用します。
UIスレッドへのディスパッチは自動で行われない
Observable.Timer
などのように非同期に動作するIObservable<T>
を作って画面にバインドしたいとき、Slim
版ではUIスレッドへのディスパッチ、つまりスレッド切り替えが行われずに値が流れます。そうなると、画面要素の別スレッドからのアクセスになるためInvalidOperationException
が発生してしまいます。
対策としては通常の(ReadOnly)ReactiveProperty<T>
を使うほか、明示的にディスパッチしてやればいいようです。つまり、以下の実装となります。
var rp = Observable.Interval(TimeSpan.FromSeconds(1))
.ObserveOnUIDispatcher() // dispatch to UI thread
.ToReadOnlyReactivePropertySlim();
当然ですが、非同期で動作するわけではないIObservable<T>
から作った(ReadOnly)ReactivePropertySlim<T>
なら全く問題なく画面にバインドできます。
書き換え可能なReactivePropertySlim<T>
はIObservable<T>
から作れない
IObservable<T>
からは、ReadOnlyReactivePropertySlim<T>
しか作れません。理由は以下となります1。
機能的に低下した所は他に、ToReactivePropertySlimがありません。これは、Sourceから流れてくるのとValueへのセットの二通りで値が変化する(Mergeされてる)のが気持ち悪いというか、使いみちあるのそれ?みたいに思ったからです。ない、とはいわないまでも、存在がおかしい。のでいっそ消しました。かわりにToReadOnlyReactivePropertySlimがあります。値の変化はSourceからのみ。このほうが自然でしょふ。
よって、IObservable<T>
から作りつつValue
から値をセットしたい(BindingMode
がTwoWay
やOneWayToSource
も含む)場合は素直にReactiveProperty<T>
を使いましょう。
もちろん、IObservable<T>
から作らずに普通にnew
する場合、BindingMode=TwoWay
でもReactivePropertySlim<T>
をTextbox
のText
にバインドすることはできます。しかし後述の理由によりほとんど多くのケースではValidatableReactiveProperty<string>
(v 9.0未満の場合はReactiveProperty<string>
)を使うことになります。
DataAnnotations属性を使ったバリデーションはできない
例えばTextBox
の入力内容でバリデーションをかけたい場合、System.ComponentModel.DataAnnotations
名前空間にある属性を使ってReactiveProperty<string>
で以下の実装をすることができます。
using System.ComponentModel.DataAnnotations;
public class ViewModel
{
// 必須かつ 0以上100以下の数値のみ受付
[Required("入力が必須です")]
[Range(0, 100, "入力値が範囲外です")]
public ReactiveProperty<string> InputedText { get; }
}
これはReactivePropertySlim<T>
には削ぎ落とされた機能になるため使用できません。画面入力が完全に任意入力可能なことってあまりないと思いますので、バリデーションを実現するためにReactivePropertySlim<T>
は使えないことになります。
v 9.0より古いバージョンの場合、TextBox
などの入力項目にバインドするものはDataAnnotations
を使うためにReactiveProperty<T>
を使うことになるでしょう。
v 9.0以降の場合、DataAnnotations
を使わずにValidatableReactiveProperty<T>
を使用することでバリデーションの実装が可能です。
ReactiveCommand
とReactiveCommand<T>
の使い分け
表に書いた通り、コマンドに引数があるかどうかで使い分けます。
※ v 9.0以降の場合、基本的にはより軽量で高速なReactiveCommandSlim
またはReactiveCommandSlim<T>
を使用します。
引数なしコマンド
以下のケースにおけるOnClickedCommand
とLoadedCommand
はReactiveCommand
です。
<UserControl x:Class="Sample.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
<!-- 以下略 -->
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Button Content="Button"
Command="{Binding OnClickedCommand}" />
</UserControl>
using System.Diagnostics;
public class ViewModel
{
public ReactiveCommand LoadedCommand { get; }
public ReactiveCommand OnClickedCommand { get; }
public ViewModel()
{
// Subscribeの中身はActionなので() => { }で記述する
LoadedCommand = new ReactiveCommand().
WithSubscribe(() => Debug.WriteLine("Loaded"));
// 引数なしなのでこんなふうにも略記できる
OnClickedCommand = new ReactiveCommand().WithSubscribe(Write);
void Write() => Debug.WriteLine("OnClicked");
}
}
引数ありコマンド
以下のケースにおけるOnClickedCommand
はCommandParameter
でBoolean
を指定しているためReactiveCommand<bool>
です。以下の例の他にも、EventToReactiveCommand
を使う場合などでReactiveCommand<T>
が登場するでしょう。
<UserControl x:Class="Sample.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib"
<!-- 以下略 -->
>
<Button Content="Ok"
Command="{Binding OnClickedCommand}" >
<Button.CommandParameter>
<system:Boolean>True</system:Boolean>
</Button.CommandParameter>
</Button>
<Button Content="Cancel"
Command="{Binding OnClickedCommand}" >
<Button.CommandParameter>
<system:Boolean>False</system:Boolean>
</Button.CommandParameter>
</Button>
</UserControl>
using System.Diagnostics;
public class ViewModel
{
public ReactiveCommand<bool> OnClickedCommand { get; }
public ViewModel()
{
// Subscribeの中身はAction<bool>
OnClickedCommand = new ReactiveCommand().
WithSubscribe(isOkClicked =>
{
if (isOkClicked) // OKボタンクリック時処理
else // Cancelボタンクリック時処理
});
}
}
ReactiveCommand
とReactiveCommandSlim
の使い分け
ReactiveProperty
のケースと同様に、Slim
版はCanExecuteChanged
イベントの購読がUIスレッドにディスパッチされなくなっています。そのぶんインスタンスを作る時間は70倍、基本的なユースケースでは13倍の速度差があるそうです。コマンドはボタンなどの画面部品にバインドすることが多いので、UIスレッドを2個以上増やして並列に相互作用したい、といったケース2でもなければ全部Slim
でよいと思います。
なお、ReactiveCommandSlim
はAsyncReactiveCommand
のように、実行可能状態をほかのCommand
と共有できるようになりました。この点からもSlim
でないほうを使うケースはほとんどなくなるでしょう。以下のサンプルコードは上記の記事からの引用です。
// CanExecute のソースになる IObservable<bool>
IObservable<bool> someCanExecuteSource = ...;
// これを共有することで CanExecute の状態を共有可能にする
ReactivePropertySlim<bool> sharedStatus = new();
// command1 と command2 で CanExecute の状態が同期される
// AsyncReactiveCommand はコマンド実行中に自動的に CanExecute が false になるので command1 が実行中は sharedStatus 経由で command2 の CanExecute も false になる
AsyncReactiveCommand command1 = someCanExecuteSource.ToAsyncReactiveCommand(sharedStatus)
.WithSubscribe(async () =>
{
await Task.Delay(3000); // 何か時間のかかる処理
});
ReactiveCommandSlim command2 = someCanExecuteSource.ToReactiveCommandSlim(sharedStatus)
.WithSubscribe(() =>
{
// do something
});
ReactiveCommand
とAsyncReactiveCommand
の使い分け
ReactiveCommand
はIObservable<bool>
から生成することもできます。最後の値がCanExecute()
の戻り値になるため、例えばButton
ならIsEnabled
を管理しなくても以下のように作ったReactiveCommand
をバインドすることで簡単に押下可否を制御できるようになっています。
public class ViewModel
{
public ReactiveCommand OnClickedCommand { get; }
public ViewModel()
{
// 5秒おきに押せる/押せないが切り替わるボタン
OnClickedCommand = Observable.Interval(TimeSpan.FromSeconds(5)).
Select(i => i % 2 == 0).
ToRectiveCommand().
WithSubscribe(() =>
{
System.Diagnotics.Debug.WriteLine("Clicked!");
});
}
}
ただし、上記方法ではボタンを連打したら連打しただけ"Clicked!"が出力されてしまいます。これは、「ボタンの押下時処理(Subscribe
に指定したAction
)が完了するまでボタンを押下不可にする」という制御がかかっていないためです。
これを本来のボタン押下可否のIObservable<bool>
と一緒に制御するのは、CombineLatestValuesAreAllTrue
など使えば実装可能ですが煩雑ですし頻出パターンです。そこで、Subscribe
に指定したasync
メソッドが完了するまでCanExecute()
がfalse
になるコマンドとしてAsyncReactiveCommand
を利用できます。
public class ViewModel
{
public AsyncReactiveCommand OnClickedCommand { get; }
public ViewModel()
{
// 5秒おきに押せる/押せないが切り替わり、二重押しもできないボタン
OnClickedCommand = Observable.Interval(TimeSpan.FromSeconds(5)).
Select(i => i % 2 == 0).
ToAsyncRectiveCommand().
WithSubscribe(async () => // 非同期メソッドにする
{
await Task.Delay(1000); // なんか重い処理
System.Diagnotics.Debug.WriteLine("Clicked!");
});
// Taskが返ってくればいいので非同期処理しないならこれでもOK
WithSubscribe(() =>
{
System.Diagnotics.Debug.WriteLine("Clicked!");
return Task.CompletedTask;
});
}
}
上記実装をすると、OnClickedCommand
はボタン押し後に"Clicked"が表示されるまでの約1秒間、つまり非同期メソッドが完了するまで押せない状態になります。
ただし、この「完了するまで再度ボタンが押せない状態」を他のボタンのAsyncReactiveCommand
と共有することはできません。これをしたい場合は、Reactive.Bindings.Notifiers
名前空間のBusyNotifier
を使って以下のように実装できます。ただしこのパターンはAsync
でないReactiveCommand
にする必要があります。
using Reactive.Bindings.Notifiers;
public class ViewModel
{
public BusyNotifier BusyNotifier { get; } = new();
public ReactiveCommand OnClickedCommand1 { get; }
public ReactiveCommand OnClickedCommand2 { get; }
public ReactiveCommand OnClickedCommand3 { get; }
public ViewModel()
{
var isEnabled = Observable.Interval(TimeSpan.FromSeconds(5)).
Select(i => i % 2 == 0).
// BusyNotifierはIObservable<bool>
// ただしBusyなときにtrueなので反転する
CombineLatest(BusyNotifier, (x, y) => x && !y);
// 5秒おきに押せる/押せないが切り替わるボタン
// 二重押しと他のボタンが押してる間も押せない
OnClickedCommand1 = isEnabled.
ToRectiveCommand().
WithSubscribe(async () => await OnClicked("1"));
OnClickedCommand2 = isEnabled.
ToRectiveCommand().
WithSubscribe(async () => await OnClicked("2"));
OnClickedCommand3 = isEnabled.
ToRectiveCommand().
WithSubscribe(async () => await OnClicked("3"));
// 例のため1秒間押せなくしたいのでasyncメソッドにしているが、
// 同期処理でいいなら普通のvoidにすればOLK
async Task OnClicked(string no)
{
if (BusyNotifier.IsBusy) return;
using (BusyNotifier.ProsessStart())
{
await Task.Delay(1000);
System.Diagnotics.Debug.WriteLine($"Button{no} Clicked!");
}
}
}
}
ReactiveCommad<T>
とAsyncReactiveCommand<T>
の使い分けは全く同じなので省略します。
ReacitveCollection<T>
とReadOnlyReacitveCollection<T>
の使い分け
ReactiveProperty<T>
とReadOnlyReactiveProperty<T>
のケースと同様に、ObservableCollection<T>
から流れてきた値や、他のReactiveCollection<T>
の内容を元に値が一意に決まるような場合はReadOnlyReacitveCollection<T>
を使いましょう。
サーバーから取得したデータ群に、画面操作で新しくデータを増やしたいときはReactiveCollection<T>
が向いています。例えば「行追加ボタン」のあるケースです。
2つの共通点として、ソースとなったIObservable<T>
から値が流れてきた場合、すべてAdd
されます。そのため必要なら適切なタイミングでClear
する必要があることに注意してください。
ReadOnlyReacitveCollection<T>
とReadOnlyReacitveProperty<IEnumerable<T>>
の使い分け
こちらは特殊パターンです。以下の2つは直感的には同じに見えませんか?
- コレクションの各要素をコードから操作したりしない。コレクションの中身はすべてソースとなる
IObservable<T>
から決まる - コレクションの操作をコードからはしない。内容が更新される場合はすべて
Clear
してAddRange
する(洗い替え)
データを一覧表示する画面を考えてみてください。一覧画面ではサーバーから受け取った複数件のデータを画面表示しますが、画面からデータを追加したり各行の内容を更新できる必要がない場合があります。このような場合にはコレクションへのAdd
、Reset
、Update
、Delete
の各操作と監視が不要です(というかデータ取得のたびに1件ずつAdd
のイベントが起きられても困る)。
そのため、Model側ではIEnumerable<T>
でデータを持っておき、それをReadOnlyReactiveProperty<IEnumerable<T>>
にする方法が考えられます。
public class Model : INotifyPropertyChanged
{
public IEnumerable<MyData> Received { get; private set => /* 変更通知処理 */ }
public void Request()
{
// サーバへのデータ問い合わせ
}
}
public class ViewModel
{
// 取得したデータ(型名が長い...)
public ReadOnlyReactivePropertySlim<IEnumerable<MyData>> Items { get; }
public ViewModel(Model model)
{
// IObservable<IEnumerable<T>>を生成してModelのデータを監視
Items = model.ObserveProperty(m => m.Received).
// もし必要ならSelectメソッドを噛ましてデータを変換する
// C#9.0以降でNullable=enableなら型指定が必要
// (ないとTが<IEnumerable<MyData>>?に推論される)
ToReadOnlyReactivePropertySlim<IEnumerable<MyData>>();
}
}
ReadOnlyReactiveProperty<IEnumerable<T>>
を使うことによる利点と欠点を述べます。
- 利点
- Modelがデータを受信したとき、
Received
の値変更の通知が1回だけになる-
ObservableCollection<T>
からReadOnlyReactiveCollection<T>
にする場合、洗い替えして10件のデータになるとClear
の1回とAdd
の10回で変更通知が11回起きるため、場合によっては速度改善になりえる
-
- 要素を追加・削除しないことが明確になる
- Modelがデータを受信したとき、
- 欠点
- 1要素だけを追加・削除したりすることができない
- 内容の更新は監視できない。
ListView
等のItemSource
に指定しているとき、特定の要素の内容を変えたりしても変更は通知されない- 例えば
IsSelected
を表示用データにバインドさせたい場合、Items
から要素のどれかのIsSelected
が変更されたかの監視をViewModelからはできない3。これはObserveElementProperty
が(ReadOnly)ObservableCollection<T>
の拡張メソッドのため。
- 例えば
個人的には以下の条件をすべて満たす場合、ReactiveCollection<T>
ではなくReactiveProperty<IEnumerable<T>>
にしています。
- コレクションの各要素の監視が不要
- コレクションが更新されるときは常に洗い替えで全件更新
IEnumerable<T>
からReadOnlyReactiveCollection<T>
の生成
「Modelは常に洗い替えでデータを取得するが、各要素の状態を監視する必要があるためViewModelではReactiveCollection<T>
で保持したい」というケースがあります。この場合はSelectMany
を使うことで、IObservable<IEnumerable<T>>
からReadOnlyReactiveCollection<T>
を作ることが可能です。
ただし、ModelのIEnumerable<T>
プロパティが洗い替えされるときは一度ReadOnlyReactiveCollection<T>
をクリアする必要があります。クリアしないと前節の通り全件Add
されるからです。ToReadOnlyReactiveCollection<T>()
の第一引数にIObservable<Unit>
を渡すとOnNext
されたときにクリアされるコレクションを作れるので、検索ボタンなどプル型でModelのデータが更新される場合は更新トリガーの前にクリアするようにしましょう。
public class Model : INotifyPropertyChanged
{
public IEnumerable<MyData> Received { get; private set => /* 変更通知処理 */ }
public void Request()
{
// サーバへのデータ問い合わせ
}
}
public class ViewModel
{
private Model _model;
// コレクションをクリアするためのオブザーバー
private Subject<Unit> _clearSubject = new();
public ReadOnlyReactiveCollection<MyData> Items { get; }
public ReadOnlyReactivePropertySlim<int> SelectedCount { get; }
public ViewModel(Model model)
{
_model = model;
// IObservable<IEnumerable<MyData>>を生成してModelのデータを監視
Items = model.ObserveProperty(m => m.Received).
// SelectManyでIObservable<T>に変換
SelectMany(mydata => mydata /* 必要なら別のTに変換する処理を書く */).
ToReadOnlyReactiveCollection(_clearSubject);
// データのIsSelectedを監視して変わったらその度にカウント
SelectedCount = Items.
ObserveElementProperty(item => item.IsSelected).
Select(_ => Items.Count(item => item.IsSelected)).
ToReadOnlyReactivePropertySlim();
}
/// <summary>検索ボタン押下時処理</summary>
private void OnSearchButtonClicked()
{
// 現在のデータをクリアしてから取得しにいく
_clearSubject.OnNext(Unit.Default);
_model.Request();
}
}
あとがき
VBでWinFormsをいじっている間は「リアクティブプログラミング?なにそれおいしいの?」という気持ちでいましたが、オブザーバーパターンを理解すると「これはすごいぞ!」と理解できるようになりました。今ではRx
なしでコーディングすることはできません。
ただ、まだまだ使いこなせてない機能が多いです。本稿は記事作成時点での理解で書いているため間違っている部分があればどしどし指摘していただけると幸いです。
なお、本稿で一番重要なのは以下だと思っています。
- 理由がなければ
ReactivePropertySlim<T>
を使う