JavaFXでダイアログの入力チェック制御をちゃんと考えてみる

# この記事について

将来的に書く予定の「JavaFX で DynamoDB Viewer作ってみた」記事の１ステップ。
結構大きな話になると思うので、少しずつ技術ポイント毎に記事を書いて、ある一定程度の要件を満たせた段階で前述まとめ記事書く予定。

第一回記事：[DynamoDBの情報を読み込んでJavaFXで表示してみる](https://qiita.com/silverbox/items/eba824a9f003df67181e)
第二回記事：[JavaFXで動的にテーブル列を設定する](https://qiita.com/silverbox/items/904a01d561014d800696)
第三回記事：[AWS java SDKでDynamoDBテーブル情報を取得してみる](https://qiita.com/silverbox/items/3caefb782f1bb269490f)
第四回記事：[JavaFX の TableView の選択範囲をクリップボードにコピーする。](https://qiita.com/silverbox/items/19e131bd3da02940a8c1)
第五回記事：[JavaFX でコンポーネント作って動的生成してみる](https://qiita.com/silverbox/items/3128fd2128374e11c1f7)
第六回記事：[DynamoDBのデータ型をjava SDKから把握してみる。](https://qiita.com/silverbox/items/3a434ec4a85262f973c9)
第七回記事：[JavaFXでジェネリック使って入力ダイアログ作成してみる](https://qiita.com/silverbox/items/bb3cee5a328ccc63246c)
第八回記事：[JavaFXで各種サイズ制御](https://qiita.com/silverbox/items/17b7982683be06c32240)
第九回記事：[JavaFXのDialogで入力値チェック制御](https://qiita.com/silverbox/items/ad7542db5d34a442dce5)
第十回記事：[DynamoDBでPartiQLをjavaSDKで実行してみる。](https://qiita.com/silverbox/items/4c80f34285b8691af93a)

※これまでの記事が基本になってます。メソッドなど細かい部分で再説明していない部分があります。不明点などありましたらコメントなど頂けたら対応しようと思います。

# 今回の追加機能

- キャンセルの時にも走っていたチェックなどを整理して、OKボタンの時のみチェックやDB更新確認を実行
- 読み込み時のダイアログ表示がちゃんとしてなかったので制御
- PartiQLでは、テーブル名にハイフン（ - ）があるとエラー（Unexpected from source）になるのでテーブル名をダブルコーテーションで括るように

# 現在の進捗
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530376/69d22090-1f35-2ab6-3859-03de499b50b8.png)

[この時点でのソース(github)](https://github.com/silverbox/dynamodbtool/tree/12th-dialogmanagement)

# 状況

第九回記事：[JavaFXのDialogで入力値チェック制御](https://qiita.com/silverbox/items/ad7542db5d34a442dce5)で、入力値チェック制御を行ったが、キャンセル時にもチェックが走ってしまい煩わしい。いいかげんちゃんと制御したい。

# JavaFXの標準ライブラリのソースを見てみる

重要なイベントを見て、制御方法を考える

## javafx.scene.control.Dialog

### closeイベント

```java:Dialog.java（※省略部分あり＆説明コメント追加）

    public final void close() {
        if (isClosing) return;
        isClosing = true;

        // ダイアログの結果取得処理が走る。結果がnullなら後続処理でcancel扱いになる。
        final R result = getResult();

        // nullにした時、Cancelボタンが無いモードなら閉じない
        if (result == null && ! dialog.requestPermissionToClose(this)) {
            isClosing = false;
            return;
        }

        // 結果がnullの時、ダミーの結果をセットして再度closeする
        if (result == null) {
            // 中略

            // キャンセル処理を強制的に実行する
            impl_setResultAndClose(cancelButton, false);
        }

        // DIALOG_HIDINGイベント実行
        Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_HIDING));

        // DIALOG_CLOSE_REQUESTイベント実行
        DialogEvent closeRequestEvent = new DialogEvent(this, DialogEvent.DIALOG_CLOSE_REQUEST);
        Event.fireEvent(this, closeRequestEvent);

        // DIALOG_CLOSE_REQUESTイベントの中で、isConsumedされたら閉じない
        if (closeRequestEvent.isConsumed()) {
            isClosing = false;
            return;
        }

        dialog.close();

        Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_HIDDEN));

        isClosing = false;
    }

```

### getResult

```java:
    /**
     * A property representing what has been returned from the dialog. A result
     * is generated through the {@link #resultConverterProperty() result converter},
     * which is intended to convert from the {@link ButtonType} that the user
     * clicked on into a value of type R. Refer to the {@link Dialog} class
     * JavaDoc for more details.
     */
    public final ObjectProperty<R> resultProperty() {
        return resultProperty;
    }

    public final R getResult() {
        return resultProperty().get();
    }

    public final void setResult(R value) {
        this.resultProperty().set(value);
    }
```

なになに、resultConverterProperty メソッドを通して作成される。この関数はクリックされたボタンによって変換される、と。

### getResultConverter

```java
    /**
     * API to convert the {@link ButtonType} that the user clicked on into a
     * result that can be returned via the {@link #resultProperty() result}
     * property. This is necessary as {@link ButtonType} represents the visual
     * button within the dialog, and do not know how to map themselves to a valid
     * result - that is a requirement of the dialog implementation by making use
     * of the result converter. In some cases, the result type of a Dialog
     * subclass is ButtonType (which means that the result converter can be null),
     * but in some cases (where the result type, R, is not ButtonType or Void),
     * this callback must be specified.
     */
    public final ObjectProperty<Callback<ButtonType, R>> resultConverterProperty() {
        return resultConverterProperty;
    }

    public final Callback<ButtonType, R> getResultConverter() {
        return resultConverterProperty().get();
    }

    public final void setResultConverter(Callback<ButtonType, R> value) {
        this.resultConverterProperty().set(value);
    }
```

渡されたButtonTypeによって結果を返すCallbackをセットせよ、という事ですね。
すなわち、現時点のresultConverterは制御不十分、と。

# 設計

以下のポイントがある。

 - OKした時の入力値チェックして駄目なら閉じない
 - キャンセルした時は入力値状態によらず閉じる
 - 大元ダイアログでOKする時はDB更新確認

これらの判断には「押されたボタン」「入力されている値が正常か」の情報が必要。

嫌なポイントが一つ。resultConverter でnullを返したとしても、cancelボタンが存在すると、cancel処理が強制的に走ってしまい、OKボタンを押したのに最終的にキャンセルボタンを押した時と同じようになるという（これはDialogクラスの設計ミスだと思う）。

## resultConverter内修正

- OKボタンの時のみ値チェックをする
- その時、押されたボタンを保持しておく（前述「押されたボタン」情報）
- 同じく、チェック結果を保持しておく（前述「入力されている値が正常か」情報）
- 値チェックがFalseの時は、nullでなく、前述ポイント対策として、ダミーの空の値を返しておく

```java:AbsDynamoDbInputDialog.java
	private ButtonData buttonData;

	// ・・中略・・

	public AbsDynamoDbInputDialog(R dynamoDbRecord, String dialogTitle) {
		// ・・中略・・
		setResultConverter((dialogButton) -> {
			buttonData = dialogButton == null ? null : dialogButton.getButtonData();
			if (buttonData  == ButtonData.OK_DONE) {
				isValidData = isFinalValidationOk();
				return isValidData ? getEditedDynamoDbRecord() : getEmptyAttr();
			}
			return null;
		});
		// ・・中略・・
	}

	// ・・中略・・

	abstract boolean isFinalValidationOk();
	abstract R getEditedDynamoDbRecord();
	abstract R getEmptyAttr();

	// ・・中略・・

	protected ButtonData getButtonData() {
		return buttonData;
	}
```

## CloseRequestで終了制御

- Cancelボタンの時（OKボタンでない時）は終了OK
- OKボタンの時、値チェックが駄目なら終了NG（dialogEventをconsume）

```java:AbsDynamoDbInputDialog.java
	// コンストラクタ内
	this.setOnCloseRequest(this::doFinalConfirmation);

	// ・・中略・・

	protected void doFinalConfirmation(DialogEvent dialogEvent) {
		if (buttonData != ButtonData.OK_DONE) {
			return;
		}
		if (!isValidData) {
			dialogEvent.consume();
		}
	}
```

## 大元ダイアログで、確認処理を入れる

doFinalConfirmationを継承して確認処理追加

- 継承元で、閉じるのをキャンセルされて（isConsumed）いたらスルー
- 押されたボタンがOKの時だけチェック

```java:DynamoDbRecordInputDialog.java

	// ・・中略・・
	@Override
	protected void doFinalConfirmation(DialogEvent dialogEvent) {
		super.doFinalConfirmation(dialogEvent);
		if (!dialogEvent.isConsumed() && getButtonData() == ButtonData.OK_DONE) {
			Dialog<ButtonType> dialog = new Dialog<>();
			dialog.setContentText("This DynamoDB record will be update. Is it OK?");
			dialog.getDialogPane().getButtonTypes().addAll(ButtonType.YES, ButtonType.NO);
			dialog.showAndWait().ifPresent(buttonType -> {
				if (buttonType != ButtonType.YES) {
					dialogEvent.consume();
				}
			});
		}
	}
```

# その他修正

- 読み込みダイアログがちゃんと制御できてなかったので、
[JavaFX show loading dialog for longer operations](https://stackoverflow.com/questions/29963542/javafx-show-loading-dialog-for-longer-operations)を参考に、Taskを使った制御に変更
- PartiQLのテーブル名をダブルコーテーションで括るように

# 次回予定

csv、json形式などでのファイル出力

# 参考にさせて頂いたページ

[JavaFX show loading dialog for longer operations](https://stackoverflow.com/questions/29963542/javafx-show-loading-dialog-for-longer-operations)
[dynamodb PartiQL SELECT query returns ValidationException: Unexpected from source](https://stackoverflow.com/questions/65888399/dynamodb-partiql-select-query-returns-validationexception-unexpected-from-sourc)