かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

MVVMをリアクティブプログラミングで快適にReactivePropertyオーバービュー

最新版

最新版はこちらになります。

MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 前編 - Qiita

本文

MVVM + リアクティブプログラミングの組み合わせを快適にするためのライブラリのReactiveProperty解説記事です。

github.com

以下のNuGetから入手できます。

www.nuget.org

ReactivePropertyの特徴

ReactivePropertyは、MVVM + Rxを支援するための機能を有したライブラリです。以下のような機能を持っています。

  • ReactivePropertyクラス
  • ReactiveCommandクラス
  • ReactiveCollectionクラス
  • ReadOnlyReactiveCollectionクラス
  • INotifyPropertyChanged/INotifyCollectionChangedをIObservableに変換するメソッド群
  • その他Reactive ExtensionsにないちょっとしたIObservableファクトリメソッド
  • 通知系のNotifier系クラス
  • IFilteredReadOnlyObservableCollectionインターフェース

ReactivePropertyクラス

このライブラリのコア機能です。変更通知プロパティを簡単に定義することが出来ます。その代りBinding時のPathが長くなるというデメリットもあります。例えばViewModelなどにNameという変更通知機能を持ったプロパティを定義するには以下のようにします。

public class PersonViewModel
{
    public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();
}

ReactivePropertyクラスは、Valueプロパティで値の設定と取得ができます。またINotifyPropertyChangedを実装しています。以下のように使うことが出来ます。

// Console Applicationでは自分でSynchronizationContextをセットしないといけない
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());

var vm = new PersonViewModel();
vm.Name.PropertyChanged += (_, __) => Console.WriteLine($"Name changed {vm.Name.Value}");

vm.Name.Value = "tanaka";
Console.ReadKey();
vm.Name.Value = "kimura";
Console.ReadKey();

一行すると以下のようになります。

Name changed tanaka
Name changed kimura

XAML系プラットフォームでの使用

もともとMVVMパターンを補助するライブラリですので、XAML系プラットフォームで使います。Universal Windows PlatformやWPFやXamarinなどで使うことが出来ます。ここでは、UWPでの利用を説明したいと思います。

先ほどのPersonViewModelをバインドするために、コードビハインドにプロパティとして設定します。

public sealed partial class MainPage : Page
{
    public PersonViewModel ViewModel { get; } = new PersonViewModel();

    public MainPage()
    {
        this.InitializeComponent();
    }
}

XAMLでは、以下のようにプロパティ名.Value(Valueをつけないといけない)でパスを指定します。

<Page x:Class="App1.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App1"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBox Text="{x:Bind ViewModel.Name.Value, Mode=TwoWay}" />
        <TextBlock Text="{x:Bind ViewModel.Name.Value, Mode=OneWay}" />
    </StackPanel>
</Page>

実行すると、以下のようにTextBoxとTextBlockで値の同期がとれます。

f:id:okazuki:20151202220451p:plain

ReactivePropertyはIObservableからも生成可能

このようにReactivePropertyを使うと、ViewModelの基本クラスを指定することなく簡単に変更通知機能を持ったプロパティを定義できます。この他にも、ReactivePropertyの名前の通りReactive Extensionsを使ってReactivePropertyを作成することが出来ます。

具体的にはIObservableに対してToReactivePropertyを呼ぶことでIObservableから値が発行されたら、それをValueに保持するというReactivePropertyが作成できます。

例えば以下のような感じになります。

public class PersonViewModel
{
    public ReactiveProperty<string> Name { get; } = Observable.Interval(TimeSpan.FromSeconds(1))
        .Select(x => $"tanaka {x}")
        .ToReactiveProperty();
}

1秒間隔で値を更新するReactivePropertyになります。実行すると1秒間隔で表示が更新されます。

f:id:okazuki:20151202221103p:plain

ReactivePropertyもIObservable

ReactivePropertyがIObservableから生成可能なことはわかりました。さらに言うと、ReactivePropertyもIObservableとしての特性を持っているので、ReactivePropertyを加工してさらに別のReactivePropertyを作るといったことも可能です。

例えば以下のようになります。

public class PersonViewModel
{
    public ReactiveProperty<string> Input { get; } = new ReactiveProperty<string>("");

    public ReactiveProperty<string> Output { get; }

    public PersonViewModel()
    {
        this.Output = this.Input
            .Delay(TimeSpan.FromSeconds(1))
            .Select(x => x.ToUpper())
            .ToReactiveProperty();
    }
}

