Skip to content


DataTable vs. DataView - Wicket-Komponenten wiederverwendet

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.

datatable-komponente-filter-sort

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. dataview-komponente-filter-sort

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.

Tags:

Veröffentlicht in Allgemein, Wicket, .

Datenbankeinträge auswählen mit der CheckGroup

Ich wurde gefragt, ob in meinem Buch (Praxisbuch Wicket) die Verwendung der CheckGroup-Komponente erläutert wird. Die Anwort ist einfach: ja.

Was in dem Beispiel aber nicht gezeigt wird, ist das dynamische Erzeugen der Auswahlliste durch Daten aus einer Datenbank. Der Absender hat sich aber genau für diesen Fall interessiert, so dass ich an dieser Stelle mal ein einfaches Beispiel geben möchte, wie man soetwas realisieren kann.

Das Beispiel

package de.wicketpraxis.web.blog.pages.questions.checkgroup.fromdb;

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

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Check;
import org.apache.wicket.markup.html.form.CheckGroup;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.util.CollectionModel;
import org.apache.wicket.spring.injection.annot.SpringBean;

import de.wicketpraxis.persistence.beans.User;
import de.wicketpraxis.persistence.dao.UserDao;

public class CheckGroupFromDbPage extends WebPage
{
  @SpringBean
  UserDao _userDao;
  
  public CheckGroupFromDbPage()
  {
    LoadableDetachableModel<List<User>> userListModel=new LoadableDetachableModel<List<User>>()
    {
      @Override
      protected List<User> load()
      {
        return _userDao.findAll(0, 10);
      }
    };
    
    final IModel<Collection<Integer>> selectedModel = new CollectionModel<Integer>(new ArrayList<Integer>());
    
    add(new FeedbackPanel("feedback"));
    
    Form<Void> form=new Form<Void>("form")
    {
      @Override
      protected void onSubmit()
      {
        info("Elemente selectiert: "+selectedModel.getObject());
      }
    };
    
    CheckGroup<Integer> checkGroup=new CheckGroup<Integer>("checkGroup",selectedModel);
    form.add(checkGroup);
    
    checkGroup.add(new ListView<User>("list",userListModel)
    {
      @Override
      protected void populateItem(ListItem<User> item)
      {
        item.add(new Check<Integer>("check",Model.of(item.getModelObject().getId())));
        item.add(new Label("name",item.getModelObject().getName()));
      }
    });
    
    add(form);
    
  }
}

Und das Markup-File:

<html>
  <head>
    <title>CheckGroup from DB</title>
  </head>
  <body>
    <div wicket:id="feedback"></div>
    
    <form wicket:id="form">
      <wicket:container wicket:id="checkGroup">
      <wicket:container wicket:id="list">
        <input wicket:id="check" type="checkbox"> <span wicket:id="name"></span><br>
      </wicket:container>
      </wicket:container>
      <input type="submit" value="Auswählen">
    </form>
  </body>
</html>

Die Variable _userDao beinhaltet die Datenbankzugriffsklasse. Da wir die Variable als Field definiert haben, können wir ohne weiteres innerhalb der Klasse darauf zugreifen. Wicket sorgt für alle über die SpringBean-Annotation initialisierten Felder dafür, dass es mit der Serialisierung der Komponente keine Probleme gibt.

Als nächstes erstellen wir ein Modell, dass die gewünschte Auswahlmenge ermittelt. An dieser Stelle könnte man über ein CascadingLoadableDetachableModel (kein Wicket-Standard, siehe Buch Seite 65 oder hier im Blog) auch auf die Modelldaten eines Eingabefeldes zurückgreifen um die Abfrage einsprechende zu beeinflussen zu können. Das Ergebis ist eine Liste von Objekten.

Als zweiter erstellen wir ein Modell, in dem die IDs der ausgewählten Elemente landen. Um den Aufwand einer eigenen Implementierung einer Modellklasse für eine Collection von persistenten Objekten zu sparen, legen wir nur die ID des Elements, nicht das Element selbst in der Auswahlliste ab.

Das Feedbackpanel dient zur Anzeige die Message, die in der onSubmit-Methode des Formulars ausgegeben wird. Das Formular hat in diesem Fall kein eigenes Modell (nur der Einfachheit halber wird in dem Beispiel auf die Verwendung eines CompoundPropertyModel verzichtet).

Jetzt kommt der spannende Teil. Der CheckGroup-Komponente wird als Parameter das Modell übergeben, in der die Ausgewählten Elemente gesammelt werden sollen. Dann wird in der ListView für alle zur Verfügung stehenden Elemente eine Check-Komponente hinzugefügt, die als Modell nicht das Objekt, sondern die ID des Datenbankeintrags übergeben bekommt.

Funktionsweise

Wenn das Formular abgeschickt wird, ermittelt Wicket alle Check-Komponenten, die aktiviert waren. Für diese Komponenten wird der Inhalt des Modells ermittelt und der Auswahlliste hinzugefügt. Abschließend wird das Modell, dass die Auswahlliste beinhaltet entsprechend gesetzt. Dabei können beliebige Typen für die Auswahlliste benutzt werden, solange sie serialisierbar sind. Kommen die Daten aus einer Datenbank empfiehlt sich der Einsatz eines Primärschlüssels, anhand dessen das Objekt ermittelbar bleibt.

Tags:

Veröffentlicht in Allgemein, Wicket, .