C#
.NET
WPF
ReactiveProperty

AsyncReactiveCommandでWPFのお手軽ダブルクリック抑制

More than 1 year has passed since last update.


背景

世の中には特に意味はなくても、ボタンをダブルクリックする人種がいるのです。

「ボタンはダブルクリックするものだと思っていた」

「不安なのでダブルクリックした」

「せっかちだから」

理由はそれぞれですが、

当然クリックイベントに結び付けられた処理は2回行われてしまい、

(押した人からすると)意図しない動作をし、激怒します。

AsyncReactiveCommandを使用すると簡単にこれを防ぐことが出来ます。

あなたのプログラムに意図せず2回動作したらまずい処理(ex.課金アイテムの使用、発注書の送信など)が含まれていたら、AsyncReactiveCommandを検討する価値はあります。


ReactiveProperty v2.8以前

AsyncReactiveCommand登場前のV2.8以前ではCommandとは別にプロパティを作り、コマンドの実行可否を結びつける必要がありました。

参考:ReactiveProperty で2度押し防止(Using使ったやつ)

しかし複数のCommandを1つのプロパティに結びつけた場合、

全てのCommandが同時に押せなくなってしまうため、

それはそれで複数のCommand(ボタン)を連続で押したいせっかちな人は激怒します。

かといってダブルクリックを防止するためだけに、全てのCommandで

A. プロパティを同じ数用意する

B. RxのBufferなどで一定時間以内のイベントをまとめる

のもめんどうです。

Aの場合ですとかんな感じです。


v2.8以前

class MainWindowViewModel

{
public ReactiveProperty<bool> IsBusy1 { get; }
= new ReactiveProperty<bool>(false);
public ReactiveProperty<bool> IsBusy2 { get; }
= new ReactiveProperty<bool>(false);

public ReactiveCommand ButtonClickAsyncCommand1 { get; }
public ReactiveCommand ButtonClickAsyncCommand2 { get; }

public MainWindowViewModel()
{
ButtonClickAsyncCommand1 = IsBusy1.Select(x => !x).ToReactiveCommand();
ButtonClickAsyncCommand2 = IsBusy2.Select(x => !x).ToReactiveCommand();

ButtonClickAsyncCommand1.Subscribe(async _ =>
{
IsBusy1.Value = true;
Debug.WriteLine("Clicked ASync 1");
await Task.Delay(500);
IsBusy1.Value = false;

});
ButtonClickAsyncCommand2.Subscribe(async _ =>
{
IsBusy2.Value = true;
Debug.WriteLine("Clicked ASync 2");
await Task.Delay(500);
IsBusy2.Value = false;

});
}
}


無駄に長いです。

上記リンク記事の最後にもあるように

「IsBusy プロパティを持った ReactiveCommand」

があればこれを解決できます。

そしてこれがまさにv2.9で登場したAsyncReactiveCommandです。


AsyncReactiveCommandを使用

AsyncReactiveCommandを使用してコードを書き直してみます。


AsyncReactiveCommand使用

class MainWindowViewModel

{
public AsyncReactiveCommand ButtonClickAsyncCommand1 { get; }
= new AsyncReactiveCommand();
public AsyncReactiveCommand ButtonClickAsyncCommand2 { get; }
= new AsyncReactiveCommand();

public MainWindowViewModel()
{
ButtonClickAsyncCommand1.Subscribe(async _ =>
{
Debug.WriteLine("Clicked ASync 1");
await Task.Delay(500);
});
ButtonClickAsyncCommand2.Subscribe(async _ =>
{
Debug.WriteLine("Clicked ASync 2");
await Task.Delay(500);
});
}
}


短い。

動作は

スクリーンショット 2017-02-11 13.25.45.png

からボタンをクリックすると

スクリーンショット 2017-02-11 13.25.30.png

になり、0.5秒経過すると上の画面に戻ります。

それぞれのCommandは独立しているので、片方が押された後、別のCommandをすぐ押すこともできます。

なお、Commandが同時に動くのを防ぎたいのであれば、

ReactiveProperty v2.9.0とv3.0.0-pre5をリリースしました。 - かずきのBlog@hatena

のように共通プロパティを使用することも出来ます。


まとめ

AsyncReactiveCommandを使用することで簡単に意図しないダブルクリックを抑制できました。

上記リンクでもそうですが、AsyncReactiveCommandは重たい処理や同時に実行されたくない処理で使用する、というシーンで説明されることが多いです。

しかし重たい処理でなくとも、意図せず2回実行されては困る場合にも有用です。

またバックグラウンドで動く処理などの場合、UI側に処理したことを伝える必要があります。

ただ単に処理が動いたことを伝えたいのであれば、

上記コードのように処理後にAsyncReactiveCommandを0.5秒ほど待機させれば、その間ボタンがグレイアウトしますのでView側には何も書かずにすみます。


おまけ

AsyncReactiveCommandのコードスニペット

これ書いてから、そもそもAsyncReactiveCommandの記事自体がQiitaに無いことに気づきました。

これ使ってガンガンAsyncReactiveCommandを書いて下さい。


環境

VisualStudio2015

.NET Framework 4.6

C#6

ReactiveProperty