読者です 読者をやめる 読者になる 読者になる

システム開発で思うところ

JavaEEを主にシステム開発をしながら思うところをツラツラを綴る

JSFのSelectItem(Table)をAjaxで更新

JSF Ajax lombok

前回vermeer.hatenablog.jp

は単体のセレクトボックスの更新でした。

今回はテーブル構造のリストの一部を更新したケースです。 Ajaxでは繰り返し行の一部だけを更新しています。*1

今回はControllerに変換に使用するクラスを保持するというやり方をしていますが、実際の開発ではServiceを経由してリストのデータを取得するというやり方もあると思います。
その場合は、初期表示時のリストについては、Service側もしくはDTOからFormに変換する際に設定しておくというやり方になると思います。

明細行(1行)Form

package com.mycompany.samplejsf.domain.selectitem;

@Builder @Data
public class ItemForm {

    private Integer seq;
    private Integer genderCode;
    private JsfSelectItem genderEnum;

    private Integer snackCode;
    private JsfSelectItem snackList;

}

明細行(全明細)Form

package com.mycompany.samplejsf.domain.selectitem;

@Builder @Data
public class ItemForms {

    @Singular
    private List<ItemForm> forms;

    public ItemForm get(int rowIndex) {
        return this.forms.get(rowIndex);
    }

}

Builderパターンの実装も@Builder(lombok)で簡単にできます。またlombokの@Builder+@SingularはListへの初期化メソッドに単数と複数の両方を自動生成してくれるので便利なのでお勧めです。

ManagedBean

package com.mycompany.samplejsf.domain.selectitem;

@Named(value = "selectItemTableAjax")
@SessionScoped
@NoArgsConstructor
public class SelectItemTableController implements Serializable {

    private static final long serialVersionUID = 1L;

    @Getter @Setter
    private ItemForms genderSnackTable;

    private Map<Gender, Map<Integer, String>> genderShackMap;

    @PostConstruct
    public void init() {
        Map<Gender, Map<Integer, String>> enumMap = new EnumMap<>(Gender.class);
        Map<Integer, String> maleSnack = this.maleSnack();
        enumMap.put(Gender.MALE, maleSnack);
        Map<Integer, String> femaleSnack = this.femaleSnack();
        enumMap.put(Gender.FEMALE, femaleSnack);
        this.genderShackMap = enumMap;

        Integer defaultMaleSnackCode = this.genderShackMap.get(Gender.MALE).entrySet().iterator().next().getKey();
        Integer defaultFemaleSnackCode = this.genderShackMap.get(Gender.FEMALE).entrySet().iterator().next().getKey();
        this.genderSnackTable = ItemForms.builder()
                .form(ItemForm.builder()
                        .seq(1)
                        .genderCode(Gender.MALE.getCode())
                        .genderEnum(JsfSelectItem.of(Gender.class))
                        .snackCode(defaultMaleSnackCode)
                        .snackList(JsfSelectItem.of(this.genderShackMap.get(Gender.MALE)))
                        .build())
                .form(ItemForm.builder()
                        .seq(2)
                        .genderCode(Gender.FEMALE.getCode())
                        .genderEnum(JsfSelectItem.of(Gender.class))
                        .snackCode(defaultFemaleSnackCode)
                        .snackList(JsfSelectItem.of(this.genderShackMap.get(Gender.FEMALE)))
                        .build())
                .build();
        this.printLog("init");
    }

    public void submit() {
        this.printLog("submit");
    }

    public void changeRowGender(AjaxBehaviorEvent event) {
        int index = JsfAjaxBehaviorEvent.of(event).uiRepeatRowIndex();
        ItemForm form = genderSnackTable.get(index);
        Gender gender = EnumCodeProperty.codeOf(Gender.class, form.getGenderCode());
        form.setSnackCode(this.genderShackMap.get(gender).entrySet().iterator().next().getKey());
        form.setSnackList(JsfSelectItem.of(this.genderShackMap.get(gender)));
        this.printLog("ajax " + index);
    }

    private Map<Integer, String> maleSnack() {
        Map<Integer, String> map = new LinkedHashMap<>();
        map.put(1, "大福");
        map.put(2, "おはぎ");
        map.put(3, "みたらしだんご");
        map.put(4, "せんべい");
        return map;
    }

    private Map<Integer, String> femaleSnack() {
        Map<Integer, String> map = new LinkedHashMap<>();
        map.put(5, "チョコ");
        map.put(6, "クッキー");
        map.put(7, "プリン");
        map.put(8, "ゼリー");
        return map;
    }

