かずきのBlog@hatena

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

MVVMとリアクティブプログラミングを支援するライブラリ「ReactiveProperty v2.0」オーバービュー

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

blog.okazuki.jp

ReactivePropertyとは

ReactivePropertyとは、ReactiveProperty<T>クラスを中心とした、MVVMパターンでのリアクティブプログラミングを強力にサポートするライブラリです。

導入方法

NuGetからインストールできます。

Install-Package ReactiveProperty

対象プラットフォーム

以下のプラットフォームで動作します。

  • .NET Framework 4
  • .NET Framework 4.5
  • Windows store app 8/8.1
  • Windows Phone Sliverlight 8/8.1
  • Windows Phone app 8.1
  • Xamarin.Android
  • Xamarin.iOS

ReactivePropertyの基本

ReactivePropertyはMVVMパターンのViewModelのクラスでReactiveProperty<T>クラスをプロパティとしてもつのが特徴です。

ReactivePropertyを使ったViewModelのコードは以下のようになります。

using Reactive.Bindings;

namespace RxPropEdu
{
    public class MainWindowViewModel
    {
        public ReactiveProperty<string> Input { get; private set; }
        public ReactiveProperty<string> Output { get; private set; }
    }
}

そして、ViewModelクラスのコンストラクタでReactiveProperty間の関連を定義していきます。ReactivePropertyは、IObservable<T>を実装しているので、LINQを使って柔軟にプロパティを定義することが出来ます。

以下にコード例を示します。

using Reactive.Bindings;
using System;
using System.Reactive.Linq;

namespace RxPropEdu
{
    public class MainWindowViewModel
    {
        public ReactiveProperty<string> Input { get; private set; }
        public ReactiveProperty<string> Output { get; private set; }

        public MainWindowViewModel()
        {
            this.Input = new ReactiveProperty<string>(""); // デフォルト値を指定してReactivePropertyを作成
            this.Output = this.Input
                .Delay(TimeSpan.FromSeconds(1)) // 1秒間待機して
                .Select(x => x.ToUpper()) // 大文字に変換して
                .ToReactiveProperty(); // ReactiveProperty化する
        }
    }
}

ReactivePropertyを作成する基本的な方法は、以下の2つです。

  • new演算子を使って生成する
    • コンストラクタの引数にデフォルト値を指定する。指定しない場合は、その型のでデフォルト値が使われる
  • IObservable<T>に対してToReactiveProperty拡張メソッドを呼ぶ

上記サンプルでは、Inputプロパティでnewを使って生成して、OutputプロパティでToReactivePropertyを使った生成をしています。

Viewへのバインド

ReactivePropertyをViewへバインドするには、ReactivePropertyのValueメソッドをバインドします。

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:RxPropEdu" x:Class="RxPropEdu.MainWindow"
    Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <StackPanel>
        <Label Content="入力" />
        <TextBox Text="{Binding Input.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Label Content="出力" />
        <TextBlock Text="{Binding Output.Value}" />
    </StackPanel>
</Window>

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

f:id:okazuki:20150222170615p:plain

ReactivePropertyのCommand

ReactivePropertyは、ReactiveCommandというICommandを実装したクラスを提供しています。このクラスの特徴は、IObservable<bool>から生成することが出来る点です。Commandの実行可否を予めIObservable<bool>に合成しておいて、ToReactiveCommandでCommand化することで、CanExecuteChangedイベントが適切に発行されるCommandとして使うことが出来ます。もちろんnewで生成することもできます。そのときは、常に実行可能なCommandとしてふるまいます。

CommandのExecuteが呼ばれたときの処理はSubscribeメソッドで指定します。先ほど作成したViewModelのInputプロパティが空じゃないときに、Inputプロパティを空にするCommandを追加したコードは以下のようになります。

using Reactive.Bindings;
using System;
using System.Reactive.Linq;

namespace RxPropEdu
{
    public class MainWindowViewModel
    {
        public ReactiveProperty<string> Input { get; private set; }
        public ReactiveProperty<string> Output { get; private set; }

        public ReactiveCommand ClearCommand { get; private set; }