このようにしてXAMLからBindingします。x:BindではTextBoxのプロパティが変わったタイミングで値を反映することが出来ないのでここだけ、通常のBinidngをつかっています。

<Page x:Class="App1.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App1"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      x:Name="Page"
      mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBox Text="{Binding ViewModel.Input.Value, ElementName=Page, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <TextBlock Text="{x:Bind ViewModel.Output.Value, Mode=OneWay}" />
    </StackPanel>
</Page>

実行すると、1秒遅れで入力した文字が大文字になって出力されます。

f:id:okazuki:20151202222032p:plain

読み取り専用のReactiveProperty

先ほどの例のOutputプロパティのようにReactivePropertyのValueに値をセットされないほうが望ましいケースもあります。そういうときは、IObservableからToReadOnlyReactiveProperty()とすることで値をセットできないReadOnlyReactivePropertyが作成できます。

public class PersonViewModel
{
    public ReactiveProperty<string> Input { get; } = new ReactiveProperty<string>("");

    public ReadOnlyReactiveProperty<string> Output { get; }

    public PersonViewModel()
    {
        this.Output = this.Input
            .Delay(TimeSpan.FromSeconds(1))
            .Select(x => x.ToUpper())
            .ToReadOnlyReactiveProperty();
    }
}

値がセットできない以外は普通のReactivePropertyと、ほぼ同じように使うことができます。

ReactivePropertyのmode

ReactivePropertyは、コンストラクタやToReactivePropertyメソッドなどのファクトリメソッドでmodeを指定することが出来ます。modeにはReactivePropertyModeのEnum型が指定可能です。 以下のように定義されています。

[Flags]
public enum ReactivePropertyMode
{
    None = 0,
    DistinctUntilChanged = 1,
    RaiseLatestValueOnSubscribe = 2
}

DistinctUntilChangedを指定すると、同じ値が設定されてもOnNextやOnPropertyChangedを発行しなくなります。RaiseLatestValueOnSubscribeを設定すると、ReactivePropertyをSubscribeしたタイミングで現在の値をOnNextに発行するかどうかを制御できます。デフォルトでは、両方のmodeが設定されています。

mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueSubscribe;

このmodeを設定することで、ReactiveProeprtyのIObservableとしての特性を多少カスタマイズできます。

既存のModelとの接続

ReactivePropertyは、MVVMパターンのVMで使うことを主目的として作られています。(別にModelで使っても構いません)そのため、既存のPlaneなC#のModelのプロパティと同期をとるための便利な方法が用意されています。

INotifyPropertyChangedを実装したModelからの作成

例として、以下のようなINotifyPropertyChangedを実装したクラスがあるとします。

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private static readonly PropertyChangedEventArgs NamePropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(Name));

    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            if (this.name == value) { return; }
            this.name = value;
            this.PropertyChanged?.Invoke(this, NamePropertyChangedEventArgs);
        }
    }

}

このNameプロパティの変更を監視して、値を更新するViewModelはReactivePropertyの機能を使わないで書くと通常以下のような感じに書きます。

public class PersonViewModel : IDisposable
{
    private Person Model { get; set; }
    public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();

    public PersonViewModel(Person model)
    {
        this.Model = model;
        model.PropertyChanged += this.Model_PropertyChanged;
    }

    private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch (e.PropertyName)
        {
            case nameof(Person.Name):
                this.Name.Value = ((Person)sender).Name;
                break;
        }
    }

    public void Dispose()
    {
        this.Model.PropertyChanged -= this.Model_PropertyChanged;
        this.Model = null;
    }
}

非常にめんどくさいです。プロパティが増えたら大変なことになりそうです。ということで、ReactivePropertyでは、INotifyPropertyChangedを実装したクラスに対してObservePropertyというプロパティの変更をIObservableに変換する機能を提供しています。IObservableにしてしまえばToReactivePropertyメソッドを使ってReactivePropertyに変換できます。したがって上記のコードは以下のように書けます。

public class PersonViewModel : IDisposable
{
    private CompositeDisposable Disposable { get; } = new CompositeDisposable();
    public ReactiveProperty<string> Name { get; }

    public PersonViewModel(Person model)
    {
        this.Name = model.ObserveProperty(x => x.Name)
            .ToReactiveProperty()
            .AddTo(this.Disposable);
    }

    public void Dispose()
    {
        this.Disposable.Dispose();
    }
}

ReactivePropertyのDisposeメソッドを呼び出すとModelのプロパティの変更監視を辞めるので、CompositeDisposableにReactivePropertyを集めておくことで、プロパティが増えてもあとで一括で購読を解除することが出来て便利です。

