第7章: リピータ (Repeater Components)

Webアプリケーションでは、データベースから取得した一覧データをテーブルやリストとして繰り返し表示する場面が頻繁にあります。 Wicket ではこのような繰り返し表示をリピータコンポーネントで実現します。 本章では、ListView、RepeatingView、DataView、DataTable の4つのリピータを、用途に応じた使い分けとともに詳しく解説します。

リピータの概要

リピータコンポーネントとは、コレクション(ListIterator)の各要素に対して、 HTMLマークアップ上の同じ構造を繰り返し生成するコンポーネントの総称です。 Swing の JTableJList に相当するものと考えると理解しやすいでしょう。

Wicket はユースケースに応じて複数のリピータを提供しています。まず全体像を把握しましょう。

コンポーネント パッケージ 特徴 主な用途
ListView wicket-core シンプル、List をそのまま渡す 小規模な一覧表示
RepeatingView wicket-core 子コンポーネントを動的に追加 異種コンポーネントの動的生成
DataView wicket-core IDataProvider 経由でデータ取得、ページング対応 大量データの一覧表示
DataTable wicket-extensions カラム定義、ソート、ページングを統合した高機能テーブル 業務画面の本格的なテーブル

リピータの基底クラスには AbstractRepeaterRefreshingViewLoop など他にもありますが、 実務で頻繁に使用するのは上記4つです。本章ではこれらに集中して解説します。

ListView

ListView は最もシンプルで使いやすいリピータです。 java.util.List(またはそのモデル)を受け取り、各要素に対して populateItem() メソッドを呼び出します。 少量のデータをメモリ上に保持してそのまま表示するケースに最適です。

基本的な使い方

果物の名前リストを <ul> で表示する最も基本的な例を見てみましょう。

FruitPage.java
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.list.ListItem;

import java.util.Arrays;
import java.util.List;

public class FruitPage extends WebPage {
    public FruitPage() {
        List<String> fruits = Arrays.asList(
            "りんご", "みかん", "ぶどう", "バナナ");

        add(new ListView<>("fruitList", fruits) {
            @Override
            protected void populateItem(
                    ListItem<String> item) {
                item.add(new Label("name",
                    item.getModelObject()));
            }
        });
    }
}
FruitPage.html
<wicket:extend>
  <h2>果物リスト</h2>
  <ul>
    <li wicket:id="fruitList">
      <span wicket:id="name">果物名</span>
    </li>
  </ul>
</wicket:extend>

ListView は HTML 上で wicket:id が付与された要素(この例では <li>)をテンプレートとして使い、 リストの要素数だけその構造を繰り返し生成します。 populateItem() の引数 ListItem は、各要素をラップするコンテナコンポーネントです。 item.getModelObject() で現在の要素を取得できます。

HTML テンプレート内に書かれている「果物名」などのテキストは、開発時のプレビュー用ダミーテキストです。 Wicket がレンダリングする際にはコンポーネントが生成した内容に置き換えられるため、 デザイナーはブラウザで HTML ファイルを直接開いてデザインを確認できます。

オブジェクトリストの表示

実務では、単純な文字列リストではなくオブジェクトのリストを表示することがほとんどです。 以下は Employee(従業員)クラスのリストをテーブルとして表示する例です。

Employee.java
import java.io.Serializable;

public class Employee implements Serializable {
    private String name;
    private String department;
    private int age;

    public Employee(String name, String department, int age) {
        this.name = name;
        this.department = department;
        this.age = age;
    }

