かずきのBlog@hatena

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

空の IObservable からの ReactiveProperty と ReactiveCommand の生成

先日、こんな質問を頂きました。

とりあえず値が来てないシーケンスで作ってそうに見えたので回答したのですが、確かに慣れてないと理解に時間がかかるなぁと思ったので、ついでにブログネタにしてしまおうという魂胆です。

単純化してみよう

ということで、ReactiveProperty や ReactiveCommand は IObservable<T> から ToReactiveProperty や ToReactiveCommand メソッドを使って生成できます。 例えば…

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

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {
            // Subscribe すると false を返す IObservalbe<bool>
            var seq = Observable.Return(false);

            // ReactiveProperty と ReactiveCommand を生成
            var rp = seq.ToReactiveProperty();
            var rc = seq.ToReactiveCommand();

            Console.WriteLine(rp.Value); // False
            Console.WriteLine(rc.CanExecute()); // False
        }
    }
}

こんな風に false を返すシーケンスから作ると、どちらも false になります。

では、空のシーケンスで試してみましょう。以下のような感じになります。

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

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {
            // Subscribe しても何も返さないシーケンス
            var seq = Observable.Never<bool>();

            // ReactiveProperty と ReactiveCommand を生成
            var rp = seq.ToReactiveProperty();
            var rc = seq.ToReactiveCommand();

            Console.WriteLine(rp.Value); // False
            Console.WriteLine(rc.CanExecute()); // True
        }
    }
}

ReactiveProperty のケース

ReactiveProperty はシーケンスから値が発行されていない状態でも、何らかの値は Value プロパティに保持していないといけません。値を返さずに例外を投げる(例えば独自例外で、ReactiveProperty に値がないよ!っていうのを表すものを作って、それをスローするとか)というのも考えられますが、ReactiveProperty の使われ方的に Value にアクセスしたら例外が出るケースがあるというのは使いにくさの割にメリットがないです。

ということで、ReactiveProperty は初期値を持ってます。初期値なんて設定した覚えないんだけど??となるかもしれませんが、ToReactiveProperty 拡張メソッドの定義を見てみると第二引数にあります。

ReactiveProperty/ReactiveProperty.cs at e3640f66e0e04b43b77dc44029fc416de8c060f3 · runceel/ReactiveProperty · GitHub

        /// <summary>
        /// <para>Convert to two-way bindable IObservable&lt;T&gt;</para>
        /// <para>PropertyChanged raise on ReactivePropertyScheduler</para>
        /// </summary>
        public static ReactiveProperty<T> ToReactiveProperty<T>(this IObservable<T> source,
            T initialValue = default(T),
            ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe, IEqualityComparer<T> equalityComparer = null) =>
            new ReactiveProperty<T>(source, initialValue, mode, equalityComparer);

T initialValue = default(T) の部分です。この initialValue が ReactiveProperty のコンストラクターに渡されて、最終的に何も値が来てないときの初期値として設定されます。

そのため、空のシーケンスに対して ToReactiveProperty をすると bool 型の場合は default(bool) の結果である false が初期値として設定されます。結果として Console.WriteLine(rp.Value) は False と表示されます。

ReactiveCommand のケース

ReactiveCommand は IObservable<bool> の結果を CanExecute として返します。ReactiveCommand は、空のシーケンスから作られたときの初期値として default(bool) ではなく true を使っています。 これは ReactiveCommand が使われるケースで、普通に new ReactiveCommand() したときに実行できないコマンドよりも実行可能なコマンドが求められるケースのほうが圧倒的に多いという理由で、CanExecute の戻り値は初期値が true になっています。

ToReactiveCommand の定義を見てみても明示的に initialValue は true になっています。

ReactiveProperty/ReactiveCommand.cs at e3640f66e0e04b43b77dc44029fc416de8c060f3 · runceel/ReactiveProperty · GitHub

        /// <summary>
        /// CanExecuteChanged is called from canExecute sequence on UIDispatcherScheduler.
        /// </summary>
        public static ReactiveCommand ToReactiveCommand(this IObservable<bool> canExecuteSource, bool initialValue = true) =>
            new ReactiveCommand(canExecuteSource, initialValue);

そのため、空のシーケンスに対して ToReactiveCommand を呼び出すと、実行可能なコマンド(CanExecute の結果が true) が出来上がります。