間にSelectメソッドなどを挟むことで、Modelの値を加工してViewModelに反映するということも簡単に書けるようになります。

public PersonViewModel(Person model)
{
    this.Name = model.ObserveProperty(x => x.Name)
        .Select(x => $"{x}さん")
        .ToReactiveProperty()
        .AddTo(this.Disposable);
}

双方向の値の同期

上記の例はModelからViewModelへの単一方向の値の同期の仕方になります。この方法に加えて何か任意のタイミングで値をModelに書き戻すことでViewModelからModelへの値の更新も可能です。ただ、単純なViewModelのプロパティとModelのプロパティの双方向同期の方法も提供しています。INotifyPropertyChangedを実装したクラスに対してToReactivePropertyAsSynchronizedを呼び出すことで、双方向同期のReactivePropertyが生成されます。

public PersonViewModel(Person model)
{
    this.Name = model.ToReactivePropertyAsSynchronized(x => x.Name)
        .AddTo(this.Disposable);
}

convert引数とconvertBack引数にラムダ式を指定することで単純な値の変換処理を間に挟み込むことが出来ます。

public PersonViewModel(Person model)
{
    // 名前にさんが入ってる人がいると破たんするロジックですが雰囲気をつかんでもらえれば…
    this.Name = model.ToReactivePropertyAsSynchronized(x => x.Name,
        convert: x => $"{x}さん",
        convertBack: x => x.Replace("さん", ""))
        .AddTo(this.Disposable);
}

INotifyPropertyChangedを実装していないModelとの接続

初期値としてModelの値をReactivePropertyに取り込み、ReactivePropertyの値に変更があったらModelに書き戻すだけの機能もあります。ModelがINotifyPropertyChangedを実装していないので、VMからMへの一方通行の値の同期です。

ReactiveProperty.FromObjectがその機能を提供します。以下のように使います。

public PersonViewModel(Person model)
{
    this.Name = ReactiveProperty.FromObject(model, x => x.Name);
}

ToReactivePropertyAsSynchronizedと同様にconvertとconvertBack引数を渡すことで間に値の変換をかませることが出来ます。

入力値のバリデーション

ReactivePropertyには、入力値の検証機能もあります。一番単純な方法はReactivePropertyの生成後にSetValidateNotifyErrorで検証ロジックを渡すことです。以下のようなコードで必須入力チェック機能付きのReactiveProeprtyを生成できます。

public class PersonViewModel
{
    public ReactiveProperty<string> Name { get; }

    public PersonViewModel()
    {
        this.Name = new ReactiveProperty<string>()
            .SetValidateNotifyError(x => string.IsNullOrWhiteSpace(x) ? "Error!!" : null);
    }

}

ReactivePropertyは、INotifyDataErrorInfoを実装しているためWPFではWPFの組み込みのバリデーションエラーメッセージの表示機能が使えます。UWPなどのINotifyDataErrorInfoによる検証エラーメッセージの無い環境では、ReactivePropertyのObserveErrorChangedをエラー情報にしてReadOnlyReactivePropertyなどに変換することでエラーメッセージを取得できます。

以下のような感じになります。

public class PersonViewModel
{
    public ReactiveProperty<string> Name { get; }

    public ReadOnlyReactiveProperty<string> NameErrorMessage { get; }

    public PersonViewModel()
    {
        this.Name = new ReactiveProperty<string>()
            .SetValidateNotifyError(x => string.IsNullOrWhiteSpace(x) ? "Error!!" : null);
        this.NameErrorMessage = this.Name
            .ObserveErrorChanged
            .Select(x => x?.Cast<string>()?.FirstOrDefault())
            .ToReadOnlyReactiveProperty();
    }

}

このViewModelを以下のように画面でバインドすることでエラーメッセージが表示されるようになります。

<Page x:Class="App1.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App1"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBox Text="{x:Bind ViewModel.Name.Value, Mode=TwoWay}" />
        <TextBlock Text="{x:Bind ViewModel.NameErrorMessage.Value, Mode=OneWay}" />
    </StackPanel>
</Page>

実行すると以下のようになります。

f:id:okazuki:20151203210343p:plain

f:id:okazuki:20151203210411p:plain

DataAnnotationsをサポートしているプラットフォーム(Windows PhoneのSilverlight以外)では、DataAnnotationsを使った検証機能もサポートしています。SetValidateAttirbuteメソッドに、属性をつけたReactivePropertyを渡すことで設定可能です。必須入力の場合の例を以下に示します。

