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に設定する