本文へスキップ
BecomeCoder

C# MVVMコース · 第6章 ViewModel ― 画面の状態と操作 · レッスン23

CanExecute ― 実行可否を制御する

ブラウザで完結

導入

タイトルが空のまま「追加」ボタンが押せてしまうと、空のタスクができてしまいます。CanExecutefalseを返している間、Viewは自動的にボタンをグレーアウトします。ただし、NewTaskTitleが変わったタイミングでViewに「もう一度CanExecuteを聞き直して」と伝える必要があります。それがRaiseCanExecuteChanged()です。

図解

sequenceDiagram
    participant V as View(TextBox)
    participant VM as ViewModel
    participant C as AddCommand
    V->>VM: NewTaskTitle が変わる
    VM->>C: RaiseCanExecuteChanged()
    C-->>V: CanExecuteChanged イベント
    V->>C: CanExecute() を再確認
    V->>V: ボタンの活性/非活性を更新

サンプル

using System.Windows.Input;

var addCommand = new RelayCommand(() => Console.WriteLine("追加"), () => false);
addCommand.CanExecuteChanged += (s, e) => Console.WriteLine("CanExecuteChangedが発火");

Console.WriteLine(addCommand.CanExecute(null));   // False
addCommand.RaiseCanExecuteChanged();              // CanExecuteChangedが発火

class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool>? _canExecute;
    public RelayCommand(Action execute, Func<bool>? canExecute = null) { _execute = execute; _canExecute = canExecute; }
    public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
    public void Execute(object? parameter) => _execute();
    public event EventHandler? CanExecuteChanged;
    public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
  • RaiseCanExecuteChanged()は「CanExecuteの結果が変わったかもしれないので再確認して」とViewへ伝えるメソッド
  • ViewModel側は、CanExecuteの判定材料になるプロパティ(NewTaskTitle等)が変わるたびにこれを呼ぶ責任を持つ

演習

using System.Windows.Input;
using System.ComponentModel;
using System.Runtime.CompilerServices;

var vm = new AddTaskViewModel();
int canExecuteChangedCount = 0;
vm.AddCommand.CanExecuteChanged += (s, e) => canExecuteChangedCount++;

Assert(vm.AddCommand.CanExecute(null) == false, "空タイトルなら追加不可");

vm.NewTaskTitle = "牛乳を買う";
Assert(vm.AddCommand.CanExecute(null) == true, "タイトルがあれば追加可能");
Assert(canExecuteChangedCount == 1, "タイトル変更でCanExecuteChangedが1回発火する");

vm.NewTaskTitle = "";
Assert(vm.AddCommand.CanExecute(null) == false, "空に戻すと再び不可");
Assert(canExecuteChangedCount == 2, "変更のたびに発火する");

Console.WriteLine("全テスト成功");

class AddTaskViewModel : ViewModelBase
{
    private string _newTaskTitle = "";
    public string NewTaskTitle
    {
        get => _newTaskTitle;
        set
        {
            if (SetProperty(ref _newTaskTitle, value))
                AddCommand.RaiseCanExecuteChanged();
        }
    }
    public RelayCommand AddCommand { get; }

    public AddTaskViewModel()
    {
        AddCommand = new RelayCommand(() => { }, () => !string.IsNullOrWhiteSpace(NewTaskTitle));
    }
}

// TODO: ICommand を実装する RelayCommand を、RaiseCanExecuteChanged() 付きで完成させてください
class RelayCommand : ICommand
{
    ___
}

abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? name = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(name);
        return true;
    }
}

void Assert(bool condition, string name)
{
    if (!condition) throw new Exception($"FAIL: {name}");
}
  • 期待される出力: 全テスト成功
ヒント1を見る

RelayCommandは前レッスンの実装にpublic void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);を追加するだけです

ヒント2を見る

CanExecuteChangedイベント自体は前レッスンと同じpublic event EventHandler? CanExecuteChanged;のままでよい

まとめ

  • CanExecutefalseの間、Viewはボタンを自動的に非活性化する
  • 判定材料が変わったらRaiseCanExecuteChanged()で再確認を促す必要がある
  • 「いつ呼ぶか」を忘れるとボタンが更新されない、というMVVMでありがちなバグを防げる

次回: 完成に近づいたTaskListViewModelを、Serviceをテスト用の偽物に差し替えてテストします。

実際に動かしてみよう

本文のサンプルや演習のコードは、コードブロック右上の「コピー」ボタンでコピーして、下のエディタに貼り付ければそのまま実行できます。

C# — ブラウザ内で実行

ブラウザ内でC#を動かす環境を読み込みます(初回のみ数秒)。
スクロールして表示された時点でも自動で読み込まれます。