CanExecuteChangedめんどくさい問題を調べてみた


# 概要

ICommandはWPFでは出番が多いが、実装が面倒と評判ではある。
その実装で楽をするために、DelegateCommandやRelayCommandと呼ばれる実装方法が定番になっているが、CanExecuteChangedを呼ぶ処理の実装はまちまちである(下手すると実装されていない)。

ここではCanExecuteChangedを呼ぶ処理の実装を何種類か挙げる。

# まずはCanExecuteChangedを呼んでいない例

"DelegateCommand"でググるとそこそこ出てくる。

```C#:TypicalDeletgateCommand.cs
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を見るとこんなことになってたりする。

```xml:HidoiButton.xaml
<Button Command="{Binding HidoiCommand}"
        IsEnabled="{Binding IsHidoiEnabled}"/>
````

にゃーん(社会性フィルター)

# 定番: RaiseCanExecuteChanged\(\)

CanExecuteChangedを叩けるようにする定番の方法としては、RaiseCanExecuteChanged()メソッドを用意するというのがある。
古いPrismはこの方式だった(今はObservesProperty()で楽ができる)。

```C#:RaiseCanExecuteChanged.cs
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](http://gushwell.ldblog.jp/archives/52321430.html)
[CommandManager.InvalidateRequerySuggested Method - Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.input.commandmanager.invalidaterequerysuggested?view=netframework-4.7.2)

RequerySuggestedイベントを手動で発生させたいときは、CommandManager.InvalidateRequerySuggested()を適宜呼ぶ必要がある。これはどこから呼んでも全CommandのCanExecuteChangedが呼ばれることになるので、その点は楽になる。

```C#:RequerySuggested.cs
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を発火させる方がより望ましいだろう。

```C#:VMDeletgateCommand.cs
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からは離れてしまうが、きれいな方法。