今回のケースに当てはめてみよう

再現コードを GitHub に上げてもらうようにお願いしたら上げてもらえました!

GitHub - AdonisLeavis/WpfApp1: ReactiveCommandの動きを確認するプロジェクト

該当部分は ViewModels フォルダーの中の MainWindowViewModel クラスになります。

<feff>using Reactive.Bindings;
using System.Reactive.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Reactive.Bindings.Extensions;

namespace WpfApp1.ViewModels
{
    public class MainWindowViewModel : Bases.ViewModelBase
    {
        public ReactiveProperty<CellViewModel> CurrentCell { get; set; } = new ReactiveProperty<CellViewModel>();

        public ReactiveCommand SetCurrentCellCommand { get; set; } = new ReactiveCommand();
        public ReactiveCommand ClearCurrentCellCommand { get; set; } = new ReactiveCommand();
        public ReactiveCommand TestCommand { get; set; }

        public MainWindowViewModel()
        {
            SetCurrentCellCommand.Subscribe(_ =>
            {
                var flag = false;
                if(CurrentCell.Value != null)
                {
                    flag = !CurrentCell.Value.IsFirstFlag;
                }
                var curCell = new CellViewModel() { IsFirstFlag = flag };
                CurrentCell.Value = curCell;
            });

            ClearCurrentCellCommand.Subscribe(_ => CurrentCell.Value = null);

            var testProp = new[]
            {
                CurrentCell.Select(a => a!=null),
                CurrentCell.Where(a => a != null).Select(a => a.IsFirstFlag),
                // CurrentCell.Select(a => a != null && a.IsFirstFlag), ★ こっちだと正しく動作する
            }.CombineLatestValuesAreAllTrue().ToReactiveProperty();
            testProp.Subscribe(a => System.Console.WriteLine($"来たよ{a}"));

            TestCommand = new[]
            {
                CurrentCell.Select(a => a!=null),
                CurrentCell.Where(a => a != null).Select(a => a.IsFirstFlag),
                // CurrentCell.Select(a => a != null && a.IsFirstFlag), ★ こっちだと正しく動作する
            }.CombineLatestValuesAreAllTrue().ToReactiveCommand();
            TestCommand.Subscribe(_ => System.Console.WriteLine(CurrentCell.Value.IsFirstFlag));
        }
        
    }
}

TestCommand が今回の質問の現象が起きているクラスになります。CombineLatestValuesAreAllTrue メソッドを呼び出した結果に対して ToReactiveCommand をしてます。 CombineLatestValuesAreAllTrue 拡張メソッドは、CombineLatest で全てが true かどうかをチェックしているだけのメソッドです。

以下のように定義されています。

        /// <summary>
        /// Lastest values of each sequence are all true.
        /// </summary>
        public static IObservable<bool> CombineLatestValuesAreAllTrue(
            this IEnumerable<IObservable<bool>> sources) =>
            sources.CombineLatest(xs => xs.All(x => x));

もとになっている CombineLatest メソッドとはどんなメソッドかというと、sources の全ての IObservable<T> の最後の値に対して、処理をした結果を後続に流す IObservable<T> を返すメソッドになります。 試しに以下のようなコードを書いてみます。

// Subscrie すると true を返すシーケンス
var source1 = Observable.Return(true);
var source2 = Observable.Return(true);

// CombineLatest で and をとった結果を返すシーケンスにする
var result = new[] { source1, source2 }.CombineLatest(xs => xs[0] & xs[1]);
// 表示
result.Subscribe(x => Console.WriteLine(x)); // True

CombineLatest で and をとってます。今回は True を返すシーケンスを 2 つ合成しているので True & True をして True になりますね。どちらかでも Observable.Return(false) にすると false になります。

時間の経過とともに値を発行していくと…?

固定の値を返す IObservable<T> の時の動作はわかりました。次は時間の経過とともに値が発行されるもので試してみます。

// bool を発行する IObservable<bool> を 2 つ用意
var source1 = new Subject<bool>();
var source2 = new Subject<bool>();

// CombineLatest で and をとった結果を返すシーケンスにする
var result = new[] { source1, source2 }.CombineLatest(xs => xs[0] & xs[1]);
// 表示
result.Subscribe(x => Console.WriteLine($"Result = {x}"));

