LoginSignup
20
23

More than 3 years have passed since last update.

ReactivePropertyによるViewModel実装への移行ガイド

Last updated at Posted at 2020-04-22

概要

WPFなどでMVVMな作り方をしている際に、

  • ReactivePropertyってなんか良さそう
  • でも今までの作り方と同じことするのにどう書けばいいんだろう
  • 気にはなるけど、とりあえず今まで通りに書いちゃおう……

という思いをされたことがある方のために、移行の際につまずきがちなポイントをまとめてみます。

通常のプロパティによるViewModel実装あるある

変更通知付きのプロパティを普通に作ると、次のような感じになると思います。

private string _name;
public string Name
{
    get => this._name;
    set
    {
        if (this._name != value)
        {
            this._name = value;
            this.RaisePropertyChanged();
        }
    }
}

Prismなどのライブラリに用意されているメソッドを使えば、ここまで短くなります。

private string _name;
public string Name
{
    get => this._name;
    set => this.SetProperty(ref this._name, value);
}

これならスニペットなどを駆使してじゃんじゃん生やすスタイルでも、まぁなんとかなるかもしれません。
しかし、ここに色々足していくと悩ましくなってきます。
FirstNameとLastNameの2プロパティから、getter onlyのFullNameプロパティを作ってみます。

private string _firstName;
public string FirstName
{
    get => this._firstName;
    set => this.SetProperty(ref this._firstName, value);
}

private string _lastName;
public string LastName
{
    get => this._lastName;
    set => this.SetProperty(ref this._lastName, value);
}

public string FullName => this.FirstName + " " + this.LastName;

おっと、これだとFullNameの変更が通知されません。足さないと。

private string _firstName;
public string FirstName
{
    get => this._firstName;
    set
    {
        if (this.SetProperty(ref this._firstName, value))
        {
            this.RaisePropertyChanged(nameof(this.FullName));
        }
    }
}

private string _lastName;
public string LastName
{
    get => this._lastName;
    set
    {
        if (this.SetProperty(ref this._lastName, value))
        {
            this.RaisePropertyChanged(nameof(this.FullName));
        }
    }
}

public string FullName => this.FirstName + " " + this.LastName;

うーむ、setterが太ってしまった。
そもそもプロパティのsetterがどんどん太っていくのは良いのか?ということも考え直したいところです。
SetPropertyだけなら式で書けますが、一度ブロックになっちゃうと、色々処理を突っ込みたくなるのが人間というもの。
プロパティの変更をハンドラで監視するスタイルにしてみようかしら。

public MainWindowViewModel()
{
    this.PropertyChanged += (sender, args) =>
    {
        if (args.PropertyName == nameof(this.FirstName) ||
            args.PropertyName == nameof(this.LastName))
        {
            this.RaisePropertyChanged(nameof(this.FullName));
        }
    };
}

private string _firstName;
public string FirstName
{
    get => this._firstName;
    set => this.SetProperty(ref this._firstName, value);
}

private string _lastName;
public string LastName
{
    get => this._lastName;
    set => this.SetProperty(ref this._lastName, value);
}

public string FullName => this.FirstName + " " + this.LastName;

ぐぬぬ……プロパティ名の文字列比較が気持ち悪い……(個人差があります)。
それに、ラムダ式を直接イベントにアタッチすると購読解除ができないので、寿命の異なる外部オブジェクトが絡むとメモリリークの原因になります。INotifyPropertyChanged自体が非常にプリミティブな機能しか提供していないので、もっとcoolな仕組みが欲しいなぁ。

お、チームメンバーが変更をコミットしたようです。どれどれ……

    this._firstName = "hoge";

バッキングフィールドのサイレント変更はおいコラやめろください!
触れる以上、触ってしまう人が出てくるのは自明です。この形でプロパティを定義する以上、避けられません。

このように

  • setterがおでぶちんになるのが気になってきた
  • プロパティ変更通知を漏れなく書いて回る生活に疲れた
  • 変更を監視してハンドリングするスタイルをすっきり書きたい
  • 結局内側からはバッキングフィールド触り放題なのをなんとかしたい

などで悩んでいる人にオススメなのが、ReactivePropertyです。

ReactivePropertyによるViewModel

ReactivePropertyを使って書き直してみましょう。

private readonly CompositeDisposable _cd = new CompositeDisposable();

public MainWindowViewModel()
{
    this.FirstName = new ReactiveProperty<string>().AddTo(_cd);
    this.LastName = new ReactiveProperty<string>().AddTo(_cd);
    this.FullName = this.FirstName.CombineLatest(
        this.LastName,
        (f, l) => f + " " + l).ToReadOnlyReactiveProperty().AddTo(_cd);
}

public void Dispose() => this._cd.Dispose();

public ReactiveProperty<string> FirstName { get; }
public ReactiveProperty<string> LastName { get; }
public ReadOnlyReactiveProperty<string> FullName { get; }

短い!圧倒的短さ!その反面、不慣れでギョッとする表記もあるかと思うので、解説します。

読み書きする通常のプロパティはReactivePropertyに置き換える

