導入
タイトルが空のまま「追加」ボタンが押せてしまうと、空のタスクができてしまいます。RelayCommandのcanExecuteにラムダを渡しておけば、呼ばれるたびにそのときの最新の状態を確認できます。C#のICommandのような「実行可否が変わったことを通知するイベント」はこのコースのCommandにはありません。画面側は必要なタイミングでcanExecute()を呼び直すだけでよい、というシンプルな設計です。
図解
flowchart LR
T["TextField(View)"] -->|入力のたびに| VM["setNewTaskTitle(value)"]
VM --> F["newTaskTitleフィールドが変わる"]
B["Button(View)"] -->|表示のたびに| CE["addCommand.canExecute()"]
CE -->|現在のnewTaskTitleを見て判定| F
説明
小さな例、AddTaskViewModelで確認します。ポイントは、canExecuteに渡すラムダがnewTaskTitleというフィールドそのものを参照していることです。呼ばれるたびにそのときの値を見るので、別に「変わったことを通知する」仕組みは要りません。
public class AddTaskViewModel extends ViewModelBase {
private String newTaskTitle = "";
private final Command addCommand;
public AddTaskViewModel() {
addCommand = new RelayCommand(() -> {}, () -> !newTaskTitle.isBlank());
}
public void setNewTaskTitle(String value) {
newTaskTitle = value;
raisePropertyChanged("newTaskTitle");
}
public Command getAddCommand() {
return addCommand;
}
}
AddTaskViewModel vm = new AddTaskViewModel();
System.out.println(vm.getAddCommand().canExecute()); // false(空文字なので不可)
vm.setNewTaskTitle("牛乳を買う");
System.out.println(vm.getAddCommand().canExecute()); // true
() -> !newTaskTitle.isBlank()は、呼ばれるたびにそのときのnewTaskTitleを読みにいくラムダ。値を1回だけコピーしているわけではないRelayCommandはコンストラクタで受け取ったラムダを保持しているだけなので、AddTaskViewModelのインスタンスを再生成しない限り、常に最新の状態を反映する- 画面側(第7章)は、入力欄の値が変わるたびに
canExecute()を呼び直してボタンの活性・非活性を切り替える
やってみよう
setNewTaskTitle("")で空文字に戻したとき、canExecute()が再びfalseになることを、下のテストで確認してみましょう。
演習
エディタは4つのタブに分かれています――ViewModelBase.java/Command.java/RelayCommand.java(すべて前レッスンまでに完成させた土台)・AddTaskViewModel.java(今日のTODO)・AddTaskViewModelTest.java(テスト)です。
import java.util.ArrayList;
import java.util.List;
public interface PropertyChangedListener {
void onPropertyChanged(String propertyName);
}
public abstract class ViewModelBase {
private final List<PropertyChangedListener> listeners = new ArrayList<>();
public void addPropertyChangedListener(PropertyChangedListener listener) {
listeners.add(listener);
}
protected void raisePropertyChanged(String propertyName) {
for (PropertyChangedListener listener : listeners) {
listener.onPropertyChanged(propertyName);
}
}
}
public interface Command {
void execute();
default boolean canExecute() { return true; }
}
import java.util.function.BooleanSupplier;
public class RelayCommand implements Command {
private final Runnable action;
private final BooleanSupplier canExecuteCheck;
public RelayCommand(Runnable action) {
this.action = action;
this.canExecuteCheck = null;
}
public RelayCommand(Runnable action, BooleanSupplier canExecute) {
this.action = action;
this.canExecuteCheck = canExecute;
}
public void execute() {
action.run();
}
public boolean canExecute() {
return canExecuteCheck == null || canExecuteCheck.getAsBoolean();
}
}
// TODO: AddTaskViewModel を完成させてください
// ・newTaskTitle: String(既定は空文字)
// ・setNewTaskTitle(value): newTaskTitle を更新し、"newTaskTitle" を通知する
// ・addCommand: RelayCommand。canExecute は newTaskTitle が空でないこと
// ・getAddCommand(): addCommand を返す
public class AddTaskViewModel extends ViewModelBase {
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class AddTaskViewModelTest {
@Test
void titleNoUmuDeCanExecuteGaSeigyoSareru() {
AddTaskViewModel vm = new AddTaskViewModel();
assertFalse(vm.getAddCommand().canExecute(), "空タイトルなら追加不可");
vm.setNewTaskTitle("牛乳を買う");
assertTrue(vm.getAddCommand().canExecute(), "タイトルがあれば追加可能");
vm.setNewTaskTitle("");
assertFalse(vm.getAddCommand().canExecute(), "空に戻すと再び不可");
}
}
- 期待される結果: 1件のテストが成功
ヒント1を見る
private String newTaskTitle = ""; private final Command addCommand; public AddTaskViewModel() { addCommand = new RelayCommand(() -> {}, () -> !newTaskTitle.isBlank()); }
ヒント2を見る
public void setNewTaskTitle(String value) { newTaskTitle = value; raisePropertyChanged("newTaskTitle"); } public Command getAddCommand() { return addCommand; }
まとめ
canExecuteに渡すラムダは、呼ばれるたびにそのときのフィールドの値を見る。だから「変わったことを通知する」仕組みは不要- ボタンの活性・非活性は、画面側が必要なタイミングで
canExecute()を呼び直すだけで実現できる - 「いつ
canExecute()を呼び直すか」は次章、View(JavaFX)を接続するときに扱います
次回: 完成に近づいたTaskListViewModelを、Serviceをモックに差し替えてテストします。