かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Reactive Extensions とか知らない人向け ReactiveProperty のはじめの使い方

ReactiveProperty は MVVM + Rx でプログラム組むときにいい感じにしてくれるものですが MVVM だけでも大変なのに Rx なんて魔法みたいなものを覚えないといけないなんて…!!ということで学習コストが高いので導入をためらうことがあると思います。

当然、何かを使うということは導入のためのコストを払って開発を通じて回収していかないといけないので、そこのバランスが取れないものをあんまり入れると大変なことになりますよね。 ということで Rx 知らない人向けに Rx 成分無しで ReactiveProperty 使って感覚を掴むための使い方を紹介したいと思います。

導入

ReactiveProperty の NuGet パッケージを追加して終わりです。

UWP, Xamarin.Forms, WPF のような XAML を使うものでいい感じで動きますが、まぁ使えそうだと思ったら XAML 系のプラットフォームじゃなくてももちろん OK です!

ViewModel で使ってみよう

さて、MVVM パターンでは変更通知機能を持ったプロパティを定義して XAML でバインディングして同期をとったりします。 これがメンドクサイ。

例えばこんな感じになります。

using System.ComponentModel;

namespace RpHello.Xamarin
{
    public class MainPageViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string _input;

        public string Input
        {
            get => _input;
            set
            {
                _input = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Input)));
            }
        }
    }
}

もうちょっと簡単に書けるように工夫できますが、何度も書くのは単純にめんどくさいです。めんどくさいことは個人的に楽しくないです。

では、ReactiveProperty を使って書き換えてみましょう。 こんな感じになります。

using Reactive.Bindings;

namespace RpHello.Xamarin
{
    public class MainPageViewModel
    {
        public ReactiveProperty<string> Input { get; } = new ReactiveProperty<string>();
    }
}

アプローチとしては、INotifyPropertyChanged の実装を ViewModel クラスにするのではなく、プロパティ自身に実装させておくというものになります。 データバインディングのパスが以下のように .Value を追加しないといけないという制約がつきますが簡単に変更通知のプロパティが定義できます。

<!-- ReactiveProperty を使わない場合の Binding -->
<Entry Text-"{Binding Input}" />
<!-- ReactiveProperty を使う場合の Binding -->
<Entry Text-"{Binding Input.Value}" />
補足

WPF では DataContext に設定する型は INotifyPropertyChanged を実装してないとメモリリークの原因になるので使わなくても実装しましょう

とまぁ、これだけなので簡単に使えますね。 最初の最初はこれくらいの感覚で使ってみてもいいかもしれません。

入力値のチェックをしたい

入力値チェック機能が組み込まれているので簡単に入力値チェックをすることが出来ます。使い方は System.ComponentModel.DataAnnotations の属性をつけて初期化時にバリデーション機能を有効化するメソッドを呼び出します。

こんな感じで。

using Reactive.Bindings;
using System.ComponentModel.DataAnnotations;

namespace RpHello.Xamarin
{
    public class MainPageViewModel
    {
        // 検証ルールを System.ComponentModel.DataAnnotations で指定
        [Required(ErrorMessage = "入力は必須です")]
        public ReactiveProperty<string> Input { get; }

        public MainPageViewModel()
        {
            // フィールドの初期化ではフィールドの参照が出来ない(() => Inputがエラーになる)ので初期化をコンストラクタに移動
            Input = new ReactiveProperty<string>()
                // 検証機能を有効化するメソッド
                .SetValidateAttribute(() => Input);
        }
    }
}

ReactiveProperty には HasErrors プロパティがあって入力値にエラーがあるかどうかが簡単に判断できます。

public void Foo()
{
    if (Input.HasErrors)
    {
        // エラー有り
    }
    else
    {
        // エラーなし
    }
}

コマンド

コマンドは ReactiveCommand ですが、何か既存で ICommand の実装クラスがあるなら Rx 成分が不要な場合は使わなくてもいいです。はい。

Rx 成分でよく使うもの

複数の入力項目があって値が変わったらすべての入力項目の値を舐めて何かしたいということが結構ある気がします。

例えば苗字と名前の入力項目があって、確認用にフルネームを入力が更新されるたびにくっつけて表示するようなケースですね。

そんな時によくつかうのが Rx のメソッドの CombineLatest です。これは ReactiveProperty の値が変わったら、それぞれのプロパティの最後の値を使って処理をすることが出来るようなものです。

using Reactive.Bindings;
using System;
using System.ComponentModel.DataAnnotations;
using System.Reactive.Linq;

namespace RpHello.Xamarin
{
    public class MainPageViewModel
    {
        public ReactiveProperty<string> FirstName { get; } = new ReactiveProperty<string>();
        public ReactiveProperty<string> LastName { get; } = new ReactiveProperty<string>();
        public ReactiveProperty<string> FullName { get; } = new ReactiveProperty<string>();
        public MainPageViewModel()
        {
            new[]
            {
                FirstName,
                LastName,
            }.CombineLatest(x => $"{x[0]} {x[1]}")
            .Subscribe(x => FullName.Value = x);
        }

    }
}

まず、変わったら何かしたいプロパティ(今回の場合は FirstName と LastName)を配列にまとめあげます。そして CombineLatest を呼び出すと引数のラムダ式に IList<T> 型としてわたってきます。今回の例では x[0] に FirstName の最後の値が入ってて x[1] に LastName の最後の値が入っています。

あとは、それをいい感じに加工します(今回は単純な連結ですね)

最後に Subscribe メソッドで加工された値を FullName に代入しています。Subscribe メソッドは加工された値を受け取って何か処理をするための場所です。

これで完成ですが1つだけ。Subscribe で ReactiveProperty に代入するだけの場合は ToReactiveProperty メソッドや ToReadOnlyReactiveProperty メソッドに置き換えが可能です。ReadOnly がついている方は、代入が出来ない ReadOnlyReactiveProperty を生成します。今回の例のような場合は FullName を直接編集することはないので ReadOnly のほうが望ましい例になります。

ということで、こんな感じになります。

using Reactive.Bindings;
using System;
using System.ComponentModel.DataAnnotations;
using System.Reactive.Linq;

namespace RpHello.Xamarin
{
    public class MainPageViewModel
    {
        public ReactiveProperty<string> FirstName { get; } = new ReactiveProperty<string>();
        public ReactiveProperty<string> LastName { get; } = new ReactiveProperty<string>();
        public ReadOnlyReactiveProperty<string> FullName { get; }
        public MainPageViewModel()
        {
            FullName = new[]
            {
                FirstName,
                LastName,
            }.CombineLatest(x => $"{x[0]} {x[1]}")
            .ToReadOnlyReactiveProperty();
        }

    }
}

まとめ

こんな感じで単純な PropertyChanged の実装の簡略化のため + 入力値検証 + CombineLatest くらいを使うくらいの雰囲気で試してみてみるといいのかなぁと思ったので書いてみました。

本当は ReactiveCommand とか AsyncReactiveCommand とか ReactiveCollection とか ReadOnlyReactiveCollection とか etc... いっぱいあるので Rx の勉強がてら使ってみて頂けたらいいな!って思ってます。