        public MainWindowViewModel()
        {
            this.Input = new ReactiveProperty<string>(""); // デフォルト値を指定してReactivePropertyを作成
            this.Output = this.Input
                .Delay(TimeSpan.FromSeconds(1)) // 1秒間待機して
                .Select(x => x.ToUpper()) // 大文字に変換して
                .ToReactiveProperty(); // ReactiveProperty化する

            this.ClearCommand = this.Input
                .Select(x => !string.IsNullOrWhiteSpace(x)) // Input.Valueが空じゃないとき
                .ToReactiveCommand(); // 実行可能なCommandを作る
            this.ClearCommand.Subscribe(_ => this.Input.Value = "");
        }
    }
}

XAMLでは、CommandプロパティにBindingすることで使います。

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:RxPropEdu" x:Class="RxPropEdu.MainWindow"
    Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <StackPanel>
        <Label Content="入力" />
        <TextBox Text="{Binding Input.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Label Content="出力" />
        <TextBlock Text="{Binding Output.Value}" />
        <Button Content="CLEAR" Command="{Binding ClearCommand}" />
    </StackPanel>
</Window>

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

TextBoxが空のときはCLEARボタンは押せない。

f:id:okazuki:20150222172131p:plain

何かを入力するとボタンが押せるようになる。

f:id:okazuki:20150222172212p:plain

ボタンを押すとTextBoxがクリアされ、ボタンがまた押せない状態になる。

f:id:okazuki:20150222172315p:plain

通常のC#のクラスからのReactivePropertyの生成

ReactivePropertyは、MVVMパターンでModelとViewModelを繋ぐために、INotifyPropertyChangedを実装したクラスのプロパティをReactiveProperty<T>に変換する機能を提供しています。

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

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace RxPropEdu
{
    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
        {
            if (object.Equals(field, value)) { return; }
            field = value;
            var h = this.PropertyChanged;
            if (h != null) { h(this, new PropertyChangedEventArgs(propertyName)); }
        }

        private string name;

        public string Name
        {
            get { return this.name; }
            set { this.SetProperty(ref this.name, value); }
        }

        private int age;

        public int Age
        {
            get { return this.age; }
            set { this.SetProperty(ref this.age, value); }
        }

    }
}

ModelからViewModelへの単一方向のバインド

ModelのクラスからViewModelへの一方向の値の伝搬機能を持つViewModelをReactivePropertyを使って作成するには、ReactivePropertyで定義されているINotifyPropertyChangedの拡張メソッドのObservePropertyを呼び出してプロパティの変更通知をIObservableに変換してから、ToReactivePropertyでReactivePropertyに変換します。

コード例を以下に示します。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Linq;

namespace RxPropEdu
{
    public class PersonViewModel
    {
        public ReactiveProperty<string> Name { get; private set; }
        public ReactiveProperty<string> Age { get; private set; }

        public PersonViewModel(Person model)
        {
            this.Name = model
                .ObserveProperty(x => x.Name) // Nameプロパティを監視するIObservableに変換
                .ToReactiveProperty(); // ReactivePropertyに変換

            this.Age = model
                .ObserveProperty(x => x.Age) // Ageプロパティを監視するIObservableに変換
                .Select(x => x.ToString()) // LINQで加工して
                .ToReactiveProperty(); // ReactivePropertyに変換1
        }
    }
}

このように宣言的に、ModelからViewModelへ値を伝搬する処理が定義できます。

ModelとViewModelの双方向のバインド

ModelとViewModelのプロパティの双方向の値の伝搬機能を持つViewModelをReactivePropertyを使って作成するには、INotifyPropertyChangedの拡張メソッドであるToReactivePropertyAsSynchronizedメソッドを使用します。引数には、対象のプロパティを選択するラムダ式と、ModelからViewModelへ値を伝搬するときの変換処理のconvert引数と、ViewModelからModelへ値を伝搬するときの変換処理のconvertBackなどがあります。使用例を以下に示します。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;

namespace RxPropEdu
{
    public class PersonViewModel
    {
        public ReactiveProperty<string> Name { get; private set; }
        public ReactiveProperty<string> Age { get; private set; }

        public PersonViewModel(Person model)
        {
            this.Name = model
                .ToReactivePropertyAsSynchronized(x => x.Name); // Nameプロパティと双方向で同期するReactivePropertyを作る

            this.Age = model
                .ToReactivePropertyAsSynchronized(
                    x => x.Age, // Ageプロパティを
                    convert: x => x.ToString(), // M -> VMのときは文字列に変換
                    convertBack: x => int.Parse(x)); // VM -> Mの時にはintに変換
        }
    }
}

ViewModelからModelへの単一方向のバインド

