👌

ReactiveProperty v9.0.0 をリリースしました

2023/02/12に公開

ReactiveProperty v9.0.0 をリリースしました。
NuGet から導入できます。

https://www.nuget.org/packages/ReactiveProperty/9.0.0

https://www.nuget.org/packages/ReactiveProperty.Core/9.0.0

更新概要

今回のアップデートはパフォーマンスの向上と System.Reactive パッケージに依存していない ReactiveProperty.Core パッケージでの開発出来るカバー範囲の拡大を行いました。
今までよりも ReactiveProperty パッケージではなく ReactiveProperty.Core パッケージだけで開発が出来るようになるケースが多くなると思います。

更新内容一覧

ReactiveProperty から ReactiveProperty.Core へ移動させたクラスやメソッド

  • AsyncReactiveCommand<T> を移動
  • ObserveProperty 拡張メソッドを移動
  • ToReactivePropertySlimAsSynchronized 拡張メソッドを移動
  • Notifier 系クラスの BooleanNotifier, BusyNotifier, CountNotifier, MessageBroker, AsyncMessageBroker を移動
    • これらのクラスは、今まではスレッドセーフでしたがスレッドセーフではなくなりました。どうしても従来の動作と同じにしたい場合は ReactiveProperty パッケージの中にある、クラス名の後ろに Legacy を追加されたクラスがあるので、そちらを利用してください。まぎらわしいので、インテリセンスに出さないように設定していますが、今後出してほしいという要望があればインテリセンスに出すようにもしようと思います。

ReactiveProperty.Core へ追加したクラスやメソッド

  • ReactiveCommandSlim<T> を追加
  • CompositeDisposableReactive.Bindings.Disposables 名前空間 (新設) に追加
  • Reactive.Bindings.TinyLinq 名前空間を追加して以下の IObservable<T> などへの拡張メソッドを追加
    • Select, Where, CombineLatest (CombineLatest だけ IEnumerable<IObservable<T>> への拡張メソッド)
  • ValidatableReactiveProperty<T> を追加

ReactiveProperty へ追加したメソッド

  • IReadOnlyCollection<T> のすべての要素のプロパティの変更を監視するための CollectionUtilities.ObserveElementProperty メソッドと ObserveElementObservableProperty メソッドを追加しました。

更新内容詳細

ReactiveCommandSlim

ReactiveCommand の軽量版になります。大きな違いは従来の ReactiveCommand との違いは CanExecuteChanged イベントを UI スレッド上にディスパッチをしなくなった点です。もし、UI スレッド上でイベントを必ず発生させる必要がある場合は ReactiveCommandSlim のソースになった IObservable<bool> 側で ObserveOn メソッドを使って明示的に設定をしてください。

さらに ReactivePropertySlim と同じ形で実装することで各種パフォーマンスが大きく改善しています。ベンチマークは以下のようになります。

Method Mean Error StdDev Median
CreateReactiveCommand 291.931 ns 5.6965 ns 10.5589 ns 291.178 ns
CreateReactiveCommandSlim 4.313 ns 0.1293 ns 0.1080 ns 4.269 ns
BasicUsecaseForReactiveCommand 1,187.294 ns 22.8930 ns 21.4141 ns 1,179.896 ns
BasicUsecaseForReactiveCommandSlim 91.861 ns 1.8934 ns 3.5096 ns 91.750 ns

上から順に ReactiveCommandnew したもの、ReactiveCommandSlimnew したもの、ReactiveCommand の基本的な機能を一通り使ったケース、ReactiveCommandSlim の基本的な機能を一通り使ったケースになります。インスタンス化のケースで 70 倍以上、基本的な機能を使用したケースでも 13 倍の性能差があります。

また AsyncReactiveCommand と同じように IReactiveProperty<bool> を複数コマンドで共有することで実行可否状態を複数コマンド間で簡単に共有出来るようになっています。

// CanExecute のソースになる IObservable<bool>
IObservable<bool> someCanExecuteSource = ...;
// これを共有することで CanExecute の状態を共有可能にする
ReactivePropertySlim<bool> sharedStatus = new();

// command1 と command2 で CanExecute の状態が同期される
// AsyncReactiveCommand はコマンド実行中に自動的に CanExecute が false になるので command1 が実行中は sharedStatus 経由で command2 の CanExecute も false になる
AsyncReactiveCommand command1 = someCanExecuteSource.ToAsyncReactiveCommand(sharedStatus)
    .WithSubscribe(async () =>
    {
        await Task.Delay(3000); // 何か時間のかかる処理
    });
