概要
ICommandはWPFでは出番が多いが、実装が面倒と評判ではある。
その実装で楽をするために、DelegateCommandやRelayCommandと呼ばれる実装方法が定番になっているが、CanExecuteChangedを呼ぶ処理の実装はまちまちである(下手すると実装されていない)。
ここではCanExecuteChangedを呼ぶ処理の実装を何種類か挙げる。
まずはCanExecuteChangedを呼んでいない例
"DelegateCommand"でググるとそこそこ出てくる。
public class DelegateCommand : ICommand
{
Action<object> _execute;
Predicate<object> _canExecute;
public DelegateCommand(Action<object> execute, Predicate<object> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return canExecute?(parameter) ?? true;
}
public void Execute(object Parameter)
{
_execute(parameter);
}
}
エラー一覧に「CanExecuteChangedは使用されていません」と出てきたりする。
そしてxamlを見るとこんなことになってたりする。
<Button Command="{Binding HidoiCommand}"
IsEnabled="{Binding IsHidoiEnabled}"/>
にゃーん(社会性フィルター)
定番: RaiseCanExecuteChanged()
CanExecuteChangedを叩けるようにする定番の方法としては、RaiseCanExecuteChanged()メソッドを用意するというのがある。
古いPrismはこの方式だった(今はObservesProperty()で楽ができる)。
public class DelegateCommand : ICommand
{
//他のメソッドなどは省略
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
ただし、RaiseCanExecuteChanged()の呼び忘れには注意が必要。
それ以前の問題としてめんどくさい。
もうひとつ定番: CommandManager.RequerySuggested
これもググると結構出てくる。
CommandManagerが、UIでのフォーカス変更などを検出してRequerySuggestedイベントを発生させてくれる。これに連動してCanExecuteChangedを発生させるという方法である。
ただし、特定の操作に連動してイベントを発生させる仕様上、必要なときにイベントが発生していない事態が発生しうる。
→どうやら「ICommandをバインド可能なコントロールに対して何らかの操作をしたとき」に発生するようだ。このあたりはさらに調べる必要がありそう。
参考:
WPFサンプル:ICommand インターフーイスを実装してカスタムコマンドを作成する:Gushwell's Dev Notes
CommandManager.InvalidateRequerySuggested Method - Microsoft Docs
RequerySuggestedイベントを手動で発生させたいときは、CommandManager.InvalidateRequerySuggested()を適宜呼ぶ必要がある。これはどこから呼んでも全CommandのCanExecuteChangedが呼ばれることになるので、その点は楽になる。
public class DelegateCommand : ICommand
{
//他のメソッドなどは省略
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
雑な方法: ViewModelのPropertyChangedを拾う
なんだかんだで、CanExecute()の戻り値はViewModelのプロパティの組み合わせになっていることが多い。
そんなときは、ViewModelのPropertyChangedに連動してCanExecuteChangedを呼び出せばどうにかなる。
もちろん、CanExecute()で使う全プロパティにおいて、正しくNotifyPropertyChangedが呼び出されていることが前提となる。
例えば、ViewModelにINotifyDataErrorInfoが実装されていて、うっかりHasErrorsを使うと…などとなりがち。
この例ではHasErrorsのSetterでNotifyPropertyChangedを呼ぶようにしてしまえばとりあえず動くが、ErrorsChangedイベントも購読してCanExecuteChangedを発火させる方がより望ましいだろう。
public class DelegateCommand : ICommand
{
Action<object> _execute;
Predicate<object> _canExecute;
public DelegateCommand(INotifyPropertyChanged viewModel, Action<object> execute, Predicate<object> canExecute = null)
{
viewModel.PropertyChanged += viewModel_PropertyChanged;
_execute = execute;
_canExecute = canExecute;
}
void viewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//手抜き 本来はCanExecuteの値が変わる時だけ呼び出すべき
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
if(_canExecute == null) { return true; }
else { return _canExecute(parameter); }
}
public void Execute(object Parameter)
{
_execute(arameter);
}
}
美しい方法たち
紹介記事はたくさんあるので検索してほしい(面倒になっただけ)
いずれもNuGetパッケージが必要になる。
美しい方法1: ObservesProperty()を使う
Prism6で使用可能。
「ViewModelのPropertyChangedを拾う」のような汚さから解放される。
美しい方法2: ReactiveCommandを使う
これはDelegateCommandからは離れてしまうが、きれいな方法。