    private void printLog(String label) {
        System.out.println("label = " + label);

        this.getGenderSnackTable().getForms().stream()
                .forEachOrdered(form -> {
                    Gender gender = EnumCodeProperty.codeOf(Gender.class, form.getGenderCode());
                    String snackName = this.genderShackMap.get(gender).get(form.getSnackCode());

                    System.out.println("SEQ:" + form.getSeq() + ":GenderCode:" + form.getGenderCode()
                                       + ":GenderValue:" + gender.getProperty() + ":SnackCode:" + form.getSnackCode() + ":SnackName:" + snackName);

                });
    }
}


JSFの選択行情報を取得する部品クラス

package com.mycompany.samplejsf.infrastructure.part.jsf;

@RequiredArgsConstructor(staticName = "of") @EqualsAndHashCode
public class JsfAjaxBehaviorEvent {

    private final AjaxBehaviorEvent event;

    public int uiRepeatRowIndex() {
        String clientId = this.event.getComponent().getParent().getClientId();
        Pattern pat = Pattern.compile("[0-9]*$");
        Matcher mat = pat.matcher(clientId);
        if (mat.find() == false) {
            throw new IllegalArgumentException("this method must call ui:repeat ajax event");
        }
        return Integer.parseInt(mat.group());
    }
}

過去記事の同名クラスへの差分のみ記載しています。
this.event.getComponent().getParent()indexを取得できれば良かったのですが、私には出来ませんでした。。正規表現は苦肉の策です。


xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <style type="text/css">
        td, th { border: 1px #000000 solid; }
    </style>

    <h:head>
        <meta charset="UTF-8" />
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <title>JSF SELECT ITEM</title>
    </h:head>
    <body>
        <form jsfc="h:form">
            <h1>JSF SELECT ITEM PATTERN Table + Ajax</h1>
            <table>
                <thead>
                    <tr>
                        <th>SEQ</th>
                        <th>性別</th>
                        <th>お菓子</th>
                    </tr>
                </thead>
                <tbody>
                    <tr jsfc="ui:repeat" value="#{selectItemTableAjax.genderSnackTable.forms}" var="row" varStatus="varStatus" id="rep">
                        <td>
                            #{row.seq}
                        </td>

                        <td>
                            <select jsfc="h:selectOneMenu" value="#{row.genderCode}">
                                <option jsfc="f:selectItems" value="#{row.genderEnum.values}" />
                                <f:ajax listener="#{selectItemTableAjax.changeRowGender}" render="snack" event="change" />
                            </select>
                        </td>

                        <td>
                            <select id="snack" jsfc="h:selectOneMenu" value="#{row.snackCode}">
                                <option jsfc="f:selectItems" value="#{row.snackList.values}" />
                            </select>
                        </td>
                    </tr>
                </tbody>
            </table>
            <dir>
                <input type="submit" jsfc="h:commandButton" value="状態取得" action="#{selectItemTableAjax.submit()}"/>
            </dir>
        </form>
    </body>
</html>

デザインをHtmlという前提の場合、繰り返し部分は ui:repeat を使います。
CSSは手を抜いています。)

実行結果

操作前

f:id:vermeer-1977:20161220180536p:plain

操作後

f:id:vermeer-1977:20161220180535p:plain

コンソールログ
label = init
SEQ:1:GenderCode:1:GenderValue:gender_male:SnackCode:1:SnackName:大福
SEQ:2:GenderCode:2:GenderValue:gender_female:SnackCode:5:SnackName:チョコ
label = ajax 1
SEQ:1:GenderCode:1:GenderValue:gender_male:SnackCode:1:SnackName:大福
SEQ:2:GenderCode:1:GenderValue:gender_male:SnackCode:1:SnackName:大福

さいごに

表題とは関係ありませんが、明細全体をファーストクラスコレクションにしておくことで明細行全体としての関心事を明確に凝集させています。 今回は明細全体での振舞いは実装していませんが、全選択時の振舞いなど実際の開発ではあると思います。 あとから凝集させることももちろんできますが、事前にファーストクラスコレクションとしてまとめておく癖をつけておけばリファクタリング時に「関心事」の塊が明確なのでControllerにロジックが漏れにくいと思います。 プロジェクトによっては凝集のために新たにクラスを作成しようとすると、それだけでも仰々しいと思われて抵抗されるケースがあります。
始めからクラスになっていれば、そういうことにはなりにくいです。*2
意味があるのかな?何を実装するのかな?このくらいなら別に良いだろう、と思われる気がするファーストクラスコレクションですが、地味に効いてくる作法だと思います。

*1:行位置の取得の部分は試行錯誤したところなので、正しいやり方かどうか分かりません。

*2:「凝集したクラスを作るだけ」と思っても「聖域としてのコード」を優先されることは多々あります。これは正しい間違っているというのとは違います。