152
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

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

前に書いた記事が 2015 年のものだったので 2020 年用に書き直していこうと思います。ReactiveProperty 7.1.0 を想定して書いています。また、WPF か UWP か Xamarin.Forms での MVVM 開発の基本的な知識と Reactive Extensions の基本的な知識があることを前提に記載しています。

他の記事はこちらです。

ReactiveProperty とは

ReactiveProperty は Cysharp の @neuecc さんが開発したライブラリになります。
現在も、UniRx や UniTask などでの成果をたまにバックポートするプルリクエストをくれます。多謝。

GitHub でソースコードは公開されています。

WPF や UWP や Xamarin.Forms などの XAML を使用するクライアント サイドのアプリケーション開発でよく採用される MVVM パターンをサポートする機能やリアクティブ プログラミングとシームレスに繋ぐ機能が含まれています。主に XAML 系プラットフォームで MVVM を採用するときに省力化のために採用されることが多い ReactiveProperty ですが、特に MVVM の何かに依存しているクラスライブラリではないので、利用しようと思えば何処でも利用可能なライブラリになります。

コア機能のターゲットフレームワークは .NET Standard 2.0 で、その他に WPF や UWP などのプラットフォーム固有機能は、それぞれのプラットフォーム向けのパッケージが提供されています。

提供している NuGet パッケージ

Package Id Version and downloads Description
ReactiveProperty ReactiveProperty の基本的な機能が入ったパッケージ。基本的にはこのパッケージを参照する。
ReactiveProperty.Core Reactive Extensions に依存していない ReactivePropertySlim や ReadOnlyReactivePropertySlim が含まれます。Rx は使わないけど INotifyPropertyChanged の実装の省力化のために ReactiveProperty を使いたいケースではこちらを参照してください。
ReactiveProperty.WPF EventToReactiveProperty や EventToReactiveCommand が含まれます。.NET Framework 4.6.1 や .NET Core 3.0 以降で利用可能です。
ReactiveProperty.UWP UWP 用のEventToReactiveProperty や EventToReactiveCommand が含まれます。
ReactiveProperty.XamarinAndroid Xamarin Android 向け(Forms 向けではないです)の各種拡張メソッドが含まれています。
ReactiveProperty.XamariniOS Xamarin iOS 向け(Forms 向けではないです)の各種拡張メソッドが含まれています。

参照すべきパッケージ

  • WPF の開発
    • ReactiveProperty.WPF (EventToReactiveCommand, EventToReactiveProperty を使用する場合)
    • ReactiveProperty (EventToReactiveCommand, EventToReactiveProperty が必要ない場合)
  • UWP の開発
    • ReactiveProperty.UWP (EventToReactiveCommand, EventToReactiveProperty を使用する場合)
    • ReactiveProperty (EventToReactiveCommand, EventToReactiveProperty が必要ない場合)
  • Xamarin.Forms の開発
    • ReactiveProperty
  • Xamarin Android の開発
    • ReactiveProperty.XamarinAndroid
  • Xamarin iOS の開発
    • ReactiveProperty.XamariniOS

基本機能

ReactiveProperty の基本機能は ReactiveProperty クラスと ReactivePropertySlim クラスになります。ReactiveProperty クラスと ReactivePropertySlim クラスは INotifyPropertyChanged インターフェースと IObservable インターフェースを実装した Value プロパティを持つクラスです。
以下のように使用できます。

var rp = new ReactivePropertySlim<string>("初期値");
Console.WriteLine(rp.Value); // 初期値
rp.Value = "別の値を設定";
Console.WriteLine(rp.Value); // 別の値を設定

INotifyPropertyChanged インターフェースを持っているので PropertyChanged イベントを購読することで値に変更があったときに処理が行えますが、これは WPF や UWP などのデータバインディングで使用するためのものなので、値に変更があったときの処理は Reactive Extensions のメソッドを使用して行います。

Subscribe メソッドを使うことで、簡単に値に変更があったときの処理が行えます。

var rp = new ReactivePropertySlim<string>("初期値");
rp.Subscribe(x => Console.WriteLine($"Subscribe: {x}")); // Subscribe: 初期値
rp.Value = "別の値"; // Subscribe: 別の値

MVVM パターンのアプリケーションでは、プロパティに変更があったことを INotifyPropertyChanged インターフェースを実装して PropertyChanged イベントを発行することで View に伝えます。そのため、ViewModel クラスに INotifyPropertyChanged を実装してプロパティに変更通知の機能を実装します。
この変更通知のコードは以下のように同じようなコードの実装の繰り返しなので非常に冗長になります。

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