// 実際に値を発行していきます
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);
Console.WriteLine("source2.OnNext(true)");
source2.OnNext(true);
Console.WriteLine("source1.OnNext(false)");
source1.OnNext(false);
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);

実行するとどうなるでしょう?Subject 型は OnNext で値を発行しない限り値は発行されません。そのため Subscribe した時点では、まだ何も値が来てないので、Subscribe しただけでは Console.WriteLine は実行されません。コードの公判の OnNext をしているところで source1 で True、source2 で True を発行したタイミングで初めて CombineLatest の結果を Subscribe したところに処理が来ます。後は、source1 に対して false、true と値の発行をしてるので、そのたびに CombineLatest が実行されます。

つまり結果はこうなります。

source1.OnNext(true)
source2.OnNext(true)
Result = True
source1.OnNext(false)
Result = False
source1.OnNext(true)
Result = True

両方の値がそろって初めて CombineLatest の後ろに値が流れます。試しに source1 からだけ値を何回も発行するようにコードを書き換えて…

// bool を発行する IObservable<bool> を 2 つ用意
var source1 = new Subject<bool>();
var source2 = new Subject<bool>();

// CombineLatest で and をとった結果を返すシーケンスにする
var result = new[] { source1, source2 }.CombineLatest(xs => xs[0] & xs[1]);
// 表示
result.Subscribe(x => Console.WriteLine($"Result = {x}"));

// 実際に値を発行していきます
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);
Console.WriteLine("source1.OnNext(false)");
source1.OnNext(false);
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);
Console.WriteLine("source1.OnNext(false)");
source1.OnNext(false);
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);
Console.WriteLine("source1.OnNext(false)");
source1.OnNext(false);

実行しても CombineLatest の結果の Subscribe の処理は実行されません。以下のような実行結果になります

source1.OnNext(true)
source1.OnNext(false)
source1.OnNext(true)
source1.OnNext(false)
source1.OnNext(true)
source1.OnNext(false)

これを踏まえてコードを見てみよう

TestCommand の生成は以下のように行われています。

            TestCommand = new[]
            {
                CurrentCell.Select(a => a!=null),
                CurrentCell.Where(a => a != null).Select(a => a.IsFirstFlag),
                // CurrentCell.Select(a => a != null && a.IsFirstFlag), ★ こっちだと正しく動作する
            }.CombineLatestValuesAreAllTrue().ToReactiveCommand();

CurrentCell は ReactiveProperty<CellViewModel> 型になります。この CurrentCell プロパティは初期値が設定されていないのでデフォルトで null が入っています。 一つ目の CurrentCell.Select(a => a != null) は初期状態だと null が流れてくるので true が返されています。しかし、二つ目のほうは CurrentCell.Where(a => a != null).Select(a => a.IsFirstFlag) としてるため、初期値の null は Where 句で弾かれてしまい後ろの Select は実行されないので、初期状態では 何も値を発行しないシーケンス になります。

CombineLatest は、すべてのシーケンスの値が出そろわないと何もしないので、初期状態では空の IObservable<bool> になります。そのため TestCommand の CanExecute メソッドはデフォルト値の true を返します。そのため、初期状態では実行可能なコマンドになっています。

「★ こっちだと正しく動作する」のコメントに書かれている方は、CurrentCell の初期値の null がわたってきたときには false を返します。そのため初期状態では false を返すシーケンスに対して ToReactiveCommand をするので CanExecute メソッドの戻り値は false になります。そのため実行不可能なコマンドが出来上がります。

今回したかったことは?

具体的な仕様はわかりませんが、IsFirstFlag は false > true > false > true > false > ... のように変わっていく値のように見えます。SetCurrentCellCommand で、そのような制御が行われています。

            SetCurrentCellCommand.Subscribe(_ =>
            {
                var flag = false;
                if(CurrentCell.Value != null)
                {
                    flag = !CurrentCell.Value.IsFirstFlag;
                }
                var curCell = new CellViewModel() { IsFirstFlag = flag };
                CurrentCell.Value = curCell;
            });

このプログラムは、今回の問題調査用に最小限に絞られたプロジェクトなので、全体最適化という観点で見たら違う実装になるかもしれませんが、今回与えれらた情報だけで自分で作るなら MainWindowViewModel クラスは以下のようになるかな…と思いました。まぁ、基本的に★コメントで示された、こっちだと正しく動作するというのと同じです。C# の新しめの書きかた使った方がシンプルかな?って感じ。

