導入
タイトルが空のまま「追加」ボタンが押せてしまうと、空のタスクができてしまいます。CanExecuteがfalseを返している間、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;のままでよい
まとめ
CanExecuteがfalseの間、Viewはボタンを自動的に非活性化する- 判定材料が変わったら
RaiseCanExecuteChanged()で再確認を促す必要がある - 「いつ呼ぶか」を忘れるとボタンが更新されない、というMVVMでありがちなバグを防げる
次回: 完成に近づいたTaskListViewModelを、Serviceをテスト用の偽物に差し替えてテストします。