#概要
C#でMVVMパターンを組んで開発しようとした際に役に立つライブラリの一つにReactivePropertyがあります。
ただ、これを使って書くと、従来とは記述が大きく変わってしまいますので、それなりに慣れが必要となります。
以下、そんなReactivePropertyの使い方についてのFAQ集となります(自分が躓いたところばかりなので実質備忘録)。
※2017/12/31追記:ReactivePropertyのメンテナーの一人でいらっしゃるかずき(Kazuki Ota)さんから指摘を賜り、記事を大幅に修正しました。
FAQ集
Q. 具体的にはどう便利なの?
-
INotifyPropertyChanged
インターフェースを実装したり、そうした通知機能がコミコミのクラスを継承したりする必要がない。要するに 記述量が少なくて済む - 「値が変更された時にアクションを起こす」「ボタンを押すなどの操作をした時にアクションを起こす」ことを、
Subscribe
メソッドで購読することにより表現できる。要するに setアクセサーにごちゃごちゃ書かなくていい - 入力した値のチェック機能 がある。つまり、「入力内容が全て正常な場合のみ、入力ボタンを有効にする」などといった処理を楽に書くことができる
/* --------------- */
/* |従来の記述方法| */
/* --------------- */
using System.ComponentModel;
class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string parameter)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(parameter));
}
class ViewModel : ViewModelBase
{
private int selectedMode;
public int SelectedMode {
get{ return selectedMode; }
set{
if(value == selectedMode)
return;
selectedMode = value;
DoFunc(selectedMode);
NotifyPropertyChanged(nameof(SelectedMode));
}
}
}
/* --------------------------------- */
/* |ReactivePropertyにおける記述方法| */
/* --------------------------------- */
using System;
using System.ComponentModel;
using Reactive.Bindings;
class ViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
public ReactiveProperty<int> SelectedMode {get;} = new ReactiveProperty<int>();
// コンストラクタ内で
SelectedMode.Subscribe(x => DoFunc(x));
}
Q. あれ、ReactivePropertyだとINotifyPropertyChanged要らないんじゃなかったっけ?
確かに書かなくてもプログラムは動きます。ただ、Visual C#の仕様上、上記のように継承していないと メモリーリークの可能性があります。……アイエエエ!?ナンデ!?メモリーリークナンデ!?
参考にすべき資料:
【WPF】ViewModelがINotifyPropertyChangedを実装していないとメモリリークする件 - aridai.NET
MVVMでメモリリークしちゃってました 原因と対策編 - かずきのBlog@hatena
また、メモリーリーク絡みで言いますと、ReactiveProperty
やReactiveCommand
などはIDisposable
を継承していますので、使用後はDispose
する必要があります。単独で自己完結しているような型ならGC任せでも良いのですが、後述の質問「Q. 既存のModel・ViewModelを全部書き換えるの辛くない?」のように他のオブジェクトがイベントによって紐付けられている場合、Dispose
しておくことが望ましいでしょう。具体的にはこんな感じ。
class ViewModel : INotifyPropertyChanged, IDisposable {
public event PropertyChangedEventHandler PropertyChanged;
// Disposeが必要なReactivePropertyやReactiveCommandを集約させるための仕掛け
private CompositeDisposable Disposable { get; } = new CompositeDisposable();
// ReactivePropertyやReactiveCommandを用意する
public ReactiveProperty<int> Hoge{ get; }
public ReactiveCommand Fuga{ get; };
public ViewModel(){
// AddToメソッドでDisposeしたいオブジェクトをDisposableプロパティ(の実体)に登録する
this.Hoge = new ReactiveProperty<int>().AddTo(this.Disposable);
this.Fuga = new ReactiveCommand ().AddTo(this.Disposable);
}
public void Dispose(){
// まとめてDisposeする
Disposable.Dispose();
}
}
参考資料:
ReactivePropertyの後始末 - かずきのBlog@hatena
Q. ReactiveProperty<T>はどう記述すればいいの?
ReactiveProperty<T>
型はT型の変数を入れる箱のようなもの。
通知機能を内包しているのでそのままXAMLにBindingできるし、値の変更を反映させるのも簡単。
なお、ReactiveProperty<T>
型からT
型の値を取り出す際は、Value
プロパティを参照する(C#側でもXAML側でも同様)。
class ViewModel : INotifyPropertyChanged{
// プロパティ
// 宣言と同時に初期化 or コンストラクタで初期化する場合は「private set;」の記述不要
public ReactiveProperty<int> SelectedMode { get; private set; }
// メソッド内での記述
//デフォルト値で初期化
SelectedMode = new ReactiveProperty<int>();
//指定した数値(この場合は3)で初期化
SelectedMode = new ReactiveProperty<int>(3);
// 値を引き出す・書き換える際はValueプロパティを参照すること
int selectedMode = SelectedMode.Value;
SelectedMode.Value = 2;
}
<Combobox SelectedIndex = {Binding SelectedMode.Value, Mode=TwoWay}>
Q. ReactiveProperty<T>の値が変更した時の動作はどう記述すればいいの?
Subscribe
メソッドを使う。これの引数にIObserver<T>
を渡すことで購読できるので、「値が変更した際にはこういったアクションを起こす」といったことをシンプルに記述できる。
なお、以下の例ではラムダ式を渡しているが、using System;
しないとラムダ式をIObserver<T>
に変換できないので注意!
using System;
//値が変更された時の動作
SelectedMode.Subscribe(_ => DoFunc(SelectedMode.Value));
//値が変更された時の動作(色々行うので{}が必要)
SelectedMode.Subscribe(_ => {
DoFunc1(SelectedMode.Value);
DoFunc2(SelectedMode.Value);
DoFunc3(SelectedMode.Value);
});
Q. ReadOnlyReactivePropertyって何?
まず、「ReactiveProperty<T>
は他のReactiveProperty<T>
から作成できる」ことを説明しておきましょう。例えば「あるTextBox
の中身を書き換えると、それを加工した結果がTextBlock
に表示される」ような動きが可能になります。要するに連動ですね。
using System.Reactive.Linq;
// 変数を宣言
public ReactiveProperty<int> Input1 { get; } = ReactiveProperty<int>();
public ReactiveProperty<int> Input2 { get; } = ReactiveProperty<int>();
public ReactiveProperty<int> Output1 { get; }
public ReactiveProperty<bool> Output2 { get; }
public ReactiveProperty<string> Output3 { get; }
// 定義する際に、どう加工するかをメソッドチェーンで示す
// (Output1は、Input1を2倍にした数値となる)
Output1 = Input1.Select(x => x * 2).ToReactiveProperty();
// この「Select」は、普通のLINQと同様、型変換にも使える
// (Output2は、Input1が偶数ならtrue、機数ならfalseとなる)
Output2 = Input1.Select(x => (x % 2 == 0)).ToReactiveProperty();
// 2つ以上のReactivePropertyを合成することも可能
// (Output3は、Input1 == Input2なら"同じ", それ以外なら"違う"となる)
Output3 = Input1.CombineLatest(Input2, (x, y) => (x == y ? "同じ" : "違う")).ToReactiveProperty();
そして上記のように連動している場合、「連動先は連動元で決まるから、連動先は読み取り専用でも構わない」ということがよくあります。そういった際に用いるのがReadOnlyReactiveProperty
です。
……ReadOnlyReactiveProperty
の使い方は、ReactiveProperty
だった箇所を単純に置き換えるだけですので省略します(上記で言えばOutput1
~Output3
に用いる)。
なお、CombineLatestメソッドは、3つ以上のReactivePropertyを合成するのにも使えます。
using System.Reactive.Linq;
public ReactiveProperty<int> Input1 { get; }
public ReactiveProperty<string> Input2 { get; }
public ReactiveProperty<bool> Input3 { get; }
public ReactiveProperty<decimal> Input4 { get; }
public ReactiveProperty<string> Output1 { get; }
public ReactiveProperty<string> Output2 { get; }
Output1 = Input1.CombineLatest(Input2, Input3,
(x, y, z) => func1(x, y, z)
).ToReactiveProperty();
Output2 = Input1.CombineLatest(Input2, Input3, Input4,
(x, y, z, w) => func2(x, y, z, w)
).ToReactiveProperty();
Q. ラムダ式の「() => hoge();」と「_ => hoge();」って何が違うの?
前者は「引数を受け取らず、hoge()の返り値を返す」、後者は「引数を1つ取り、hoge()の返り値を返す」といった意味です。実は 変数名として「_」1文字だけでも全然アリ なのでその値を使えるのですが、「_」1文字にすることで「引数は取るけど使わないよ」といった意思表示をすることができます。
実はReactivePropertyの場合、Subcribe
メソッド内でラムダ式を使うと、引数で呼び出し元の変数の中身を使用することができます。例えば、ReactiveProperty<Type> X
に対してX.Subcribe(x => hoge(x));
といったコードが書けます(この際、ラムダ式内の変数xはType
型となる)。もっとも、ReactiveCommand
型だとobject
型が引数の型にやってきますが……。
using System;
// 値が変更された時の動作
SelectedMode.Subscribe(_ => DoFunc(SelectedMode.Value));
// 実はこういった風に書ける
SelectedMode.Subscribe(x => DoFunc(x));
// 更に、DoFuncメソッドが引数1のみでオーバーロードが無ければここまで略せる(意味は上と等価)
SelectedMode.Subscribe(DoFunc);
しかし、ReactiveProperty<Type> X
が 購読するイベント内でXの中身を使用しない 場合、X.Subcribe(_ => hoge());
といった風に 「_」を引数名として使用することで明示する ことができるのです。
Q. さっきから言ってる「購読」ってどういう意味なの?
「購読」を説明する前に、まず「イベント」について説明しましょう。
イベントとは、「作業を完了した」「ボタンを押した」など、「別の行動を起こすためのキッカケとなる行動」のことです。
ここで、「別の行動」を起こすオブジェクト(イベント発生側)と「キッカケとなる行動」を起こすオブジェクト(イベント受取側)は 基本的に別である ことに注意しましょう。
イベントに関係する処理としては、大きく分けて次の3種類があります。
- 処理1:イベント発生側に、イベント受取側の情報を登録する
- 処理2:イベント発生側が、「イベントを起こした」という情報をイベント受取側に送信する
- 処理3:イベント発生側から、イベント受取側の情報を削除する
(画像引用元:++C++; // 未確認飛行 C)
そして「購読」とは、この処理1のことを指します。つまり、ReactiveProperty<T>
がSubcribe
メソッドでラムダ式を受け取る(=購読する)場合、ReactiveProperty<T>
が「(値を変更する)イベント発生側」、ラムダ式が「イベント受取側」となります。
また、後述するReactiveCommand
にもSubcribe
メソッドがありますが、こちらもReactiveCommand
が「(ボタン操作などの)イベント発生側」となります。
Q. ReactiveCommandはどう記述すればいいの?
先にICommand
について説明すると、これはボタン操作などの「コマンド」をMVVM用に抽象化したものです。コマンドには「実行できるか」という要素(CanExecute
メソッド)と「実行時に何をするか」という要素(Execute
メソッド)があり、それぞれを実装する必要があります。
一方、ReactiveCommand
型を使うと、そうした実装の手間を大幅に省くことができます。こちらはReactiveProperty<T>
型と異なり、.Value
を付けなくてもいいことに注意しましょう。
/* --------------- */
/* |従来の記述方法| */
/* --------------- */
using System;
using System.Windows.Input;
public class CommandBase : ICommand
{
Action action;
public bool CanExecute(object parameter) => true;
public event EventHandler CanExecuteChanged;
public void Execute(object parameter) { action(); }
public CommandBase(Action action) { this.action = action; }
}
class ViewModel : INotifyPropertyChanged
{
public ICommand ButtonCommand{ get; private set; }
private void ButtonAction(){(中略)}
public ViewModel(){
ButtonCommand = new CommandBase(ButtonAction);
}
}
/* --------------------------------- */
/* |ReactivePropertyにおける記述方法| */
/* --------------------------------- */
using Reactive.Bindings;
using System;
class ViewModel : INotifyPropertyChanged{
public ReactiveCommand ButtonCommand {get;} = new ReactiveCommand();
private void ButtonAction(){(中略)}
public MainViewModel(){
ButtonCommand.Subscribe(_ => ButtonAction());
}
}
<Button Command = {Binding ButtonCommand}>
Q. ReactiveCommandもSubscribeでイベントを購読するんだよね?
その通りです。ReactiveProperty<T>
では「値が変更された時に発動」していましたが、ReactiveCommand
では「コマンドが実行された時に発動」します。
Q. 「次の条件を満たした場合にコマンドを有効にする」処理って書くの面倒なんだよねー
そういった時にこそReactivePropertyです。ReactivePropertyにはToReactiveCommand
という拡張メソッドがあり、これを使うとIObservable<bool>
からReactiveCommand
型を生成できます。これから、
「IObservable<bool>
がtrueになった(=条件を満たした)ときにだけReactiveCommand
先のオブジェクトが有効になる」
といった処理を簡単に記述できるようになります。
また、複数の条件を組み合わせることで、「全ての条件を満たした時」や「どれか1つの条件を満たした時」といった有効条件も記述可能です。
using System;
using Reactive.Bindings;
using System.Reactive.Linq;
// 何らかのコマンド
public ReactiveCommand ButtonCommand {get; private set; }
// 何らかのフラグ
public ReactiveProperty<bool> ButtonFlg1 { get; } = new ReactiveProperty<bool>();
public ReactiveProperty<bool> ButtonFlg2 { get; } = new ReactiveProperty<bool>();
public ReactiveProperty<bool> ButtonFlg3 { get; } = new ReactiveProperty<bool>();
// フラグが有効な時にのみコマンドを有効にする(例えばボタンのコマンドが無効になっている際は、ボタン自体も無効色になる)
ButtonCommand = ButtonFlg1.ToReactiveCommand();
// フラグが2つとも有効な場合にのみコマンドを有効にする
ButtonCommand = ButtonFlg1.CombineLatest(ButtonFlg2, (x,y) => x & y).ToReactiveCommand();
// フラグが3つとも有効な場合にのみコマンドを有効にする
ButtonCommand = new[] { ButtonFlg1, ButtonFlg2, ButtonFlg3 }
.CombineLatest(x => x.All(y => y)).ToReactiveCommand();
// フラグが1つでも有効な場合にのみコマンドを有効にする
ButtonCommand = ButtonFlg1.CombineLatest(ButtonFlg2, (x,y) => x | y).ToReactiveCommand();
// フラグが1つでも有効な場合にのみコマンドを有効にする
ButtonCommand = new[] { ButtonFlg1, ButtonFlg2, ButtonFlg3 }
.CombineLatest(x => x.Any(y => y)).ToReactiveCommand();
// フラグが全て有効な場合にのみコマンドを有効にする
ButtonCommand = new[] { ButtonFlg1, ButtonFlg2, ButtonFlg3 }
.CombineLatestValuesAreAllTrue().ToReactiveCommand();
// フラグが全て無効な場合にのみコマンドを有効にする
ButtonCommand = new[] { ButtonFlg1, ButtonFlg2, ButtonFlg3 }
.CombineLatestValuesAreAllFalse().ToReactiveCommand();
Q. 入力値チェックってどう書けばいいの?
-
ValidationAttribute
を継承したクラスをReactiveProperty<T>
にあてがう。「IsValid
メソッドがtrueを返す=入力値が正常」ということなので注意 - 以下のケースでは「入力した文字列がint型にパースできるか」をValidation内容としているが、これは入力が文字列だからこそできるのであって、例えば
ReactiveProperty<int>
に対してはその手が使えない(パース可否でValidationできない)ことに注意
// バリデーション用のクラス
// (以下のサンプルは、int型にパースできた時のみ「入力値が正常」だと判断する)
using System.ComponentModel.DataAnnotations;
public class IntValidationAttribute : ValidationAttribute {
public override bool IsValid(object value)
=> int.TryParse(value.ToString(), out var _);
}
// バリデーションを設定する
[IntValidationAttribute]
public ReactiveProperty<string> X {get;} = new ReactiveProperty<string>();
[IntValidationAttribute]
public ReactiveProperty<string> Y {get;} = new ReactiveProperty<string>();
// バリデーションが通る(入力値が正常)な時のみコマンドを有効にする
ButtonCommand = X.ObserveHasErrors.Select(x => !x).ToReactiveCommand();
// バリデーションが通らない(入力値が異常)な時のみコマンドを有効にする
ButtonCommand = X.ObserveHasErrors.ToReactiveCommand();
// 2つのバリデーションが通る時のみコマンドを有効にする
ButtonCommand = new[]{
ButtonFlg1.ObserveHasErrors,
ButtonFlg2.ObserveHasErrors
}.CombineLatest(x => x.All(y => !y)).ToReactiveCommand();
Q. ICommandやReactiveCommandってどのオブジェクトのどのイベントに使えるの?
-
Button
のClick
イベント -
MenuItem
のClick
イベント
これら以外のどのイベントに使えるかを募集中です!
上記のようなイベントぐらいにしか使えないと思いこんでいましたが、Interaction.Triggers
を使用すれば オブジェクトに対する任意のイベント にICommand
やReactiveCommand
をBindingすることができます!
(当然ながら、System.WIndows.Interactivity.dllへの参照追加が必要)
<Window xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
<Grid>
<TextBlock x:Name="lblMessage">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseEnter">
<i:InvokeCommandAction Command="{Binding MouseEnterCommand}"/>
</i:EventTrigger>
<i:EventTrigger EventName="MouseLeave">
<i:InvokeCommandAction Command="{Binding MouseLeaveCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBlock>
</Grid>
</Window>
上記の例では、TextBlock
オブジェクトにおけるMouseEnter
イベントとMouseLeave
イベントについて、コードビハインドではなくCommandを渡して処理させています。
Q. 既存のModel・ViewModelを全部書き換えるの辛くない?
INotifyPropertyChanged
を使用する従来の記述法だと、Viewに紐づけされたプロパティを変更通知するため、set
側にゴリゴリ書いて実装していました。それらを全部ReactiveProperty<T>
で書き換えるのが難しい場合には、INotifyPropertyChanged
を実装したクラスをReactiveProperty<T>
側に接続することができます。
// Model側(各種プロパティを実装した側)
// ここでBindableBaseは、Prism.Mvvm.BindableBaseのような、
// INotifyPropertyChanged実装済みのヘルパークラスだとする
public class Model : BindableBase{
private int x;
public int X{
get => x;
set{
if(x != value){
x = value;
OnPropertyChanged();
}
}
}
}
// ViewModel側(ReactivePropertyで中継する側)
public class ViewModel : INotifyPropertyChanged{
private Model model = new Model();
public ReactiveProperty<T> Property1 { get; }
public ReactiveProperty<T> Property2 { get; }
public ReactiveProperty<T> Property3 { get; }
public MainWindowViewModel()
{
// BindingのModeがTwoWayなプロパティ(つまり双方向)
Property1 = model.ToReactivePropertyAsSynchronized(m => m.X);
// BindingのModeがOneWayなプロパティ(つまりView→ViewModelのみ)
Property2 = model.ObserveProperty(m => m.X).ToReactiveProperty();
// BindingのModeがOneWayToSourceなプロパティ(つまりViewModel→Viewのみ)
Property3 = ReactiveProperty.FromObject(model, m => m.X);
}
}
参考資料
- MVVMをリアクティブプログラミングで快適にReactivePropertyオーバービュー - かずきのBlog@hatena
- INotifyPropertyChanged実装のありえない面倒くささと、ReactivePropertyの信じられない素晴らしさ - Qiita
- ReactiveCommand で Subscribe しようとして謎のエラーに悩まされていた話
- PrismとReactivePropertyで簡単MVVM!
- 【雑記】イベントの購読とその解除 - C# によるプログラミング入門 _ ++C++; // 未確認飛行 C
- こわくないReactive Extensions超入門 - Qiita
- C#の主要インターフェース解説:IObservable、IObserver - がりらぼ
- 【WPF】ViewModelがINotifyPropertyChangedを実装していないとメモリリークする件 - aridai.NET
- MVVMでメモリリークしちゃってました 原因と対策編 - かずきのBlog@hatena
- [WPF] コントロールの任意のイベントとコマンド xaml上で関連付ける - Netplanetes
- ReactivePropertyの後始末 - かずきのBlog@hatena