INotifyPropertyChangedを実装していないクラスや、ModelからViewModelへの値の伝搬が必要ないケースではReactivePropertyクラスに定義されているFromObjectメソッドを使うことで、初期値がModelのプロパティの値で、ViewModelからModelへの単一方向のバインドが出来ます。これもToReactivePropertyAsSynchronizedメソッドと同様にconvert引数とconvertBack引数があります。

コード例を以下に示します。

using Reactive.Bindings;

namespace RxPropEdu
{
    public class PersonViewModel
    {
        public ReactiveProperty<string> Name { get; private set; }
        public ReactiveProperty<string> Age { get; private set; }

        public PersonViewModel(Person model)
        {
            this.Name = ReactiveProperty.FromObject(
                model, // もとになるModelを指定して
                x => x.Name); // プロパティを指定する

            this.Age = ReactiveProperty.FromObject(
                model, // もとになるModelを指定して
                x => x.Age, // プロパティを指定する
                convert: x => x.ToString(), // M -> VMの変換処理
                convertBack: x => int.Parse(x)); // VM -> Mの変換処理
        }
    }
}

バリデーション

ReactivePropertyは、入力値の検証機能を提供しています。様々な方法で値を検証することが出来ます。まずは基本となるメソッドから紹介します。

ReactivePropertyに対して、SetValidateNotifyError(Func<IObservable<T>, IObservable<string>> validator)というメソッドが定義されています。これの引数は、ReactivePropertyの入力を表すIObservableをエラーの有無を表すIObservableに変換する処理を渡します。エラーがあるときはエラーを表す文字列を返し、エラーが無いときはnullを返します。

Nameプロパティに空文字を入れるとエラーになる場合の定義を以下に示します。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.Reactive.Linq;

namespace RxPropEdu
{
    public class PersonViewModel
    {
        public ReactiveProperty<string> Name { get; private set; }

        public PersonViewModel(Person model)
        {
            this.Name = model
                .ToReactivePropertyAsSynchronized(x => x.Name)
                .SetValidateNotifyError((IObservable<string> ox) => // 入力値のストリーム
                    Observable.Merge(
                        ox.Where(x => string.IsNullOrWhiteSpace(x)).Select(_ => "Error"), // 空文字のときはエラーメッセージを返す
                        ox.Where(x => !string.IsNullOrWhiteSpace(x)).Select(_ => default(string))) // 空文字以外のときはエラーがないのでnull
                );
        }
    }
}

ReactivePropertyクラスは、INotifyDataErrorInfoを実装しているので、WPFではValidation.ErrorsのErrorContentをバインドすることでエラーの内容を取得することが出来ます。これは、ReactiveProperty固有ではなくWPFの内容なので、以下の記事のWPFの値の検証の箇所を参照してください。

WPF4.5入門 その55 「Binding その1」 - かずきのBlog@hatena

WPF以外のプラットフォームでは、INotifyDataErrorInfoによる検証エラーのサポートがないため、以下に紹介する検証エラーに関するReactivePropertyのプロパティを使用してエラーメッセージをハンドリングします。

  • ObserveErrorChangedプロパティ:バリデーションエラーの内容に変更があったときに値が流れてくるIObservable<IEnumerable>
  • HasErrorsプロパティ:エラーの有無を返すプロパティ
  • ObserveHasErrorsプロパティ:バリデーションエラーの内容に変更があったときにエラーの有無が流れてくるIObservable<bool>

エラーメッセージを取り出すにはObserveErrorChangedでIEnumerableがnullじゃないときに最初のstring型の要素を取り出します。nullの時はエラーが無いときなので、その時はその時で、エラーメッセージを空にする必要があります。コード例を以下に示します。

this.NameError = Observable.Merge(
        this.Name.ObserveErrorChanged.Where(x => x == null).Select(_ => default(string)), // エラーのないときはnull
        this.Name.ObserveErrorChanged.Where(x => x != null).Select(x => x.OfType<string>().FirstOrDefault()) // エラーのあるときは最初のstring
    )
    .ToReactiveProperty();

SetValidateNotifyErrorメソッドにはIObservable以外にも単一の値を受け取り、単一のエラーを返すオーバーライドがあります。これを使うと、リアクティブプログラミングっぽくなくなりますが、素直に書くことができます。

this.Name = model
    .ToReactivePropertyAsSynchronized(x => x.Name)
    .SetValidateNotifyError((string x) =>
    {
        return string.IsNullOrWhiteSpace(x) ? "Error" : null;
    });

