Um Daten zur Anzeige zu bringen, reicht im einfachsten Fall eine ListView-Komponente. Wenn die Anforderungen komplexer werden, macht sich der Einsatz der DataTable-Komponente recht bald bezahlt. Welche Möglichkeiten diese Komponente bietet und wie man mit ein wenig Mehraufwand auf außergewöhnliche Anforderungen reagieren kann, zeigt der folgende Beitrag.
Vorarbeiten
Damit das Beispiel für jeden Nachvollziehbar wird, müssen wir einige Vorarbeiten leisten. Zuerst erstellen wir eine Klasse, die einen Eintrag in der Datenbank symbolisiert.
package de.wicketpraxis.web.blog.pages.questions.data;
import java.io.Serializable;
public class SomeBean implements Serializable
{
String _vorname;
String _name;
int _alter;
public SomeBean(String vorname, String name, int alter)
{
_vorname=vorname;
_name=name;
_alter=alter;
}
public String getVorname()
{
return _vorname;
}
public String getName()
{
return _name;
}
public int getAlter()
{
return _alter;
}
}
Um Datensätze zu filtern, erstellen wir eine Hilfsklasse, in der die Werte für den Filter abgelegt werden.
package de.wicketpraxis.web.blog.pages.questions.data;
import java.io.Serializable;
public class SomeBeanFilter implements Serializable
{
String _name;
String _vorname;
public String getName()
{
return _name;
}
public void setName(String name)
{
_name = name;
}
public String getVorname()
{
return _vorname;
}
public void setVorname(String vorname)
{
_vorname = vorname;
}
public boolean match(SomeBean bean)
{
boolean ret=true;
if (_name!=null)
{
if (!bean.getName().startsWith(_name)) ret=false;
}
if (_vorname!=null)
{
if (!bean.getVorname().startsWith(_vorname)) ret=false;
}
return ret;
}
}
Damit wir nicht eine Liste von Daten von Hand erzeugen müssen, erstellen wir einen Generator, der die Datenbank simuliert.
package de.wicketpraxis.web.blog.pages.questions.data;
import java.util.ArrayList;
import java.util.List;
public class SomeBeanGenerator
{
public static List<SomeBean> getBeans(int size, SomeBeanFilter filter)
{
List<SomeBean> ret=new ArrayList<SomeBean>();
for (int i=0;i<size;i++)
{
SomeBean bean = new SomeBean(getVorname(i),getName(i),getAlter(i));
if (filter!=null)
{
if (filter.match(bean))
{
ret.add(bean);
}
}
else ret.add(bean);
}
return ret;
}
private static int getAlter(int pos)
{
return (pos % 7) + (pos/9);
}
static String getVorname(int pos)
{
switch (pos % 5)
{
case 0: return "Klaus";
case 1: return "Susi";
case 2: return "Petra";
case 3: return "Axel";
}
return "Bert";
}
static String getName(int pos)
{
switch (pos % 8)
{
case 0: return "Schmidt";
case 1: return "Meier";
case 2: return "Schulz";
case 3: return "Schuster";
case 4: return "Müller";
case 5: return "Francis";
case 6: return "Friedrich";
}
return "Sommerfeld";
}
}
Um Daten durch die DataTable-Komponente zur Anzeige zu bringen, müssen wir das IDataProvider-Interface implementieren. Da wir die Daten auch sortieren und filtern möchten, implementieren wir zusätzlich das ISortableDataProvider und das IFilterStateLocator-Interface.
package de.wicketpraxis.web.blog.pages.questions.data;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.apache.wicket.extensions.markup.html.repeater.data.sort.ISortState;
import org.apache.wicket.extensions.markup.html.repeater.data.table.ISortableDataProvider;
import org.apache.wicket.extensions.markup.html.repeater.data.table.filter.IFilterStateLocator;
import org.apache.wicket.extensions.markup.html.repeater.util.SingleSortState;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
public class SomeBeanDataProvider implements ISortableDataProvider<SomeBean>, IFilterStateLocator
{
ISortState _sortState=new SingleSortState();
SomeBeanFilter _filter=new SomeBeanFilter();
final static int LIST_SIZE=123;
private List<SomeBean> _list;
public Iterator<? extends SomeBean> iterator(int first, int count)
{
initList();
List<SomeBean> ret=_list;
if (ret.size()>(first+count))
{
ret = ret.subList(first, first+count);
}
return ret.iterator();
}
public IModel<SomeBean> model(SomeBean object)
{
return Model.of(object);
}
public int size()
{
initList();
return _list.size();
}
public void detach()
{
_list=null;
}
public ISortState getSortState()
{
return _sortState;
}
public void setSortState(ISortState state)
{
_sortState=state;
}
public Object getFilterState()
{
return _filter;
}
public void setFilterState(Object state)
{
_filter=(SomeBeanFilter) state;
}
private void initList()
{
if (_list==null)
{
final int nameSort;
final int alterSort;
if (_sortState!=null)
{
nameSort = _sortState.getPropertySortOrder("name");
alterSort = _sortState.getPropertySortOrder("alter");
}
else
{
nameSort = ISortState.NONE;
alterSort = ISortState.NONE;
}
_list = getSortedList(nameSort, alterSort,_filter);
}
}
private List<SomeBean> getSortedList(final int nameSort, final int alterSort,SomeBeanFilter filter)
{
List<SomeBean> result = SomeBeanGenerator.getBeans(LIST_SIZE,filter);
Collections.sort(result,new Comparator<SomeBean>()
{
public int compare(SomeBean o1, SomeBean o2)
{
int compName=o1.getName().compareTo(o2.getName());
int compAlter=new Integer(o1.getAlter()).compareTo(o2.getAlter());
switch (nameSort)
{
case ISortState.NONE:
compName=0;
break;
case ISortState.DESCENDING:
compName=-compName;
break;
}
switch (alterSort)
{
case ISortState.NONE:
compAlter=0;
break;
case ISortState.DESCENDING:
compAlter=-compAlter;
break;
}
if (compName!=0) return compName;
return compAlter;
}
});
return result;
}
}
Erläuterungen
Die implementieren Schnittstellen stellen im wesentlichen folgende Funktionen bereit:
- Anzahl der Einträge in der Datenbank (in unserem Beispiel fest auf 123 eingestellt)
- ein Ausschnitt der Einträge (von, bis)
- Eine Implementierung von ISortState (welche Spalte ist in welche Richtung sortiert)
- Ein Objekt,in dem die Filterwerte abgelegt werden (SomeBeanFilter)
Auf diese Weise kann man die Ergebnisliste sortieren, filtern und einen Ausschnitt davon darstellen. Außerdem ist bekannt, wie viel Einträge vorhanden sind.
DataTable - DefaultDataTable
Die DefaultDataTable-Komponente unterscheidet sich von der DataTable-Komponente im wesentlichen nur dadurch, dass bereits die Seitennavigation und die Spaltenüberschriften eingeblendet werden. Daher greifen wir in diesem Beispiel direkt auf diese Komponente zurück.
package de.wicketpraxis.web.blog.pages.questions.datatable;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.extensions.markup.html.repeater.data.table.DefaultDataTable;
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.filter.FilterForm;
import org.apache.wicket.extensions.markup.html.repeater.data.table.filter.FilterToolbar;
import org.apache.wicket.extensions.markup.html.repeater.data.table.filter.TextFilteredPropertyColumn;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.model.Model;
import de.wicketpraxis.web.blog.pages.questions.data.SomeBean;
import de.wicketpraxis.web.blog.pages.questions.data.SomeBeanDataProvider;
public class DataTablePage extends WebPage
{
public DataTablePage()
{
List<IColumn<SomeBean>> columns=new ArrayList<IColumn<SomeBean>>();
columns.add(new TextFilteredPropertyColumn<SomeBean,String>(Model.of("Vorname"),"vorname"));
columns.add(new TextFilteredPropertyColumn<SomeBean,String>(Model.of("Name"),"name","name"));
columns.add(new PropertyColumn<SomeBean>(Model.of("Alter"),"alter","alter"));
SomeBeanDataProvider dataProvider=new SomeBeanDataProvider();
FilterForm form=new FilterForm("form",dataProvider);
DefaultDataTable<SomeBean> dataTable = new DefaultDataTable<SomeBean>("dataTable",columns,dataProvider,10);
dataTable.addTopToolbar(new FilterToolbar(dataTable,form,dataProvider));
form.add(dataTable);
add(form);
}
}
Zuerst definieren wir eine Liste von Spalten, in der wir dann verschieden Spaltentypen verwenden. Die TextFilteredPropertyColumn vereint die Eingabe eines Filters, die Sortierung der Spalte und die Anzeige der Spalte in einer Komponente. Da für die Spalte "Vorname" das Attribut für die Sortierung weggelassen wurde, kann nach dieser Spalte auch nicht sortiert werden. Die letzte Spalte ist die einfachste Form einer Spaltendefinition und dient nur der Anzeige. Damit die Filter zum Einsatz kommen können, muss man die Tabelle mit einem Formular umschließen. Dazu muss die FilterForm-Komponente verwendet werden. Außerdem muss man zusätzlich eine FilterToolbar hinzufügen, damit die Eingabefelder auch angezeigt werden.
Da die DataTable-Komponente generisch ist (man kann Spalten hinzufügen oder weglassen, man kann die FilterToolbar deaktivieren, etc.), muss man in so einem Fall keine Anpassungen am Markup vornehmen. Das Markup sieht daher auch recht übersichtlich aus.
<html>
<head>
<title>DataTable Page</title>
</head>
<body>
<form wicket:id="form">
<input type="hidden" wicket:id="focus-tracker"></input>
<input type="hidden" wicket:id="focus-restore"></input>
<table wicket:id="dataTable"></table>
</form>
</body>
</html>
Auffällig sind hier die zwei versteckten Eingabefelder, die die Komponente für (mich nicht ganz nachvollziehbare) Javascript-Fokus-Funktionen benutzt. Außerdem muss man feststellen, dass die DataTable-Komponente einfach nur an einen Table-Tag gebunden wurde. Das restliche Markup bringt die Komponente mit. Das bedeutet gleichzeitig, dass man kaum Einfluss auf das Markup innerhalb der Tabelle hat, wenn man die Komponente nicht mit einem eigenen Markup überschreibt.
DataView
Wenn man doch mehr Kontrolle über das Markup haben möchte, kann man sich aus dem Fundus der Komponenten bedienen, die auch bei der DataTable zum Einsatz kommen. Im folgenden stellen wir eine funktionsäquivalente Komponente mit einer DataView-Komponente und anderen Komponenten nach.
package de.wicketpraxis.web.blog.pages.questions.dataview;
import org.apache.wicket.extensions.markup.html.repeater.data.sort.OrderByLink;
import org.apache.wicket.extensions.markup.html.repeater.data.table.NavigatorLabel;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.navigation.paging.PagingNavigator;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.model.CompoundPropertyModel;
import de.wicketpraxis.web.blog.pages.questions.data.SomeBean;
import de.wicketpraxis.web.blog.pages.questions.data.SomeBeanDataProvider;
import de.wicketpraxis.web.blog.pages.questions.data.SomeBeanFilter;
public class DataViewPage extends WebPage
{
public DataViewPage()
{
SomeBeanDataProvider dataProvider=new SomeBeanDataProvider();
Form<SomeBeanFilter> form = new Form<SomeBeanFilter>("form",new CompoundPropertyModel<SomeBeanFilter>(dataProvider.getFilterState()));
form.add(new TextField<String>("name"));
form.add(new TextField<String>("vorname"));
form.add(new OrderByLink("sortName","name",dataProvider));
form.add(new OrderByLink("sortAlter","alter",dataProvider));
DataView<SomeBean> dataView = new DataView<SomeBean>("dataView",dataProvider)
{
@Override
protected void populateItem(Item<SomeBean> item)
{
item.setDefaultModel(new CompoundPropertyModel<SomeBean>(item.getModel()));
item.add(new Label("vorname"));
item.add(new Label("name"));
item.add(new Label("alter"));
}
};
dataView.setItemsPerPage(10);
form.add(new PagingNavigator("navigator",dataView));
form.add(new NavigatorLabel("label",dataView));
form.add(dataView);
add(form);
}
}
Zuerst erstellen wir ein Formular, dass die Daten in der FilterBean des DataProviders ablegt. Wie im oberen Beispiel fügen wir alle weiteren Komponenten als Kindkomponenten zu diesem Formular hinzu. Für die Sortierung greifen wir auf die OrderByLink-Komponente zurück, die auch in der DataTable-Komponente benutzt wird. Die DataView-Komponente erwartet als Datenlieferanten ebenfalls eine IDataProvider-Implementierung. Anders als bei der DataTable greifen wir in diesem Fall nicht auf einer Liste von Spalten zurück, sondern erzeugen beliebige Komponenten, die wir dann später im Markup in die entsprechenden Spalten sortieren. Das wir für das item das DefaultModel überschreiben, dient nur zur Förderung einer gewissen Schreibfaulheit, da man sich so für jedes Label die Angabe eines Models erspart. Wir begrenzen außerdem die angezeigten Datensätze auf 10 Einträge. Der PagingNavigator und das NavigatorLabel werden ebenso in der DataTable-Komponente benutzt, so dass wir auch dafür keine eigene Komponente schreiben müssen.
Zusätzlich zum Markup definieren wir außerdem noch eine Properties-Datei, um die angezeigten Texte anzupassen (z.B. Tooltips bei den Links).
<html>
<head>
<title>DataTable Page</title>
</head>
<body>
<form wicket:id="form">
<table>
<thead>
<tr>
<td colspan="3">
<span wicket:id="label"></span>
<span wicket:id="navigator"></span>
</td>
</tr>
<tr>
<th>Vorname</th>
<th><a wicket:id="sortName">Name</a></th>
<th><a wicket:id="sortAlter">Alter</a></th>
</tr>
<tr>
<th><input wicket:id="vorname"></th>
<th><input wicket:id="name"></th>
<th></th>
</tr>
</thead>
<tbody>
<tr wicket:id="dataView">
<td><span wicket:id="vorname"></span></td>
<td><span wicket:id="name"></span></td>
<td><span wicket:id="alter"></span></td>
</tr>
</tbody>
</table>
</form>
</body>
</html>
PagingNavigator.last=Ende
PagingNavigator.first=Start
PagingNavigator.previous=Eins zurück
PagingNavigator.next=Eins weiter
PagingNavigation.page=Zur Seite ${page}
NavigatorLabel=Gezeigt wird ${from} bis ${to} von ${of}
Wie man sieht, muss man sehr viel mehr Aufwand für das richtige Markup betreiben. Allerdings eröffnen sich dadurch natürlich wesentlich mehr Möglichkeiten, direkt auf das Markup Einfluss nehmen zu können.
Im Bild sieht man die Komponente mit einem eingetragenen Wert für einen Filter und nach Alter sortiert.
Zusammenfassung
Wie man an diesem Beispiel erkennen kann, bietet Wicket sehr komplexe und mächtige Komponenten. Trotzdem kann man jederzeit eigene Anpassungen vornehmen und durch den geschickten Einsatz von Teilkomponenten recht schnell die gewünschte Flexibilität erreichen. Der Aufwand hält sich dabei in Grenzen. Allerdings fehlen das eine oder andere Mal ausreichend Dokumentationen oder Beispiele, so dass man nicht um ein Studium der Quelltexte umhinkommt. Doch je mehr Verbreitung Wicket findet, wird auch dieses Problem über kurz oder lang gelöst werden können.