StrutsでListや配列のActionFormを使用する

今回は、StrutsのActionFormにList型や配列型のプロパティを用意する方法を説明します。
これを使用すると、繰り返しの入力フィールドを表示することができます。

2種類の方法を紹介します。1つは配列を使う方法で、もう1つはListを使う方法です。
配列を使ったほうが、構造はシンプルですが、Listを使ったほうが使い勝手がよいです。

サンプルソースはこちらです。
kenkyu05.zip

StrutsによるWebアプリケーションスーパーサンプル[PR]
Strutsに関しては、この本が、詳細かつ実践的な内容で解説しています。
リンク先のAmazonの評価を見ると分かりますが、だれもが「わかりやすい」と感じ、良い評価を集めているようです。


StrutsによるWebアプリケーションスーパーサンプル
Amazonでは分かりやすいと評判です。

第2版が出版されています。StrutsによるWebアプリケーションスーパーサンプル第2版

配列を使う方法

ActionFormに配列型のプロパティを用意します。配列の1要素が、1明細のプロパティになります。
サンプルプログラムのArrayFormでは、次のように配列型のプロパティを用意しています。

	private String[] detailField1;
private String[] detailField2;

これで、n行2列をあらわすことができます。
図にするとこんなイメージになります。
配列型のプロパティをもつActionForm
setterとgetterは、配列をセットするsetterと、配列を取得するgetterを作成すればOKです。インデックスを使ったアクセッサメソッドは不要です。
public String[] getDetailField1()
public void setDetailField1(String[] detailField1)

Actionでは、配列を作成してセットしておけばOKです。配列型なので固定長になりますが、内部処理としてはListで作成し、最後にtoArrayしてもよいと思います。このあたりが配列を使った場合の使い勝手の悪さです。

JSPでは、<c:foreach>></c:foreach>>タグで繰り返して表示します。配列の要素数ぶん繰り返します。
<c:foreach var="dummy" items="${ArrayForm.detailField1}" varstatus="s"></c:foreach>
textタグのvalue属性にELを使って値を設定しています。

ちょっと話はそれますが、forEachタグのvarStatus属性は、ループカウンタ的なものを保持しているオブジェクトです。
varStatusで指定した変数名sを使用して、s.indexと書くと、0から始まるループカウントとして使えます。
varStatusは他にも次のようなプロパテイを持っています。

プロパティ 説明
current 現在の要素
index 0から始まるインデックス
count 1から始まる要素の位置
first 最初の要素かどうか(boolean)
last 最後の要素かどうか(boolean)
begin forEachタグのbegin属性の値
end forEachタグのend属性の値
step forEachタグのstep属性の値

JSP2.0のELの場合は、textタグのvalue属性にELで記述することができますが、JSP1.2の場合は、これができません。
その場合には、通常のHTMLタグである<input type="text" />タグで、記述するしかないと思います。
(他のうまい方法があったら教えてください)
これでvalue属性に対して、<c:out></c:out>タグで記述すれば、OKでした。JSP1.2のJSTLの場合はこのような記述になります。


<input name="detailField2" type="text" value="<c:out value="${ArrayForm.detailField2[s.index]}">"/></c:out>

タグの中にタグがあるので、ちょっと気持ち悪いです。

Listを使う方法

ActionFormにList型のプロパティを用意します。このListには1行分の情報をあらわすJavaBeanを格納します。
サンプルプログラムListFormでは、インナークラスDetailというクラスが、1行分の情報をあらわすJavaBeanとなります。
private List detailList = new ArrayList();
ArrayFormのときと同じようにn行2列にするために、Detailクラスは2つのフィールドdetailField1と、detailField2を持ちます。
このListに3つのDetailオブジェクトを格納した場合は、3行分の明細を持つという意味になります。
図にしてあらわすと、このようになります。
List型のプロパティをもつActionForm
インナークラスである必要性はありませんが、ActionForm内の1明細をあらわすオブジェクトなので、不要にソースファイルを増やすよりもインナークラスで十分かと判断しました。
またstaticなインナークラスでもよいのですが、これまたDetailオブジェクト単独で存在することは想定しないので、staticにはしませんでした。このことにより、Detailオブジェクトを生成するためには、必ずListFormのインスタンスが必要になります。
Detail detail = listForm.new Detail(); //インスタンス.newという記述方法

getterとsetterは、通常のフィールドと同じように、Listプロパティに対するgetterとsetterを作成します。インナークラスDetailの各フィールドにも、getter,setterを用意します。
そして最も重要なアクセッサメソッドであるgetDetailを作成します。これは、List内の1要素を取得するメソッドです。ListにはDetailオブジェクトをn個格納するので、インデックスを引数に、Detailオブジェクトを1つ取得できるメソッドを用意するわけです。
このメソッドでは、なにやらややこしいことを最初にしていますが、この処理の説明はあとでします。
基本的には、
return (Detail)detailList.get(index);
これが全てです。

Actionクラスの処理は、ArrayActionと比べてシンプルです。Listを使っているので、可変個数のデータを格納できます。
たとえば、DBから検索した結果が何件になるか分からなくても、1レコード得るごとに、Detailオブジェクトを生成して、Listにaddすればよいだけです。下記は、10明細をdetailListにaddしている例です。

	List detailList = new ArrayList();