ReactiveCommandSlim command2 = someCanExecuteSource.ToReactiveCommandSlim(sharedStatus)
    .WithSubscribe(() =>
    {
        // do something
    });

ValidatableReactiveProperty<T>

バリデーション機能を持った IReactiveProperty<T> の実装になります。バリデーション機能を持った軽量の ReactiveProperty<T> を使用したい場合はこちらのクラスを利用してください。
従来はバリデーション機能が必要な場合は ReactiveProperty<T> を使用して、そうじゃない場合は ReactivePropertySlim<T> を使用するといった形でしたが、今後はバリデーション機能が必要な場合も軽量な実装の ValidatableReactiveProperty<T> を使用することでパフォーマンス面のメリットを得ることが出来ます。

これで ReactiveProperty<T> のメリットは PropertyChanged イベントを必ず UI スレッド上で実行するようにするという点だけになりました。このメリットのためだけに性能劣化を受け入れるかどうかという判断基準になります。

ReactiveProperty<T>ReactivePropertySlim<T> の性能差は以下のようになります。圧倒的に Slim のほうが早くなります。

Method Mean Error StdDev Median
CreateReactivePropertyInstance 67.878 ns 1.3961 ns 2.7230 ns 68.221 ns
CreateReactivePropertySlimInstance 5.880 ns 0.1770 ns 0.5218 ns 5.714 ns
BasicForReactiveProperty 2,708.497 ns 53.9674 ns 55.4206 ns 2,715.572 ns
BasicForReactivePropertySlim 60.237 ns 1.2479 ns 2.3438 ns 59.238 ns

上から順に ReactiveProperty<T> のインスタンス化、ReactivePropertySlim<T> のインスタンス化、ReactiveProperty<T> の基本的な機能を使用したケース、ReactivePropertySlim<T> の基本的な機能を使用したケースになります。インスタンス化で 13 倍、基本的な機能を使用したケースでは 45 倍ほど Slim のほうが早くなります。

バリデーションのためだけに、この性能劣化を受け入れるのはメリットに対してデメリットが大きいので、バリデーション機能に特化した軽量版の IReactiveProperty<T> の実装を追加しました。以下のようにシンプルにバリデーションロジックを指定するケースのベンチマーク結果になります。

[Benchmark]
public ReactiveProperty<string> ReactivePropertyValidation()
{
    var rp = new ReactiveProperty<string>("")
        .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "invalid" : null);
    rp.Value = "xxx"; // valid
    rp.Value = ""; // invalid
    return rp;
}

[Benchmark]
public ValidatableReactiveProperty<string> ValidatableReactivePropertyValidation()
{
    var rp = new ValidatableReactiveProperty<string>(
        "",
        x => string.IsNullOrEmpty(x) ? "invalid" : null);
    rp.Value = "xxx"; // valid
    rp.Value = ""; // invalid
    return rp;
}
Method Mean Error StdDev Median
ReactivePropertyValidation 4,954.138 ns 93.2171 ns 107.3490 ns 4,942.650 ns
ValidatableReactivePropertyValidation 704.852 ns 12.8322 ns 10.7155 ns 708.510 ns

また、以下のような DataAnnotations によるバリデーションと通常のクラスからの ReactiveProperty の生成を行うよくあるケースでもベンチマークをとりました。以下のように ViewModel にありがちなパターンを実装したクラスを 2 つ用意します。

// ReactiveProperty を使用したケース
public class ReactivePropertyVM : IDisposable
{
    private Person _model = new();

    [Required]
    public ReactiveProperty<string> Name { get; }

    public ReactivePropertyVM()
    {
        Name = _model.ToReactivePropertyAsSynchronized(x => x.Name,
            ignoreValidationErrorValue: true)
            .SetValidateAttribute(() => Name);
    }

    public void Dispose()
    {
        Name.Dispose();
    }
}

// ValidatableReactiveProperty を使用したケース
public class ValidatableReactivePropertyVM : IDisposable
{
    private Person _model = new();

    [Required]
    public ValidatableReactiveProperty<string> Name { get; }