また、DataAnnotationsをサポートしているプラットフォームでは、以下のようにDataAnnotationsによる値の検証もサポートしています。サポートしているプラットフォームでは、一番手軽に出来る値の検証なのでおすすめです。

使用するメソッドはSetValidateAttributeメソッドです。コード例を以下に示します。

// 定義部分
[Required(ErrorMessage = "Error!!")]
public ReactiveProperty<string> Name { get; private set; }

// インスタンス化処理
this.Name = new ReactiveProperty<string>()
    .SetValidateAttribute(() => this.Name);

入力値の検証と、ViewModelからModelへの値の書き戻しの連携

ToReactivePropertyAsSynchronizedメソッドとFromObjectメソッドがViewModelからModelへ値を書き戻すときに、ViewModelでの入力値の検証が終わったもののみを通すようにする機能があります。それぞれのメソッドの引数にあるignoreValidationErrorValueをtrueに設定することで有効になります。。

this.Name = model
    .ToReactivePropertyAsSynchronized(
        x => x.Name,
        ignoreValidationErrorValue: true) // 検証エラーのある値はModelに渡さない
    .SetValidateNotifyError((string x) => string.IsNullOrWhiteSpace(x) ? "Error" : null);

これでModelの状態をクリーンに保つことが出来るようになります。

Viewのイベント引数をReactivePropertyにバインドするEventToReactiveProperty

ReactivePropertyは、Viewで発生したイベントをViewModelのReactivePropertyに渡す機能を提供しています。この機能を使うと、マウス系のイベントからマウスの座標を抜き出して、VMのプロパティにセットするということが可能になります。

EventToReactivePropertyは、イベント引数の値を任意の値に変換するReactiveConverter&ltT, U>を使用します。ReactiveConverterのOnConvertメソッドでイベント引数のIObservableから、変換結果のIObservableにする処理を記述します。

例えば、MouseEventArgsからPointへ変換する処理は以下のように記述します。

using Reactive.Bindings.Interactivity;
using System;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Input;

namespace RxPropEdu
{
    public class MouseEventToPoiintConverter : ReactiveConverter<MouseEventArgs, Point>
    {
        protected override IObservable<Point> OnConvert(IObservable<MouseEventArgs> source)
        {
            return source
                .Select(x => x.GetPosition((IInputElement)this.AssociateObject));
        }
    }
}

XAMLでは、EventTriggerの下に、EventToReactivePropertyを設定します。EventToReactivePropertyのReactivePropertyプロパティに、Converterによる変換結果を格納するReactivePropertyをバインドして、EventToReactivePropertyのタグの下に、Converterを設定します。

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:RxPropEdu" 
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
    xmlns:Interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.NET45" 
    x:Class="RxPropEdu.MainWindow"
    Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Border Padding="50">
        <Border Background="Pink">

            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseLeftButtonDown">
                    <Interactivity:EventToReactiveProperty ReactiveProperty="{Binding MousePoint}">
                        <local:MouseEventToPoiintConverter/>
                    </Interactivity:EventToReactiveProperty>
                </i:EventTrigger>
            </i:Interaction.Triggers>
            <TextBlock Text="{Binding Message.Value}" />
        </Border>
    </Border>
</Window>

この例を実行するとマウスのボタンを押した地点の座標が画面に表示されます。

f:id:okazuki:20150222203551p:plain

Viewのイベント引数をReactiveCommandに渡すEventToReactiveCommand

EventToReactivePropertyが、イベント引数の結果をReactivePropertyに設定します。それのReactiveCommand版がEventToReactiveCommandになります。使用方法は、EventToReactivePropertyと同じです。

以下のようなCommandとReactivePropertyを持ったViewModelがあるとします。

using Reactive.Bindings;
using System.Reactive.Linq;
using System.Windows;

namespace RxPropEdu
{
    public class MainWindowViewModel
    {
        public ReactiveCommand<Point> MouseDownCommand { get; private set; }

        public ReactiveProperty<string> Message { get; private set; }

        public MainWindowViewModel()
        {
            this.MouseDownCommand = new ReactiveCommand<Point>();
            this.Message = this.MousePoint
                .Select(x => string.Format("({0}, {1})", x.X, x.Y))
                .ToReactiveProperty();
        }
    }
}