基本クラスの導入などで若干省力化は出来ますが、書いていてあまり楽しいコードではありません。ReactiveProperty を使うとプロパティの定義は以下のようになります。この省力化が出来ることが ReactiveProperty の導入の 1 つの動機になります。

public ReactivePropertySlim<string> Name { get; } = new ReactivePropertySlim<string>();

XAML 側では以下のようにバインドします。上が普通の書き方で、下が ReactiveProperty の場合の書き方になります。

<!-- 普通のプロパティへのバインド -->
<TextBlock Text="{Binding Name}" />
<!-- ReactiveProperty へのバインド -->
<TextBlock Text="{Binding Name.Value}" />

ReactiveProperty を使った場合は実際の値を持っているのは ReactiveProperty の Value プロパティなので .Value が Binding のパスに必要になります。

Slim と無印の違い

ReactiveProperty には ReactiveProperty クラスと ReactivePropertySlim クラスの 2 種類のクラスがあります。この後紹介する読み取り専用の ReadOnlyReactiveProperty も Slim と無印があります。

原則としては Slim がついている方で良いケースではなるべく Slim を使うことを想定しています。Slim のほうが機能が少ないですが Slim のほうが性能が桁違いに良いです。

Slim との機能面での差異は後程説明します。

Reactive Extensions との統合

ReactiveProperty には ReadOnlyReactiveProperty や ReadOnlyReactivePropertySlim というクラスがあり、これは IObservable から作成できます。読み取り専用のプロパティとして動作する点が特徴です。
例えば 1 秒ごとに 1, 2, 3, 4, ... のようにカウントアップしていく IObservable<int> の変数 ox に対して ToReadOnlyReactivePropertyToReadOnlyReactivePropertySlim を呼び出すことで作成できます。この場合 IObservable<int> から新しい値が発行されると Value プロパティの値が更新される ReadOnlyReactivePropertyReadOnlyReactivePropertySlim が作成されます。

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

var subject = new Subject<int>(); // IObservable<int> の実装
var rp = subject.ToReadOnlyReactivePropertySlim(0); // 初期値 0 で作成
Console.WriteLine(rp.Value); // 0

// 100 を発行
subject.OnNext(100);
Console.WriteLine(rp.Value); // Value の値が 100 になる

// 9999 を発行
subject.OnNext(9999);
Console.WriteLine(rp.Value); // Value の値が 9999 になる

ReactiveProperty や Reactive Extensions を使うと様々なものが IObservable として扱えます。つまり、様々なものを ReadOnlyReactiveProperty/ReadOnlyReactivePropertySlim にすることが出来ます。

ReactiveProperty は IObservable

ReactiveProperty や ReactivePropertySlim や ReadOnlyReactiveProperty や ReadOnlyReactivePropertySlim は全て IObservable として扱えます。そのため以下のように Reactive Extensions の Select などのメソッドを使って加工した ReactiveProperty の値を ReadOnlyReactiveProperty に変換できます。

例えば文字を大文字に変換して ReadOnlyReactiveProperty に変換する処理は以下のようになります。

var rp = new ReactivePropertySlim<string>("okazuki"); // 初期値 okazuki
var rrp = rp.Select(x => x.ToUpper()).ToReadOnlyReactivePropertySlim(); // 大文字にして ReadOnlyReactiveProperty に変換

Console.WriteLine($"rp.Value = {rp.Value}, rrp.Value = {rrp.Value}"); // rp.Value = okazuki, rrp.Value = OKAZUKI

// 値を更新
rp.Value = "xin9le";
Console.WriteLine($"rp.Value = {rp.Value}, rrp.Value = {rrp.Value}"); // rp.Value = xin9le, rrp.Value = XIN9LE