using Reactive.Bindings;
using System.Reactive.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Reactive.Bindings.Extensions;
using Reactive.Bindings.Notifiers;
using System.Diagnostics;

namespace WpfApp1.ViewModels
{
    public class MainWindowViewModel : Bases.ViewModelBase
    {
        public ReadOnlyReactiveProperty<CellViewModel> CurrentCell { get; }

        public ReactiveCommand SetCurrentCellCommand { get; } = new ReactiveCommand();
        public ReactiveCommand ClearCurrentCellCommand { get; } = new ReactiveCommand();
        public ReactiveCommand TestCommand { get; }

        public MainWindowViewModel()
        {
            CurrentCell = Observable.Merge(
                SetCurrentCellCommand
                    .Select(_ => new CellViewModel { IsFirstFlag = (!CurrentCell.Value?.IsFirstFlag) ?? false }),
                ClearCurrentCellCommand
                    .Select(_ => default(CellViewModel))
            ).ToReadOnlyReactiveProperty();

            TestCommand = CurrentCell.Select(x => x?.IsFirstFlag ?? false)
                .ToReactiveCommand()
                .WithSubscribe(() => Debug.WriteLine(CurrentCell.Value.IsFirstFlag));
        }
    }
}

オリジナルリポジトリーを Fork して書き換えました。

GitHub - runceel/WpfApp1: ReactiveCommandの動きを確認するプロジェクト

コードスメル

ja.wikipedia.org

とりあえずせっかくソースコードもらったので、思ったことをつらつらと書いておきます。

  • 今回の IsFirstFlag は、名前からして最初の要素だということを表してるのかな?と思ったら違ったので名前は違う方がよさそう
  • もし、IsFirstFlag が裏の処理にも密接にかかわってくるなら IsFirstFlag の値の制御は、ViewModel よりは Model でやっといたほうがいいのでは…?と思った。もしくは、この画面だけの都合なら、CellViewModel のステートにせずに MainWindowViewModel のステートってだけでもいいのではと思った。
  • C# の新しい書きかたは使えるところではガンガン使おう。

まとめ

ということで、今回質問を受けて、確かにわかりにくいなぁと思ったのでまとめました。 それでは楽しいリアクティブプログラミングを!

ReactiveProperty v6.2.0 をリリースしました

ReactiveProperty の DataAnnotations によるバリデーションのエラーメッセージの改善をしました。

今まで

以下のような定義のプロパティで

[Required(ErrorMessage = "{0} は必須入力項目です。")]
public ReactiveProperty<string> Name { get; }

というようなアノテーションをつけたときのエラーメッセージは「Value は必須入力項目です」でした。

今回の対応

「Name は必須入力項目です」になります。

さらに

[Required(ErrorMessage = "{0} は必須入力項目です。")]
[Display(Name = "名前")]
public ReactiveProperty<string> Name { get; }

にすると「名前 は必須入力項目です。」になります。 その他にもリソースにも対応しているので、エラーメッセージの国際化対応がやりやすくなっています。

各種リンク

NuGet パッケージはこちら

www.nuget.org

リポジトリーはこちら。何かあったら Issue 立ててください。日本語でも英語でも OK です。

github.com

ReactiveProperty v6.1.4 をリリースしました

各種ライブラリの更新と Pull Request の取り込みになります。 大きな変更はありません。

導入は以下の NuGet から

www.nuget.org

余談

余談ですが、最近個人的に気になってる Uno Platform でも試してみましたが動かすのに成功しませんでした…。 う~ん。WebAssembly 上なのでスレッドの制約やらなんやらかなぁ。

ReactiveProperty v6.1 をリリースしました

プルリクエストをマージしてリリースするだけの簡単なお仕事。 詳細は GitHub のリリースページから!

Release v6.1.2 · runceel/ReactiveProperty · GitHub

プルリクエストや Issue への投稿いつもありがとうございます。多謝!

ReactiveProperty v6.0.2 をリリースしました

しました。

Release v6.0.2 · runceel/ReactiveProperty · GitHub

メジャーバージョンが上がってます

ということで、1 つ破壊的変更があります。 これまで WPF では EventToReactiveCommandEventToReactiveProperty を使うのに Blend SDK のアセンブリの Behavior を使用していました。

