本文へスキップ
BecomeCoder

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

canExecute ― 実行可否を制御する

ブラウザで完結

導入

タイトルが空のまま「追加」ボタンが押せてしまうと、空のタスクができてしまいます。RelayCommandcanExecuteにラムダを渡しておけば、呼ばれるたびにそのときの最新の状態を確認できます。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.javaCommand.javaRelayCommand.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をモックに差し替えてテストします。

実際に動かしてみよう

このレッスンのサンプルは、実務と同じように役割ごとの .java ファイルへ分けてあります。下のエディタは最初からその複数ファイルが入った状態で、上のタブでファイルを切り替えられます。そのまま「▶ 実行」を押せば全ファイルをまとめて解釈して動かせます。@Test の付いたテストがあるレッスンでは、テストメソッドごとに ✅/❌ の一覧(Red/Green)が出るので、まずテストを赤くしてから実装で緑にする、というTDDの回し方をその場で体験できます(本物のJVMではなく、JUnit・Mockitoの主要な書き方まで再現した学習用シミュレータです)。

Java — ブラウザ内で実行(学習用シミュレータ)

Javaの教材サブセットを動かす学習用シミュレータを読み込みます(本物のJVMではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。