5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【WPF】BooleanNotifierをToReactiveCommandしたときの挙動について【ReactiveProperty7.5.1】

Last updated at Posted at 2020-12-20

2020年12月20日深夜

ReactivePropertyBooleanNotifierクラスに関する記事です。

ToReactiveCommandしたときに、事故りそうなケースをまとめました。
長くなったので、最後のまとめを読んでいただければと思います。

2020年12月21日真夜中 追記

「なぜそうなっているのか」について、2011年の記事を引用して背景を詳しく説明されています。
BooleanNotifierをToReactiveCommandしたときの挙動、が思っていたのと違う理由
@soi様から頂いたコメントより)

2020年12月24日昼間 追記

@okazuki様から頂いたコメントを受け追記)

公式のドキュメントが増補されました。
日頃ReactivePropertyを使わせていただいている身としては恐れ多い限りです。

先述のsoi様の記事にある通り、使いどころを考えて使うのが大前提でありつつ
もしつかうのであれば、引数にinitialValue: n.Valueとして変数のステータスを渡すのがシンプルかつ安全です。

1218_03.png
キャプチャ画像出典:https://okazuki.jp/ReactiveProperty/features/Notifiers.html#booleannotifier

概要

私はC#のWPFアプリケーションを作っています。
使用しているライブラリの1つはReactivePropertyです。
ReactivePropertyはNuGetパッケージマネージャーからインストール可能です。
2020年12月18日現在の最新版は7.5.1です。

1218_00.png

先日、ReactivePropertyBooleanNotifierクラスを使っていて、少し混乱することがありました。
よくよく調べてみると、混乱が解消されました。
この記事は、混乱したことと解消の経緯について書き残す目的があります。

検証環境

  • Windows 10 Home
  • Visual Studio 2019 Community
  • ReactiveProperty 7.5.1

BooleanNotifierクラスの簡単な説明

BooleanNotifierIObservable<bool>を実装したシンプルなクラスです。
掃除機の電源ボタンのように、オン(True)とオフ(False)をスイッチする使い方ができます。

特徴 嬉しいこと(個人の感想)
IObservable<bool>を実装している ToReactiveCommandの拡張メソッドが使える
SwitchValueメソッドを備えている TrueFalseの切り替え(トグル)をスタイリッシュに記述できる

真偽値のトグルを、ReactivePropertyBooleranNotifierのそれぞれで書いてみます。

// (1)ReactiveProptertyで書いた場合
var m = new ReactiveProperty<bool>();
m.Value = !m.Value;

// (2)BooleranNotifierで書いた場合
var n = new BooleanNotifier();
n.SwitchValue();

BooleanNotifierの詳細な説明は、下記ドキュメントに記載されています。

ReactiveProperty documentation - BooleanNotifier

【本題】BooleanNotifierを使って混乱したこと

BooleanNotifierIObservableを実装しているので、ReactiveCommandのソースにできます。

It can use to source of ReactiveCommand.