イベントのようにアプリが動いている間に、任意のタイミングで何かが起きるようなケースのものも IObservable と扱うことが出来ます。例えば INotifyPropertyChanged インターフェースの PropertyChanged イベントを IObservable として扱うための ObserveProperty 拡張メソッドが Reactive.Bindings.Etensions 名前空間に定義されています。ObserveProperty でラムダ式でプロパティを指定すると、そのプロパティの変更があるたびに値が発行される IObservable になります。それに対して ToReadOnlyReactiveProperty を呼ぶことで ReadOnlyReactiveProperty が生成されます。

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

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace ConsoleApp6
{
    class Program
    {
        static void Main(string[] args)
        {
            var p = new Person();

            // Name プロパティを IObservable にして ReadOnlyReactiveProperty に変換
            var name = p.ObserveProperty(x => x.Name)
                .ToReadOnlyReactivePropertySlim();
            Console.WriteLine($"name.Value = ${name.Value}"); // 空文字

            // 元の値が変わると ReadOnlyReactiveProperty の Value も更新される
            p.Name = "okazuki";
            Console.WriteLine($"name.Value = ${name.Value}"); // okazuki
        }
    }

    // INotifyPropertyChanged の実装クラス
    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

ReactiveProperty を使う上での一つの勘所として、IObservable に変換できるものを見極めて ReadOnlyReactiveProperty に変換するというのがあります。

UI スレッドへのイベントの自動ディスパッチ (Slim には無い機能)

ReactiveProperty と ReadOnlyReactiveProperty は、Value プロパティが変わったときの PropertyChanged イベントを自動で UI スレッドで発行する機能があります。
WPF 以外の UWP や Xamarin.Forms では、UI スレッド以外で PropertyChanged イベントを発行するとアプリケーションがクラッシュする可能性があるので自動で UI スレッドでイベントが発行してくれる機能は便利な側面があります。

ただし以下のようなデメリットがあります。

  • UI スレッドでイベントを発行するためにスレッドの切り替えが行われるので Value が変化しても直後に PropertyChanged イベントは起きない
    • Value を書き換えた直後に PropertyChanged で行ってる処理が完了していることを想定しているコードはうまく動かない
  • UI スレッドが複数あるケースでは破綻する
    • WPF や UWP では複数の UI スレッドがあるケースがあるので、この場合 UI スレッドが 1 つしかない想定の ReactiveProperty を使うとエラーが頻発するので使用しないでください。必ず Slim を使うか、後述する回避策を使用してください

ReactiveProperty のスレッド切り替えは ReactivePropertyScheduler.SetDefault メソッドで指定が可能です。指定しない場合は自動的にアプリケーションの UI スレッドに切り替えるスケジューラーが指定されます。デフォルトの挙動は以下のようになります。

WPF アプリケーションのボタンクリックイベントで以下のようなコードを書くとデバッグコンソールに以下のように表示されます。

クリックイベント
var latestValue = "";
var rp = new ReactiveProperty<string>();
rp.PropertyChanged += (s, e) => latestValue = rp.Value;
rp.Value = "okazuki";
Debug.WriteLine($"{rp.Value}, {latestValue}");
rp.Value = "xin9le";
Debug.WriteLine($"{rp.Value}, {latestValue}");
デバッグコンソール
okazuki, 
xin9le, 

この挙動を変えるにはアプリケーションの起動時の処理で ReactiveProperty が使用するスケジューラーを置き換えます。例えば WPF では App クラスの Startup イベントで置き換えが出来ます。以下のコードは即座に実行する ImmediateScheduler を設定しています。

App.xaml.cs
public partial class App : Application
{
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        ReactivePropertyScheduler.SetDefault(ImmediateScheduler.Instance);
    }
}

この変更をした後に実行すると以下のような内容がデバッグコンソールに表示されます。

デバッグコンソール
okazuki, okazuki
xin9le, xin9le

アプリケーションが単一の UI スレッドであることが保証されている場合は便利な機能ですが、挙動などを把握していないと意図したとおりに動かないと感じる原因になるので注意してください。

入力値のバリデーション (Slim には無い機能)

ReactiveProperty には、入力された値のバリデーションを行う機能があります。ReactiveProperty の SetValidateNotifyError メソッドで入力値のチェック処理が書けます。エラーがある場合はエラーメッセージを返して、エラーがない場合は null を返します。

例えば空文字の入力を許可しない ReactiveProperty は以下のようになります。

MainWindowViewModel.cs
using Reactive.Bindings;
using System.ComponentModel;

namespace WpfApp1
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ReactiveProperty<string> Name { get; }

        public MainWindowViewModel()
        {
            Name = new ReactiveProperty<string>()
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "空文字はダメ" : null);
        }
    }
}

これを WPF の Window の DataContext に設定して TextBox とバインドします。

MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel Margin="10">
        <TextBox Text="{Binding Name.Value, UpdateSourceTrigger=PropertyChanged}"
                 ToolTip="{Binding Path=(Validation.Errors)/ErrorContent, RelativeSource={RelativeSource Self}}"/>
    </StackPanel>
</Window>

そうすると、以下のようにエラー時には赤枠が表示されて ToolTip でエラーメッセージが表示されます。

image.png

ウィンドウの初期表示の段階でバリデーションエラーが出ていると見た目よろしくないので初回のバリデーションエラーをスキップする機能も提供しています。
ReactiveProperty のコンストラクタや ToReactiveProperty の mode で ReactivePropertyMode で指定可能です。列挙型で | 演算子で複数指定できます。デフォルト値は ReactivePropertyMode.Default なので、それに加えて ReactivePropertyMode.IgnoreInitialValidationError を追加します。以下のようなコードになります。

