ということで書いていきましょう。
といっても二度押し防止系は AsyncReactiveCommand
使うと楽。以上です。
例えば非同期処理が終わるまで押せないボタンを実現したい場合は以下のような ViewModel になります。
using Reactive.Bindings; using System.ComponentModel; using System.Threading.Tasks; namespace DoubleClickApp { publicclass MainWindowViewModel : INotifyPropertyChanged { publicevent 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>
実行すると、いい感じに処理中はボタン押せなくなります。
複数個非同期処理があって、どれかが実行中は他のボタンを押せなくしたいんだ
世の中はシンプルじゃなくて複数の非同期処理があって、どれかが実行中はボタンが押せないようにしたいということはよくあります。
AsyncReactiveCommand
は、IReactiveProperty<bool>
から生成することもできるのですが、この方法で作った AsyncReactiveCommand
は押せない状態を共有します。
つまり、以下のように同じ IReactiveProperty<bool>
を元に生成した AsyncReactiveCommand
を
using Reactive.Bindings; using System.ComponentModel; using System.Threading.Tasks; namespace DoubleClickApp { publicclass MainWindowViewModel : INotifyPropertyChanged { publicevent 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>
以下のようになります。
やったね!
ギブアップ
入力エラーが無くなったら押せるボタンが複数あるんだけど、それの二度押し防止をしつつボタンは同時に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 { publicclass MainWindowViewModel : INotifyPropertyChanged { publicevent 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 { publicclass MainWindowViewModel : INotifyPropertyChanged { publicevent 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 = "終了!!"; } } }
まとめ
AsyncReactiveCommand
割と便利。