この Blend SDK は Visual Studio 2019 から同梱されなくなっていて、公式の NuGet パッケージもない状態になりました。そのため、それを置き換える OSS の Behavior のライブラリーである XAML Behaviors for WPF を使用するようにしました。

github.com

そのため、クラス名は同じですが参照元アセンブリや、名前空間が変わっているため、この機能を使用している場合にはコードの変更が必要になります。

更新手順

以下の手順で更新可能です。

  1. ReactiveProperty を v6 以上に更新する
  2. Blend SDK の参照を消す(System.Windows.InteractivityMicrosoft.Expression.Intaractions)
  3. XAML 内の xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"xmlns:i="http://schemas.microsoft.com/xaml/behaviors" に変更する
  4. xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" がある場合も xmlns:i="http://schemas.microsoft.com/xaml/behaviors" に置き換える

小さな新機能

INotifyPropertyChanged インターフェースを実装したクラスの変更通知機能を持ったクラスのプロパティから ReactiveProperty を生成して同期をとるための ToReactivePropertyAsSynchronized のソースからターゲット、ターゲットからソースへの変換ロジックを Rx を使って書けるオーバーロードが追加されています。

例えば ReactivePropertySlim<int>ReactiveProperty<string> で値を変換しつつ同期したいケースで、ReactiveProperty<string> が空文字のときは 0 にして、それ以外のケースでは数字としてパース出来るなら ReactivePropertySlim<int> に書き戻したいとします。こんな感じになります。

public class MainWindowViewModel
{
    public ReactivePropertySlim<int> Source { get; } = new ReactivePropertySlim<int>();
    public ReactiveProperty<string> Target { get; }

    public MainWindowViewModel()
    {
        Target = Source.ToReactivePropertyAsSynchronized(x => x.Value,
            convert: ox => ox.Select(x => x.ToString()), // int -> string
            convertBack: ox => Observable.Merge(
                ox.Where(x => string.IsNullOrEmpty(x)).Select(_ => 0), // 空文字は 0
                ox.Where(x => int.TryParse(x, out _)).Select(x => int.Parse(x)))); // それ以外で数字に変換可能だったら数字にする
    }
}

画面と適当にバインドすると以下のように動きます。Target プロパティを TextBox にバインドして、Source プロパティを TextBlock にバインドしています。

f:id:okazuki:20190726213916g:plain

これまでは ReactiveProperty にバリデーションエラーがある場合のみソースへの変更の反映をスキップするという形しかできませんでしたが、Rx を間に差し込めるようになったので Where などで自由にフィルタリングが出来ます。用途に応じてお使いください。

WPF on .NET Core 3.0 対応

これまでも WPF on .NET Core 3.0 に普通に NuGet から導入可能でしたが、EventToReactivePropertyEventToReactiveCommand は利用できませんでした。 このバージョンから .NET Core 3.0 でも、これらの機能が使えるようにしました。

ReactiveProperty v6 以降と .NET Core 3.0 の WPF のプロジェクトに追加して、Microsoft.Xaml.Behaviors.Wpf パッケージを追加することで使えるようになります。 注意点として、Microsoft.Xaml.Behaviors.Wpf パッケージは、まだ .NET Core 対応のパッケージが出ていないため警告がでます。各自の判断で NU1701 の警告を抑止してお使いください。

ReactiveProperty で二度押し防止 2019 年 6 月版

改版履歴

  • ReactiveProperty v5.6.0 に合わせてアップデート

本文

ということで書いていきましょう。

といっても二度押し防止系は AsyncReactiveCommand 使うと楽。以上です。 例えば非同期処理が終わるまで押せないボタンを実現したい場合は以下のような ViewModel になります。

using Reactive.Bindings;
using System.ComponentModel;
using System.Threading.Tasks;

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

        public AsyncReactiveCommand HeavyProcessCommand { get; }

        public ReactivePropertySlim<string> Message { get; }

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();
            HeavyProcessCommand = new AsyncReactiveCommand()
                .WithSubscribe(HeavyProcessAsync);
        }

        private async Task HeavyProcessAsync()
        {
            Message.Value = "処理開始!!";
            await Task.Delay(3000);
            Message.Value = "処理終了!!";
        }
    }
}

XAML 側はこんな感じで普通に適当なボタンなどの Command プロパティに紐づけるだけです。