MainWindowViewModel.cs
using Reactive.Bindings;
using System.ComponentModel;

namespace WpfApp1
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ReactiveProperty<string> Name { get; }

        public MainWindowViewModel()
        {
            Name = new ReactiveProperty<string>(mode: ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError)
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "空文字はダメ" : null);
        }
    }
}

初回実行時には、バリデーションエラーが起きません。

image.png

何かを入力して、再度空文字にするとエラーが表示されます。

image.png

また、バリデーションは DataAnnotations で指定可能です。DataAnnotations には必須入力を表す Required や特定の値の範囲にあることをチェックする Range や正規表現にマッチするかどうかをチェックする RegulerExpression など様々な属性が提供されています。
必要があれば独自のチェックロジックの属性を作成可能です。

ReactiveProperty に対して属性をつけて SetValidateAttribute メソッドを使ってラムダ式で対象の属性がついたプロパティを指定することでバリデーションが有効になります。

public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    // 属性を指定する
    [Required(ErrorMessage = "名前は必須入力項目です")]
    public ReactiveProperty<string> Name { get; }

    public MainWindowViewModel()
    {
        // SetValidateAttribute メソッドで属性によるチェックを行うことを指定する
        Name = new ReactiveProperty<string>()
            .SetValidateAttribute(() => Name);
    }
}

バリデーションエラーのメッセージは、ObserveValidationErrorMessage メソッドで IObservable<string> として取得できます。そのため以下のようなコードを書くことでエラーメッセージをハンドリングできます。

using Reactive.Bindings;
using System;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            var rp = new ReactiveProperty<string>()
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "必須だよ" : null);
            rp.ObserveValidationErrorMessage()
                .Subscribe(x => Console.WriteLine($"エラーメッセージが変わったよ {x}"));

            rp.Value = "okazuki";
            rp.Value = null;
        }
    }
}

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

エラーメッセージが変わったよ 必須だよ
エラーメッセージが変わったよ
エラーメッセージが変わったよ 必須だよ

最初のほうで ReactiveProperty は IObservable から作れるという説明をしました。なので、ObserveValidationErrorMessage の結果に対して ToReadOnlyReactivePropertySlim などを呼び出すとエラーメッセージを保持する ReadOnlyReactivePropertySlim などを作ることができます。
これを使って好きな場所にエラーメッセージを表示することができます。

ReactiveProperty のモード

バリデーションでも登場しましたが、ReactiveProperty, ReactivePropertySlim, ReadOnlyReactiveProperty, ReadOnlyReactivePropertySlim の生成時に mode 引数で挙動を若干カスタマイズ可能です。

mode は ReactivePropertyMode 列挙型で以下の値を指定できます。

  • ReactivePropertyMode.DistinctUntilChanged
    • Value に同じ値が設定された場合 OnNext や PropertyChanged を発行しない
  • ReactivePropertyMode.RaiseLatestValueOnSubscribe
    • Subscribe をしたときに現在の値で OnNext を呼び出す
  • ReactivePropertyMode.Defult
    • 何も指定しないときのデフォルト値。DistinctUntilChanged と RaiseLatestValueOnSubscribe を指定したのと同じ。
  • ReactivePropertyMode.IgnoreInitialValidationError
    • 最初のバリデーションエラーを無視する
  • ReactivePropertyMode.None
    • 特に何も指定しない

上記の値は | 演算子で区切って指定可能です。

C# のクラスとの接続 (Slim にはない機能)

ReactiveProperty は、普通の C# のクラスのプロパティをもとにして作ることが出来ます。

双方向同期

C# のクラスが INotifyPropertyChanged インターフェースを実装している場合は ToReactivePropertyAsSynchronized メソッドを使って特定のプロパティと同期した ReactiveProperty が生成されます。

using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = person.ToReactivePropertyAsSynchronized(x => x.Name);

            // ReactiveProperty を更新すると
            rp.Value = "okazuki";
            // 生成元のオブジェクトのプロパティも更新されます
            Console.WriteLine(person.Name); // okazuki

            // 生成元のオブジェクトのプロパティが更新されると
            person.Name = "xin9le";
            // ReactiveProperty の値も更新されます
            Console.WriteLine(rp.Value); // xin9le
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

ToReactivePropertyAsSynchronized メソッドには、プロパティの値を変換するロジックを含めることが出来ます。