for (int i = 0; i < 10; i++){
Detail detail = listForm.new Detail();
detail.setDetailField1("明細1-" + i);
detail.setDetailField2("明細2-" + i);
detailList.add(detail);
}
listForm.setDetailList(detailList);

JSPの記述も、ちょっとしたコツを守れば、配列を使った時よりもシンプルになります。

	<c:foreach var="detail" items="${ListForm.detailList}">
</c:foreach>

コツは次の2点です。

  1. forEachタグのループ変数varの名前をdetailにします。
    これは、ListFormの1要素を得るためのアクセッサメソッドが、getDetailで作ったからです。
  2. 明細部のフィールド全てに対して、name="detail" indexed="true"にします。
    List内の1要素DetailのdetailField1プロパティを取得しつつ、明細データという意味でindexedをtrueに設定します

こうすることで、めでたくList内の明細データがHTMLに表示されます。テキストフィールドの出力結果は次のようになります。


<input name="detail[1].detailField1" value="abcdefg" type="text" />

indexed=”true”とすると、name属性にインデックス[1]が付加されるようになります。これをsubmitするとStrutsは都合よくActionFormに格納しようとします。
Strutsのこの明細形式のリクエストデータを受け取ると、まずActionFormを生成します。これはListFormをnewすることになります。
次に、detail[1]の部分から、getDetail(1)を実行し、Detailオブジェクトを得ます。
そして、得られたDetailオブジェクトのdetailField1の値に、value値”abcdefg”をセットします。
listForm.getDetail(1).setDetailField1("abcdefg");
このような処理が行われるわけです。 name="detail[1].detailField1" value="abcdefg"にはそのような意味が含まれています。

ところが、ListFormをnewした直後に、getDetail(1)としても、detailListは空っぽなので、このままではListの要素数を超えたアクセスによる例外(ArrayIndexOutOfBoundsException)が発生しかねません。
それを避ける処理が、ListFormのgetDetailで書いてあった処理です。

if (detailList.size() - 1 < index){
while(detailList.size() - 1 < index){
detailList.add(new Detail());
}
}

これは、Listの要素数を超えたアクセスをされた時に、要素数分のDetailオブジェクトをその場で追加してしまう処理です。
ちょっと乱暴な気がしますが、これによりsubmitされてきた件数に合わせて、Detailオブジェクトがnewされるようになります。

まとめ

オススメするのは、List型のプロパティを使う方法です。とても扱いやすく便利だと思います。
List型のプロパティを使う際のポイントは次の5つです。

  1. 明細の1要素をあらわすクラスを作成する
  2. 明細の1要素へのアクセッサメソッドを用意する
  3. 要素数を超えたアクセスに対する処理を記述する
  4. JSPのループ変数名に、1要素をあらわすプロパティを使用する
  5. Strutsのhtml:タグで、indexed属性をtrueに設定する

StrutsでListや配列のActionFormを使用する」への6件のフィードバック

  1. ピンバック: civic site
  2. 以前お騒がせした、初心者のDOONです。
    いまや、中級者のなりました(冗談です)

    scope=”request”なので、
    このActionFormにListで作成して、表示したあとにですね、
    画面からhtml:textに入力して、その値をsubmitで取得するとします。
    その時ActionFormのsetterメソッドが走って、画面の入力値を設定すると思います。
    そのときListはnew ArrayListしかしていないので、
    要素数OVERが起きるのではないでしょうか?
    でも、起きないのは、なぜでしょう?
    setterにもListの要素数拡大が必要な気がしています。

    もしよろしければ、教えてください。

  3. うーん、私がバカですね。すいません。
    もう一度聞きます。もしよろしければ、教えてください。
    jspを表示させたら、scope=requestなんで、ActionFormはなくなりますよね。
    そのあとに、submitしたら、新しいActionFormができて、そのhtml:textのsetterメソッドが
    先に動くのではないでしょうか?
    その前にgetDetailが動くのは、なぜでしょうか?

  4. JSPで出力した結果を”ブラウザの”HTMLソースを表示して見てみてください。
    <input name=”detail[1].detailField1″ value=”abcdefg” type=”text” />
    こんな感じになっています。あとは記事にも書いてある通りなんですが、
    これをサーバーへsubmitすると、
       detail[1].detailField1=abcdefg
    という情報が送信されます。Strutsはこれを受け取ると、
       listForm.getDetail(1).setDetailField1(“abcdefg”);
    というのと同じような処理を行います。つまりlistFormからgetDetailを実行し要素の1個目を取得しようとします。

    >html:textのsetterメソッドが先に動くのではないでしょうか?その前にgetDetailが動くのは、なぜでしょうか?
    という疑問点については、
    set先がActionFormではなく、ActionFormの中のListの中のn番目のBeanにsetするという指定になっているからです。
    テキストフィールドに入力した値をどこにセットしていると思いますか?ActionFormにsetしているのではありません。
    ActionFormからListをgetして、Listからn番目の要素をgetして(これがgetDetail)、getしたBeanに対して値をセットしているのです。なので値をセットするためには、先にgetDetailで取得する対象となるBeanをgetしなければならないのです。

  5. わかりやすい解説、痛み入ります。
    ありがとうございました。
    いつもの通り、もう少し勉強します。いつもすいません。
    参考にしているのですが、高度すぎて、いつも挫折しています。
    また、よろしくお願いします。

    初心者にもどります。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)