    public ValidatableReactivePropertyVM()
    {
        Name = _model.ToReactivePropertySlimAsSynchronized(x => x.Name)
            .ToValidatableReactiveProperty(
                () => Name, 
                disposeSource: true); // ToReactivePropertySlimAsSynchronized で生成された ReactivePropertySlim も まとめて Dispose するための引数
    }

    public void Dispose()
    {
        Name.Dispose();
    }
}

ベンチマーク部分のコードは以下のようになります。

[Benchmark]
public ReactivePropertyVM ReactivePropertyValidationFromPoco()
{
    var vm = new ReactivePropertyVM();
    vm.Name.Value = "valid";
    vm.Name.Value = "";
    vm.Dispose();
    return vm;
}

[Benchmark]
public ValidatableReactivePropertyVM ValidatableReactivePropertyValidationFromPoco()
{
    var vm = new ValidatableReactivePropertyVM();
    vm.Name.Value = "valid";
    vm.Name.Value = "";
    vm.Dispose();
    return vm;
}

結果は以下のようになりました。

Method Mean Error StdDev Median
ReactivePropertyValidationFromPoco 9,642.258 ns 181.2835 ns 208.7663 ns 9,617.409 ns
ValidatableReactivePropertyValidationFromPoco 4,282.720 ns 57.8372 ns 54.1010 ns 4,287.605 ns

最初のシンプルなケースでは 7 倍、Poco を使った ViewModel の実装のケースでも倍近い性能改善があります。Poco の方は DataAnnotations の属性を取得するためにリフレクションを使用しているため、その部分の処理時間が大きくなるためこのような結果になっていると考えられます。

System.Reactive を追加しなくても本当に必要最低限のことが出来るようにするための機能

いくつかクラスを追加した後にサンプルを書こうと思ったら、やっぱり LINQ が恋しくなりました。あと CompositeDisposable が無いと話にならないと思ったので本当に必要最低限だけ追加しました。

Reactive.Bindings.Disposables.CompositeDisposable を追加しています。IObservable<T> の拡張メソッドとして LINQ の基本のキの SelectWhere を追加しました。Delay とかみたいに処理を遅らせるようなものは見栄えはいいのですが、実際には SelectWhere があれば軽いものであれば大体書けてしまうと思います。

また ReactiveCommand 系を生成するために CombineLatestIObservable<bool> を束ねて ToAsyncReactiveCommand()ToReactiveCommandSlim() をしたくなったので IEnumerable<IObservable<bool>> に対しての拡張メソッドとして CombineLatest を追加しています。

とりあえずのやっつけ実装なので本家と比べると遅いし効率もそんなに良くないと思いますが…。もし、それ以上が欲しい場合は追加で System.Reactive を追加するか ReactiveProperty の方のパッケージを追加して使ってくださいというコンセプトです。

IReadOnlyCollection<T> 向けの ObserveElementProperty, ObserveElementObservableProperty, ObserveElementPropertyChanged メソッド

これまでは ObservableCollection<T> などのように INotifyCollectionChanged インターフェースを実装したコレクションをターゲットにしていましたが、読み取り専用の IReadOnlyCollection<T> に対しても同様のことが出来るようにしました。以下のように Reactive.Bindings.Helpers.CollectionUtilities の静的メソッドとして定義しています。

var people = new[]
{
    new Person { FirstName = "Kazuki", LastName = "Ota" },
    new Person { FirstName = "Taro", LastName = "Tanaka" },
    new Person { FirstName = "Nobunaga", LastName = "Oda" },
};

var disposable = CollectionUtilities.ObserveElementProperty(people, x => x.FirstName)
    .Subscribe(PropertyPack<Person, string> x =>
    {
        // Name プロパティの変更時の処理
    });

まとめ

ReactiveProperty の久しぶりのメジャーバージョンアップになります。
このバージョンアップからは Rx の機能をヘビーに使うといった用途でない場合は、ほとんどのケースで ReactiveProperty.Core パッケージで開発が出来るようになると思います。Rx の機能をヘビーに使う場合には、これまで通り ReactiveProperty のパッケージを参照してください。

まだドキュメント類が 9.0.0 向けに更新されていませんが、基本的な機能については以下のフォルダーに WPF で作ったサンプルがあるので参考にしてください。

https://github.com/runceel/ReactiveProperty/tree/main/Samples/ReactivePropertyCoreSample.WPF

Microsoft (有志)

Discussion