<Window x:Class="DoubleClickApp.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:DoubleClickApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel>
        <TextBlock Text="{Binding Message.Value}" />
        <Button Content="Click me!!" Command="{Binding HeavyProcessCommand}" />
    </StackPanel>
</Window>

実行すると、いい感じに処理中はボタン押せなくなります。

f:id:okazuki:20190620195731g:plain

複数個非同期処理があって、どれかが実行中は他のボタンを押せなくしたいんだ

世の中はシンプルじゃなくて複数の非同期処理があって、どれかが実行中はボタンが押せないようにしたいということはよくあります。 AsyncReactiveCommand は、IReactiveProperty<bool> から生成することもできるのですが、この方法で作った AsyncReactiveCommand は押せない状態を共有します。

つまり、以下のように同じ IReactiveProperty<bool> を元に生成した AsyncReactiveCommand

using Reactive.Bindings;
using System.ComponentModel;
using System.Threading.Tasks;

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

        private ReactivePropertySlim<bool> SharedStatus { get; }

        public AsyncReactiveCommand HeavyProcessCommand1 { get; }
        public AsyncReactiveCommand HeavyProcessCommand2 { get; }
        public AsyncReactiveCommand HeavyProcessCommand3 { get; }

        public ReactivePropertySlim<string> Message { get; }

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();

            SharedStatus = new ReactivePropertySlim<bool>(true); // 初期状態は実行可能で
            HeavyProcessCommand1 = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcess1Async);
            HeavyProcessCommand2 = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcess2Async);
            HeavyProcessCommand3 = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcess3Async);
        }

        private async Task HeavyProcess1Async()
        {
            Message.Value = "1 番開始!";
            await Task.Delay(3000);
            Message.Value = "1 番終了!!";
        }
        private async Task HeavyProcess2Async()
        {
            Message.Value = "2 番開始!";
            await Task.Delay(3000);
            Message.Value = "2 番終了!!";
        }
        private async Task HeavyProcess3Async()
        {
            Message.Value = "3 番開始!";
            await Task.Delay(3000);
            Message.Value = "3 番終了!!";
        }
    }
}

以下のように各々のボタンにバインドすると

<Window x:Class="DoubleClickApp.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:DoubleClickApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel>
        <TextBlock Text="{Binding Message.Value}" />
        <Button Content="One" Command="{Binding HeavyProcessCommand1}" />
        <Button Content="Two" Command="{Binding HeavyProcessCommand2}" />
        <Button Content="Three" Command="{Binding HeavyProcessCommand3}" />
    </StackPanel>
</Window>

以下のようになります。

f:id:okazuki:20190620115915g:plain

やったね!

もっと複雑なケース

入力エラーが無くなったら押せるボタンが複数あって、それの二度押し防止をしつつ、他のボタンの処理が走ってる間は押せないようにしたい!というケース。 その場合は、IObservable<bool> から AsyncReactiveCommand を作る ToAsyncReactiveCommand メソッドの引数に、状態共有をするための IReactiveProperty<bool> を渡してあげれば OK です。この機能は ReactiveProperty v5.6.0 で追加されました。

例えば Input プロパティにエラーが無い時に押せる重い処理のコマンドが 2 つある場合には以下のようになります。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

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

        public AsyncReactiveCommand HeavyProcessCommand1 { get; }
        public AsyncReactiveCommand HeavyProcessCommand2 { get; }

        public ReactivePropertySlim<string> Message { get; }

        private ReactivePropertySlim<bool> SharedCanExecuteState { get; }

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

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();

            Input = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Input);

            // エラーが無くなったら押せる重たい処理のコマンド
            SharedCanExecuteState = new ReactivePropertySlim<bool>(true);
            HeavyProcessCommand1 = Input.ObserveHasErrors
                .Inverse()
                .ToAsyncReactiveCommand(SharedCanExecuteState) // 状態共有用の IReactiveProperty<bool> を渡す
                .WithSubscribe(HeavyProcessAsync);
            HeavyProcessCommand2 = Input.ObserveHasErrors
                .Inverse()
                .ToAsyncReactiveCommand(SharedCanExecuteState) // 状態共有用の IReactiveProperty<bool> を渡す
                .WithSubscribe(HeavyProcessAsync);
        }

        private async Task HeavyProcessAsync()
        {
            Message.Value = "開始!";
            await Task.Delay(3000);
            Message.Value = "終了!!";
        }
    }
}

