Wicket ist ein Webframework nach dem MVC-Prinzip. Dabei orientiert sich Wicket in vielen Dingen eher an Grundprinzipien der Anwendungsentwicklung von Desktop-Anwendungen. Eine wesentliche Rolle spielen dabei die Modelle, die in Komponenten benutzt werden und die die Daten liefern, die durch verschiedenen Komponenten zur Anzeige gebracht werden. Dabei gibt es aber gerade in diesem Bereich sehr viele Irritationen. Problematisch erweist sich dabei vielleicht der Umstand, dass man auch ohne Modelle zu benutzen, mit Wicket bereits Daten zur Anzeige bringen kann. Man versteht dann erst einmal nicht, wie sich dass von einer Anwendung z.B. in PHP unterscheiden soll. Betrachtet man die Label-Komponente (die häufig für die Darstellung herangezogen wird) näher, stellt man fest, dass auch der Aufruf ohne Model intern dazu führt, dass der Übergabeparameter in ein Model gelegt wird.
In einem kleinen Beispiel möchte ich veranschaulichen, wie die verschiedenen Model-Klassen zum Einsatz kommen können und warum man immer Modelle benutzen sollte.
Wir erstellen zuerst eine Model-Klasse, die eine Liste von Ergebnissen liefert. In diesem Fall werden die Einträge zwar fest kodiert, könnten aber genauso gut aus der Datenbank kommen.
package de.wicketpraxis.web.blog.pages.migration.model;
public class Result
{
String _name;
Double _betrag;
public Result(String name, double betrag)
{
_name=name;
_betrag=betrag;
}
public String getName()
{
return _name;
}
public Double getBetrag()
{
return _betrag;
}
}
package de.wicketpraxis.web.blog.pages.migration.model;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.model.LoadableDetachableModel;
public class ResultListModel extends LoadableDetachableModel<List<Result>>
{
@Override
protected List<Result> load()
{
List<Result> ret=new ArrayList<Result>();
ret.add(new Result("Popcorn",3.15));
ret.add(new Result("Eis",2.80));
ret.add(new Result("Eintritt",5.50));
return ret;
}
}
Das LoadableDetachableModel ist immer dann die beste Wahl, wenn Daten dynamisch erzeugt werden sollen und jedes mal sicher gestellt werden soll, dass die aktuellsten Daten verwendet werden. Wir das Model während eines Request geladen, werden die Daten so lange vorgehalten, bis die detach()-Methode aufgerufen wird, und die Liste zurückgesetzt wird. Das Model liefert uns in diesem Fall 3 Datensätze der Result-Klasse.
Die Beispielliste ist eine Liste von Posten auf einer Rechnung. Wir möchten aber zum Betrag auch noch den MwSt-Anteil ausweisen. Dazu schreiben wir ein allgemeines Model, dass von einer Erweiterung des LoadableDetachableModel abgeleitet ist: CascadingLoadableDetachableModel.
package de.wicketpraxis.web.blog.pages.migration.model;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import de.wicketpraxis.web.blog.model.CascadingLoadableDetachableModel;
public class MwStAnteilModel extends CascadingLoadableDetachableModel<Double, Double>
{
static final Double MWST=19.0;
public MwStAnteilModel(IModel<? extends Double> parent)
{
super(parent);
}
@Override
protected Double load(Double brutto)
{
if (brutto!=null)
{
return brutto*MWST/100.0;
}
return null;
}
}
Das Model entnimmt dem referenzierten Model den Betrag und ermittelt den MwSt-Anteil, den es seinerseits bereitstellt. Der Wert wird auch hier bis zum Aufruf von detach() gepuffert. Als Referenzmodell kann jede Klasse dienen, die IModel implementiert.
Jetzt fügen wir die Bestandteile zusammen:
package de.wicketpraxis.web.blog.pages.migration.model;
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.list.ListItem;
import org.apache.wicket.markup.html.list.PropertyListView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.PropertyModel;
import de.wicketpraxis.web.blog.model.CascadingLoadableDetachableModel;
public class SomeModelsPage extends WebPage
{
public SomeModelsPage()
{
ResultListModel resultList=new ResultListModel();
add(new PropertyListView<Result>("list",resultList)
{
@Override
protected void populateItem(ListItem<Result> item)
{
IModel<Result> model = item.getModel();
item.add(new Label("name",new PropertyModel<String>(model,"name")));
item.add(new Label("betrag"));
item.add(new Label("mwst",new MwStAnteilModel(new PropertyModel<Double>(model,"betrag"))));
}
});
IModel<Double> summeModel = new CascadingLoadableDetachableModel<Double, List<Result>>(resultList)
{
@Override
protected Double load(List<Result> liste)
{
if (liste!=null)
{
double summe=0;
for (Result r : liste)
{
summe=summe+r.getBetrag();
}
return summe;
}
return null;
}
};
add(new Label("summe",summeModel));
add(new Label("mwst",new MwStAnteilModel(summeModel)));
}
}
Zuerst erstellen wir eines Instanz des ResultListModel. Die Ergebnisliste wird durch eine PropertyListView dargestellt. Der Unterschied zur ListView-Komponente besteht darin, dass bei Komponenten ohne eigenes Model die Id als Property-Expression herangezogen wird, um das Model für die Anzeige der Daten zu ermitteln. Die Schreibarbeit, die man für das Label "name" aufwenden müsste, kann man auf diese Weise erheblich reduzieren. Wie man beim Label mit der Id "mwst" erkennen kann, können die Modelle beliebig geschachtelt werden.
Um die Summe aller Einträge zu ermitteln, könnten wir jetzt die Ergebnisliste durchgehen und die Summe berechnen. Doch der Konstruktor einer Komponente wird ja nur einmal aufgerufen. Ändert sich durch eine Aktion auf der Seite oder in einer Komponente etwas an den zugrunde liegenden Daten, dann wird die Summe so nicht automatisch aktualisiert. Daher erstellen wir ein Model, dass diese Berechnung immer wieder durchführt. Dann können wir die aktuelle Summe und auch hierfür den MwSt-Anteil anzeigen lassen. Sollte sich an den Daten in der Datenbank etwas ändern, dann werden automatisch alle angezeigten Werte neu berechnet.
In diesem Beispiel sind mehrere wichtige Themen versteckt:
- Alle Daten sollten in einem Model abgelegt werden. Daten sollten nicht im Konstruktor einer Komponente aufbereitet werden.
- Für Modelle, die nicht an eine Komponente gebunden sind, wird die detach()-Methode nicht automatisch aufgerufen. Die CascadingLoadableDetachableModel-Klasse behandelt diesen Umstand entsprechend.
- Model-Klassen kann man wiederverwenden.
- Das Beispiel veranschaulicht, wie man z.B. die Summe aller sichtbaren Einträge über den Model-Weg ermitteln kann.
Ich hoffe, ich konnte wieder etwas Licht in das Dunkel rund um die Model-Klassen bringen. Fragen und Anregungen gerne in dem Kommentaren oder per Email.