LoginSignup
297
337

More than 3 years have passed since last update.

【C#】ReactiveProperty全然分からねぇ!って人向けのFAQ集【修正済】

Last updated at Posted at 2017-12-30

概要

 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

 また、メモリーリーク絡みで言いますと、ReactivePropertyReactiveCommandなどは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側でも同様)。

C#側の記述.cs
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;
}
XAML側の記述.xml
<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だった箇所を単純に置き換えるだけですので省略します(上記で言えばOutput1Output3に用いる)。

 なお、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:イベント発生側から、イベント受取側の情報を削除する

observableobserver.png
(画像引用元:++C++; // 未確認飛行 C)

 そして「購読」とは、この処理1のことを指します。つまり、ReactiveProperty<T>Subcribeメソッドでラムダ式を受け取る(=購読する)場合、ReactiveProperty<T>が「(値を変更する)イベント発生側」、ラムダ式が「イベント受取側」となります。
 また、後述するReactiveCommandにもSubcribeメソッドがありますが、こちらもReactiveCommandが「(ボタン操作などの)イベント発生側」となります。

Q. ReactiveCommandはどう記述すればいいの?

 先にICommandについて説明すると、これはボタン操作などの「コマンド」をMVVM用に抽象化したものです。コマンドには「実行できるか」という要素(CanExecuteメソッド)と「実行時に何をするか」という要素(Executeメソッド)があり、それぞれを実装する必要があります。
 一方、ReactiveCommand型を使うと、そうした実装の手間を大幅に省くことができます。こちらはReactiveProperty<T>型と異なり、.Valueを付けなくてもいいことに注意しましょう。

C#側の記述.cs
/* --------------- */
/* |従来の記述方法| */
/* --------------- */
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());
    }
}
XAML側の記述.xml
<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ってどのオブジェクトのどのイベントに使えるの?

  • ButtonClickイベント
  • MenuItemClickイベント

 これら以外のどのイベントに使えるかを募集中です!
 上記のようなイベントぐらいにしか使えないと思いこんでいましたが、Interaction.Triggersを使用すれば オブジェクトに対する任意のイベントICommandReactiveCommandをBindingすることができます!
(当然ながら、System.WIndows.Interactivity.dllへの参照追加が必要)

サンプルXAML.xml
<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);
    }
}

参考資料

297
337
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
297
337