XAML 側は以下のような感じ。

<Window x:Class="DoubleClickApp.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:DoubleClickApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel>
        <TextBlock Text="{Binding Message.Value}" />
        <TextBox Text="{Binding Input.Value, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Click me!!" Command="{Binding HeavyProcessCommand1}" />
        <Button Content="Click me!!" Command="{Binding HeavyProcessCommand2}" />
    </StackPanel>
</Window>

以下のように動きます。状態共有しつつエラーが無い時だけ押せるようになります。

f:id:okazuki:20190620152114g:plain

まとめ

AsyncReactiveCommand 割と便利。

以下古い内容

ギブアップ

入力エラーが無くなったら押せるボタンが複数あるんだけど、それの二度押し防止をしつつボタンは同時に1つしか押せないようにたいんだよね。

入力エラーの有無は IObservable<bool> で簡単に取れるので、それを ToReactiveProperty して、その ReactiveProperty<bool> から AsyncReactiveCommand を作れば勝つる!!と思うけど、それをやると以下のケースで死にます。

  • 入力エラーを無くす
  • どれかボタンを押す
  • 非同期処理が走ってる間に入力項目をエラーにして、再度エラーを無くす
  • 非同期処理が終わってなくてもボタンが押せるようになる!!

コードとしてはこんなイメージです。ダメな例。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

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

        private ReactiveProperty<bool> SharedStatus { get; }

        public AsyncReactiveCommand HeavyProcessCommand { get; }

        public ReactivePropertySlim<string> Message { get; }

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

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();

            Input = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Input);

            SharedStatus = Input
                .ObserveHasErrors
                .Inverse()
                .ToReactiveProperty();
            HeavyProcessCommand = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcessAsync);
        }

        private async Task HeavyProcessAsync()
        {
            Message.Value = "開始!";
            await Task.Delay(3000);
            Message.Value = "終了!!";
        }
    }
}

上記コードはコマンドは一つですけど、まぁ同じダメなことが起こります。ReactiveProperty<bool> が外部から書き換えられてしまうのが問題ですね。あくまで AsyncReactiveCommand 間でのステートの共有用にとどめたほうが問題が起きないです。

因みにボタンが 1 つのみの場合は以下のように普通に行けるので安心してください。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

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

        public AsyncReactiveCommand HeavyProcessCommand { get; }

        public ReactivePropertySlim<string> Message { get; }

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

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();

            Input = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Input);

            // エラーが無くなったら押せる重たい処理のコマンド
            HeavyProcessCommand = Input.ObserveHasErrors
                .Inverse()
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcessAsync);
        }

        private async Task HeavyProcessAsync()
        {
            Message.Value = "開始!";
            await Task.Delay(3000);
            Message.Value = "終了!!";
        }
    }
}

ReactiveProperty v5.5.1 をリリースしました

v5.5.1 をリリースしました。 v5.5.0 は欠番というか、依存ライブラリーのバージョン上げするの忘れてたので隠しました。

更新内容

この問題に対応しました。まぁ確かにレアケースで、こんな使い方をすることは無いとは思うのですが念のため。

github.com

あと System.Reactive を現時点の最新版の 4.1.5 にしています。

インストール

いつも通り NuGet から。

www.nuget.org

MVVM でイベント引数の値を ViewModel のコマンドに渡す方法

こちらを見て、そういえばさらっと書いてるだけだったなぁと思ったので…。

elf-mission.net

イベント引数を ViewModel で使いたい

マウス系イベントや選択系イベントは、イベント引数にしか入ってない値とかもあったりして使いたくなりますよね。 まぁ、イベントハンドラーを普通に書いて、そこからコマンド呼んでも大したことはないんですが、一応ライブラリーによっては、仕組みが用意されていたりします。

ReactiveProperty の場合

UWP と WPF 向けに用意しています。 EventToReactiveCommand と EventToReactiveProperty になります。EventToReactiveCommand がトリガーの引数を変換処理も挟みつつ ReactiveCommand の Execute メソッドの引数に渡します。EventToReactiveProperty がトリガーの引数を変換処理も挟みつつ ReactiveProeprty の Value に設定します。

