LoginSignup
19
20

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-04-12

概要

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

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

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

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

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を見るとこんなことになってたりする。

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

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

定番: RaiseCanExecuteChanged()

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

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
CommandManager.InvalidateRequerySuggested Method - Microsoft Docs

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

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を発火させる方がより望ましいだろう。

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からは離れてしまうが、きれいな方法。

19
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
20