public class PersonViewModel
{
    [Required(ErrorMessage = "Error!!")]
    public ReactiveProperty<string> Name { get; }

    public ReadOnlyReactiveProperty<string> NameErrorMessage { get; }

    public PersonViewModel()
    {
        this.Name = new ReactiveProperty<string>()
            .SetValidateAttribute(() => this.Name);
        this.NameErrorMessage = this.Name
            .ObserveErrorChanged
            .Select(x => x?.Cast<string>()?.FirstOrDefault())
            .ToReadOnlyReactiveProperty();
    }

}

これで、先ほどと同じ結果になります。

検証エラーとModelとの同期

Modelと同期するためのToReactivePropertyAsSynchronizedとFromObjectメソッドには、ignoreValidationErrorValueという引数があり、この引数にtrueを設定することで検証エラーがあるときには、Modelに値を書き込まないようにするオプションがあります。必須入力チェックを通過したときのみModelに値を書き戻す場合のコード例をいかに示します。

public class PersonViewModel
{
    [Required(ErrorMessage = "Error!!")]
    public ReactiveProperty<string> Name { get; }

    public ReadOnlyReactiveProperty<string> NameErrorMessage { get; }

    public PersonViewModel(Person model)
    {
        this.Name = model.ToReactivePropertyAsSynchronized(x => x.Name,
            ignoreValidationErrorValue: true)
            .SetValidateAttribute(() => this.Name);
        this.NameErrorMessage = this.Name
            .ObserveErrorChanged
            .Select(x => x?.Cast<string>()?.FirstOrDefault())
            .ToReadOnlyReactiveProperty();
    }

}

検証エラーの監視

ReactivePropertyにはObserveHasErrorsプロパティがあり、このプロパティを通してReactivePropertyのエラーの有無を監視することが出来ます。ObserveHasErrorsプロパティはIObservable<bool>型なので、複数のReactivePropertyとCombineLatestで束ねることで、全てのReactivePropertyにエラーがないときなどの処理を簡単に記述できるようになっています。

public class PersonViewModel
{
    [Required(ErrorMessage = "Error!!")]
    public ReactiveProperty<string> Name { get; }

    [Required(ErrorMessage = "Error!!")]
    [RegularExpression("[0-9]+", ErrorMessage = "Error!!")]
    public ReactiveProperty<string> Age { get; }

    public PersonViewModel(Person model)
    {
        this.Name = new ReactiveProperty<string>()
            .SetValidateAttribute(() => this.Name);
        this.Age = new ReactiveProperty<string>()
            .SetValidateAttribute(() => this.Age);

        new[]
            {
                this.Name.ObserveHasErrors,
                this.Age.ObserveHasErrors
            }
            .CombineLatest(x => x.All(y => !y))
            .Where(x => x)
            .Subscribe(_ =>
            {
                Debug.WriteLine("エラーの無い時の処理");
            });

    }

}

ReactiveCommand

ReactivePropertyには、MVVMパターンのICommandの実装としてReactiveCommandクラスを提供しています。ReactiveCommandの特徴は、IObservable<bool>から生成可能という点です。IObservable<bool&lgt;に対して、ToReactiveCommand拡張メソッドを呼び出すことでTrueが発行されたときだけ実行可能なコマンドが作成されます。自前でプロパティを監視して、コマンドの実行可否を判定する必要がなくなる点が特徴です。

コマンドが実行されたときの処理はSubscribeメソッドで指定します。

例えばすべてのプロパティの入力にエラーがなくなったら処理をしたいというコマンドは以下のように定義できます。

public class PersonViewModel
{
    [Required(ErrorMessage = "Error!!")]
    public ReactiveProperty<string> Name { get; }

    [Required(ErrorMessage = "Error!!")]
    [RegularExpression("[0-9]+", ErrorMessage = "Error!!")]
    public ReactiveProperty<string> Age { get; }

    public ReactiveCommand CommitCommand { get; }

    public PersonViewModel()
    {
        this.Name = new ReactiveProperty<string>()
            .SetValidateAttribute(() => this.Name);
        this.Age = new ReactiveProperty<string>()
            .SetValidateAttribute(() => this.Age);

        this.CommitCommand = new[]
            {
                this.Name.ObserveHasErrors,
                this.Age.ObserveHasErrors
            }
            .CombineLatest(x => x.All(y => !y))
            .ToReactiveCommand();
        this.CommitCommand.Subscribe(async _ => await new MessageDialog("OK").ShowAsync());

    }

}

以下のようにXAMLにバインドできます。

