2020年12月20日深夜
ReactivePropertyのBooleanNotifier
クラスに関する記事です。
ToReactiveCommand
したときに、事故りそうなケースをまとめました。
長くなったので、最後のまとめを読んでいただければと思います。
2020年12月21日真夜中 追記
「なぜそうなっているのか」について、2011年の記事を引用して背景を詳しく説明されています。
BooleanNotifierをToReactiveCommandしたときの挙動、が思っていたのと違う理由
(@soi様から頂いたコメントより)
2020年12月24日昼間 追記
(@okazuki様から頂いたコメントを受け追記)
公式のドキュメントが増補されました。
日頃ReactiveProperty
を使わせていただいている身としては恐れ多い限りです。
先述のsoi様の記事にある通り、使いどころを考えて使うのが大前提でありつつ
もしつかうのであれば、引数にinitialValue: n.Value
として変数のステータスを渡すのがシンプルかつ安全です。
キャプチャ画像出典:https://okazuki.jp/ReactiveProperty/features/Notifiers.html#booleannotifier
概要
私はC#のWPFアプリケーションを作っています。
使用しているライブラリの1つはReactivePropertyです。
ReactiveProperty
はNuGetパッケージマネージャーからインストール可能です。
2020年12月18日現在の最新版は7.5.1です。
先日、ReactiveProperty
のBooleanNotifier
クラスを使っていて、少し混乱することがありました。
よくよく調べてみると、混乱が解消されました。
この記事は、混乱したことと解消の経緯について書き残す目的があります。
検証環境
- Windows 10 Home
- Visual Studio 2019 Community
- ReactiveProperty 7.5.1
BooleanNotifierクラスの簡単な説明
BooleanNotifier
はIObservable<bool>
を実装したシンプルなクラスです。
掃除機の電源ボタンのように、オン(True
)とオフ(False
)をスイッチする使い方ができます。
特徴 | 嬉しいこと(個人の感想) |
---|---|
IObservable<bool> を実装している |
ToReactiveCommand の拡張メソッドが使える |
SwitchValue メソッドを備えている |
True とFalse の切り替え(トグル)をスタイリッシュに記述できる |
真偽値のトグルを、ReactiveProperty
とBooleranNotifier
のそれぞれで書いてみます。
// (1)ReactiveProptertyで書いた場合
var m = new ReactiveProperty<bool>();
m.Value = !m.Value;
// (2)BooleranNotifierで書いた場合
var n = new BooleanNotifier();
n.SwitchValue();
BooleanNotifier
の詳細な説明は、下記ドキュメントに記載されています。
ReactiveProperty documentation - BooleanNotifier
【本題】BooleanNotifierを使って混乱したこと
BooleanNotifier
はIObservable
を実装しているので、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キーを押して実行した直後の画面が以下のようになります。
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
なのに、なぜ画面起動直後のボタンの状態に差があるのか?(再掲) -
BooleanNotifier
とReactiveProperty
の違いはなにか?
ソースのinitialValuse
を踏襲してボタンの活性/非活性が決まるものと勝手に思い込んでいましたが、そうではないみたいです。
ソースコードを読んでみる
ReactiveProperty
はGitHubスター数500を超える超優良ライブラリです。
そんなソースコードが誰でも閲覧できます。
BooleanNotifier
がどういう経路をたどってReactiveCommand
になるのかもソースコードを読めば分かります。
ToReactiveCommand
拡張メソッド
第二引数のデフォルト引数はtrue
です。
第二引数を指定しなければ、ボタンは活性化した状態になることが推定されます。
public static ReactiveCommand ToReactiveCommand(this IObservable<bool> canExecuteSource, bool initialValue = true) =>
new ReactiveCommand(canExecuteSource, initialValue);
しかし、ReactiveProperty
をソースにしたボタンは、ToReactiveCommand
の第二引数を指定しなくとも、初期状態が非活性のボタンが生成されました。
ReactiveCommand
コマンドの活性/非活性はICommand
のインターフェイスのCanExecute()
の実装で決まります。
ReactiveCommand
の活性/非活性はprivate
プロパティのIsCanExecute
で決まるようです。
public bool CanExecute() => IsCanExecute;
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);
});
}
initialValuse
はToReactiveCommand
と同じ値をとるので、IsCanExecute
はtrue
になります。
ソースのcanExecuteSource
にはメソッドチェーンで処理がぶら下がっており、最後にお馴染みのSubscribe
でIsCanExecute
プロパティにソースの値を代入しています。
ソースであるBooleanNotifier
とReactiveProperty
の値が変更されたら、ボタンの状態も変わりそうです。
BooleanNotifier
もReactiveProperty
もnew
しかしていない。
ソース側のSubscribe
で何かしている?
参考)メソッドチェーンの途中の処理については本家okazuki殿の記事を参照ください。
Reactive Extensions再入門 その23「重複を排除するメソッド」
Reactive Extensions再入門 その45「Scheduler」
BooleanNotifier
とReactiveProperty
BooleanNotifier
を購読すると、値がset
されたときにOnNext
で通知がなされます。
public IDisposable Subscribe(IObserver<bool> observer) => boolTrigger.Subscribe(observer);
// ...
public bool Value
{
// 中略
set
{
boolTrigger.OnNext(value);
}
}
一方、ReactiveProperty
では
public IDisposable Subscribe(IObserver<T> observer)
{
// 中略
if (IsRaiseLatestValueOnSubscribe)
{
observer.OnNext(LatestValue);
}
// 中略
}
念のためif
条件がTrue
になることも確認しました。
System.Console.WriteLine(reactiveProperty.IsRaiseLatestValueOnSubscribe);
// コンソール出力: True
Subscribe
の中でOnNext()
が記述されているため、ToReactiveCommand
して購読した時点で現在の真偽値がバインド先に通達されていた
ということが分かりました。
まとめ
だんだん自分でも何が言いたいのか分からなくなってきました。
まとめます。
BooleanNotifier
の場合は、ソースの状態をボタンへ反映させるために、ToReactiveCommand
の引数に初期状態を渡す必要があります。
BooleanNotifierButtonCommand = booleanNotifier.ToReactiveCommand(initialValue:false);
ReactiveProperty
の場合は、ソースの状態は勝手にボタンに反映されます。
裏を返せば、ToReactiveCommand
の引数に渡す真偽値は意味を持ちません。(ソースの状態が優先されます)
事故ってるパターン
-
BooleanNotifier
は非活性にしたいのに、非活性になってない -
ReactiveProperty
はToReactiveCommand
に引数true
を与えて活性にしたいと思ったのに、ソースが優先されて非活性になっている。ボタンが押せない。
public PrismWindow2ViewModel()
{
var booleanNotifier= new BooleanNotifier(initialValue: false);
var reactiveProperty = new ReactiveProperty<bool>(initialValue: false);
BooleanNotifierButtonCommand = booleanNotifier.ToReactiveCommand();
ReactivePropertyButtonCommand = reactiveProperty.ToReactiveCommand(initialValue: true);
}
以上です
何か間違いがございましたら、指摘していていただければ幸いです。