    // getter / setter 省略
    public String getName() { return name; }
    public String getDepartment() { return department; }
    public int getAge() { return age; }
}
EmployeeListPage.java
public class EmployeeListPage extends WebPage {
    public EmployeeListPage() {
        List<Employee> employees = Arrays.asList(
            new Employee("田中太郎", "開発部", 30),
            new Employee("佐藤花子", "営業部", 25),
            new Employee("鈴木一郎", "人事部", 35),
            new Employee("高橋美咲", "開発部", 28)
        );

        add(new ListView<>("empList", employees) {
            @Override
            protected void populateItem(
                    ListItem<Employee> item) {
                Employee emp = item.getModelObject();
                item.add(new Label("name",
                    emp.getName()));
                item.add(new Label("dept",
                    emp.getDepartment()));
                item.add(new Label("age",
                    String.valueOf(emp.getAge())));
            }
        });
    }
}
EmployeeListPage.html
<wicket:extend>
  <h2>従業員一覧</h2>
  <table>
    <thead>
      <tr>
        <th>名前</th>
        <th>部署</th>
        <th>年齢</th>
      </tr>
    </thead>
    <tbody>
      <tr wicket:id="empList">
        <td wicket:id="name">名前</td>
        <td wicket:id="dept">部署</td>
        <td wicket:id="age">年齢</td>
      </tr>
    </tbody>
  </table>
</wicket:extend>

populateItem() 内で item.getModelObject() を呼ぶと現在の行に対応する Employee オブジェクトが取得できます。 あとは各フィールドの値をそれぞれ Label に渡すだけです。

ListView はリスト全体をメモリ上に保持します。 数百件を超えるようなデータを扱う場合は、後述する DataViewDataTable の使用を検討してください。 また、ListView はデフォルトでは setReuseItems(false) であるため、 リクエストのたびにすべてのアイテムが再生成されます。 フォームを含む ListView を使う場合は setReuseItems(true) を設定しないと入力値が保持されません。

RepeatingView

RepeatingView は、子コンポーネントを動的にプログラムで追加していくリピータです。 ListView がリストの各要素に対して同じ populateItem() を呼ぶのに対し、 RepeatingView では各子コンポーネントをそれぞれ独自に構成できます。

ItemPage.java
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.markup.html.WebMarkupContainer;

public class ItemPage extends WebPage {
    public ItemPage() {
        List<String> items = Arrays.asList(
            "項目A", "項目B", "項目C");

        RepeatingView rv = new RepeatingView("items");
        add(rv);

        for (String item : items) {
            // newChildId() で一意の ID を自動生成
            WebMarkupContainer container =
                new WebMarkupContainer(
                    rv.newChildId());
            rv.add(container);
            container.add(
                new Label("label", item));
        }
    }
}
ItemPage.html
<wicket:extend>
  <div>
    <div wicket:id="items">
      <span wicket:id="label">
        項目
      </span>
    </div>
  </div>
</wicket:extend>

RepeatingView のポイントは rv.newChildId() メソッドです。 Wicket ではコンポーネントツリー内で同一階層のコンポーネントは一意の wicket:id を持つ必要があるため、 newChildId() が自動的に "0", "1", "2", ... のような一意の ID を生成してくれます。

RepeatingView vs ListView の使い分け