ReactiveProperyは、バッキングフィールドを内包し、その変更をPropertyChangedイベントと、Reactive ExtensionsのベースとなるIObservableによって通知してくれるオブジェクトです。このため、setterにおける変更通知処理やバッキングフィールドが完全に隠蔽されます。
値そのものはValueプロパティで公開されています。Xamlでバインディングする際や、コード上から値を読み書きする際は、プロパティ名.Valueに対して行います。

<TextBox Text="{Binding FirstName.Value, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding LastName.Value, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding FullName.Value}" />
    this.FirstName.Value = "John";
    this.LastName.Value = "Doe";

じゃあReactivePropertyを保持するViewModelは、INotifyPropertyChangedじゃなくてもいいじゃん!と思いがちですが、実際はPropertyChangedを実装していないとメモリリークを起こすので、気をつけましょう。

getter onlyプロパティは他のReactiveProperty(IObservable)を使って組み立てる

通常のプロパティ実装では、返り値に含まれる値が変更されたタイミングで手動で通知を呼ぶ、というお察しプログラミングが要求されていました。
ReactivePropertyでは発想を大きく転換して、ReactivePropertyのインタフェースであるIObservableに対するオペレータを駆使して値を加工し、それをToReadOnlyReactivePropertyすることでプロパティを生成します。
これにより、値の構成要素に変更があったら自動的に通知がなされるようになっています。

IObservableに対するオペレータって何よ?という話ですが、そこからはReactive Extensionsの深遠なる世界の領域です。様々なことができますが、ここではこれまでのプロパティの作り方の代替手段となるものだけ解説します。

1つのプロパティの値を変換(加工)したい

Selectを使います。

// FullNameを大文字にしたgetter onlyプロパティを作る
this.UpperFullName = this.FullName.Select(s => s?.ToUpper()).ToReadOnlyReactiveProperty().AddTo(_cd);

FullNameが変更されたら、それがSelectに渡した関数の引数(s)に入り、それを加工した結果(s?,ToUpper())が読み取り専用プロパティになる、という流れです。

2つ以上のプロパティを利用した値を作りたい

前述のようにCombineLatestを使います。

this.FullName = this.FirstName.CombineLatest(
    this.LastName,
    (f, l) => f + " " + l).ToReadOnlyReactiveProperty().AddTo(_cd);

この例では2つですが、3つ以上のプロパティでも可能です。その場合は、関数で受ける引数が増えます。

重要なのは、値の組み立てにReactiveProperty(IObservable)から得た値以外を混ぜないことです。IObservableでない要素が変更されて値が変化したとしても、それは通知されません。

生成したプロパティはまとめて破棄する

ReactivePropertyはIDisposableを継承しているため、基本的にはDisposeする必要があります。
1つ1つをDisposeしていくのは面倒なので、CompositeDisposableに拡張メソッドAddToで突っ込んでおき、Disposeでまとめて始末するというパターンが推奨されています。
これに伴い、ViewModelに相当するクラスはIDisposableを継承し、Disposeを実装しておく必要があります。
参考:https://blog.okazuki.jp/entry/2016/04/30/073755

setterでできていたあれこれはReactivePropertyではどうするの?

setterについ書いちゃってた、あんな処理こんな処理の置き換え方をまとめます。

変更通知後に処理をぶらさげたい

これはReactive Extensionsの一般的な使い方として、Subscribeすればよいです。

this.UpperFullName.Subscribe(s => Debug.WriteLine("更新されたよ: " + s)).AddTo(_cd);

イベントハンドラをぶら下げるノリでSubscribeしてください。
これの返り値をDisposeすることで購読解除になるので、AddToしておきましょう。

変更前の値を利用した処理をぶらさげたい

PairWiseというオペレータが便利です。

this.UpperFullName.Pairwise().Subscribe(pair =>
{
    Debug.WriteLine(pair.OldItem + "->" + pair.NewItem);
}).AddTo(_cd);

差分を取ったりするのに使えます。

変更通知前に処理を差し込みたい

変更通知前イベントのようなものが定義されていないため、できません。
入力値の検証機能(バリデーション)はあるので、そういう用途であればそちらを使うことになります。
参考:https://qiita.com/YSRKEN/items/5a36fb8071104a989fb8#q-%E5%85%A5%E5%8A%9B%E5%80%A4%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%A3%E3%81%A6%E3%81%A9%E3%81%86%E6%9B%B8%E3%81%91%E3%81%B0%E3%81%84%E3%81%84%E3%81%AE

getterで値を返すときに処理を差し込みたい(副作用を起こしたい)

そんな邪悪なことはやめましょう。

まとめ

ReactivePropertyを使うことで、肥大化しがちなViewModelの実装がスリムになり、IObservableベースでよりフレキシブルに書けるようになります。通常のプロパティと混在していても問題なく使えるので、段階的に試してみるのはいかがでしょうか。

参考記事

この記事とほぼ同じ立ち位置の記事を書いてる途中で発見して心折れかけましたが、切り口の違いはあるはずなのでご容赦を……。

20
23
5

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
20
23