<Page x:Class="App1.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App1"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBox Text="{x:Bind ViewModel.Name.Value, Mode=TwoWay}" />
        <TextBox Text="{x:Bind ViewModel.Age.Value, Mode=TwoWay}" />
        <Button Content="Commit"
                Command="{x:Bind ViewModel.CommitCommand}" />
    </StackPanel>
</Page>

入力エラー(Ageとバインドしてるところに数字じゃない文字列が入っている)があるとボタンが押せなくて

f:id:okazuki:20151203212625p:plain

入力エラーがなくなるとボタンが押せるようになります。

f:id:okazuki:20151203212709p:plain

ReactiveCommandには、CommandParameterを受け取るReactiveCommand<T>という型引数を受け取るバージョンのものも用意しています。

ReactiveCollectionクラス

ReactiveCollectionクラスは、指定したスケジューラ上(何も指定しない場合はUIスレッド上)でコレクションの操作とイベントの発行を行う機能を追加したObservableCollectionクラスになります。また、IObservableからToReactiveCollectionを呼び出すことで、IObservableから値が発行されるたびにコレクションに要素を追加するという機能を持ったコレクションとして生成することも可能です。

例えば以下のようにすることで、1秒ごとに値を追加するコレクションが作成できます。

public class PersonViewModel
{
    public ReactiveCollection<long> TimerCollection { get; } = Observable
        .Interval(TimeSpan.FromSeconds(1))
        .ToReactiveCollection();
}

また、AddOnScheduler、RemoveOnSchedulerなどの追加削除などのメソッド名にOnSchedulerがついたメソッドを呼び出すことでReactiveCollectionが紐づいているSchedulerを使ってコレクション操作とイベントの発行を行うように指示することが出来ます。

public class PersonViewModel
{
    public ReactiveCollection<long> SampleCollection { get; } = new ReactiveCollection<long>();

    private Random Random { get; } = new Random();

    public ReactiveCommand AddCommand { get; } = new ReactiveCommand();

    public PersonViewModel()
    {
        this.AddCommand.Subscribe(async _ => await Task.Run(() => 
        {
            // バックグランドのスレッドからUIスレッド上で要素を追加することが可能
            this.SampleCollection.AddOnScheduler(this.Random.Next());
        }));
    }
}

重たいバックグラウンド処理の結果をUIスレッド上で要素を追加したいときなどに便利です。コンストラクタでSchedulerを渡すことで、デフォルトのUIスレッド上以外でのコレクション操作に対応させることも可能です。

ReadOnlyReactiveCollection

このクラスは、ObservableCollectionやReactiveCollectionと同期した読み取り専用のコレクションをとして機能します。ObservableCollectionやReactiveCollectionでToReadOnlyObservableCollectionメソッドを呼び出すことで生成できます。

その時に、Func<T, U>型の変換ロジックを渡すことで、ObservableCollectionの型を変換した結果のコレクションとして機能させることが出来ます。ちょうど、ModelのコレクションをViewModelのコレクションとして提供するといったケースで便利に使用できます。また、コレクションから値が削除されるタイミングで、IDisposable型の場合Disposeを自動で呼び出す機能もあります。

基本的な使い方を以下に示します。

public class MainPageViewModel
{
    private PeopleManager Model { get; } = new PeopleManager();
    public ReadOnlyReactiveCollection<PersonViewModel> People { get; }

    public MainPageViewModel()
    {
        // ModelのPersonのコレクションをPersonViewModelのコレクションにする
        this.People = this.Model.People.ToReadOnlyReactiveCollection(x => new PersonViewModel(x));
    }
}

public class PersonViewModel
{
    //各種プロパティ
    public PersonViewModel(Person model)
    {
        // modelとvmの接続処理
    }
}

public class PeopleManager
{
    public ObservableCollection<Person> People { get; } = new ObservableCollection<Person>();

    // Peopleを処理するコードなど
}

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    // 各種プロパティ
}

色々なものをIObservableに変換するメソッド

ReactivePropertyは、ReactivePropertyクラスやReactiveCommandクラスなどをIObservableから生成する関係上、色々なものをIObservableに変換するメソッドを提供しています。

INotifyPropertyChangedをIObservableに変換する

PropertyChangedAsObservable拡張メソッドはINotifyPropertyChangedのイベントの発生をIObservable>NotifyPropertyChangedEventArgs<に変換します。

var p = new Person();
p.PropertyChangedAsObservable()
    .Subscribe(x => Debug.WriteLine($"{x.PropertyName}が変更されました"));