観点 ListView RepeatingView
データソース List<T> を渡す プログラムで子を追加
各アイテムの構造 すべて同一構造(populateItem() 各子コンポーネントで自由に構成可能
子コンポーネントの種類 同種のコンポーネントを繰り返す 異種のコンポーネントを混在できる
典型的な用途 テーブルの行、リスト項目 タブの動的追加、ダッシュボードのウィジェット配置
コード量 少ない(匿名クラス1つ) やや多い(ループで手動追加)

同じ構造のアイテムを繰り返すなら ListView、 異なる種類のコンポーネントを動的に追加したい場合は RepeatingView を選びましょう。 多くの場面では ListView で十分です。

DataView と IDataProvider

DataView は、大量データの効率的な一覧表示を目的としたリピータです。 ListView がリスト全体をメモリに保持するのに対し、 DataViewIDataProvider インタフェースを介して必要な範囲のデータだけを取得します。 これにより、データベースに数万件のレコードがあっても、1ページ分(例えば10件)だけを取得して表示できます。

IDataProvider の実装

IDataProvider<T> は以下の3つのメソッドを実装する必要があります。

EmployeeDataProvider.java
import org.apache.wicket.markup.repeater.data.IDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;

import java.util.Iterator;

public class EmployeeDataProvider
        implements IDataProvider<Employee> {

    @Inject
    private EmployeeService employeeService;

    @Override
    public Iterator<? extends Employee> iterator(
            long first, long count) {
        // DB から指定範囲のデータだけを取得
        return employeeService
            .find(first, count).iterator();
    }

    @Override
    public long size() {
        // データの総件数(ページング計算に使用)
        return employeeService.count();
    }

    @Override
    public IModel<Employee> model(Employee object) {
        // 各行のモデル。Detachable にすると
        // セッションサイズを削減できる
        return Model.of(object);
    }
}

iterator()firstcount パラメータは、SQL の OFFSETLIMIT に直接対応します。 サービス層やリポジトリ層で適切にページング付きクエリを実装してください。 全件取得してから Java 側でスライスする実装では、IDataProvider を使うメリットがなくなります。

DataView の使い方

IDataProvider を実装したら、それを DataView に渡して使います。 コンストラクタの第3引数はページあたりの表示件数です。 PagingNavigator を追加すると、ページ切り替えのナビゲーションが自動的に表示されます。

EmployeeDataPage.java
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.html.navigation.paging.PagingNavigator;

public class EmployeeDataPage extends WebPage {
    public EmployeeDataPage() {
        DataView<Employee> dataView =
          new DataView<>("employees",
              new EmployeeDataProvider(),
              10) {
            @Override
            protected void populateItem(
                    Item<Employee> item) {
                Employee emp =
                    item.getModelObject();
                item.add(new Label("name",
                    emp.getName()));
                item.add(new Label("dept",
                    emp.getDepartment()));
            }
        };
        add(dataView);

        // ページングナビゲータを追加
        add(new PagingNavigator(
            "navigator", dataView));
    }
}
EmployeeDataPage.html
<wicket:extend>
  <h2>従業員一覧</h2>
  <table>
    <thead>
      <tr>
        <th>名前</th>
        <th>部署</th>
      </tr>
    </thead>
    <tbody>
      <tr wicket:id="employees">
        <td wicket:id="name">名前</td>
        <td wicket:id="dept">部署</td>
      </tr>
    </tbody>
  </table>

  <!-- ページングナビゲータ -->
  <div wicket:id="navigator">
    ページング
  </div>
</wicket:extend>

DataViewpopulateItem()ListView のものとほぼ同じ構造です。 違いは引数の型が ListItem ではなく Item であること、 そしてデータの取得方法が IDataProvider 経由になっている点です。

DataView のコンストラクタ第3引数 10 は「1ページあたり10件表示」を意味します。 PagingNavigator は、総件数をもとに自動的にページリンク(前へ / 1 / 2 / 3 / ... / 次へ)を生成します。 ユーザーがページリンクをクリックすると、IDataProvider.iterator() が新しい範囲で呼び出されます。

DataTable(wicket-extensions)

DataTable は Wicket Extensions モジュールが提供する高機能なテーブルコンポーネントです。 カラム定義、ヘッダーの自動生成、ソート、ページングを統合しており、 業務アプリケーションのマスタ一覧画面やデータ管理画面に最適です。

DataTable を使うには、pom.xmlwicket-extensions の依存を追加します。

pom.xml(依存関係の追加)
<dependency>
    <groupId>org.apache.wicket</groupId>
    <artifactId>wicket-extensions</artifactId>
    <version>${wicket.version}</version>
</dependency>

DataTable の基本的な使い方は、(1) カラムのリストを定義し、(2) IDataProvider を渡してテーブルを構築する、というものです。 便利なサブクラス DefaultDataTable を使えば、ページングとヘッダーが自動的に組み込まれます。

EmployeeTablePage.java
import org.apache.wicket.extensions.markup.html.repeater.data.table.*;
import org.apache.wicket.model.Model;

import java.util.ArrayList;
import java.util.List;

public class EmployeeTablePage
        extends WebPage {
    public EmployeeTablePage() {
        List<IColumn<Employee, String>>
            columns = new ArrayList<>();

        // PropertyColumn: プロパティ名で値を取得
        // 第1引数=ヘッダー表示名
        // 第2引数=ソートキー
        // 第3引数=プロパティ式
        columns.add(new PropertyColumn<>(
            Model.of("名前"),
            "name", "name"));
        columns.add(new PropertyColumn<>(
            Model.of("部署"),
            "department", "department"));
        columns.add(new PropertyColumn<>(
            Model.of("年齢"),
            "age", "age"));

        // DefaultDataTable は
        // ヘッダー + ページングを自動付与
        DataTable<Employee, String> table =
            new DefaultDataTable<>(
                "employeeTable",
                columns,
                new EmployeeDataProvider(),
                20);
        add(table);
    }
}
EmployeeTablePage.html
<wicket:extend>
  <h2>従業員マスタ</h2>

  <!-- DataTable が自動的に
       thead/tbody/ページングを生成 -->
  <table wicket:id="employeeTable">
  </table>
</wicket:extend>

HTML 側のコードは驚くほど簡潔です。空の <table> タグに wicket:id を指定するだけで、 DefaultDataTable がヘッダー行、データ行、ページングナビゲーションをすべて自動生成してくれます。

IColumn の種類

Wicket Extensions は様々な IColumn 実装を提供しており、テーブルの各列の振る舞いを細かく制御できます。

カラムクラス 説明 使用例
PropertyColumn オブジェクトのプロパティ値をテキスト表示 名前、部署名など通常のテキストカラム
AbstractColumn セル内容を自由にカスタマイズ可能な抽象クラス ボタンやリンクを含むアクション列
LambdaColumn ラムダ式で表示値を生成(Wicket 8以降) 複数フィールドの結合表示、フォーマット処理
HeaderlessColumn ヘッダーなしのカラム アイコンや操作ボタンだけの列

例えば、LambdaColumn を使うと、表示値を柔軟に生成できます。

LambdaColumn の使用例
// 名前と部署を結合して「田中太郎(開発部)」と表示
columns.add(new LambdaColumn<>(
    Model.of("従業員情報"),
    emp -> emp.getName() + "(" + emp.getDepartment() + ")"
));

// AbstractColumn でアクション列を作成
columns.add(new AbstractColumn<Employee, String>(
        Model.of("操作")) {
    @Override
    public void populateItem(
            Item<ICellPopulator<Employee>> cellItem,
            String componentId,
            IModel<Employee> rowModel) {
        cellItem.add(new AjaxLink<Void>(componentId) {
            @Override
            public void onClick(AjaxRequestTarget target) {
                Employee emp = rowModel.getObject();
                // 編集ページへ遷移など
                setResponsePage(new EmployeeEditPage(emp));
            }
        });
    }
});

ソート対応(ISortableDataProvider)

DataTable でカラムヘッダーをクリックしてソートできるようにするには、 IDataProvider の代わりに ISortableDataProvider を実装(または SortableDataProvider を継承)します。

SortableEmployeeDataProvider.java
import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortableDataProvider;
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder;

public class SortableEmployeeDataProvider
        extends SortableDataProvider<Employee, String> {

    public SortableEmployeeDataProvider() {
        // デフォルトのソート順を設定
        setSort("name", SortOrder.ASCENDING);
    }

    @Override
    public Iterator<? extends Employee> iterator(
            long first, long count) {
        // getSort() で現在のソート情報を取得
        SortParam<String> sort = getSort();
        String sortProperty = sort.getProperty();
        boolean ascending = sort.isAscending();

        return employeeService
            .find(first, count, sortProperty, ascending)
            .iterator();
    }

    @Override
    public long size() {
        return employeeService.count();
    }

    @Override
    public IModel<Employee> model(Employee object) {
        return Model.of(object);
    }
}

PropertyColumn のコンストラクタ第2引数(ソートキー)を指定しておくと、 ISortableDataProvider と組み合わせてヘッダークリックでソートが切り替わるようになります。 ソートキーを null にすると、そのカラムはソート不可になります。

ソート可能/不可能なカラムの定義
// ソート可能: 第2引数にソートキーを指定
columns.add(new PropertyColumn<>(
    Model.of("名前"), "name", "name"));

// ソート不可: 第2引数を省略(2引数のコンストラクタを使用)
columns.add(new PropertyColumn<>(
    Model.of("備考"), "remarks"));

DefaultDataTable は内部で HeadersToolbarNavigationToolbarNoRecordsToolbar を自動的に追加します。 より細かいカスタマイズが必要な場合は、DataTable を直接使い、必要な Toolbar を手動で追加してください。

ページング

Wicket はリピータと連携するページングコンポーネントを複数提供しています。 DataView と組み合わせて使う場合と、DataTable に組み込まれている場合があります。

コンポーネント 動作 備考
PagingNavigator ページ全体をリロードしてページ切替 DataView と組み合わせて使用
AjaxPagingNavigator AJAXでページ切替(部分更新) DataView と組み合わせて使用
NavigationToolbar DataTable 内蔵のページング DefaultDataTable で自動追加

AjaxPagingNavigator

PagingNavigator はページ全体をリロードしますが、 AjaxPagingNavigator を使えばテーブル部分だけを AJAX で部分更新できます。 ユーザー体験が大幅に向上するため、実務ではこちらを使うことが多いでしょう。

AjaxPagingNavigator の使い方
import org.apache.wicket.ajax.markup.html.navigation.paging.AjaxPagingNavigator;

// DataView を WebMarkupContainer で囲む(AJAX更新対象を明示)
WebMarkupContainer tableContainer =
    new WebMarkupContainer("tableContainer");
tableContainer.setOutputMarkupId(true);
add(tableContainer);

DataView<Employee> dataView =
    new DataView<>("employees",
        new EmployeeDataProvider(), 10) {
    @Override
    protected void populateItem(Item<Employee> item) {
        Employee emp = item.getModelObject();
        item.add(new Label("name", emp.getName()));
        item.add(new Label("dept", emp.getDepartment()));
    }
};
tableContainer.add(dataView);

// AjaxPagingNavigator を追加
add(new AjaxPagingNavigator("navigator", dataView) {
    @Override
    protected void onAjaxEvent(AjaxRequestTarget target) {
        // テーブルコンテナを AJAX で再描画
        target.add(tableContainer);
        super.onAjaxEvent(target);
    }
});
対応する HTML
<div wicket:id="tableContainer">
  <table>
    <thead>
      <tr><th>名前</th><th>部署</th></tr>
    </thead>
    <tbody>
      <tr wicket:id="employees">
        <td wicket:id="name">名前</td>
        <td wicket:id="dept">部署</td>
      </tr>
    </tbody>
  </table>
</div>

<div wicket:id="navigator">ページング</div>

AjaxPagingNavigator で AJAX 更新するためには、更新対象のコンポーネントに setOutputMarkupId(true) を設定する必要があります。 これにより Wicket がコンポーネントに HTML の id 属性を自動付与し、 JavaScript で部分更新のターゲットを特定できるようになります。

ページングのカスタマイズ

デフォルトの PagingNavigator の見た目はシンプルですが、 サブクラスで newPagingNavigation()newNavigation() をオーバーライドすることで レンダリングをカスタマイズできます。また、CSS を使って見た目を調整することも可能です。

ページング表示件数の動的変更
// 1ページあたりの表示件数をプルダウンで変更
List<Integer> choices = Arrays.asList(10, 25, 50, 100);
DropDownChoice<Integer> rowsPerPage =
    new DropDownChoice<>("rowsPerPage",
        new Model<>(10), choices);

rowsPerPage.add(new AjaxFormComponentUpdatingBehavior("change") {
    @Override
    protected void onUpdate(AjaxRequestTarget target) {
        dataView.setItemsPerPage(
            rowsPerPage.getModelObject());
        target.add(tableContainer);
    }
});
add(rowsPerPage);

上記のように DataView.setItemsPerPage() を使えば、表示件数を動的に変更できます。 AJAX と組み合わせれば、ページをリロードせずにスムーズに表示件数を切り替えられます。

リピータの選び方

最後に、4つのリピータコンポーネントの選び方をまとめます。 プロジェクトの要件に応じて適切なコンポーネントを選択してください。

コンポーネント ユースケース データ規模 ページング ソート
ListView シンプルな一覧表示。メモリ上の小規模リスト 小(数十件程度) なし なし
RepeatingView 動的に異なる種類のコンポーネントを追加 なし なし
DataView 大量データの効率的な一覧表示。ページング対応 大(数万件以上可) あり 手動実装
DataTable カラム定義、ソート、ページングを統合した本格テーブル 大(数万件以上可) あり(内蔵) あり(内蔵)

迷ったときの指針: まず ListView で実装を始めて、データ量が増えたりページングが必要になったら DataView に移行し、 カラムソートやヘッダー自動生成が欲しくなったら DataTable にアップグレードする、という段階的なアプローチがおすすめです。 DataViewDataTable はどちらも IDataProvider を使うため、移行は比較的容易です。

実務での組み合わせ例

以下に典型的な実務パターンをまとめます。

画面の例 推奨リピータ 理由
ナビゲーションメニュー ListView 項目数が少なく固定的
ダッシュボードのウィジェット RepeatingView 各ウィジェットの種類が異なる
検索結果一覧 DataView + AjaxPagingNavigator 結果件数が可変でページングが必要
マスタ管理テーブル DefaultDataTable ソート・ページング・ヘッダーが必要
CSV取込プレビュー ListView 取込データが小規模でプレビュー用
タブの動的追加 RepeatingView タブ数が動的に変化する

本章で紹介したリピータは、次章の「AJAX 対応」と組み合わせることで、さらに使い勝手が向上します。 AJAX による部分更新を活用すると、ページ遷移なしでテーブルの更新・ソート・フィルタリングを実現でき、 デスクトップアプリケーションに近い操作感のWebアプリケーションを構築できます。

実務でハマりやすい注意点

  • 問題
    RefreshingView の再描画後に、行内で入力していた値が消える。編集系テーブルで特に致命的になりやすい。
    原因
    再描画のたびに item が新しく生成され、前回の入力状態を引き継げない。行の同一性が保たれていない。
    対策
    item 再利用戦略を設定し、同じモデル行は再利用する。再描画前後で入力値が残ることをテストで確認する。
  • 問題
    ページ送りした後に、並び順や表示内容が急に不自然になる。ユーザーから「順番が変だ」と指摘されやすい。
    原因
    クライアント側ソートとサーバ側ページングを混在しているため、処理基準が一致しない。表示中データと全体データで順序がズレる。
    対策
    ソートとページングはどちらもサーバ側で統一する。特に件数が多い一覧ではこの方針を固定する。
  • 問題
    標準 DataTable では、ボタン配置や操作導線の細かな要件を満たしにくい。後からの仕様追加で詰まりやすい。
    原因
    既成のトップツールバー構成が要件に合っておらず、拡張ポイントだけでは調整しきれない。
    対策
    DataTable を直接組み立て、必要なツールバーを個別に追加する。最初に拡張前提の構成にしておくと後工程が楽になる。