(引用元:ReactiveProperty documentation - BooleanNotifier

さっそく、ToReactiveCommand拡張メソッドでReactiveCommandをつくってみます。
比較参考のために、ReactiveProperty<bool>も使います。

成果物のゴールイメージ

  • ReactiveCommandをバインドしたボタンを配置します。
  • 画面起動時のボタンは非活性(押せない状態)にします。

ビューモデル

ビューモデルは以下のようになります。
Prismテンプレートを使っています。

public class PrismWindow2ViewModel : BindableBase
{
    // これらのコマンドをボタンにバインドします
    public ReactiveCommand BooleanNotifierButtonCommand { get; }
    public ReactiveCommand ReactivePropertyButtonCommand { get; }

    public PrismWindow2ViewModel()
    {
        var booleanNotifier= new BooleanNotifier(initialValue: false);
        var reactiveProperty = new ReactiveProperty<bool>(initialValue: false);

        BooleanNotifierButtonCommand = booleanNotifier.ToReactiveCommand();
        ReactivePropertyButtonCommand = reactiveProperty.ToReactiveCommand();
    }
}

ビュー

ビューは以下のようになります。
BooleanNotifierをソースにしたボタンを上方に、ReactivePropertyをソースにしたボタンを下方に配置してます。

<Window x:Class="ReactivePropertyPlayground.Views.PrismWindow2"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"             
             prism:ViewModelLocator.AutoWireViewModel="True"
             Height="150" Width="300">
    <StackPanel>
        <Label Content="ソースの初期値は false"/>
        <Button 
            Command="{Binding BooleanNotifierButtonCommand}" 
            Content="booleanNotifier.ToReactiveCommand();"
            Margin="10"/>
        <Button 
            Command="{Binding ReactivePropertyButtonCommand}" 
            Content="reactiveProperty.ToReactiveCommand();"
            Margin="10"/>
    </StackPanel>
</Window>

実行画面

Visual StudioでF5キーを押して実行した直後の画面が以下のようになります。

1218_02.png

ReactiveCommandのソース 画面起動直後の状態
BooleanNotifier(initialValuse: false) 押せる
ReactiveProperty(initialValuse: false) 押せない

期待していたイメージと違う

ビューモデルでは、ソースの初期値をfalseにしていました。

var booleanNotifier= new BooleanNotifier(initialValue: false);
var reactiveProperty = new ReactiveProperty<bool>(initialValue: false);

ボタンのソースの初期値はどちらもinitialValuse: falseなのに、なぜ画面起動直後のボタンの状態に差があるのか?

画面起動時はどちらのボタンも非活性な状態であることを期待していました。
しかし、実際はBooleanNotifierをソースにしたボタンは押せる状態でした。
ReactivePropertyは押せない状態であり、期待通りでした。

期待通りにするためには

画面起動時に、ボタン(ReactiveCommandバインド)を非活性にするためには、ToReactiveCommand拡張メソッドの引数に初期値falseを与えます。

BooleanNotifierButtonCommand = booleanNotifier.ToReactiveCommand(initialValue:false);

以上です。
これで期待通りに動きます。

【裏コース】デフォルト引数の話

疑問

ToReactiveCommandに引数をあたえることで、確実にバインドしたボタンの初期状態を既定できることが分かりました。
しかし、若干の二度手間感が残りました。

  • IObservableのソースにinitialValuseを渡して、なおかつToReactiveCommandの引数にも同じようにinitialValueを渡す必要があるのはなぜか?
  • ボタンのソースの初期値はどちらもinitialValuse: falseなのに、なぜ画面起動直後のボタンの状態に差があるのか?(再掲)
  • BooleanNotifierReactivePropertyの違いはなにか?

ソースのinitialValuseを踏襲してボタンの活性/非活性が決まるものと勝手に思い込んでいましたが、そうではないみたいです。

ソースコードを読んでみる

ReactivePropertyはGitHubスター数500を超える超優良ライブラリです。
そんなソースコードが誰でも閲覧できます。

BooleanNotifierがどういう経路をたどってReactiveCommandになるのかもソースコードを読めば分かります。

ToReactiveCommand拡張メソッド

第二引数のデフォルト引数はtrueです。
第二引数を指定しなければ、ボタンは活性化した状態になることが推定されます。

public static ReactiveCommand ToReactiveCommand(this IObservable<bool> canExecuteSource, bool initialValue = true) =>
            new ReactiveCommand(canExecuteSource, initialValue);

(引用元:https://github.com/runceel/ReactiveProperty/blob/main/Source/ReactiveProperty.NETStandard/ReactiveCommand.cs)

しかし、ReactivePropertyをソースにしたボタンは、ToReactiveCommandの第二引数を指定しなくとも、初期状態が非活性のボタンが生成されました。

ReactiveCommand

コマンドの活性/非活性はICommandのインターフェイスのCanExecute()の実装で決まります。

ReactiveCommandの活性/非活性はprivateプロパティのIsCanExecuteで決まるようです。

public bool CanExecute() => IsCanExecute;

(引用元:https://github.com/runceel/ReactiveProperty/blob/main/Source/ReactiveProperty.NETStandard/ReactiveCommand.cs)

ReactiveCommandコンストラクター
public ReactiveCommand(IObservable<bool> canExecuteSource, IScheduler scheduler, bool initialValue = true)
{
    IsCanExecute = initialValue;
    Scheduler = scheduler;
    CanExecuteSubscription = canExecuteSource
        .DistinctUntilChanged()
        .ObserveOn(scheduler)
        .Subscribe(b =>
        {
            IsCanExecute = b;
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
        });
}

(引用元:https://github.com/runceel/ReactiveProperty/blob/main/Source/ReactiveProperty.NETStandard/ReactiveCommand.cs)

initialValuseToReactiveCommandと同じ値をとるので、IsCanExecutetrueになります。

ソースのcanExecuteSourceにはメソッドチェーンで処理がぶら下がっており、最後にお馴染みのSubscribeIsCanExecuteプロパティにソースの値を代入しています。

ソースであるBooleanNotifierReactivePropertyの値が変更されたら、ボタンの状態も変わりそうです。

BooleanNotifierReactivePropertynewしかしていない。
ソース側のSubscribeで何かしている?

参考)メソッドチェーンの途中の処理については本家okazuki殿の記事を参照ください。

Reactive Extensions再入門 その23「重複を排除するメソッド」
Reactive Extensions再入門 その45「Scheduler」

BooleanNotifierReactiveProperty

BooleanNotifierを購読すると、値がsetされたときにOnNextで通知がなされます。

public IDisposable Subscribe(IObserver<bool> observer) => boolTrigger.Subscribe(observer);

// ...

public bool Value
{
    // 中略
    set
    {
        boolTrigger.OnNext(value);
    }
}

(引用元:https://github.com/runceel/ReactiveProperty/blob/main/Source/ReactiveProperty.NETStandard/Notifiers/BooleanNotifier.cs)

一方、ReactivePropertyでは

public IDisposable Subscribe(IObserver<T> observer)
{
    // 中略
    if (IsRaiseLatestValueOnSubscribe)
    {
        observer.OnNext(LatestValue);
    }
    // 中略
}

(引用元:https://github.com/runceel/ReactiveProperty/blob/main/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs)

念のためif条件がTrueになることも確認しました。


System.Console.WriteLine(reactiveProperty.IsRaiseLatestValueOnSubscribe);
// コンソール出力: True

Subscribeの中でOnNext()が記述されているため、ToReactiveCommandして購読した時点で現在の真偽値がバインド先に通達されていた
ということが分かりました。

まとめ

だんだん自分でも何が言いたいのか分からなくなってきました。
まとめます。

BooleanNotifierの場合は、ソースの状態をボタンへ反映させるために、ToReactiveCommandの引数に初期状態を渡す必要があります。

BooleanNotifierButtonCommand = booleanNotifier.ToReactiveCommand(initialValue:false);

ReactivePropertyの場合は、ソースの状態は勝手にボタンに反映されます。

裏を返せば、ToReactiveCommandの引数に渡す真偽値は意味を持ちません。(ソースの状態が優先されます)

事故ってるパターン

  • BooleanNotifierは非活性にしたいのに、非活性になってない
  • ReactivePropertyToReactiveCommandに引数trueを与えて活性にしたいと思ったのに、ソースが優先されて非活性になっている。ボタンが押せない。
public PrismWindow2ViewModel()
{
    var booleanNotifier= new BooleanNotifier(initialValue: false);
    var reactiveProperty = new ReactiveProperty<bool>(initialValue: false);

    BooleanNotifierButtonCommand = booleanNotifier.ToReactiveCommand();
    ReactivePropertyButtonCommand = reactiveProperty.ToReactiveCommand(initialValue: true);
}

1218_02.png

以上です

何か間違いがございましたら、指摘していていただければ幸いです。

5
2
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?