さらに、特定のプロパティの変更に特化したObservePropertyメソッドがあります。これは、変更後のプロパティの値が流れてきます。

var p = new Person();
p.ObserveProperty(x => x.Name)
    .Subscribe(x => Debug.WriteLine($"変更後のプロパティの値 {x}"));

INotifyCollectionChangedをIObservableに変換する

CollectionChangedもPropertyChangedと同じようにIObservableに変換できます。

var col = new ObservableCollection<Person>();
col.CollectionChangedAsObservable()
    .Subscribe(x => Debug.WriteLine($"{x.Action}が実行されました"));

この他に、AddやRemoveなどに特化したObserveXXXChangedメソッドがあります。

var col = new ObservableCollection<Person>();
col.ObserveAddChanged()
    .Subscribe(x => Debug.WriteLine($"{x.Name}が追加されました"));

コレクションの要素の変更を監視する

あるようでなかなか無い処理として、コレクションの中の要素のプロパティが変更されたかどうかを監視する拡張メソッドがあります。ObserveElementPropertyChangedでPropertyChangedの監視を行い、ObserveElementPropertyで指定したプロパティの変更を監視します。

var col = new ObservableCollection<Person>();
// プロパティの変更を監視する
col.ObserveElementPropertyChanged()
    .Subscribe(x => Debug.WriteLine($"{x.EventArgs} {x.Sender}"));
// 特定のプロパティの変更を監視する
col.ObserveElementProperty(x => x.Name)
    .Subscribe(x => Debug.WriteLine($"{x.Instance} {x.Property} {x.Value}"));

ObserveElementPropertyのプロパティがReactivePropertyの場合のObserveElementObservablePropertyという拡張メソッドも提供しています。

そのほかにも

この他にも各種拡張メソッドを提供しています。

CatchIgnore

例外を無視してObservable.Emptyを返却します。

fooObservable.CatchIgnore().Subscribe(x => ...);

例外発生時の処理を指定することもできます。

fooObservable.CatchIgnore(ex => Debug.WriteLine(ex)).Subscribe(x => ...);

CombineLatestValuesAreAllTrue

IEnumerable<IObservable<bool>>の拡張メソッドで、最後に発行された値がすべてtrueの場合trueを後続に流します。

CombineLatestValuesAreAllFalse

IEnumerable<IObservable<bool>>の拡張メソッドで、最後に発行された値がすべてfalseの場合trueを後続に流します。

CanExecuteChangedAsObservable

ICommandのCanExecuteChangedイベントをIObservable<EventArgs>に変換します。

ErrorChangedAsObservable

INotifyDataErrorInfoのErrorChangedイベントをIObservableに変換します。

ObserveErrorInfo

INotifyDataErrorInfoでエラー状態に変化のあったプロパティの値を発行するIObservableを生成します。

Inverse

IObservable<bool>から発行されたbool値を反転して後続に流します。

Pairwise

以下のショートカットです。

ox.Zip(ox.Skip(1), (x, y) => new OldNewPair<T>(x, y))

OldNewPair型はOldItem, NewItemで値にアクセスできます。

OnErrorRetry

リトライを行います。

ox.OnErrorRetry(ex => Debug.WriteLine(ex)).Subscribe(x => ...); // エラー時に処理をするリトライ
ox.OnErrorRetry(ex => Debug.WriteLine(ex), 10).Subscribe(x => ...); // リトライ回数を指定するバージョン
ox.OnErrorRetry(ex => Debug.WriteLine(ex), TimeSpan.FromSecond(5)).Subscribe(x => ...); // リトライ間隔を指定するバージョン
ox.OnErrorRetry(ex => Debug.WriteLine(ex), 10, TimeSpan.FromSecond(5)).Subscribe(x => ...); // リトライ回数と間隔を指定するバージョン

ToUnit

IObservable<T>をIObservable<Unit>に変換します。

Notifier系クラス

ReactivePropertyには、IObservable>bool<, IObservable>CountChangedStatus<を便利に扱うためのBooleanNotifyerクラスとCountNotifierクラスがあります。さらに、指定した時間の後に、値を発行するScheduledNotifierというクラスを提供しています。

BooleanNotifier

TrueとFalseを切り替えるだけのシンプルなクラスです。TurnOnでTrueにしてTurnOffでFalseにしてValueプロパティに直接値を設定することでTrue,Falseを設定できます。

var n = new BooleanNotifier();
n.Subscribe(x => Debug.WriteLine(x));

n.TurnOn(); // true
n.TurnOff(); // false
n.Value = true; // true
n.Value = false; // false

