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)

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

# 今回の追加機能

詳細情報の表示ダイアログ及びデータの更新
※このツールを使用して更新して不具合その他により問題が発生しても、当方は責任を持てません。申し訳無いですが、重要なデータの更新にはバックアップを取得しておくなど自己責任でお願いします。

# 現在の進捗

ダイアログによる詳細表示。またレコードの編集画面にもなっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530376/cb186bb7-1ca3-511c-dcb1-ae6cc5db99d5.png)

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

# 仕様を決める

## 入力方法

 - テーブルセルのインラインエディタ
 - ダイアログ

が考えられる。今回はスカラー型なのでインラインエディタでもいいが、ドキュメント型やデータ追加の時を考えると、１レコードに対する編集ダイアログという形にしたい。

## 更新タイミング

ダイアログでOKする時。ダイアログにOKとCancelを付ける。

# 入力ダイアログ作成

ステップを追って作成していく。今後ドキュメント型などの編集やデータ追加を考えると、固定デザインで対応するのは困難なので、動的生成で対応していく。

## Step1.ダイアログの基本継承元を決める

JavaFXにはDialogというクラスがある。画面を普通に作るのと何が違うのか気になる。ソースを見ると、以下の事が解る。

- ジェネリックが指定可能
- DialogPaneを基本にした画面
- Modalで動く
- HeavyweightDialog（FXDialogの実装）

```java:Dialog.javaのクラス名宣言部とコンストラクタ
public class Dialog<R> implements EventTarget {
    //  ・・中略・・
    public Dialog() {
        this.dialog = new HeavyweightDialog(this);
        setDialogPane(new DialogPane());
        initModality(Modality.APPLICATION_MODAL);
    }
```

詳細は公式ページ [クラスDialog<R>](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/Dialog.html) を見る事にする。すべてを把握する事はすぐには難しいが、入力ダイアログとして必要な機能を備えているような事が解る。実際これを継承してTextInputDialogクラスが存在する。以下引用部分を気を付けて作る事にする。

> 重要な注意事項: Dialogクラスを拡張して独自のダイアログを作成するすべての開発者は、result converterプロパティの重要性を理解しておくことが重要です。R型がVoidでもButtonTypeでもない場合は、必ずresult converterを設定する必要があります。これを怠った開発者には、result converterを介してButtonTypeから変換できなかったことを示すClassCastExceptionsがコードで返されます。
ほとんどの開発者にとっては、Alertクラス(事前定義された通知形式のアラート用)か、2つの事前作成済ダイアログ(TextInputDialogおよびChoiceDialog)のいずれかをニーズに応じて使用するほうが便利です。

## Step2.ダイアログの継承階層を考える

DynamoDBのデータは階層的になっている。しかもListの中にMapがあったり、Mapの中にListがあったりする。ダイアログも再帰的（？）に開けなくてはいけない。
結果として、以下のような継承構造にした。※2021-02-23階層変更しました

 - AbsDynamoDbInputDialog<R>：基底クラス。ジェネリック型Rはレコード全体の型。  
例：```Map<String, AttributeValue>```  
その他各ダイアログで共通の処理を担当
    - AbsDynamoDbSetInputDialog<T>：セット型の基底クラス。Tはセットの型（例：```String```）  
    セット型に共通の処理を担当。
       - DynamoDbBinarySetInputDialog：バイナリセット型ダイアログ  
       - DynamoDbNumberSetInputDialog：数値セット型ダイアログ  
       - DynamoDbStringSetInputDialog：文字セット型ダイアログ  
    - AbsDynamoDbDocumentInputDialog<T>：ドキュメント型の基底クラス。  
    ボタンや子ダイアログ関係の制御処理を担当
      - DynamoDbListInputDialog：ドキュメント型（リスト）のダイアログ     
      - DynamoDbMapInputDialog：ドキュメント型（マップ）のダイアログ  
         - DynamoDbRecordInputDialog：DynamoDBレコードの初期ダイアログ  
        キー属性を上に持ってきたり、編集不可にする制御


抽象型の２つ以外が実際のダイアログ。それぞれに対応するデータ型に関して、TextFieldの値のセットや更新時にバイナリセットに変換する処理を担当する。
前述理由により、基底クラス（AbsDynamoDbDocumentInputDialog）でそれを継承しているダイアログを呼ぶという変な相互依存が出来てしまっている。ただ、今回はジェネリック方を結構有効に使えたと思う。

## Step3.粛々と実装

今回は特に技術的な重要な点は無かったが、以下のポイントは皆さんの役に立つかもしれないので記載。

### 動的なボタンにイベントを割り当てる

その際に、押されたボタンを取得したりもしてる。

```java:DynamoDbMapInputDialog.java
		button.setOnAction((event) -> {
			Button wkBtn = (Button) event.getSource();
			actOpenEditDialog(wkBtn.getId());
		});
```

### TextFieldの背景色変える

入力不可にしたりもしてる。

```java:DynamoDbRecordInputDialog.java
		if (valNode instanceof TextField) {
			TextField textField = (TextField) valNode;
			textField.setStyle("-fx-background-color: lightgray;");
			textField.setEditable(false);
		}
```

### SdkBytesのBase64でのエンコード、デコード

```java:DynamoDbUtils.java
	public static String getBase64StringFromSdkBytes(SdkBytes sdkByte) {
		return Base64.getEncoder().encodeToString(sdkByte.asByteArray());
	}

	public static SdkBytes getSdkBytesFromBase64String(String base64Str) {
		byte[] byteAry = Base64.getDecoder().decode(base64Str);
		return SdkBytes.fromByteArray(byteAry);
	}
```

### 内部エラーが出た時、アラート出すように

JavaFXでエラー出したりはしてくれないので、エラー出そうな所に```Alert```ダイアログセットする。

```java:DynamoDbTable.java
	try {
		if (ev.getClickCount() >= 2) {
			actShowInputDialog();
		}
	} catch (Exception e) {
		Alert alert = new Alert(AlertType.ERROR, e.getMessage());
		alert.show();
	}
```

# 次回予定

データや要素の追加、データ型チェックなど。