XAMLを以下に示します。Converterは先ほどのEventToReactivePropertyと同様のものを使っています。EventToReactivePropertyとの差分は、EventToReactiveCommandのCommandプロパティにViewModelのCommandをバインドしている点です。

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:RxPropEdu" 
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
    xmlns:Interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.NET45" 
    x:Class="RxPropEdu.MainWindow"
    Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Border Padding="50">
        <Border Background="Pink">

            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseLeftButtonDown">
                    <Interactivity:EventToReactiveCommand Command="{Binding MouseDownCommand, Mode=OneWay}" >
                        <local:MouseEventToPoiintConverter/>
                    </Interactivity:EventToReactiveCommand>
                </i:EventTrigger>
            </i:Interaction.Triggers>
            <TextBlock Text="{Binding Message.Value}" />
        </Border>
    </Border>
</Window>

EventToReactivePropertyとEventToReactiveCommandでは、ConverterでWhereメソッドなどを使って、ViewからViewModelに渡す処理をフィルタリングしたり、Bufferを使ってバッファリングしたりThrottleを使って連続的に発生するイベントを間引いたりなどView側でViewModelに流す値を加工することが出来る点が特徴になります。

ReactivePropertyのコレクション ReadOnlyReactiveCollection

MVVMパターンでは、ModelのコレクションからViewModelのコレクションへ変換するというケースが多々あります。そのようなケースに対応するためModelのObservableCollectionからViewModel用のReadOnlyReactiveCollectionに変換する処理を提供しています。

PersonのObservableCollectionを持つModelからReadOnlyReactiveCollectionを生成するコード例を以下に示します。

public ReadOnlyReactiveCollection<PersonViewModel> People { get; private set; }

// 構築処理
this.People = model.People
    .ToReadOnlyReactiveCollection(x => new PeopleViewModel(x));

追加や更新削除といったModelでのコレクションの変更に追随してくれるので便利です。さらに、ReadOnlyReactiveCollectionから要素が削除されるタイミングで、コレクションの中身がIDisposableな場合はDisposeの呼び出しを行います。

ReadOnlyReactiveCollectionでは、ObservableCollectionから生成する以外に、細やかに登録・更新・削除を制御することができます。

追加・更新・削除・リセットを制御するにはReactivePropertyのCollectionChanged>T<型のIObservableからToReadOnlyReactiveCollectionメソッドで作成します。

CollectionChanged<T&gt型には、staticメソッドでAdd, Remove, Replaceが定義されていて、static readonlyなフィールドでResetという値が定義されています。これらの値を流すIObservableを作成することで、柔軟にコレクションの変更が出来るようになります。例えば、ボタンが押された時間を表す文字列を記録するアプリを考えます。以下のように4つのコマンドと選択項目を表すプロパティ、そして、記録を残すコレクションをプロパティに持ち、これらを組み合わせて登録更新削除などを行います。

public class MainViewModel
{
    public ReactiveProperty<string> SelectedItem { get; private set; }
    public ReactiveCommand AddCommand { get; private set; }
    public ReactiveCommand ResetCommand { get; private set; }
    public ReactiveCommand UpdateCommand { get; private set; }
    public ReactiveCommand DeleteCommand { get; private set; }

    public ReadOnlyReactiveCollection<string> TimestampLog { get; private set; }

    public MainViewModel()
    {
        this.SelectedItem = new ReactiveProperty<string>();

        this.AddCommand = new ReactiveCommand();
        this.ResetCommand = new ReactiveCommand();
        this.UpdateCommand = this.SelectedItem.Select(v => v != null).ToReactiveCommand();
        this.DeleteCommand = this.SelectedItem.Select(v => v != null).ToReactiveCommand();

        this.TimestampLog = Observable.Merge(
            this.AddCommand
                .Select(_ => CollectionChanged<string>.Add(0, DateTime.Now.ToString())),
            this.ResetCommand.Select(_ => CollectionChanged<string>.Reset),
            this.UpdateCommand
                .Select(_ => this.SelectedItem.Value)
                .Select(v => CollectionChanged<string>.Replace(this.TimestampLog.IndexOf(v), DateTime.Now.ToString())),
            this.DeleteCommand
                .Select(_ => this.SelectedItem.Value)
                .Select(v => CollectionChanged<string>.Remove(this.TimestampLog.IndexOf(v))))
            .ToReadOnlyReactiveCollection();
    }
}

MergeメソッドでAddCommand、ResetCommand、UpdateCommand、DeleteCommandを1本のIObservable<CollectionChanged<T>>にまとめてからコレクションに変換しています。