これは、基本的に EventTrigger の下に置く Action として想定しています。別に他のトリガーでも動きますが。 何もしないと設定した ReactiveCommand と ReactiveProperty にトリガーに渡された引数をそのまま渡します。 ただ、引数がそのままわたるのは、ちょっとなぁ…という場合には引数を ViewModel のレイヤーのオブジェクトに変換するためのコンバーターが用意されています。

例として、マウスを動かすと、その座標を画面に表示するものを作ってみようと思います。

ViewModel の作成

とりあえず、ViewModel 側でマウス座標を表す MousePosition というクラスを作ります。

namespace ReactivePropertySample
{
    public class MousePosition
    {
        public double X { get; set; }
        public double Y { get; set; }
    }
}

そして MainWindow 用の ViewModel を作ります。 こいつは ReactiveCommand<MousePosition> と、コマンドで受け取ったものを文字列に変換して ReadOnlyReactivePropertySlim にしています。

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

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

        public ReactiveCommand<MousePosition> MouseMoveCommand { get; }

        public ReadOnlyReactivePropertySlim<string> Message { get; }

        public MainWindowViewModel()
        {
            MouseMoveCommand = new ReactiveCommand<MousePosition>();
            Message = MouseMoveCommand.Select(x => $"({x.X}, {x.Y})")
                .ToReadOnlyReactivePropertySlim();
        }
    }
}

では、この ViewModel と View をつないでいきます。まず MouseMove イベントの引数の MouseEventArgs から MousePosition への変換処理を書きます。これは ReactiveConverter<変換元型名, 変換先型名> クラスを継承して OnConvert メソッドをオーバーライドして書きます。 こんな感じ。

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

namespace ReactivePropertySample
{
    public class MouseMoveToMousePositionConverter : ReactiveConverter<MouseEventArgs, MousePosition>
    {
        protected override IObservable<MousePosition> OnConvert(IObservable<MouseEventArgs> source) => source
            .Select(x => x.GetPosition((IInputElement)AssociateObject))
            .Select(x => new MousePosition
            {
                X = x.X,
                Y = x.Y,
            });
    }
}

あとは、View で EventTrigger と EventToReactiveCommand と 先ほど作成した MouseMoveToMousePositionConverter を使ってイベントと ReactiveCommand をつなぎます。

<Window
    x:Class="ReactivePropertySample.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:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.NET46"
    xmlns:local="clr-namespace:ReactivePropertySample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseMove">
            <interactivity:EventToReactiveCommand Command="{Binding MouseMoveCommand}">
                <local:MouseMoveToMousePositionConverter />
            </interactivity:EventToReactiveCommand>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Text="{Binding Message.Value}" />
    </Grid>
</Window>

実行してマウスを動かすと座標が画面に出ます。

f:id:okazuki:20190419163135p:plain

Prism の場合

Prism の場合は WPF と Xamarin.Forms 用で似たような機能が提供されています。大体同じ使い方(WPF は Trigger の Action として、Xamarin.Forms は Behavior として提供されています)なので今回は WPF の使い方を試してみます。

Prism のクラスとして InvokeCommandAction があります。 これは Command プロパティに指定したコマンドを呼び出すアクションです。

この時コマンドの引数に渡すものを指定する方法として CommandParameter プロパティに指定する方法と、TriggerParameterPath に指定する方法の 2 通りがあります。CommandParameter は普通に Binding とかを指定できます。TriggerParameterPath は、トリガーのパラメーター(EventTrigger の場合はイベント引数)から、任意のプロパティを受け渡します。

今回は、イベント引数の GetPosition メソッドを呼んだ結果を受け渡したいので…無理じゃん!

どうしよう

逃げ道としては、イベント引数をそのまま受け取る方法ですが、今回は GetPosition を呼びたい。引数は View のクラス…辛い。おとなしくコードビハインドですね。何も問題ない。

でも、こういうケースがアプリ内でたくさんあるなら部品化する価値はある。ということで部品化するとしたら、Prism の InvokeCommandAction を参考にして間に任意の変換処理を挟むための IHogeHogeConverter Converter { get; set; } プロパティでも追加してあげる感じでしょうか…?

いっそのことプルリクエストしてもいいかもしれませんね。

Livet の場合

こっちも特にイベント引数を加工するような仕組みはないように見えるので、皆どうしてるんだろう?機能欲しい?

まとめ

意外と ReactiveProperty のやつが何でもやりたい放題だった。