using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = person.ToReactivePropertyAsSynchronized(x => x.Name,
                x => x?.ToUpper(), // Person クラスから ReactiveProperty に行く途中で大文字に変換する
                x => x?.ToLower()); // ReactiveProperty から Person クラスに行く途中で小文字に変換する

            // ReactiveProperty を更新すると
            rp.Value = "OKAZUKI";
            // 生成元のオブジェクトのプロパティは小文字に変換された値が設定されます
            Console.WriteLine(person.Name); // okazuki

            // 生成元のオブジェクトのプロパティが更新されると
            person.Name = "xin9le";
            // ReactiveProperty の値も更新されて、大文字に変換されます
            Console.WriteLine(rp.Value); // XIN9LE
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

この変換処理は単純なラムダ式による変換だけでなく、IObservable 型を受け取る変換ロジックを使うことで変換処理の他に任意の条件でのフィルターや、別の IObservable との合成などが可能になります。

以下の例は特定の値が来た場合に ReactiveProperty から Person への反映を行わないようにした例になります。

using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Security.Cryptography.X509Certificates;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = person.ToReactivePropertyAsSynchronized(x => x.Name,
                (IObservable<string> ox) => ox.Select(x => x?.ToUpper()), // Person クラスから ReactiveProperty に行く途中で大文字に変換する
                // ReactiveProperty から Person クラスに行く途中で小文字に変換するが xin9le の場合は Person クラスに戻さない
                (IObservable<string> ox) => ox.Where(x => x != "xin9le").Select(x => x?.ToLower()));

            // ReactiveProperty を更新すると
            rp.Value = "OKAZUKI";
            // 生成元のオブジェクトのプロパティは小文字に変換された値が設定されます
            Console.WriteLine(person.Name); // okazuki

            // 生成元のオブジェクトのプロパティが更新されると
            rp.Value = "xin9le";
            // xin9le は Where でフィルタリングされるので Person 側には表示されない
            Console.WriteLine(person.Name); // okazuki のまま
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

ToReactivePropertyAsSynchronized はバリデーションにも対応しています。デフォルトではバリデーションエラーがあっても元になったオブジェクトのプロパティに値を書き戻しますが、ignoreValidationErrorValue 引数に true を設定することでバリデーションエラーの値を自動的に元になったオブジェクトに書き戻さなくなります。

以下の例は空文字の場合は、元になったオブジェクトに書き戻さないようにした例です。

using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = person.ToReactivePropertyAsSynchronized(x => x.Name,
                ignoreValidationErrorValue: true)
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "空" : null);

            // ReactiveProperty を更新すると
            rp.Value = "okazuki";
            // 生成元のオブジェクトのプロパティが更新されます
            Console.WriteLine(person.Name); // okazuki

            // 空文字の場合はバリデーションエラーになるので
            rp.Value = "";
            // Person クラスの値は更新されません
            Console.WriteLine(person.Name); // okazuki のまま
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

単一方向同期

INotifyPropertyChanged を実装していないクラスには ReactiveProperty.FromObject(...) メソッドを使って ReactiveProperty から元になったオブジェクトへの書き戻しのみに対応した ReactiveProperty が作成できます。
元になったオブジェクトから ReactiveProperty への値の反映は ReactiveProperty を作成した時に一度だけ行われます。

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

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = ReactiveProperty.FromObject(person, 
                x => x.Name,
                ignoreValidationErrorValue: true)
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "空" : null);

            // ReactiveProperty を更新すると
            rp.Value = "okazuki";
            // 生成元のオブジェクトのプロパティが更新されます
            Console.WriteLine(person.Name); // okazuki

            // 空文字の場合はバリデーションエラーになるので
            rp.Value = "";
            // Person クラスの値は更新されません
            Console.WriteLine(person.Name); // okazuki のまま

            // ReactiveProperty.FromObject で作ったのでプロパティを更新しても
            rp.Value = "okazuki";
            person.Name = "xin9le";
            Console.WriteLine(rp.Value); // okazuki のまま
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

INotifyPropertyChanged を実装したクラスのプロパティの値を読み取り専用で ReactiveProperty にしたい場合は既に紹介した ObserveProperty メソッドから ToReadOnlyReactiveProperty/ToReadOnlyReactivePropertySlim を呼び出して ReadOnlyReactiveProperty/ReadOnlyReactivePropertySlim に変換してください。この方法は Slim がつく ReactiveProperty も対応しています。

前編まとめ

ということで、前編では ReactiveProperty というライブラリの名前にもなっている ReactiveProperty 系のクラスのよく使うであろう機能を紹介しました。
この後おそらく中編・後編の3本仕立てで残りの ReactiveProperty の機能を解説していきたいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
152
Help us understand the problem. What are the problem?