その他ユーテリティクラス

ReactivePropertyには、そのほかに、便利な小物系クラスがいくつか定義されています。

Notifier系クラス

BooleanNotifierクラスは、IObservable<bool>として扱うことのできる、bool型の値を切り替える機能を持ったクラスです。以下のような動きをします。

var n = new BooleanNotifier();
n.Subscribe(x => Console.WriteLine(x));
n.SwitchValue(); // True
n.SwitchValue(); // False

IObservable<bool>型なのでReactiveCommandに変換して使うことも出来ます。

CountNotifierクラスは、カウンター機能を持ったIObservable<CountChangedStatus>クラスです。CountChangedStatusは、以下のような値をもったenum型です。

public enum CountChangedStatus
{
        Increment = 0,
        Decrement = 1,
        Empty = 2,
        Max = 3,
}

CountNotifierクラスは、インスタンス作成時に最大値を指定(指定しない場合はint.MaxValueになります)してIncrementメソッドとDecrementメソッドを使って値を加算・減算していきます。IncrementやDecrementが呼び出されたタイミングでCountChangedStatusを発行します。IncrementやDecrementメソッドの戻り値はIDisposable型でDisposeを呼び出すと、IncrementやDecrementで行った計算の逆の計算を行います。

var n = new CountNotifier(10);
n.Subscribe(x => Console.WriteLine(x));

var d = n.Increment();
Console.WriteLine(n.Count); // 1
d.Dispose();
Console.WriteLine(n.Count); // 0

n.Increment(10);
Console.WriteLine(n.Count); // 10
n.Decrement(5);
Console.WriteLine(n.Count); // 5

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

Increment
1
Decrement
Empty
0
Increment
Max
10
Decrement
5

ScheduledNotifierクラスは、指定した時間に値を発行する機能を持ったクラスです。以下のような動きをします。

var n = new ScheduledNotifier<int>();
n.Subscribe(x => Console.WriteLine(DateTime.Now + " > " + x));
n.Report(10);
n.Report(100, TimeSpan.FromSeconds(10));

実行結果は以下の通りです。

2015/02/22 21:11:23 > 10
2015/02/22 21:11:33 > 100

ReactiveTimer

ReactiveTimerは停止と再開が可能なTimerクラスです。StartメソッドとStopメソッドで開始、停止を行い、タイマーの発火はSubscribeで受信できます。Subscribeには何回目のタイマーの発火かを表す数字がわたってきます。

以下のようなコードで動作を確認できます。

var timer = new ReactiveTimer(TimeSpan.FromSeconds(1)); // 1秒スパン
timer.Subscribe(x => Console.WriteLine(x));

timer.Start();

Console.ReadKey();

timer.Stop();

Console.ReadKey();

timer.Start();

Console.ReadKey();

IObservableの拡張メソッド

全ての最後の値がTrueの時にTrueを流すCombineLatestValuesAreAllTrueメソッドと、その逆のCombineLatestValuesAreAllFalseメソッドがあります。このメソッドは、複数のバリデーションの結果がすべてTrueだった場合に有効にするCommandを作ったりする際にとても有用です。

ToUnitメソッドは、全てのIObservableをIObservable<Unit>に変換します。

ObserveOnUIDispatcherメソッドは処理をUIスレッド上で行うようにします。

Pairwiseメソッドは、Zip(Skip(1))のショートカットになります。

ReactivePropertyのシリアライズ

Json.NETを使ってJSON形式でシリアライズ、デシリアライズ可能です。ViewModelの値を一時的に保管するケースではJson.NETを使ってください。

まとめ

ReactivePropertyは、IObservable<T>からReactiveProperty>T<を作成するという非常にシンプルな機能をコアに様々なものをIObservableとして捉えたライブラリです。全てのものがIObservableで繋がることで、以下の図のようにMVVMの全てのレイヤがシームレスに繋がります。

f:id:okazuki:20150222212604p:plain

また、IObservableとして扱えるため当然ですがReactive Extensionsの強力な各種メソッドを使うことが出来ます。Reactive Extensionsについては以下のSlideShareから、PDFをダウンロードしてメソッド等を確認してみてください。

今更ですがRx再入門の記事をPDF化したやつをSlideShareに入れました - かずきのBlog@hatena

以上、簡単にですがReactivePropertyのv2系の紹介でした。