ReactiveCommandのソースとして、単純にIObservable<bool>のソースとして使うことが出来ます。Subject<bool>との違いは、Valueで現在の状態をとれるという点です。

CountNotifier

CountNotifierは、名前の通りカウンターです。数値のカウントが出来ます。Increment, Decrementメソッドで指定した数だけ数値を加算したり、減算したりできます。特徴的なのは、IncrementメソッドとDecrementメソッドが返すIDisposableをDisposeすることで、インクリメントやデクリメントの逆が実行されて値がもとに戻ります。CountNotifierは、CountChangedStatusのIObservableになります。CountChangedStatusは以下のように定義されているenumになります。

/// <summary>Event kind of CountNotifier.</summary>
public enum CountChangedStatus
{
    /// <summary>Count incremented.</summary>
    Increment,
    /// <summary>Count decremented.</summary>
    Decrement,
    /// <summary>Count is zero.</summary>
    Empty,
    /// <summary>Count arrived max.</summary>
    Max
}

こんな感じで使えます。

var c = new CountNotifier();
// 現在のカウンタの状態を出力
c.Subscribe(x => Debug.WriteLine(x));
// 現在のカウンタ値を出力
c.Select(_ => c.Count).Subscribe(x => Debug.WriteLine(x));
// インクリメントして
var d = c.Increment(10);
// インクリメントを取り消し
d.Dispose();
// インクリメントしてデクリメント
c.Increment(10);
c.Decrement(5);
// 現在の値を出力
Debug.WriteLine(c.Count);

出力結果は以下のようになります。

Increment
10
Decrement
0
Empty
0
Increment
10
Decrement
5
5

コンストラクタで、カウンタのMax値を指定するとその値以上にならないカウンタとかも作ることができます。

ScheduledNotifier

ScueduledNotifierクラスは、指定したScheduler上で指定した時間に値を発行するだけのシンプルなクラスです。デフォルトだと現在のスレッド上になります。スケジューラを差し替える場合はコンストラクタにスケジューラを指定します。

使い方は以下のような感じになります。

var n = new ScheduledNotifier<string>();
n.Subscribe(x => Debug.WriteLine(x));
// 即時実行
n.Report("Hello world");
// 2病後実行
n.Report("After 2 second.", TimeSpan.FromSeconds(2));

BusyNotifier

BusyNotifierメソッドは、よくある二度押し防止などに使えるクラスです。boolのIObservableとして動作します。IsBusyプロパティを持っていて現在Busyな状態なのかを判定することもできます。基本的な使い方は何かの処理の実行前にProcessStartメソッドを呼び出して、処理が終わったらProcessStartメソッドの戻り値のIDisposableをDisposeするという流れになります。

// フィールドに定義しておく
private BusyNotifier Busy { get; } = new BusyNotifier();

// 重たい処理とかで
using (this.Busy.ProcessStart())
{
    // 何か重い処理
}

また、boolのIObservableなのでReactivePropertyのソースとしても使えます。

// IsBusyやIsIdleのReactivePropertyを作るとき
// プロパティを定義しておいて
private BusyNotifyer BusyNotifier { get; } = new BusyNotifier();
public ReadOnlyReactiveProperty<bool> IsBusy { get; }
public ReadOnlyReactiveProperty<bool> IsIdle { get; }

// コンストラクタで
this.IsBusy = this.BusyNotifyer.ToReadOnlyReactiveProperty();
this.IsIdle = this.BusyNotifier.Inverse().ToReadOnlyReactiveProperty(); // Extensions名前空間にIO<bool>を反転させるInverse拡張メソッド定義してます

VからVMへのイベントの伝搬

ReactivePropertyやReactiveCommandにViewで発生した"イベント"を渡す方法を提供しています。 EventToReactivePropertyとEventToReactiveCommandクラスがその機能を提供しています。このクラスはBehaviorのActionとして実装されていて、EventTrigger(WinRT系の場合はEventTriggerBehavior)などのTriggerに設定して使用します。EventToReactivePropertyクラスとEventToReactiveCommandクラスにはReactiveConverter<T, U>クラスを挟むことで非同期な処理を間に挟むことが出来ます。例えば以下のようなコンバーターを作成することでファイルを選択して選択したパスを返すことが出来ます(コンバータという名前が適当なのかというと自信がなくなってきますが…)

using Reactive.Bindings.Interactivity;
using System;
using System.Linq;
using System.Reactive.Linq;
using Windows.Storage.Pickers;
using Windows.UI.Xaml;

