もくじ
■連打防止関連
ボタン押したときに時間がかかる処理をawaitでやるときに、ボタン連打を防止したい
→https://qiita.com/tera1707/items/a6f11bd3bf2dbf97dd40
ボタン押したときに時間がかかる処理をawaitでやるときに、ボタン連打を防止したい その2
→https://qiita.com/tera1707/items/946116bf32d0f1203006
やりたいこと
以前、掲題の内容をやりたくて、prismのDelegateCommand
クラスを使って連打防止をやってみた。
ただそのときのやり方だと、同じようなことを複数のボタンでやろうとしたときに、同じようなフラグを何個も作らないといけなくなるため、もう少しマシなやり方を探していたところ、その記事に、@unidentifiedexeさんに良いやり方のコメントを頂いた。
コードのサンプルも書いて頂いて、そのまま使えそうな感じだったのだが、一応自分でも理解しておきたいということで、練習がてらコードを纏めてみたい。
サンプルコード
WPFの画面(xaml)、ViewModelと今回作成したコマンドのクラスのコードは下記の通り。(コードビハインドは省略)
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="500" Width="800">
<StackPanel>
<Button Content="押すと2秒間処理をし、その間は自動で無効になるボタン" FontSize="25" Command="{Binding VmMyCommand1}" Margin="20"/>
<Button Content="ボタン1の有効無効をVMのフラグで切り替えるボタン" FontSize="25" Command="{Binding VmMyCommand2}" Margin="20"/>
</StackPanel>
</Window>
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;
namespace WpfApp1
{
class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
// ボタン押された時のCommand
public UnRepeatableAsyncCommand VmMyCommand1 { get; private set; }
public UnRepeatableAsyncCommand VmMyCommand2 { get; private set; }
public bool MyCamExecuteFlag
{
get { return _myCamExecuteFlag; }
set { _myCamExecuteFlag = value; OnPropertyChanged(nameof(MyCamExecuteFlag)); }
}
private bool _myCamExecuteFlag = true;
public ViewModel()
{
// (ボタン1) 押したら2秒かかる処理を非同期で行って、その間は自動で無効になるボタン
VmMyCommand1 = new UnRepeatableAsyncCommand(MyAsyncFunc, MyCanExecute);
VmMyCommand1.CanExecuteChanged += ((sender, e) => Debug.WriteLine("CanExecuteChanged1"));
// (ボタン2) ボタン1の有効無効をViewModelから切り替えるボタン
VmMyCommand2 = new UnRepeatableAsyncCommand(async () =>
{
MyCamExecuteFlag = !MyCamExecuteFlag; // CanExecuteで見るフラグ
VmMyCommand1.RaiseCanExecuteChanged(); // ★CanExecuteが変化したことを使えないと、フラグ切り替えても有効無効変わらない!
});
VmMyCommand2.CanExecuteChanged += ((sender, e) => Debug.WriteLine("CanExecuteChanged2"));
}
// 実験用 押したときに2秒かかる処理実施
public async Task MyAsyncFunc()
{
Debug.WriteLine("押された");
await Task.Delay(2000);
Debug.WriteLine("処理完了");
}
// フラグのON/OFFでボタンの有効無効を切り替える
public bool MyCanExecute()
{
return MyCamExecuteFlag;
}
}
}
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace WpfApp1
{
public class UnRepeatableAsyncCommand : ICommand
{
Func<Task> execute;
Func<bool> canExecute;
public event EventHandler CanExecuteChanged;
// 処理中フラグ
private bool isExecuting = false;
public bool IsExecuting
{
get { return isExecuting; }
set
{
isExecuting = value;
RaiseCanExecuteChanged();
}
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
// 本クラスを使う側が設定するCanExecuteに加え、処理中フラグのON/OFFを有効無効条件に加える
public bool CanExecute(object parameter) => (canExecute != null) ? (canExecute() && !isExecuting) : (!isExecuting);
// 処理実行の前後に、無効化→有効化、の処理を追加する
public async void Execute(object parameter)
{
IsExecuting = true;
await execute();
IsExecuting = false;
}
public UnRepeatableAsyncCommand(System.Func<Task> execute)
{
this.execute = execute;
}
public UnRepeatableAsyncCommand(System.Func<Task> execute, System.Func<bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
}
}
UnRepeatableAsyncCommand
が、今回作った連打防止Commandを実装したクラス。
残っている疑問
疑問
このコードの動きとしては、
- ボタンを押すと
- ボタンが無効化する(グレーアウトする)
- 2秒経つと
- ボタンが有効化する(グレーアウト解除)
という動きなのだが、
連打防止コマンドクラスの中にあるpublic async void Execute(object parameter)
の中の2か所のCanExecuteChanged?.Invoke(this, EventArgs.Empty);
をコメントアウトすると、ボタンが無効にはなるのだが、見た目がグレーアウトしなくなる。
自分の理解が足りてない部分なのだが、
ICommand
のCanExecuteChanged
イベントハンドラは、CanExecuteChangedにメソッドを入れておくと、WPFのフレームワークが、CanExecuteが変化したタイミングで勝手に入れたメソッドを呼んでくれる、というものではなかったか??
(つまり、自分でそのイベントハンドラを呼ぶようなものではないと思っていた)
サンプルコードのとおり、CanExecuteChanged?.Invoke(this, EventArgs.Empty);
をしてやると見た目も変わってくれるが、なぜそのような動きになるのか?が現状わかっていない...
(が、とりあえず動くものにはなったのでメモ代わりに残す...)
疑問への対応(21/03/23追記)
albireoさんからコメント頂いた内容をもとに、コードを直してみた。
- 画面が持つボタンを、
- ボタン1(上側のボタン)の有効無効を、ボタン2(下側のボタン)を押すと切替できるようにした。
- その切り替えは、ViewModelが持つプロパティのONOFFで行う
(つまりそのフラグの変化=CanExecuteの変化にする)
-
UnRepeatableAsyncCommand
クラスに、CanExecuteChanged()
が変化したことを知らせるためのRaiseCanExecuteChanged()
を実装追加 - ViewModelで、CanExecuteが変わるであろう部分で、該当のUnRepeatableAsyncCommandの
RaiseCanExecuteChanged()
を呼ぶようにした
これで、思ったことはひと通り出来てるだろうか...
参考
CanExecuteChangedめんどくさい問題を調べてみた
https://qiita.com/204504bySE/items/0c7d5ac6913673dc10f5