namespace App1
{
    public class FileOpenReactiveConverter : ReactiveConverter<RoutedEventArgs, string>
    {
        protected override IObservable<string> OnConvert(IObservable<RoutedEventArgs> source)
        {
            return source.SelectMany(async _ =>
            {
                var picker = new FileOpenPicker();
                picker.FileTypeFilter.Add(".snippet");
                var f = await picker.PickSingleFileAsync();
                return f?.Path;
            })
            .Where(x => x != null);

        }
    }
}

このコンバータを使って以下のようにイベントをコマンドにバインドできます。

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App1"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
      xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
      xmlns:Interactivity1="using:Reactive.Bindings.Interactivity"
      x:Class="App1.MainPage"
      mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Button Content="OpenFile...">
            <Interactivity:Interaction.Behaviors>
                <Core:EventTriggerBehavior EventName="Click">
                    <Interactivity1:EventToReactiveCommand Command="{x:Bind ViewModel.SelectFileCommand}">
                        <local:FileOpenReactiveConverter />
                    </Interactivity1:EventToReactiveCommand>
                </Core:EventTriggerBehavior>
            </Interactivity:Interaction.Behaviors>
        </Button>
        <TextBlock Text="{x:Bind ViewModel.FileName.Value, Mode=OneWay}" />
    </StackPanel>
</Page>

コードビハインドとViewModelは以下のようになっています。

using Reactive.Bindings;
using Windows.UI.Xaml.Controls;

namespace App1
{
    public sealed partial class MainPage : Page
    {
        public MainPageViewModel ViewModel { get; } = new MainPageViewModel();

        public MainPage()
        {
            this.InitializeComponent();
        }
    }

    public class MainPageViewModel
    {
        public ReactiveCommand<string> SelectFileCommand { get; }
        public ReadOnlyReactiveProperty<string> FileName { get; }

        public MainPageViewModel()
        {
            this.SelectFileCommand = new ReactiveCommand<string>();
            this.FileName = this.SelectFileCommand.ToReadOnlyReactiveProperty();
        }
    }

}

これでファイルを選択するとファイル名が表示されるようになります。EventToReactiveCommandとEventToReactivePropertyは、指定できるのがReactiveCommandかReactivePropertyかという違いだけなのでEventToReactivePropertyの使い方は割愛します。

IFilteredReadOnlyObservableCollectionインターフェース

Reactiveっぽくない機能ですが、プロパティの値をリアルタイムに監視してフィルタリングする機能を持ったコレクションです。コレクションに対して、ToFilteredReadOnlyObservableCollectionメソッドを呼び出すことで生成できます。 引数で、コレクションの対象にしたい条件を指定します。例えば以下のように使います。

var collection = new ObservableCollection<Person>();
// tanakaさん以外を含んだコレクション
var filtered = collection.ToFilteredReadOnlyObservableCollection(x => x.Name.IndexOf("tanaka") == -1);

collection.Add(new Person { Name = "okazuki1" });
collection.Add(new Person { Name = "okazuki2" });
collection.Add(new Person { Name = "okazuki3" });
collection.Add(new Person { Name = "tanaka1" });

// okazuki1, okazuki2, okazuki3
Console.WriteLine("---");
foreach (var p in filtered)
{
    Console.WriteLine(p.Name);
}

collection[1].Name = "tanaka2";

// okazuki1, okazuki3
Console.WriteLine("---");
foreach (var p in filtered)
{
    Console.WriteLine(p.Name);
}

ReactivePropertyなどがデフォルトで使うSchedulerの変更

デフォルトの挙動として、ReactivePropertyはReactivePropertyScheduler.Defaultで返されるSchedulerを使用してイベントの発行などを行います。 ReactivePropertyScheduler.Defaultは、デフォルトでUIDispatcherScheduelr.Defaultで返されるSchedulerを返します。このため、ReactivePropertyなどでは特に意識しなくてもUIスレッド上でイベント発行などがされるようになっています。

UIDispatcherSchedulerはSynchronizationContextを見ているのですが、WPFやUWPなどではUIスレッドに紐づいたSynchronizationContextがあるのに対してコンソールアプリケーションや単体テストでは、これがデフォルトでないという問題があります。そういう時は、ReactivePropertyScheduler.SetDefault(IScheduler scheduler)を呼び出すことで明示的にReactivePropertyなどで使用するデフォルトのスケジューラを設定できるようになっています。

まとめ

長々と書きましたが、ReactivePropertyの機能をなんとなく書き出してみました。 これでしばらくは、こういう記事はいいかな?