• Save
Wicket ASDF
Upcoming SlideShare
Loading in...5
×
 

Wicket ASDF

on

  • 2,057 views

 

Statistics

Views

Total Views
2,057
Views on SlideShare
2,056
Embed Views
1

Actions

Likes
1
Downloads
0
Comments
0

1 Embed 1

http://www.slideshare.net 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Wicket ASDF Wicket ASDF Document Transcript

  • michael MOSMANNPRAXISBUCHWICKETPROFESSIONELLE WEB-2.0-ANWENDUNGEN ENTWICKELN
  • Mosmann Praxisbuch Wicketv Bleiben Sie einfach auf dem Laufenden: www.hanser.de/newsletterSofort anmelden und Monat für Monatdie neuesten Infos und Updates erhalten.
  • Michael MosmannPraxisbuch WicketProfessionelle Web-2.0-Anwendungenentwickeln
  • Michael Mosmann, LübeckKontakt: michael@mosmann.deAlle in diesem Buch enthaltenen Informationen, Verfahren und Darstellungen wurden nach bestemWissen zusammengestellt und mit Sorgfalt getestet. Dennoch sind Fehler nicht ganz auszuschließen.Aus diesem Grund sind die im vorliegenden Buch enthaltenen Informationen mit keiner Verpflich-tung oder Garantie irgendeiner Art verbunden. Autor und Verlag übernehmen infolgedessen keinejuristische Verantwortung und werden keine daraus folgende oder sonstige Haftung übernehmen, dieauf irgendeine Art aus der Benutzung dieser Informationen – oder Teilen davon – entsteht.Ebenso übernehmen Autor und Verlag keine Gewähr dafür, dass beschriebene Verfahren usw. freivon Schutzrechten Dritter sind. Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbe-zeichnungen usw. in diesem Buch berechtigt deshalb auch ohne besondere Kennzeichnung nicht zuder Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung alsfrei zu betrachten wären und daher von jedermann benutzt werden dürften.Bibliografische Information der Deutschen Nationalbibliothek:Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbiblio-grafie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.Dieses Werk ist urheberrechtlich geschützt.Alle Rechte, auch die der Übersetzung, des Nachdruckes und der Vervielfältigung des Buches, oderTeilen daraus, vorbehalten. Kein Teil des Werkes darf ohne schriftliche Genehmigung des Verlagesin irgendeiner Form (Fotokopie, Mikrofilm oder ein anderes Verfahren) – auch nicht für Zwecke derUnterrichtsgestaltung – reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, ver-vielfältigt oder verbreitet werden.© 2009 Carl Hanser Verlag München, www.hanser.deLektorat: Margarete MetzgerCopy editing: Jürgen Dubau, FreiburgHerstellung: Irene WeilhartUmschlagdesign: Marc Müller-Bremer, www.rebranding.de, MünchenUmschlagrealisation: Stephan RönigkDatenbelichtung, Druck und Bindung: Kösel, KrugzellAusstattung patentrechtlich geschützt. Kösel FD 351, Patent-Nr. 0748702Printed in GermanyISBN 978-3-446-41909-4
  • für meine Eltern
  • 2
  • InhaltVorwort..............................................................................................................................XIII1 Einleitung ................................................................................................................. 11.1 Warum Wicket? .......................................................................................................................3 1.1.1 Einfach, Konsistent, Offensichtlich ............................................................................4 1.1.2 Wiederverwendbarkeit ...............................................................................................4 1.1.3 Sauber getrennt...........................................................................................................5 1.1.4 Sicher..........................................................................................................................5 1.1.5 Effizient und skalierbar ..............................................................................................6 1.1.6 Komplett.....................................................................................................................6 1.1.7 Eine gute Wahl ...........................................................................................................61.2 Vorbereitung und Installation...................................................................................................7 1.2.1 Java, Maven und Eclipse ............................................................................................7 1.2.2 Versionskontrolle mit Subversion ..............................................................................71.3 Grundlagen einer Webanwendung ...........................................................................................8 1.3.1 Anwendungsschichten................................................................................................8 1.3.2 Verzeichnis und Paketstruktur..................................................................................11 1.3.3 Unit-Tests .................................................................................................................122 Aufsetzen der Teilprojekte .................................................................................... 152.1 Nomenklatur der Teilprojekte ................................................................................................152.2 Aufsetzen der Teilprojekte .....................................................................................................16 2.2.1 Projektbasis ParentPom ............................................................................................16 2.2.2 Teilprojekt Base .......................................................................................................20 2.2.3 Teilprojekte Datenbankkonfiguration.......................................................................20 2.2.4 Teilprojekt Persistenz ...............................................................................................22 2.2.5 Teilprojekt Applikationsschicht................................................................................24 2.2.6 Teilprojekt Webapp ..................................................................................................24 2.2.7 Teilprojekt ParentPom – Abschluss..........................................................................262.3 Erstellen von Eclipse-Projektdateien......................................................................................27 VII
  • Inhalt 3 Mit Leben füllen ..................................................................................................... 29 3.1 Konfiguration mit Spring ....................................................................................................... 29 3.2 Datenbankkonfiguration......................................................................................................... 30 3.2.1 Teilprojekt dbconfig................................................................................................. 30 3.2.2 Teilprojekt dbconfig-test .......................................................................................... 31 3.2.3 Teilprojekt dbconfig-schema-update........................................................................ 31 3.2.4 Schemagenerierung mit Hibernate ........................................................................... 32 3.3 Persistenz ............................................................................................................................... 33 3.3.1 Datenbankzugriff – Allgemeine Schnittstellendefinition ......................................... 33 3.3.2 Datenbankzugriff – Hilfsklassen .............................................................................. 34 3.3.3 Datenbankzugriff – User .......................................................................................... 35 3.3.4 Datenbankzugriff – Konfiguration ........................................................................... 37 3.3.5 Persistenz-Tests........................................................................................................ 38 3.3.6 Schema-Update ........................................................................................................ 40 3.4 Anwendungsschicht ............................................................................................................... 41 3.5 Präsentationsschicht............................................................................................................... 41 3.5.1 Hilfsklasse für Maven-Projekte................................................................................ 41 3.5.2 Wicket Web Application .......................................................................................... 42 3.5.3 Servlet-Konfiguration............................................................................................... 44 3.5.4 Spring-Konfiguration ............................................................................................... 46 3.5.5 Start der Anwendung................................................................................................ 46 4 Die Wicket-Architektur .......................................................................................... 49 4.1 Wicket und das HTTP-Protokoll............................................................................................ 49 4.2 Struktur .................................................................................................................................. 49 4.2.1 WebApplication ....................................................................................................... 50 4.2.2 Session ..................................................................................................................... 50 4.2.3 PageMap .................................................................................................................. 50 4.2.4 Page.......................................................................................................................... 50 4.2.5 PageStore ................................................................................................................. 51 4.2.6 Component ............................................................................................................... 51 4.3 Request-Behandlung .............................................................................................................. 51 4.3.1 Komponentenphasen ................................................................................................ 52 4.3.2 Nebenläufigkeit – Threads ....................................................................................... 52 4.4 Komponenten, Modelle, Markup ........................................................................................... 53 4.4.1 Komponenten ........................................................................................................... 53 4.4.2 Modelle .................................................................................................................... 53 4.4.3 Markup ..................................................................................................................... 53 5 Modelle ................................................................................................................... 55 5.1 Konverter ............................................................................................................................... 55 5.2 Einfache Modelle ................................................................................................................... 57 5.2.1 Modelle verändern ................................................................................................... 58 5.3 Modell-Hilfsklassen............................................................................................................... 60 5.4 Modelle und Serialisierung .................................................................................................... 61 5.4.1 DetachableModel – Dynamische Modelldaten......................................................... 61VIII
  • Inhalt 5.4.2 Kaskadierung von Modellen.....................................................................................62 5.4.3 Automatische Kaskadierung von Modellen..............................................................65 5.4.4 Datenbankzugriffsmodelle........................................................................................665.5 Komplexe Modellklassen.......................................................................................................69 5.5.1 Zugriff auf Bean-Properties......................................................................................69 5.5.2 Die Klasse PropertyModel........................................................................................72 5.5.3 CompoundPropertyModel ........................................................................................745.6 Ausgelagerte Informationen...................................................................................................76 5.6.1 Einfacher Zugriff auf Ressourcen.............................................................................76 5.6.2 ResourceModel.........................................................................................................76 5.6.3 StringResourceModel ...............................................................................................786 Komponenten......................................................................................................... 816.1 Basisklasse Component..........................................................................................................81 6.1.1 Komponentenbaum ..................................................................................................81 6.1.2 Darstellungsphasen...................................................................................................83 6.1.3 Page, Session und Application .................................................................................84 6.1.4 Komponentenpfad ....................................................................................................84 6.1.5 Modelle ....................................................................................................................84 6.1.6 Feedback ..................................................................................................................856.2 Grundlagen der Vererbung.....................................................................................................85 6.2.1 Eine Seite mit eigenen Komponenten.......................................................................85 6.2.2 Vererbung für Fortgeschrittene................................................................................916.3 Style, Locale und Variation....................................................................................................94 6.3.1 Markup-Variationen .................................................................................................946.4 Sichtbarkeit ............................................................................................................................99 6.4.1 wicket:enclosure.....................................................................................................100 6.4.2 Empfehlung zur Anwendung..................................................................................1016.5 Ajax......................................................................................................................................102 6.5.1 Ajax-Events............................................................................................................103 6.5.2 Einfache Event-Behandlung ...................................................................................104 6.5.3 Automatische Event-Behandlung ...........................................................................1057 Basiskomponenten.............................................................................................. 1097.1 Gruppierende Komponenten ................................................................................................109 7.1.1 Seiten......................................................................................................................109 7.1.2 Panel.......................................................................................................................117 7.1.3 Fragment ................................................................................................................119 7.1.4 Border.....................................................................................................................120 7.1.5 ComponentBorder ..................................................................................................125 7.1.6 WebMarkupContainer ............................................................................................1267.2 Inhaltselemente ....................................................................................................................127 7.2.1 Label und MultiLineLabel......................................................................................127 7.2.2 Lokaler Konverter ..................................................................................................129 7.2.3 XML.......................................................................................................................130 7.2.4 Das wicket:message-Tag ........................................................................................131 IX
  • Inhalt 7.2.5 Image...................................................................................................................... 132 7.3 Links .................................................................................................................................... 137 7.3.1 Von A nach B......................................................................................................... 137 7.3.2 Ajax und Links....................................................................................................... 138 7.3.3 Link-Tricks............................................................................................................. 140 7.3.4 Externe Links ......................................................................................................... 141 7.3.5 Popups.................................................................................................................... 141 7.3.6 ResourceLink ......................................................................................................... 143 7.3.7 Formularlinks ......................................................................................................... 144 7.4 Behavior............................................................................................................................... 144 7.4.1 Darf es etwas JavaScript sein? ............................................................................... 144 7.4.2 Attribute anpassen .................................................................................................. 145 7.4.3 Attribute erweitern ................................................................................................. 147 7.4.4 Ajax und Formulare ............................................................................................... 148 8 Listen und Tabellen ............................................................................................. 149 8.1 Darstellung von Listen......................................................................................................... 149 8.1.1 RepeatingView....................................................................................................... 149 8.1.2 RefreshingView...................................................................................................... 150 8.1.3 ListView................................................................................................................. 152 8.1.4 PropertyListView ................................................................................................... 153 8.1.5 ColumnListView .................................................................................................... 154 8.2 DataProvider ........................................................................................................................ 156 8.2.1 DataView ............................................................................................................... 156 8.2.2 GridView................................................................................................................ 158 8.2.3 DataGridView ........................................................................................................ 159 8.2.4 DataTable ............................................................................................................... 161 8.2.5 DefaultDataTable ................................................................................................... 162 9 Formulare ............................................................................................................. 169 9.1 Voraussetzungen .................................................................................................................. 169 9.2 Feedback .............................................................................................................................. 170 9.3 Basisklasse für alle Beispiele ............................................................................................... 171 9.4 Formulare absenden ............................................................................................................. 172 9.4.1 Absenden mit Submit-Button ................................................................................. 172 9.4.2 Button-Komponente ............................................................................................... 173 9.4.3 Submit per Ajax ..................................................................................................... 174 9.4.4 POST und GET ...................................................................................................... 175 9.5 Textfelder............................................................................................................................. 176 9.5.1 Typangabe.............................................................................................................. 178 9.5.2 Automatische Typermittlung.................................................................................. 179 9.6 Label .................................................................................................................................... 181 9.7 CheckBox ............................................................................................................................ 182 9.8 RadioButton......................................................................................................................... 185 9.9 Auswahlfelder...................................................................................................................... 186 9.9.1 Select...................................................................................................................... 186X
  • Inhalt 9.9.2 DropDownChoice...................................................................................................188 9.9.3 ListMultipleChoice.................................................................................................1909.10 Dateien hochladen................................................................................................................192 9.10.1 FileUpload..............................................................................................................192 9.10.2 MultiFileUpload .....................................................................................................1949.11 Gültigkeitsprüfung ...............................................................................................................195 9.11.1 StringValidator .......................................................................................................196 9.11.2 Minimum und Maximum .......................................................................................197 9.11.3 E-Mail ....................................................................................................................197 9.11.4 URL........................................................................................................................199 9.11.5 Eigene Validatoren .................................................................................................1999.12 FormValidator......................................................................................................................201 9.12.1 Passwortprüfung.....................................................................................................201 9.12.2 Eigene Prüfung .......................................................................................................2039.13 Ajax......................................................................................................................................205 9.13.1 AjaxFormSubmitBehavior......................................................................................205 9.13.2 AjaxFormValidatingBehavior ................................................................................207 9.13.3 AjaxComponentUpdatingBehavior ........................................................................207 9.13.4 OnChangeBehavior ................................................................................................209 9.13.5 AutoCompleteTextField .........................................................................................2109.14 AjaxEditableLabel ...............................................................................................................2129.15 Erweitertes Feedback ...........................................................................................................214 9.15.1 Feedback zum Formular .........................................................................................214 9.15.2 Feedback für die Komponente................................................................................215 9.15.3 Feedback als Rahmen .............................................................................................216 9.15.4 Feedback als Indikator............................................................................................217 9.15.5 Feedback per CSS ..................................................................................................2189.16 Generierte Formulare ...........................................................................................................2209.17 Verschachtelte Formulare ....................................................................................................22210 Sessions und Security ........................................................................................ 22510.1 Einfache Variante.................................................................................................................225 10.1.1 Eine eigene Session-Klasse ....................................................................................225 10.1.2 Geschützte Seiten ...................................................................................................226 10.1.3 Strategie..................................................................................................................226 10.1.4 WebApplication......................................................................................................227 10.1.5 Seiten......................................................................................................................22810.2 Marker an Komponenten......................................................................................................23110.3 Elemente ausblenden............................................................................................................23311 Wicket in der Praxis............................................................................................. 23511.1 Die Integration von Spring...................................................................................................23511.2 Navigation............................................................................................................................23711.3 CSS einbinden......................................................................................................................24411.4 Eigene Basiskomponenten ...................................................................................................250 XI
  • Inhalt 11.5 Komponententausch............................................................................................................. 253 11.5.1 AjaxFallbackConfirmLink ..................................................................................... 254 11.5.2 Wizard.................................................................................................................... 256 11.6 Suchmaschinenoptimierung ................................................................................................. 258 11.6.1 Pfad für BookmarkablePages ................................................................................. 258 11.6.2 SessionTimeoutPage .............................................................................................. 262 11.6.3 SEO-Links.............................................................................................................. 264 11.6.4 Servlet-Filter .......................................................................................................... 268 11.6.5 Tracking mit Google Analytics .............................................................................. 271 11.7 Ressourcen........................................................................................................................... 274 11.7.1 Dynamisch erzeugte Grafiken ................................................................................ 274 11.7.2 Automatisch generierte Thumbnails....................................................................... 276 11.7.3 Download durch Formular ..................................................................................... 277 11.7.4 Shared Resources ................................................................................................... 278 11.7.5 RSS-Feed ............................................................................................................... 280 11.8 Links auf Seiten und Ressourcen ......................................................................................... 282 11.9 Optimierungen ..................................................................................................................... 284 11.9.1 Applikation............................................................................................................. 284 11.9.2 Konverter ............................................................................................................... 284 11.9.3 Debug..................................................................................................................... 284 11.9.4 Ressource ............................................................................................................... 285 12 Fehlersuche.......................................................................................................... 287 12.1 Häufige Fehlerquellen.......................................................................................................... 287 12.1.1 Komponenten fehlen .............................................................................................. 287 12.1.2 Komponente ist bereits vorhanden ......................................................................... 287 12.1.3 Ajax funktioniert nicht ........................................................................................... 288 12.2 Unit-Tests ............................................................................................................................ 288 13 Anfang oder Ende?.............................................................................................. 293 Register............................................................................................................................ 295XII
  • VorwortIch beschäftige mich seit meiner frühen Jugend mit der Softwareentwicklung. Angefangenhat das auf einem ZX-Spektrum-Klon in Basic. Im Laufe der Jahre kamen so unterschied-liche Programmiersprachen und Betriebssysteme zusammen. Der Einstieg in die objektori-entierte Programmierung kam mit C++. Die ersten Versuche, Anwendungen mit Benutzer-oberflächen zu schreiben, habe ich auf einem Amiga unternommen. Die Anwendungenwaren klein und der Nutzerkreis beschränkt.Im Juli 1999 fing ich bei Dr. Klein und Co. an und startete mit ersten Anwendungen, dieim Internet zur Verfügung gestellt wurden. Das waren Java-Applets, die z.B. Kreditbe-rechnungen ermöglichten. Nicht viel später entstanden die ersten Webanwendungen, diedamals als Servlets und mit JavaServer Pages realisiert wurden. Die Anwendungen wurdenkomplexer und dieses Entwicklungsmodell stieß zunehmend an seine Grenzen. Mangelsguter Alternativen entstanden so im Laufe der Jahre einige selbst entwickelte Anwen-dungsframeworks. Diese linderten zwar die Probleme etwas, beseitigten das grundlegendeProblem aber nicht.Die Suche nach Alternativen ging weiter und so landete ich im März 2008 auf der Websei-te von Wicket. Ich hatte mir bis dahin so einiges angesehen: GWT, Rails, Thinwire um nureinige zu nennen. Im Gegensatz zu diesen überzeugte mich Wicket von Anfang an.Das ist nun mehr als ein Jahr her und für mich hat sich durch Wicket einiges grundlegendverändert. Webanwendungen zu schreiben ist so einfach geworden, dass man bei neuenFeatures nicht darüber nachdenkt, wie man sie realisiert, sondern im ersten Moment nur,ob man sie realisiert. Wusste man bei den alten Anwendungen genau, welche Problemstel-lungen „Bauchschmerzen“ verursachten, sind diese Anforderungen mit Wicket keine Her-ausforderung und das Entwickeln mit Wicket macht einfach nur Spaß.Die Arbeit kommt vor den Vergnügen. Doch der Spaß sollte nicht lange auf sich wartenlassen. In diesem Sinne: Let’s have some fun. XIII
  • Vorwort Danksagung Es gab eine Menge Menschen, die mich direkt und indirekt beim Schreiben dieses Buches unterstützt haben. Daher ist es unmöglich, nicht aus Versehen, den einen oder anderen zu vergessen. Die Reihenfolge sollte auf keinen Fall als Wertung missverstanden werden ☺. Ich danke natürlich meiner Frau, weil sie mich immer wieder angespornt und mir den Rü- cken frei gehalten hat. Ich danke dem Carl Hanser Verlag und da ganz besonders Frau Metzger, die immer sehr viel geduldiger und ruhiger war als ich. Vermutlich hätte ich sonst bereits das Handtuch geworfen. Dank gebührt auch Dirk Diebel, der einen Blick auf Auszüge dieses Buches werfen durfte und mir bestätigte, dass das nicht ganz unverständ- lich ist, was ich da zusammengeschrieben habe. Ohne Stephan Lamprecht wäre dieses Buch nie entstanden, weil er nicht nur den Kontakt zum Verlag hergestellt hat, sondern auch als erfahrender Autor immer wieder mit einem „Das ist ganz normal“ meine Seele gestreichelt hat. Vielen Dank an alle Wicket-Entwickler und die Wicket-Community für ein großartiges Framework und die großartige Unterstützung bei Problemen und Fragen. Und zum Schluss möchte ich noch allen danken, die in dieser Auflistung nicht vorkom- men, aber mindestens das Gefühl haben dürfen, dass sie mir während dieser Zeit das Leben leichter gemacht haben. Michael Mosmann, August 2009XIV
  • 11 EinleitungIch bin im Frühjahr 2008 auf Wicket aufmerksam geworden. Doch da hatte Wicket bereitseine längere Entwicklung hinter sich, denn es wird bereits seit 2005 entwickelt. 2006 wur-de Wicket ein offizielles Apache-Projekt, und im Januar 2007 wurde dann das erste Re-lease als Apache-Projekt veröffentlicht. Seit April 2008 setze ich Wicket mit viel Freude inProjekten ein.Die Idee zu diesem Buch entstand, nachdem ich ein paar Monate mit Wicket gearbeitethatte und die Begeisterung für das Framework immer stärker zunahm. Ich hatte zu demZeitpunkt bereits einiges über Wicket gelesen, vermisste aber oft den Praxisbezug der Bei-spiele und Lösungen.Viele Wege führen nach Rom, aber nicht jeder ist kurz oder schnell. Und so habe ich amAnfang auch das eine oder andere Mal eine falsche Abbiegung genommen, die sich zwarnicht als Sackgasse entpuppte, aber teilweise einen gehörigen Umweg darstellte. DiesesBuch soll als Straßenkarte dienen, damit man sich auf den gut ausgebauten Straßen, dieWicket bietet, zurechtfindet.Wer sollte das Buch lesen?Wer Webanwendungen entwickelt oder entwickeln möchte und dabei auf Java als Pro-grammiersprache setzt, wird nach der Lektüre dieses Buches sehr schnell Webanwendun-gen entwickeln können. Dabei werden alle Aspekte beleuchtet, die für das Erstellen einerWebanwendung wichtig und notwendig sind. Entwickler, die bereits Wicket einsetzen,profitieren von den praxisnahen Beispielen und bewährten Lösungen.KonventionenIn diesem Buch gelten folgende Konventionen: Programmcode, Verzeichnisse und Datei-namen werden innerhalb des normalen Textes als code dargestellt. Quelltexte werden ohnebesondere Überschrift als Codeblock dargestellt: public void javacode()Bei der Auszeichnung mit Überschrift wird ein Dateiname angegeben: 1
  • 1 Einleitung Listing 1.1 Beispiel.java public void andererCode() Für HTML-Quelltexte, Property- und Markup-Dateien wird dieselbe Formatierung wie für Java-Quelltexte benutzt. Wenn durch die begrenzte Breite des Buches Text umbrochen werden muss und das an dieser Stelle nicht möglich ist, wird das unwillkürliche Zeilenen- de durch ein Leerzeichen und „“ angegeben. Nur wenn das „“-Zeichen an letzter Stelle steht, ist ein unwillkürlicher Zeilenumbruch gemeint. Die nächste Zeile wird dann um zwei Leerzeichen gegenüber dem Zeilenanfang eingerückt: Text=Der Text ist zu lang, als dass er auf eine Zeile passen würde, und beginnt auf der neuen Zeile mit zwei Leerzeichen, die ebenfalls ignoriert werden müssen. Ein -Zeichen mittendrin ist in Ordnung. Da sich viele Dinge immer wiederholen, wird im Laufe des Buches das eine oder andere gekürzt. Damit man erkennen kann, ob gekürzt wurde, erscheinen an der entsprechenden Stelle drei Punkte: ... Wenn solche Kürzungen vorgenommen wurden, ist es ebenso wahrscheinlich, dass nur leichte Anpassungen an bereits bestehenden Quelltexten vorgenommen wurden. Diese werden der besseren Übersichtlichkeit halber entsprechend hervorgehoben: ... das stand hier schon doch das ist neu ... Kommandos, die in der Konsole eingeben werden müssen, sind von Natur aus nie mehr- zeilig. Es gelten daher dieselben Regeln zum Umbruch wie bei Quelltexten. Allerdings beginnen Kommandos immer mit einem Dollarzeichen, es sei denn, sie kommen innerhalb des Textes vor. Dann ergibt sich die Bedeutung aus dem Text: $ kommando Java Alle Felder einer Klasse starten mit einem Unterstrich. Wenn eine Klasse als Bean benutzt wird, dann erfolgt der Zugriff auf das Feld durch eine passende set- und get-Methode. Listing 1.2 BeispielBean.java public class BeispielBean { String _titel; public String getTitel() { return _titel; } public void setTitel(String titel) { _titel=titel; } }2
  • 1.1 Warum Wicket? Im Folgenden wird diese ausführliche Schreibweise gekürzt. Dabei werden alle Felder mit den entsprechenden Typangaben aufgeführt und exemplarisch die Methodennamen für die ersten Felder angegeben. Listing 1.3 BeispielBean.java (gekürzt) public class BeispielBean { String _titel; getTitel(),setTitel(),... } Importanweisungen werden nur aufgeführt, wenn die zu importierende Klasse nicht aus dem Beispielprojekt oder aus dem Wicket-Framework stammt und sich auch nicht in den Java-Standardbibliotheken befindet (z.B. import java.io.Serializable;). Die Kür- zung wird durch ... gekennzeichnet. Markup Für die Darstellung von Komponenten nutzt Wicket einen Template-Mechanismus. Dabei kann einer Komponente eine HTML-Datei zugeordnet werden, in der alle nötigen Struktu- ren für die Darstellung enthalten sind. Diese Datei wird im folgenden Markup genannt. Version Das Buch bezieht sich auf die Wicket-Version 1.4, die mit dem Erscheinen dieses Buches in einer finalen Version verfügbar sein wird. Die Beispiele können begrenzt auch auf Wi- cket 1.3 übertragen werden. Wenn man nicht durch andere Abhängigkeiten gezwungen ist, Wicket noch in einer alten Version einzusetzen, sollte man spätestens jetzt den Schritt wa- gen und auf Wicket in der Version 1.4 migrieren. Online Da die Entwicklung von Wicket immer weiter geht, veröffentliche ich auf der Seite http://www.wicket-praxis.de/blog/ fortlaufend Erfahrungen und Tipps aus der praktischen Arbeit. Wenn Sie mit mir in Kon- takt treten wollen, genügt eine E-Mail an: michael@mosmann.de.1.1 Warum Wicket? Für das Entwickeln von Webanwendungen stehen eine ganze Reihe von Frameworks und Technologien bereit. Die Auswahl fällt schwer, da man die meisten Anforderungen mit einer ganzen Reihe von Technologien umsetzen könnte. Wenn man dann die zu verwen- dende Programmiersprache auf Java eingrenzt, bleiben trotzdem noch einige Frameworks übrig, die um die Gunst der Programmierer wetteifern. 3
  • 1 Einleitung Aus der umfangreichen Liste der Möglichkeiten habe ich Wicket ausgewählt. Dabei hat Wicket zunächst mit recht einfachen Mitteln mein Interesse geweckt: Es gibt eine sehr einfache und schnelle Möglichkeit, ein Testprojekt aufzusetzen. Die- ses kann sofort gestartet werden und zeigt ein minimales Grundgerüst einer Wicket- Anwendung. Die bestehende Anwendung kann einfach verändert werden, was zu einem sehr schnel- len Erfolgserlebnis führt. Die Lernkurve für die ersten Gehversuche ist sehr gering. Nachdem ich auf diese Weise sehr schnell einen ersten Eindruck gewinnen konnte, habe ich mich eingehender mit Wicket beschäftigt. Nach einer kurzen Einarbeitungsphase und einigen kleinen Beispielanwendungen war ich mir sicher, dass ich Wicket für meine zu- künftigen Webprojekte einsetzen werde. Die Kriterien, die für Wicket sprechen, habe ich an eine Übersicht auf der Webseite des Wicket-Projektes 1 angelehnt, beschränke mich aber hier auf die für mich besonders wichtigen Aspekte. 1.1.1 Einfach, Konsistent, Offensichtlich Ich empfehle als Einstieg eine kleine Beispielanwendung, die man sich über die „Quick- start“-Funktion 2 generieren lassen kann. Anhand dieser Anwendung kann man einige der folgenden Punkte recht schnell nachvollziehen: Alles kann in Java realisiert werden. Das Grundprinzip erinnert an Swing und ist leicht zu verstehen. Es müssen keine Konfigurationsdateien erstellt werden. Wicket-Anwendungen können einfach für Suchmaschinen optimiert werden. Hervorragende Unterstützung bei der Fehlersuche durch ausführliche Informationen im Entwicklungsmodus. Wicket benötigt keine speziellen Vorbereitungen oder Vorarbeiten. Wicket ist einfach nur Java. Wicket-Anwendungen können über Unit-Tests getestet werden. Es ist für mich einer der größten Vorteile, dass alles in Java ausgedrückt werden muss. Es gibt nur eine Stelle, an der ein Fehler auftreten kann: im Code. Mit den sehr ausführlichen Fehlermeldungen kann der Fehler sehr schnell eingegrenzt und behoben werden. 1.1.2 Wiederverwendbarkeit Wicket ist zwar nicht das einzige Framework, bei dem man auch bei Komponenten auf Vererbung zurückgreifen kann, doch das einzige, bei dem das sehr einfach umzusetzen ist. 1 http://wicket.apache.org/introduction.html 2 http://wicket.apache.org/quickstart.html4
  • 1.1 Warum Wicket?Das ermöglicht sehr leistungsfähige und gleichzeitig effektive Komponenten. Da Kompo-nenten ihre Funktionalität vollständig in sich kapseln, kann man sie innerhalb derselbenAnwendung beliebig oft nutzen, sodass komplexe Anwendungen sehr schnell realisiertwerden können. Wicket-Komponenten können auch anwendungsunabhängig entwickeltwerden. Alle notwendigen Daten und Informationen können in eine Java-Bibliothek ausge-lagert und dann in verschiedensten Projekten eingesetzt werden. Dazu muss die Bibliotheknur in das Projekt eingebunden werden. Auf diese Weise profitiert man sogar in allennachfolgenden Wicket-Projekten von der eigenen Entwicklungsarbeit, da man oft fast alleallgemeinen Komponenten wiederverwenden kann. So entsteht auch hier eine Bibliothekvon Lösungen, wie man sie aus anderen Bereichen der Anwendungsentwicklung kennt.1.1.3 Sauber getrenntWer Wicket zum ersten Mal testet und vorher schon Erfahrungen mit JSP, JSF oder ähnli-chen Ansätzen gemacht hat, wundert sich, dass im Markup (HTML-Schnipsel, die zurKomponente gehören und für die Darstellung wichtig sind) keine Code-Bestandteile zufinden sind. Dieser Punkt irritiert so stark, dass er immer wieder z.B. in Mailinglisten the-matisiert wird. Während der Arbeit mit Wicket offenbart dieses Konzept seine Vorteile: Im Markup befinden sich nur Referenzen auf Komponenten, aber kein Code oder spe- zielle Logik. Die Komplexität und der Funktionsreichtum der Anwendung werden nur durch Java als Programmiersprache und nicht durch eine in ihrer Ausdruckstärke ein- geschränkten Auszeichnungssprache begrenzt. Programmcode entsteht auf diese Weise nur an einer Stelle und in einer Sprache. Die Markup-Dateien für die Komponenten können mit einfachen Textprogrammen oder mit HTML-Editoren bearbeitet werden. Da Wicket keine besondere Notation er- fordert, gehen bei der Bearbeitung keine Informationen verloren, sodass die Gestaltung der Vorlagen z.B. durch einen Webdesigner angepasst werden kann. So wird die Ar- beitsteilung zwischen Entwickler und Gestalter gut unterstützt. Im Zweifelsfall muss nur die versehentlich gelöschte Bezeichnung der Komponente wiederhergestellt wer- den. Auf alle positiven Skalierungseffekte im Entwicklungsprozess durch die Nutzung von Java als Programmiersprache und den darauf aufsetzenden unterstützenden Werkzeu- gen kann bei der Entwicklung von Anwendungen mit Wicket zurückgegriffen werden. Die Komponentenarchitektur unterstützt diesen Aspekt zusätzlich.1.1.4 SicherJe nach verwendetem Framework sind Webanwendungen mehr oder weniger anfällig fürCode-Injection 3-, XSS 4- und andere Attacken.3 http://en.wikipedia.org/wiki/Code_injection4 http://en.wikipedia.org/wiki/Cross-site_scripting 5
  • 1 Einleitung Wicket-Anwendungen sind sicher. In Wicket werden nur die Parameter der Nutzerin- teraktion in der URL kodiert. Der Zustand der Anwendung wird serverseitig gespei- chert, sodass eine Kodierung dieser Information in URL-Parameter nicht notwendig ist. Das Einschleusen von Schadcode über manipulierte Parameter ist somit weitestgehend ausgeschlossen. Möchte man den Zustand einer Anwendung durch URL-Parameter steuerbar gestalten, muss man diese Möglichkeit explizit bereitstellen. Da Wicket-Anwendungen vollständig in Java realisiert werden, stehen alle Sicherheits- aspekte der Java-Plattform zur Verfügung. 1.1.5 Effizient und skalierbar Jede Abstraktion oder Zwischenschicht zieht ein anderes Laufzeitverhalten nach sich. Wie stark die Einschnitte in den verschiedenen Bereichen sind, hängt von der eingesetzten Lösung ab. Wicket gehört zu den leichtgewichtigen Lösungen. Der architekturelle Überbau hat dabei kaum Auswirkungen auf die Gesamtperformance. Da Wicket vollständig in Java realisiert wurde, können zudem zusätzlich zu den klassischen Lösungsansätzen für die Lastverteilung in Webanwendungen andere, auf die Lastverteilung von Java-Anwendun- gen spezialisierte Produkte eingesetzt werden (z.B. Terracotta 5). 1.1.6 Komplett Webanwendungen können vollständig mit den in Wicket enthaltenen Komponenten umge- setzt werden. Man muss also nicht in Vorleistung gehen und sich ein eigenes Anwen- dungsgerüst schaffen. Es ist allerdings derart einfach, eigene Anpassungen vorzunehmen, eigene Komponenten zu entwickeln oder einfach fremde Komponenten zu benutzen, dass man sich schnell an die erweiterten Möglichkeiten gewöhnt und rückblickend feststellen muss, wie reduziert bisherige Lösungen und die damit realisierten Anwendungen waren. 1.1.7 Eine gute Wahl Mit Wicket zu arbeiten macht Spaß. Die Komplexität von mit Wicket realisierbaren An- wendungen übersteigt die Komplexität solcher Anwendungen, die mit anderen Webtech- nologien umgesetzt werden können, bei Weitem. Die Komponentenarchitektur gewährleis- tet dennoch eine große Übersichtlichkeit, sodass der Einarbeitungsaufwand sehr viel ge- ringer ist, was sich gerade bei der Anpassung von bestehenden Anwendungen positiv be- merkbar macht. Wer Webanwendungen entwickelt, sollte Wicket also unbedingt ausprobieren. 5 http://www.terracotta.org/6
  • 1.2 Vorbereitung und Installation1.2 Vorbereitung und Installation Um Webanwendungen mit Wicket schreiben zu können, bedarf es weniger Vorraussetzun- gen. Im Prinzip reichen Java und ein Texteditor. Wenn man ernsthaft Webanwendungen entwickeln möchte, kommt man aber um ein etwas komplizierteres Setup nicht herum. Da- für bekommt man dann aber auch eine Menge an nützlichen Funktionen und Hilfsmitteln. 1.2.1 Java, Maven und Eclipse Java Für Wicket benötigt man ein Java Development Kit (JDK) ab Version 5. Eine Java Runti- me Environment (JRE) reicht zum Entwickeln nicht aus. Unter Windows laden Sie die Installationsdateien einfach von der Javaseite 6 von Sun herunter und starten die Installa- tion. Unter Linux sollte man Java über das integrierte Paketmanagement installieren. Apache Maven Damit man den Java-Compiler nicht immer von Hand in der Kommandozeile starten muss, haben sich im Laufe der Zeit unterschiedliche Ansätze und Lösungen für dieses Problem entwickelt. In diesem Buch und auch sonst bevorzuge ich Maven für das Build-Manage- ment. Für die Installation lädt man das passende Archiv von der Apache-Maven-Seite 7 herunter und folgt den Installationsanweisungen auf der Seite. Eclipse Für das Buch empfehle ich als Entwicklungsumgebung Eclipse 8. Dafür gibt es zwei Grün- de: Eclipse ist kostenlos. Maven-Projekte können sehr einfach in Eclipse importiert werden. Alle definierten Abhängigkeiten werden dabei in die Eclipse-Projekte übernommen. Ich empfehle für dieses Buch Eclipse IDE for Java EE Developers ab Version 3.4. Ein Plugin für Maven kann installiert werden. Das ist aber nicht notwendig, weil Maven die Eclipse-Projekte ohne fremde Hilfe erzeugt. 1.2.2 Versionskontrolle mit Subversion Ein regelmäßiges Backup ist ein gutes Ruhekissen. Auf diese Weise kann man jederzeit einen älteren Zustand wiederherstellen. Spätestens, wenn man mit mehreren Entwicklern an einem Projekt arbeitet, funktioniert das Backup-Prinzip nicht mehr. Ein Versionsver- 6 http://java.sun.com/javase/downloads/index.jsp 7 http://maven.apache.org/ 8 http://www.eclipse.org/ 7
  • 1 Einleitung waltungssystem (Version Control System, VCS) wird unumgänglich. Aber auch für den einzelnen Entwickler bringt der Einsatz eines VCS erhebliche Vorteile. Man kann in der Entwicklung gefahrlos verzweigen und diesen Entwicklungszweig wieder mit dem Haupt- zweig zusammenführen. Man kann Dinge ausprobieren und jederzeit wieder auf einen älte- ren (nicht nur den letzten) Stand zurücksetzen. Subversion hat sich als Standard etabliert und kann über Plugins recht gut in Eclipse integriert werden. Empfehlungen Für die Arbeit an einem Projekt empfiehlt es sich unter Windows, sowohl das Eclipse- Plugin 9 als auch das Windows Explorer-Plugin TortoiseSVN 10 zu installieren. Unter Linux kann man alle Aufgaben über die Kommandozeile durchführen und sollte daher Subver- sion über die Paketverwaltung installieren. Da das Eclipse-Plugin eine eigene Subversion- Bibliothek mitbringt, ist darauf zu achten, dass die Subversion-Client-Version in beiden Fällen dieselbe ist, da sich das Ablageformat teilweise unterscheidet und der ältere Client dann nicht mehr auf die Daten zugreifen kann, wenn der neuere Client sie erst einmal in das neuere Format konvertiert hat. Wer keinen eigenen Subversion-Server aufsetzen möchte, kann auch auf Subversion- Hosting-Angebote 11 zurückgreifen.1.3 Grundlagen einer Webanwendung Wicket ist im Gegensatz zu Grails oder Rails kein Framework, das alle Aspekte einer Webanwendung, z.B. den Datenbankzugriff, in einem Paket bündelt. Wir müssen uns da- her selbst um die notwendigen Bibliotheken bemühen, was uns aber vor keine großen Her- ausforderungen stellen wird. 1.3.1 Anwendungsschichten Das Buch hat den Anspruch, alle wesentlichen Aspekte der Entwicklungen von Weban- wendungen abzubilden. Dazu gehört neben der Präsentationsschicht, bei der Wicket zum Einsatz kommt, die Schicht der Anwendungslogik (Business Logic) und die der Datenhal- tung (Persistenz) (siehe den Wikipedia-Eintrag zur Drei-Schichten-Architektur 12). Da auch die Präsentationsschicht vollständig in Java realisiert wird, ist die Anbindung an die ande- ren Applikationsschichten besonders einfach. Das Zusammenfügen der verschiedenen Anwendungsschichten geschieht nicht mehr durch Programmcode, sondern durch den Ein- satz eines Dependency Injection 13-Frameworks. 9 http://subclipse.tigris.org/ 10 http://tortoisesvn.tigris.org/ 11 http://www.svnhostingcomparison.com/ 12 http://de.wikipedia.org/wiki/3-Tier#Drei-Schichten-Architektur 13 http://de.wikipedia.org/wiki/Dependency_Injection8
  • 1.3 Grundlagen einer Webanwendung1.3.1.1 Persistenz mit HibernateAlle Anwendungen arbeiten mit Daten. Daten werden klassisch in Datenbanksystemenabgelegt. Relationale Datenbanksysteme bilden Daten in Tabellen ab, die miteinander ver-knüpft werden können. In den Spalten einer Tabelle werden die verschiedenen Attributeeines Datensatzes abgelegt, jede Zeile steht für einen eigenständigen Datensatz.Objektorientierte Sprachen können mit Objekten und Beziehungen von Objekten unterein-ander arbeiten. Die Datenhaltung in Tabellen unterscheidet sich derart von den Möglich-keiten objektorientierter Datenhaltung, dass die Modelle nicht automatisch aufeinanderabbildbar sind. Es gibt Speziallösungen, die Objekte und deren Beziehung in einer Objekt-datenbank 14 abbilden können. Diese Speziallösungen konnten sich bis heute aus verschie-denen Gründen nicht durchsetzen.Daher wurde für die Anbindung von relationalen Datenbanksystemen OR-Mapper 15 ent-wickelt, die es sowohl für verschiedene Programmiersprachen als auch für unterschiedlicheDatenbanksysteme gibt. Diese OR-Mapper übernehmen die Transformation der unter-schiedlichen Modelle ineinander. Dabei wird meist ein Objekt auf eine Tabelle und dieEigenschaften des Objekts auf die Spalten einer Tabelle abgebildet. Diese Methode ist beider Abbildung von komplexen Objektbeziehungen natürlich limitiert, hat gegenüber denSpeziallösungen aber den entscheidenden Vorteil, dass auf bewährte Technologien zurück-gegriffen werden kann.Für die Datenbankanbindung in Java gibt es JDBC mit den entsprechenden datenbankab-hängigen Treibern. Der Zugriff erfolgt in Tabellenform. Im Prinzip sind damit alle Anfor-derungen an eine Datenbankschnittstelle erfüllt. Die Anbindung an eigene Applikationenist dennoch recht kompliziert und mit vielen Fallstricken versehen.Eine objektorientierte Darstellung der Daten und deren Transformation in die Tabellen-struktur der Datenbank hat auf Seiten der Anwendungsentwicklung zu wesentlich schnelle-ren Ergebnissen geführt. Außerdem kann der Zugriff auf die Daten in Abhängigkeit vonder Datenbank optimiert werden. OR-Mapper stellen daher neben der Transformation derDaten meist grundlegende Datenbankoperationen zur Verfügung.Warum Hibernate?In diesem Buch und auch sonst empfehle ich Hibernate als Persistenz-Framework. DieWahl fiel auf Hibernate, weil es einfach zu benutzen und am Markt etabliert ist. Es zeich-net sich ähnlich wie Wicket durch eine gute Java-Integration aus. Natürlich kann man je-des andere Persistenz-Framework mit Wicket benutzen. Wicket bietet keine besondereHibernate-Integration, sodass die aufgezeigten Lösungsvorschläge auch auf andere Frame-works abgebildet werden können.14 http://de.wikipedia.org/wiki/Objektorientierte_Datenbank15 http://de.wikipedia.org/wiki/Objektrelationale_Abbildung 9
  • 1 Einleitung Funktionsweise Mit Hibernate kann man Klassen und deren Attribute auf Datenbanktabellen abbilden. Be- ziehungen untereinander können ebenso abgebildet werden. Hibernate sorgt dabei für die Transformation der Daten und Abhängigkeiten in ein für die Datenbank verständliches Format. Die Informationen, auf welche Tabelle und auf welchen Spalten die Objekte abge- bildet werden, können sowohl in der Klasse selbst (per Annotation) als auch in gesonder- ten Konfigurationsdateien abgelegt werden. Der Zugriff auf die Daten kann über eine ob- jektorientierte Alternative zu SQL (HQL) oder über eine Schnittstelle erfolgen. Die Trans- formation in Objekte erfolgt transparent. 1.3.1.2 Dependency Injection mit dem Spring-Framework In klassischen objektorientierten Anwendungen ist jedes Objekt selbst dafür verantwort- lich, die Abhängigkeiten aufzulösen sowie die notwendigen Ressourcen zu erzeugen und zu verwalten. Dazu muss jedes Objekt über Informationen seiner Umgebung verfügen. Dependency Injection verlagert die Verantwortung für das Finden und Verwalten von Res- sourcen und Abhängigkeiten aus dem Objekt in ein Framework. Das Framework erzeugt über Konfigurationsdateien oder Metainformationen in den Klassen die Anwendungs- module und löst die definierten Abhängigkeiten auf. Dieses Vorgehen reduziert die Ab- hängigkeit der Klasse zur Umgebung und zu konkreten Umsetzungen der abhängigen Mo- dule. Warum Spring? Das Spring-Framework wurde aus ähnlichen Gründen wie Hibernate ausgewählt. Und ähn- lich wie bei Hibernate kann auch dieser Aspekt durch ein anderes Framework abgebildet werden. Wie gut die Ergebnisse in diesem Buch dann übertragbar sind, hängt sehr stark von dem verwendeten Framework ab. Spring benutzt für die Konfiguration XML-Dateien. In diesen Dateien werden die Objekte mit Namen versehen und die Konfiguration der Attribute vorgenommen. Die Abhängigkei- ten von Objekten können über Referenzen und deren Übergabe in Attribute vorgenommen werden. Eine Anwendung lädt über Funktionen des Frameworks eine Spring-Konfigura- tion und kann dann über den Namen auf die Objekte bekannten Typs zugreifen. Spring ist somit einfach in jede Java-Anwendung integrierbar. 1.3.1.3 Anwendungslogik und Präsentation mit Wicket Die Architektur von Wicket folgt dem Model-View-Controller 16-Ansatz (Modell-Präsen- tation-Steuerung). Das Modell beinhaltet die darzustellenden Daten, der Controller über- nimmt die Steuerung der Interaktionsmöglichkeiten, und in der Präsentationsschicht wer- den diese Daten dann dargestellt. 16 http://de.wikipedia.org/wiki/MVC10
  • 1.3 Grundlagen einer Webanwendung1.3.2 Verzeichnis und PaketstrukturEs zeigt sich, dass man nie zu früh anfangen kann, seine Softwareprojekte gut zu struktu-rieren. Ich lege daher für jede Anwendungsschicht mindestens ein eigenständiges Teilpro-jekt an. Das verkürzt den Entwicklungszyklus und verbessert die Übersichtlichkeit.1.3.2.1 Projektverwaltung mit MavenFür das Erstellen der Teilprojekte und die Abhängigkeitsverwaltung setze ich Maven ein.Maven ist dabei ein Build-Tool mit vielfältigen Möglichkeiten. Alternativen zu Mavensind z.B. Ant 17 mit Ivy 18 oder Buildr 19. Build-Tools sind hauptsächlich für das Erstellender ausführbaren Programme aus vorhandenen Quelltexten zuständig. Das Aufgabenspekt-rum hat sich spätestens seit Maven in der Version 1.0 grundlegend erweitert und umfasstseither vor allem auch die Abhängigkeitsverwaltung. Weitere Funktionalitäten können fastbeliebig durch Plugins erweitert werden und reichen vom Erstellen von Dokumentations-seiten bis zum Deployment der Anwendung auf Produktivsystemen.Das Grundprinzip von Maven ist einfach. In einem Verzeichnis gibt es ein vorgeschriebe-nes Verzeichnislayout für ein Projekt: pom.xml src main java resources test java resourcesDazu benötigt Maven neben einer sehr einfach gehaltenen Projektdefinitionsdatei(pom.xml), die minimale Informationen zum Projekt enthält, die notwendigen Java-Quelltexte in standardisierten Verzeichnissen. Maven sucht dann für das Erstellen des Pro-jekts z.B. im Verzeichnis src/main/java nach Java-Dateien und kompiliert diese. Insrc/test/java befinden sich Unit-Tests, die beim vollständigen Erstellen der Anwen-dung ausgeführt werden. In den jeweiligen resource-Verzeichnissen befinden sich alleanderen Dateien (Grafiken, Texte etc.).In der Projektdefinitionsdatei können auch die Abhängigkeiten zu den anderen Teilprojek-ten oder zu anderen Bibliotheken eingestellt werden, wobei Maven diese Abhängigkeitenselbstständig auflöst und die referenzierten Bibliotheken automatisch aus einem Verzeich-nis herunterlädt und lokal vorhält. Wurde eine Bibliothek bereits einmal heruntergeladen,greift Maven auf die in einem lokalen Verzeichnis abgelegte Version zurück.Über Plugins ist es außerdem möglich, die Webanwendung direkt aus dem Projektver-zeichnis heraus starten zu können. Auf diese Weise können sogar Anpassungen an der lau-fenden Anwendung vorgenommen werden.17 http://ant.apache.org/18 http://ant.apache.org/ivy/19 http://incubator.apache.org/buildr/ 11
  • 1 Einleitung 1.3.2.2 Besser als Quickstart Wicket bietet auf der eigenen Webseite eine „Quickstart“-Funktion, die ein einfaches Wi- cket-Anwendungsgerüst erstellt, das sofort lauffähig ist. Für einen ersten Test sollte man sich mit „Quickstart“ ein Testprojekt erstellen lassen. Allerdings empfehle ich, dieses Pro- jekt für eigene Entwicklungen nicht weiter zu verwenden. Dafür gibt es verschiedene Gründe. Der Wichtigste für mich: Das Projekt benutzt angepasste Einstellungen, die so nicht nötig sind und vom Standard abweichen. Außerdem ist es hilfreich, wenn man alle Anpassungen, die man im Laufe des Entwick- lungsprozesses vorgenommen hat, selbst durchführt. Dann kann man z.B. dokumentieren, was die Anpassung bewirkt, was sich neben der besseren Dokumentation des eigenen Pro- jekts positiv auf die Fehlersuche auswirkt. 1.3.3 Unit-Tests Irren ist menschlich. Doch Fehler in Anwendungen sind ärgerlich. Fehler in der eigenen Anwendung kann man aber vermeiden. Denn je komplexer die Anwendungen werden, desto mehr Zeit benötigt man, um selbst triviale Fehler einzugrenzen. Eine Methode, die Anzahl der Fehler mit jedem Entwicklungsschritt nachhaltig einzudämmen, nennt sich „Test Driven Development“. Dabei geht es im Prinzip darum, für jede Zeile Programm wieder ein Programm zu schreiben, das die Funktionsfähigkeit und das Ergebnis überprüft. Was sich im ersten Moment etwas „verrückt“ anhört, stellt sich nach kurzer Zeit als un- glaublich entlastendes Instrument dar. Wenn das Teilprojekt mit den automatisch ablau- fenden Tests erstellt werden konnte, kann man sich ziemlich sicher sein, dass es genauso stabil läuft wie vor der letzten Anpassung. Unit-Tests (Modul- bzw. Komponententests) unterscheiden sich vom normalen Programm- code nur dadurch, dass die Namen der Klassen meist mit „Test“ anfangen und die Metho- den der Klasse z.B. „testIrgendetwas“ heißen. Wenn man mit Maven ein Projekt kompi- liert, dann werden die nach dieser Nomenklatur erstellten Klassen und Funktionen ausge- führt. Wenn dann in so einem Test ein Fehler auftritt und der Test somit fehlschlägt, kann das ganze Projekt nicht gebaut werden und wird daher auch nicht für andere Projekte, die dieses Teilprojekt eingebunden haben, zur Verfügung gestellt. So kann sichergestellt wer- den, dass alle Abhängigkeiten in einem fehlerfreien Zustand sind. Wenn in einem Test Fehler auftreten, kann man die Suche nach der Ursache sehr stark ein- schränken. Denselben Fehler innerhalb einer laufenden Anwendung zu isolieren, ist sehr viel aufwendiger. Außerdem bietet ein gut formulierter Test einen hervorragenden Sicher- heitsfallschirm für den Fall, das man z.B. Optimierungen vornimmt oder die Funktionalität erweitert. Wenn die Tests nach einer solchen Anpassung immer noch funktionieren, kann man fast davon ausgehen, dass auch die Anwendung, die auf diese Funktionen aufbaut, immer noch funktioniert.12
  • 1.3 Grundlagen einer Webanwendung1.3.3.1 Persistenz-TestsAuch mit Hibernate können Unit-Tests durchgeführt werden. Dabei kann eine temporäreDatenbank benutzt werden, die nur zur Laufzeit des Tests vorhanden ist und keinerlei Da-ten enthält. Das bedeutet, dass Nebeneffekte durch bereits bestehende Daten ausgeschlos-sen werden können. Somit kann man alle Datenbankoperationen (z.B. Löschen) ohne dieGefährdung von Produktivsystemen testen.1.3.3.2 User-Interface-TestsMit Wicket können Teile von Wicket-Anwendungen und -Komponenten getestet werden.Auch wenn die Testbarkeit sicher mit Einschränkungen behaftet ist, geht die Testabde-ckung und die Einfachheit beim Erstellen eigener Test weit über das hinaus, was mit ande-ren Frameworks realisierbar ist. Das macht Wicket in diesem Bereich einmalig.ZusammenfassungInteressanterweise unterscheidet sich unsere Auswahl nur unwesentlich von den Bibliothe-ken, die auch Grails benutzt, sicherlich auch, weil wir ebenso wie das Grails-Projekt aufetablierte Standards setzen. Auch wenn der Aufwand für das Aufsetzen eines Projektesungleich höher ist, bewahren wir so die Flexibilität, die es uns erlauben würde, z.B. einanderes Persistence-Framework einzusetzen. Allerdings ist zu beachten, dass diese Auf-wände nur einmal in einem Projekt anfallen und damit selten eine große Rolle spielen. 13
  • 1 Einleitung14
  • 2 2 Aufsetzen der Teilprojekte Wir schreiben endlich unsere erste eigene Wicket-Anwendung. Die Strukturen, die im Folgenden erstellt werden, sollten sich durch leichte Modifikationen auf andere Projekte anwenden lassen. In den folgenden Abschnitten erstellen wir zuerst das notwendige Grundgerüst mit allen Abhängigkeiten zu externen Bibliotheken und eigenen Teilprojekten. Im zweiten Schritt hauchen wir dem Ganzen Leben ein. Als Erstes sollte man sich für dieses Projekt ein Verzeichnis anlegen, in dem künftig alle Teilprojekte und sonstige Daten beheimatet sind. Es empfiehlt sich, auf Leerzeichen in den Verzeichnisnamen zu verzichten, sonst kann es zu unnötigen Komplikationen kommen.2.1 Nomenklatur der Teilprojekte Maven benutzt für das Auflösen von Abhängigkeiten drei Informationen, die in einem Pro- jekt angegeben werden müssen: groupId: Bezeichnet eine Gruppe, in der die Artefakte zusammengefasst werden. Die Gruppen- ID hat eher organisatorische Gründe und ist leider ungeeignet, Namenskollisionen zu vermeiden. Im Dateinamen der fertigen Bibliothek kommt dieser Bezeichner nicht vor, sodass es zu Dateinamenskollisionen kommen kann, wenn aus unterschiedlichen Grup- pen Artefakte mit derselben Versionsnummer und artifactId benutzt werden. artifactId: Diese ID gibt dem Projekt seinen Namen. Diese ID findet sich später auch im Datei- namen der erzeugten Bibliothek und im Projektnamen in Eclipse wieder. version: Version des Projekts. Eine gute Versionierung sollte eigentlich bei 0.1 anfangen. Ich persönlich finde es akzep- tabel, in diesem Buch bei Version 1.0 anzufangen. Allerdings wird bis zur Fertigstellung 15
  • 2 Aufsetzen der Teilprojekte die Versionsbezeichnung 1.0-SNAPSHOT lauten (das ist die Grundeinstellung von Ma- ven). Dieses Versionierungsschema empfiehlt sich aus zwei Gründen. Zum einen wird auf diese Weise das Teilprojekt als „in Arbeit“ gekennzeichnet und sorgt zum anderen bei Maven dafür, dass beim Auflösen dieser Abhängigkeiten regelmäßig geprüft wird, ob eine Version mit einem neueren Zeitstempel existiert. Wenn man allein an einem Projekt ent- wickelt, wird sich da kein Unterschied bemerkbar machen. Wenn man aber im Team ent- wickelt und ein Continuous-Build-System 1 zum Einsatz kommt, ist nur so gewährleistet, dass man die aktuellste Version eines Teilprojektes benutzt. Da die groupId aus oben genannten Gründen nicht zur Strukturierung von Projekten he- rangezogen werden kann, handhabe ich die Bezeichnung der Projekte wie folgt: In groupId schreibe ich den Domainnamen des Projekts in umgekehrter Reihenfolge. Wenn es ein Unterprojekt für eine existierende Domain ist, hänge ich ein „pro- jekt.<projektname>“ an. Der Wert in artifactId fängt mit dem Wert aus groupId an und wird, gefolgt von „--“, um einen Namen für dieses Teilprojekt ergänzt. Der Verzeichnisname für dieses Teilprojekt entspricht dem Wert in artifactId. Für dieses Beispiel benutze ich als groupId den Wert „de.wicketpraxis“. Ein Teilpro- jekt hat demzufolge als artifactId den Wert „de.wicketpraxis--teilprojekt“. Das Verzeichnis, in dem alle Daten dieses Teilprojektes enthalten sind, hat ebenso diesen Na- men.2.2 Aufsetzen der Teilprojekte Beim Erstellen der Teilprojekte arbeiten wir uns von der Basis an aufwärts, wobei die höchste Schicht die Präsentationsschicht ist, in diesem Fall also Wicket. Ziel ist es, am Ende ein vollständiges Projekt mit allen notwendigen Teilprojekten zu erhalten. Im zwei- ten Schritt wird für jede Schicht eine beispielhafte Funktionalität realisiert, die das Funk- tionieren und das Zusammenspiel der verschiedenen Schichten veranschaulicht. 2.2.1 Projektbasis ParentPom Als erstes benötigen wir ein Maven-Projekt, in dem alle teilprojektübergreifenden Einstel- lungen und Abhängigkeiten abgelegt werden. Dazu legen wir ein Projekt unter de.wicketpraxis–-parentPom an. Der Name parentPom wurde gewählt, weil in diesem Teilprojekt übergreifende Einstellungen vorgenommen werden können. Außerdem kann zum Schluss die ganze Anwendung über dieses Projekt erstellt werden, weil die Teilpro- jekte entsprechend konfiguriert wurden. 1 http://www.theasolutions.com/tutorials/scrum_agile.jsp16
  • 2.2 Aufsetzen der TeilprojekteAuf der Kommandozeile erstellt folgende Befehlssequenz ein passendes Projektverzeich-nis (alles gehört in eine Eingabezeile und wurde nur zur besseren Übersicht mehrzeiliggeschrieben): $ mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=de.wicketpraxis -DartifactId=de.wicketpraxis--parentPomDabei legt Maven erfreulicher Weise gleich ein Verzeichnis passend zur artifactId an.In diesem Verzeichnis finden sich folgende Verzeichnisse: src main java resources test java resourcesIn main/java finden sich die Quelltexte der Anwendung. In main/resources wird allesabgelegt, was in diesem Projekt sonst noch an Daten benutzt wird. Dazu gehören z.B. Bil-der oder Konfigurationsdateien. Unter test/java werden die Quelltexte für die Unit-Testsabgelegt, die in dem Test auf die Ressourcen unter test/resources zurückgreifen kön-nen. Programmcode und Daten aus dem test-Ordner werden nicht in der Bibliotheksdateiweitergegeben.Jetzt kann man mit $ mvn installauf der Kommandozeile das Projekt erstellen lassen. Wenn alles richtig konfiguriert wurdeund alle Tests erfolgreich abgeschlossen werden konnten, erscheint am Ende die Ausgabe: ... [INFO] ---------------------------------------------------------------- [INFO] BUILD SUCCESSFUL [INFO] ---------------------------------------------------------------- ...In dem generierten Projektverzeichnis befindet sich außerdem die Projektdatei pom.xmlmit folgendem Inhalt: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.wicketpraxis</groupId> <artifactId>de.wicketpraxis--parentPom</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>de.wicketpraxis--parentPom</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> </project> 17
  • 2 Aufsetzen der Teilprojekte Wie man erkennen kann, finden sich dort unsere Werte für groupId und artifactId wieder. Außerdem sind noch zwei Bereiche von besonderem Interesse. Der Parameter „packaging“ ist auf dem Wert „jar“ gesetzt, was bedeutet, dass aus diesem Projekt eine normale Bibliothek in einer JAR-Datei erzeugt wird. Der zweite interessante Aspekt ist im Bereich dependencies zu finden. Dort wird eine Bibliothek als Abhängigkeit definiert, die für das Ausführen der Unit-Tests vorhanden sein muss. Man kann auch hier wieder die drei relevanten Parameter für die Adressierung die- ser Abhängigkeit erkennen. Zusätzlich ist der Parameter scope aufgeführt, bei dem der Wert test darauf hinweist, dass diese Bibliothek nur für die Unit-Tests benutzt wird. Jetzt passen wir die Projektbeschreibungsdatei wie folgt an: Listing 2.1 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ... > <modelVersion>4.0.0</modelVersion> <groupId>de.wicketpraxis</groupId> <artifactId>de.wicketpraxis--parentPom</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <name>${pom.groupId}--${pom.artifactId} (Wicket Praxis - Parent Pom)</name> <url>http://wicketpraxis.de</url> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <executions> <execution> <id>attach-sources</id> <phase>verify</phase> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId>18
  • 2.2 Aufsetzen der Teilprojekte <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>${slf4j.version}</version> </dependency> </dependencies> <properties> <junit.version>3.8.1</junit.version> <slf4j.version>1.4.2</slf4j.version> </properties> </project>Diese Projektdefinition wird nachher von allen anderen Teilprojekten referenziert und de-finiert somit die Grundeinstellung für alle Teilprojekte. Folgende Anpassung habe ich vor-genommen: An der Stelle, wo <project ...> steht, habe ich die Darstellung aus Platzgründen gekürzt. Die Angaben an dieser Stelle entsprechen den automatisch generierten. Der Parameter packaging muss für dieses Teilprojekt auf pom gesetzt werden. Die Platzhalter wie z.B. ${pom.groupId} werden durch die aktuell definierten Inhalte ersetzt. Damit erspart man sich einerseits Schreibaufwand und kann andererseits be- stimmte Informationen an einer Stelle bündeln (z.B. die Versionen für die Abhängig- keiten im Bereich properties). Mit dem Compiler-Plugin (maven-compiler-plugin) wird die Java-Version auf 1.5 gesetzt. Damit benötigt die Anwendung mindestens ein Java 5 oder Java 6. Außerdem wird die Kodierung für die Quellcodedateien auf UTF-8 gesetzt. Das Maven-Resource-Plugin (maven-resources-plugin) sorgt dafür, dass alle Res- sourcendateien mit UTF-8-Kodierung eingebunden werden. Es empfiehlt sich, mithilfe des Maven-Source-Plugins (maven-source-plugin) auch die Quelltexte zum Projekt in ein Archiv packen zu lassen. So kann man später in der Entwicklungsumgebung sehr einfach auf die passenden Quelltexte zugreifen, ohne dass man das Projekt geöffnet haben muss. Für dieses Projekt benutzen wir junit für die Unit-Tests. Es muss mindestens eine Bib- liothek für Unit-Tests definiert sein, sonst können Projekte nicht gebaut werden. Java bietet ein integriertes Logging-Framework. Außerdem gibt es noch von Apache das log4j-Logging-Framework. Das slf4j-Projekt bietet die Möglichkeit, jederzeit das verwendete Framework austauschen zu können, ohne dass man Anwendungscode an- passen muss. Außerdem bietet slf4j eine praktische Schnittstelle, um parametrisierte Log-Ausgaben generieren zu können.Damit ist dieses Teilprojekt fertig gestellt. Jetzt müssen wir dieses Projekt als Basisprojektin alle weiteren Teilprojekte einbinden. Davor sollten wir es kurz mit mvn install erstel-len. 19
  • 2 Aufsetzen der Teilprojekte 2.2.2 Teilprojekt Base Im Laufe einer langjährigen Entwicklung sammeln sich immer mehr Funktionen, die kei- nem Projekt direkt zugerechnet werden können, sich aber einer ungemeinen Nützlichkeit im Projektalltag erfreuen. Damit diese Funktionen sich gar nicht erst in den Untiefen eines Projekts verlieren, empfehle ich, ein Teilprojekt anzulegen, in dem solche allgemeinen Funktionen dann eine passende Heimat haben. Dieses Teilprojekt erstellen wir wieder mit: $ mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=de.wicketpraxis -DartifactId=de.wicketpraxis--base Die generierte Projektdatei wird vollständig ersetzt, da wir viele Einstellungen bereits in dem übergeordneten Projekt getroffen haben und diese deshalb nicht noch einmal angege- ben werden müssen. Listing 2.2 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ... > <modelVersion>4.0.0</modelVersion> <parent> <groupId>de.wicketpraxis</groupId> <artifactId>de.wicketpraxis--parentPom</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>${pom.groupId}--base</artifactId> <name>${pom.groupId}--${pom.artifactId} (Wicket Praxis - Base)</name> </project> Damit die nötigen Informationen vom Basisprojekt übernommen werden können, muss es wie im Bereich parent referenziert werden. 2.2.3 Teilprojekte Datenbankkonfiguration Für die Datenbankanbindung werden drei Teilprojekte angelegt. Auch wenn der Aufwand im ersten Moment übertrieben wirkt, ist nur durch die Teilung eine saubere Abhängig- keitsstruktur erreichbar. Das erklärt sich sehr viel besser, wenn wir nachher diese Projekte einbinden und nutzen. 2.2.3.1 Konfigurationsprojekt für die Produktivdatenbank Wir legen ein Teilprojekt mit der artifactId de.wicketpraxis--dbconfig an. Wir passen die Projektdatei an und fügen die Abhängigkeiten für den Datenbanktreiber ein. Als Produktivdatenbank soll eine MySQL-Datenbank zum Einsatz kommen. Daher wird die Bibliothek für diesen Datenbanktreiber eingebunden. Listing 2.3 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ... > <modelVersion>4.0.0</modelVersion>20
  • 2.2 Aufsetzen der Teilprojekte <parent> ... </parent> <artifactId>${pom.groupId}--dbconfig</artifactId> <name>${pom.groupId}--${pom.artifactId} (Wicket Praxis - DB Config)</name> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> </dependencies> <properties> <mysql.version>5.1.6</mysql.version> </properties> </project>2.2.3.2 Konfigurationsprojekt Schema-UpdateHibernate bietet die Möglichkeit, aus der konfigurierten Datenbankzugriffschicht automa-tisch das Datenbankschema zu erzeugen. Dabei nimmt Hibernate ebenfalls datenbankspe-zifische Optimierungen vor, auf die man durchaus zurückgreifen sollte. Wenn man nichtgerade über umfangreiches datenbankspezifisches Wissen verfügt, dürfte das Know-howaus dem Hibernate-Projekt zu den besseren Ergebnissen führen.Auf die automatische Schemagenerierung greifen wir bei der Testdatenbank ebenfalls zu-rück. Dieses Projekt dient nur dazu, andere Zugangsdaten für die Datenbank bereitzustel-len, die mit anderen Rechten ausgestattet die entsprechenden Tabellen anlegen kann.Außerdem können wir so verhindern, dass diese Zugangsdaten auf Produktivsystemen auf-tauchen.Wir erstellen das Teilprojekt mit der artifactId de.wicketpraxis--dbconfig-schema-update. Wir definieren eine Abhängigkeit zum Projekt mit der Produktdaten-bankkonfiguration, sodass derselbe Datenbanktreiber benutzt wird.Listing 2.4 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ... > <modelVersion>4.0.0</modelVersion> <parent> ... </parent> <artifactId>${pom.groupId}--dbconfig-schema-update</artifactId> <name>${pom.groupId}--${pom.artifactId} (Wicket Praxis - DB Config Schema Update)</name> <dependencies> <dependency> <groupId>${pom.groupId}</groupId> <artifactId>${pom.groupId}--dbconfig</artifactId> <version>${pom.version}</version> </dependency> </dependencies> </project>Wenn sich der Datenbanktreiber ändern sollte, muss natürlich in beiden Fällen die noch zuerstellende Konfiguration angepasst werden. 21
  • 2 Aufsetzen der Teilprojekte 2.2.3.3 Konfigurationsprojekt für die Testdatenbank Für den Datenbanktest wird auf eine Datenbank zurückgegriffen, die ohne Installation aus- kommt und innerhalb einer beliebigen Java-Anwendung gestartet werden kann. Es ist aber ebenso praktikabel, auch während der Entwicklung auf diese Datenbank statt auf eine loka- le MySQL-Installation zurückzugreifen. Damit erspart man sich Installations- und Konfi- gurationsaufwand. Wir erstellen das Teilprojekt mit der artifactId de.wicketpraxis– -dbconfig-test und passen die Projektdatei an. Listing 2.5 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ... > <modelVersion>4.0.0</modelVersion> <parent> ... </parent> <artifactId>${pom.groupId}--dbconfig-test</artifactId> <name>${pom.groupId}--${pom.artifactId} (Wicket Praxis - DB Config Test)</name> <dependencies> <dependency> <groupId>hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>${hsqldb.version}</version> </dependency> </dependencies> <properties> <hsqldb.version>1.8.0.1</hsqldb.version> </properties> </project> Ich benutze die HSQL-Datenbank für die Testumgebung. Es können natürlich beliebige andere Datenbanken benutzt werden, für die Hibernate Unterstützung anbietet. 2.2.4 Teilprojekt Persistenz Nachdem wir nun alle Datenbankkonfigurationsprojekte angelegt (aber noch nicht konfi- guriert) haben, legen wir nun das Teilprojekt (artifactId de.wicketpraxis-- persistence) an, das den Datenbankzugriff regelt. Diese Projektdatei wird etwas um- fangreicher, denn wir müssen die anderen Teilprojekte einbinden. Listing 2.6 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ... > <modelVersion>4.0.0</modelVersion> <parent> ... </parent> <artifactId>${pom.groupId}--persistence</artifactId> <name>${pom.groupId}--${pom.artifactId} (Wicket Praxis - Persistence)</name> <dependencies> <dependency> <groupId>${pom.groupId}</groupId>22
  • 2.2 Aufsetzen der Teilprojekte <artifactId>${pom.groupId}--dbconfig</artifactId> <version>${pom.version}</version> </dependency> <dependency> <groupId>${pom.groupId}</groupId> <artifactId>${pom.groupId}--dbconfig-test</artifactId> <version>${pom.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>${pom.groupId}</groupId> <artifactId>${pom.groupId}--dbconfig-schema-update</artifactId> <version>${pom.version}</version> <scope>test</scope> </dependency> <!-- Base --> <dependency> <groupId>${pom.groupId}</groupId> <artifactId>${pom.groupId}--base</artifactId> <version>${pom.version}</version> </dependency> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${springframework.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${springframework.version}</version> <scope>test</scope> </dependency> <!-- Hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-annotations</artifactId> <version>${hibernate-annotations.version}</version> </dependency> </dependencies> <properties> <springframework.version>2.5.6</springframework.version> <hibernate.version>3.2.6.ga</hibernate.version> <hibernate-annotations.version>3.3.1.GA</hibernate- annotations.version> </properties> </project>Im ersten Abschnitt der Abhängigkeitsverwaltung (dependencies) werden die Daten-bankkonfigurationsprojekte eingebunden. Dabei werden die Konfigurationen für die Da-tenbanktests und die Schemaaktualisierung nur für die Unit-Tests eingebunden (<sco-pe>test</scope>). Die Schemaaktualisierung wird wie ein Unit-Test gestartet, sodassdann auf diese Konfiguration zurückgegriffen werden kann. 23
  • 2 Aufsetzen der Teilprojekte Im zweiten Abschnitt binden wir unsere Basisbibliothek ein. Diese Abhängigkeit wird wei- tervererbt, sodass wir in anderen Teilprojekten, die dieses Teilprojekt einbinden, nicht noch einmal diese Abhängigkeit definieren müssen. Im dritten Abschnitt wird Spring als Dependency Injection-Framework eingebunden. Au- ßerdem werden Hilfsbibliotheken für die Unit-Tests eingebunden. Im vierten Abschnitt werden alle notwendigen Hibernate-Bibliotheken definiert. 2.2.5 Teilprojekt Applikationsschicht Die Businesslogik bekommt ein eigenes Projekt (artifactId de.wicketpraxis--app). Die starke Unterteilung hilft gerade in diesem Bereich, die Grenzen der Anwendungs- schichten einzuhalten. Das Verschwimmen und Durchmischen von Anwendungs- und Per- sistenzlogik führt in den meisten Fällen zu schwer wartbarem Code. Listing 2.7 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ... > <modelVersion>4.0.0</modelVersion> <parent> ... </parent> <artifactId>${pom.groupId}--app</artifactId> <name>${pom.groupId}--${pom.artifactId} (Wicket Praxis - App)</name> <dependencies> <dependency> <groupId>${pom.groupId}</groupId> <artifactId>${pom.groupId}--persistence</artifactId> <version>${pom.version}</version> </dependency> </dependencies> </project> Im Moment benötigt die Anwendungsschicht nur die Abhängigkeit zur Persistenzschicht. Wenn im Laufe des Projekts Funktionen hinzukommen, werden diese meist in der Anwen- dungsschicht realisiert. Dann werden auch die notwendigen Abhängigkeiten an dieser Stel- le hinzugefügt. 2.2.6 Teilprojekt Webapp Das vorletzte Teilprojekt (artifactId de.wicketpraxis-webapp) ist die Präsentations- schicht. Dieses Projekt erstellt dann final auch das Anwendungsarchiv (war-Datei), das anschließend in einem Servlet-Container wie z.B. Tomcat 2 oder Jetty 3 deployt werden kann. 2 http://tomcat.apache.org/ 3 http://www.mortbay.org/jetty/24
  • 2.2 Aufsetzen der TeilprojekteListing 2.8 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ... > <modelVersion>4.0.0</modelVersion> <parent> <groupId>de.wicketpraxis</groupId> <artifactId>de.wicketpraxis--parentPom</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>${pom.groupId}--webapp</artifactId> <packaging>war</packaging> <name>${pom.groupId}--${pom.artifactId} (Wicket Praxis - Webapp)</name> <build> <plugins> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>maven-jetty-plugin</artifactId> <version>6.1.12.rc3</version> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>${pom.groupId}</groupId> <artifactId>${pom.groupId}--app</artifactId> <version>${pom.version}</version> </dependency> <!-- wicket --> <dependency> <groupId>org.apache.wicket</groupId> <artifactId>wicket</artifactId> <version>${wicket.version}</version> </dependency> <dependency> <groupId>org.apache.wicket</groupId> <artifactId>wicket-spring</artifactId> <version>${wicket.version}</version> </dependency> <dependency> <groupId>org.apache.wicket</groupId> <artifactId>wicket-extensions</artifactId> <version>${wicket.version}</version> </dependency> <!-- servlet for compile --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.4</version> <scope>provided</scope> </dependency> <dependency> <artifactId>jsp-api</artifactId> <groupId>javax.servlet</groupId> <version>2.0</version> <scope>provided</scope> </dependency> </dependencies> <properties> <wicket.version>1.4</wicket.version> </properties> </project> 25
  • 2 Aufsetzen der Teilprojekte Folgende Anpassungen sind in die Projektdatei eingeflossen: Das Jetty-Plugin (maven-jetty-plugin) startet die Webanwendung direkt aus dem Projektverzeichnis. Der Zwischenschritt, die Webanwendung in einen lokalen Servlet- Container zu deployen, entfällt. Die Anwendungsschicht wird als Abhängigkeit eingebunden. Das Wicket-Framework wird hinzugefügt. Ebenso wird eine Integrationsbibliothek für Spring eingebunden. Wenn man innerhalb von Wicket auf die Klassen ServletRequest und ServletResponse zugreifen muss, bindet man die Bibliotheken mit dem Attribut scope=provided ein. Dadurch werden diese Bibliotheken nur zum Erstellen der Klassen und beim Durchfüh- ren der Tests eingebunden. Da die Webanwendung in einem Servlet-Container ausge- führt wird, würde es sonst zu Kollisionen kommen, da der Servlet-Container eigene Versionen der Bibliotheken mitbringt. Die Besonderheit eines Maven-Projekts vom Typ Webarchiv (<packaging>war</pack- aging>) liegt in dem Ergebnis des Build-Prozesses. Es muss eine lauffähige Webanwen- dung erstellt werden. Dafür sind allerdings noch ein paar Voraussetzungen zu erfüllen. An dieser Stelle ist es erst einmal nur notwendig, ein Verzeichnis src/main/webapp/ WEB-INF/ innerhalb des Projektverzeichnisses anzulegen. In diesem Verzeichnis muss man dann eine leere Datei web.xml erstellen. Erst jetzt kann dieses Teilprojekt erfolgreich erstellt werden. 2.2.7 Teilprojekt ParentPom – Abschluss Die Projektdatei im Teilprojekt „parentPom“ ändern wir wie folgt: <project ... > ... <modules> <module>../de.wicketpraxis--base</module> <module>../de.wicketpraxis--dbconfig</module> <module>../de.wicketpraxis--dbconfig-test</module> <module>../de.wicketpraxis--dbconfig-schema-update</module> <module>../de.wicketpraxis--persistence</module> <module>../de.wicketpraxis--app</module> <module>../de.wicketpraxis--webapp</module> </modules> </project> Die Teilprojekte müssen in einer definierten Reihenfolge als Module referenziert werden. Wenn ich in diesem Projekt jetzt ein mvn install aufrufe, wird diese Funktion für jedes definierte Modul aufgerufen. Auf diese Weise kann ich das vollständige Projekt und alle Teilprojekte auf einmal erstellen lassen. In der Kombination mit mvn clean oder besser noch mvn clean install erhalte ich eine fast fertige Webanwendung, in der alle Ände- rungen aus den Teilprojekten enthalten sind. Jetzt wäre ein guter Zeitpunkt, die erstellten Dateien und Verzeichnisse in einer Versionsverwaltung abzuspeichern. Wer sich nicht si- cher ist, welche Dateien in einer Versionsverwaltung verwaltet werden sollten, kann alle temporären Dateien mit mvn clean löschen.26
  • 2.3 Erstellen von Eclipse-Projektdateien2.3 Erstellen von Eclipse-Projektdateien Alle Projekte sind angelegt. Alle weiteren Arbeiten sollten in einer entsprechenden Ent- wicklungsumgebung stattfinden. Um mit Eclipse auf die Maven-Projekte zuzugreifen, kann man aus dem fertigen Maven-Projekt eine entsprechende Eclipse-Projektdatei gene- rieren lassen. Dazu wechselt man in das entsprechende Projektverzeichnis und ruft Maven mit folgenden Parametern auf: $ mvn eclipse:eclipse –DdownloadSources=true Hinweis Unter Linux kann es notwendig sein, den Aufruf in mvn eclipse:eclipse zu ändern, da der Doppelpunkt eine Funktion hat. Im Verzeichnis werden dann die Eclipse-Projektdateien erstellt, die man dann in Eclipse öffnet. Man kann diesen Befehl auch im parentPom-Projektverzeichnis ausführen, und die Projektdateien werden in jedem Teilprojekt erstellt. Wenn neue Abhängigkeiten hinzugekommen sind oder Einstellungen verändert wurden, kann es notwendig sein, die Eclipse-Projektdateien noch einmal zu erstellen. Dazu sollte das Projekt in Eclipse aber geschlossen sein. 27
  • 2 Aufsetzen der Teilprojekte28
  • 3 3 Mit Leben füllen Nachdem wir nun die Grundstruktur der Anwendung erstellt und alle Teilprojekte mit allen Abhängigkeiten definiert haben, füllen wir das Ganze mit Inhalt.3.1 Konfiguration mit Spring Wie in Abschnitt 1.3.1.2 beschrieben, greifen wir für die Konfiguration der Anwendung auf das Spring-Framework zurück. Dazu erstellen wir in den verschiedenen Schichten die notwendigen Konfigurationsdateien und verknüpfen diese zu einer großen Anwendungs- konfiguration. Die Konfiguration wird durch XML-Dateien beschrieben. In diesen Dateien werden Ob- jekte (<bean>) definiert, indem einem Objekt eine ID zugewiesen wird und die Attribute des Objekts mit definierten Werten belegt werden. Die verschiedenen Objekte können dann über die ID verknüpft werden, indem z.B. das Attribut des einen Objekts mit einem anderen Objekt belegt wird. Für ein besseres Verständnis sollte man sich unbedingt die Dokumentation 1 des Frameworks ansehen. Das Spring-Framework (kurz: Spring) stellt neben dem reinen Dependency Injection- Mechanismus außerdem noch Hilfsfunktionen bereit, die über entsprechende XML-Tags benutzt werden können. Dazu muss man die notwendigen Definitionen in der Konfigura- tionsdatei einbinden. In der Dokumentation des Frameworks gibt es einen Abschnitt, der diesem Thema gewidmet ist (XML-Schema based configuration 2). 1 http://www.springsource.org/documentation 2 http://static.springframework.org/spring/docs/2.0.x/reference/xsd-config.html 29
  • 3 Mit Leben füllen3.2 Datenbankkonfiguration In allen Datenbankkonfigurationsteilprojekten fügen wir passende Konfigurationsdateien unterhalb des Pfades src/main/resources/de/wicketpraxis/db/config/ hinzu. In unserem Beispielprojekt benutzen wir drei verschiedene Datenbankkonfigurationen: Die erste Datenbank ist eine Datenbank auf dem Entwicklungsrechner oder die Produk- tivdatenbank, da in beiden Fällen derselbe Datenbanktreiber und dieselbe Einstellung benutzt wird. Die zweite Datenbank ist eine InMemory-Datenbank, die nur für die Unit-Tests benutzt und nach dem Beenden der Tests gelöscht wird. Die dritte Datenbank entspricht der ersten Datenbank, allerdings mit veränderten Ein- stellungen. Die Anpassungen veranlassen Hibernate, das Datenbankschema an die Struktur unserer Persistenzschicht anzupassen. Dazu werden die notwendigen Tabellen erzeugt und datenbankabhängige Optimierungen vorgenommen. 3.2.1 Teilprojekt dbconfig Wir erstellen in dem oben erwähnten Pfad die Konfiguration für die Produktivdatenbank. Diese Einstellung wird auch benutzt, wenn die Anwendung lokal ausgeführt wird. Listing 3.1 dbconfig.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd"> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost/wicketpraxis"/> <property name="username" value="wicketpraxis"/> <property name="password" value="wicketpraxis"/> </bean> <util:map id="hibernateProperties" map-class="java.util.Properties" key-type="java.lang.String" value-type="java.lang.String"> <entry key="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"></entry> <entry key="hibernate.connection.pool_size" value="10"></entry> <entry key="hibernate.statement_cache.size" value="10"></entry> </util:map> </beans> Es werden zwei Objekte definiert: Das Objekt datasource konfiguriert den JDBC-Datenbankzugriff. Dafür sind neben der Datenbanktreiberklasse das Login, das Passwort, der Servername sowie die JDBC- URL anzugeben.30
  • 3.2 Datenbankkonfiguration Im Objekt hibernateProperties werden Attribute gesetzt, die von Hibernate ausge- wertet werden. Durch das Setzen des Attributs hibernate.dialect kann Hibernate auf einen Tabellentyp zurückgreifen, der Transaktionen unterstützt.3.2.2 Teilprojekt dbconfig-testWie eben für das Teilprojekt dbconfig erstellen wir im oben erwähnten Pfad, aber imTeilprojekt dbconfig-test die Datei dbconfig-test.xml mit einem an dbconfig.xmlangelehnten Inhalt.Listing 3.2 dbconfig-test.xml <?xml version="1.0" encoding="UTF-8"?> <beans ... > <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <property name="url" value="jdbc:hsqldb:mem:test"/> <property name="username" value="sa"/> <property name="password" value=""/> </bean> <util:map id="hibernateProperties" map-class="java.util.Properties" key-type="java.lang.String" value-type="java.lang.String"> <entry key="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"></entry> <entry key="hibernate.connection.pool_size" value="10"></entry> <entry key="hibernate.statement_cache.size" value="10"></entry> <entry key="hibernate.hbm2ddl.auto" value="create-drop"></entry> <entry key="hibernate.bytecode.use_reflection_optimizer" value="false"></entry> <entry key="hibernate.show_sql" value="true"></entry> <entry key="hibernate.format_sql" value="true"></entry> </util:map> </beans>Damit wird für die Unit-Tests eine InMemory-HSQL-Datenbank konfiguriert. Außerdemwird Hibernate so konfiguriert, dass vor Beginn des Tests das Datenbankschema automa-tisch erzeugt und nach dem Test wieder automatisch gelöscht wird. So findet jeder Test dieDatenbank immer im selben Zustand vor.Hilfreich ist außerdem, dass die resultierenden SQL-Abfragen ausgegeben werden. Wemdie umfangreiche Ausgabe zu viel ist, kann die formatierte Ausgabe (format_sql=false)auch abschalten. Das Debuggen der Anwendung gestaltet sich einfacher, wenn der Reflec-tion-Optimizer deaktiviert ist.3.2.3 Teilprojekt dbconfig-schema-updateWie wir in der letzten Konfiguration gesehen haben, kann Hibernate das Schema einerDatenbank automatisch erzeugen. Das machen wir uns zunutze, wenn wir jetzt eine Konfi-guration erstellen, mit der wir das Erzeugen des Datenbank-Schemas erzwingen können.Wir legen die Datei dbconfig-schema-update.xml an, die sich ebenfalls an dbcon-fig.xml orientiert. 31
  • 3 Mit Leben füllen Listing 3.3 dbconfig-schema-update.xml <?xml version="1.0" encoding="UTF-8"?> <beans ... > <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost/wicketpraxis"/> <property name="username" value="wicketpraxis_root"/> <property name="password" value="wicketpraxis_root"/> </bean> <util:map id="hibernateProperties" map-class="java.util.Properties" key-type="java.lang.String" value-type="java.lang.String"> <entry key="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"></entry> <entry key="hibernate.hbm2ddl.auto" value="update"></entry> <entry key="hibernate.connection.pool_size" value="10"></entry> </util:map> </beans> Wie bei dbconfig-test.xml wird ein Abgleich zwischen geforderter und vorhandener Datenbankstruktur durchgeführt. Allerdings werden die alten Daten dabei nicht gelöscht (hibernate.hbm2ddl.auto=update), sondern nur angepasst. Dieses Vorgehen kann immer dann zu Problemen führen, wenn man Attribute (und damit den resultierenden Spaltennamen der Tabelle) umbenannt hat. Dann wird eine neue Spalte erzeugt. Die alte Spalte wird jedoch nicht gelöscht. In solchen Fällen sollte man die Daten- struktur nachträglich von Hand anpassen. Es ist auf diese Weise natürlich ohne Weiteres möglich, auch das Schema einer Produktiv- datenbank automatisch anpassen zu lassen. Das Risiko besteht dann allerdings darin, dass das Produktivsystem dadurch in einen unbrauchbaren Zustand versetzt wird und die An- wendung nicht mehr funktioniert. Daher empfehle ich, das Ändern des Schemas der Pro- duktivdatenbank immer von Hand oder durch die Unterstützung von darauf spezialisierten Anwendungen durchzuführen. 3.2.4 Schemagenerierung mit Hibernate Folgende Werte sind für das Attribut hibernate.hbm2dll.auto möglich: Wert Bedeutung validate prüft die Gültigkeit der Datenbanktabellen update erstellt und aktualisiert die Datenbanktabellen create erstellt die Datenbanktabellen initial create-drop erstellt die Datenbanktabelle beim Start und löscht sie am Ende Für die Testdatenbank benutzen wir daher am besten create-drop. Für das Schema- Update auf die lokale Entwicklungsdatenbank benutzen wir update. Für den normalen Zugriff würde sich die Verwendung von validate anbieten. Es empfiehlt sich aber, auf die Angabe vollständig zu verzichten, weil Datenbanken ihrerseits die gewünschten Typen32
  • 3.3 Persistenz selbst noch einmal anpassen. Damit passt das von Hibernate erwartete Ergebnis nicht zu dem von der Datenbank zurückgemeldeten, was dann einen Fehler in der Anwendung her- vorruft.3.3 Persistenz Nachdem wir die Datenbankkonfigurationen erstellt haben, können wir uns mit dem Da- tenbankzugriff beschäftigen. Damit wir das Rad nicht jedes Mal neu erfinden, erstellen wir ein paar Hilfsklassen, die allen abgeleiteten Klassen häufig benutzte Funktionen zur Ver- fügung stellt. Dazu wechseln wir in das entsprechende Teilprojekt persistence. 3.3.1 Datenbankzugriff – Allgemeine Schnittstellendefinition Alle Datenbankobjekte (Do), also Objekte, die durch die Persistenzschicht in Tabellen ab- gelegt werden, müssen folgendes Interface implementieren: Listing 3.4 DoInterface.java package de.wicketpraxis.persistence; import java.io.Serializable; public interface DoInterface<K extends Serializable> extends Serializable { public K getId(); } Dadurch ist es möglich, die eindeutige Identifikation für ein Datenbankobjekt zu ermitteln, ohne die genaue Klasse des Objektes zu kennen. Die Datenbankzugriffsklasse (Data Ac- cess Object, DAO) muss ebenfalls ein entsprechendes Interface implementieren: Listing 3.5 DaoInterface.java package de.wicketpraxis.persistence; ... public interface DaoInterface<K extends Serializable,T extends DoInterface<K>> { void delete(T o); void save(T o); void update(T o); T get(K id); T getNew(); public List<T> findAll(int offset,int max); public int countAll(); } Wenn dieses Interface vollständig implementiert ist, kann man über diese Schnittstelle neue Datenbankobjekte erstellen, diese speichern, verändern, über eine ID suchen und letztendlich auch löschen. Außerdem können alle Objekte in einer Liste dargestellt werden. 33
  • 3 Mit Leben füllen 3.3.2 Datenbankzugriff – Hilfsklassen Der Datenbankzugriff, der über die eben definierten Schnittstellen möglich ist, muss nicht für jede Klasse neu implementiert werden. Es bietet sich an, diese Funktionen in eine abs- trakte Klasse auszulagern, von der dann die konkreten Klassen abgeleitet werden. Außer- dem können wir an dieser Stelle die Integration mit Hibernate über Spring realisieren, so- dass in der abgeleiteten Klasse nur noch sehr wenige Anpassungen notwendig sind. Listing 3.6 AbstractDao.java package de.wicketpraxis.persistence; ... import org.hibernate.*; import org.springframework.transaction.annotation.Transactional; public abstract class AbstractDao<K extends Serializable,T extends DoInterface<K>> implements DaoInterface<K,T> { private static final Logger _logger = LoggerFactory.getLogger(AbstractDao.class); private Class<T> _domainClass; private SessionFactory _sessionFactory; protected AbstractDao(Class<T> domainClass) { _domainClass=domainClass; } public SessionFactory getSessionFactory() { return _sessionFactory; } public void setSessionFactory(SessionFactory sessionFactory) { _sessionFactory = sessionFactory; } public Session getSession() { return _sessionFactory.getCurrentSession(); } protected Class<T> getDomainClass() { return _domainClass; } protected Criteria getCriteria() { return getSession().createCriteria(_domainClass); } @Transactional public void delete(T object) { getSession().delete(object); } @Transactional public void save(T object) { getSession().save(object); }34
  • 3.3 Persistenz @Transactional public void update(T object) { getSession().update(object); } public T get(K id) { return (T) getSession().get(_domainClass, id); } public T getNew() { try { return _domainClass.newInstance(); } catch (InstantiationException e) { _logger.error("newInstance failed",e); } catch (IllegalAccessException e) { _logger.error("newInstance failed",e); } return null; } public List<T> findAll(int offset,int max) { Criteria criteria = getSession().createCriteria(_domainClass); if (offset!=0) criteria.setFirstResult(offset); if (max!=0) criteria.setMaxResults(max); return (List<T>) criteria.list(); } public int countAll() { Criteria criteria = getSession().createCriteria(_domainClass); criteria.setProjection(Projections.rowCount()); return (Integer) criteria.uniqueResult(); } }Wie man sieht, konnten alle Funktionen aus der Interface-Definition umgesetzt werden.Wichtig ist es, darauf hinzuweisen, dass die Annotation @Transactional für die Transak-tionssteuerung benutzt wird. Spring kümmert sich bei einer so markierten Methode darum,dass vor dem Aufruf eine Transaktion gestartet und am Ende der Methode geschlossenwird. Hinweis In abgeleiteten Klassen muss die Annotation @Transactional neu gesetzt werden, da Annotationen nicht vererbt werden.3.3.3 Datenbankzugriff – UserFür unsere Beispielanwendung greifen wir auf eine sehr einfach gehaltene Nutzerdaten-bank zurück. 35
  • 3 Mit Leben füllen Listing 3.7 User.java package de.wicketpraxis.persistence.beans; import javax.persistence.*; ... @Entity @Table(name="Users") public class User implements DoInterface<Integer> { Integer _id; String _eMail; String _name; @Id @GeneratedValue(strategy=GenerationType.AUTO) public Integer getId() { return _id; } public void setId(Integer id) { _id=id; } @Column(nullable=false,unique=true,name="email") public String getEMail() { return _eMail; } public void setEMail(String mail) { _eMail = mail; } @Column(nullable=false) public String getName() { return _name; } public void setName(String name) { _name = name; } } Die Klasse ist jetzt noch sehr übersichtlich. Wer bereits weitere Attribute identifiziert hat, kann diese einfach hinzufügen. Der Zugriff auf die Nutzer erfolgt dann über eine eigene Klasse. Diese Klasse leitet von AbstractDao ab und erbt somit alle grundlegenden Daten- bankfunktionen. Wir fügen in diesem Fall noch eine Methode hinzu, die einen Datensatz anhand der E-Mail ermittelt und als Ergebnis zurückgibt. Listing 3.8 UserDao.java package de.wicketpraxis.persistence.dao; import org.hibernate.criterion.*; ... public class UserDao extends AbstractDao<Integer, User> { public static final String BEAN_ID="userDao";36
  • 3.3 Persistenz public UserDao() { super(User.class); } public User getByEMail(String email) { return (User) getCriteria().add(Property.forName("EMail").eq(email)) .uniqueResult(); } }Damit haben wir alle Klassen zusammen, die für den Datenbankzugriff verantwortlichsind. Jetzt müssen wir die Persistenzschicht noch richtig konfigurieren.3.3.4 Datenbankzugriff – KonfigurationWir legen eine Konfigurationsdatei im Verzeichnis src/main/resources/de/wicket-praxis/persistence/ an.Listing 3.9 bean-config.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd"> <bean id="sessionFactory" class="org.springframework.orm.hibernate3. annotation.AnnotationSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> <property name="annotatedClasses"> <list> <value>de.wicketpraxis.persistence.beans.User</value> </list> </property> <property name="hibernateProperties" ref="hibernateProperties"></property> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate3. HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"></property> </bean> <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/> <bean id="userDao" class="de.wicketpraxis.persistence.dao.UserDao"> <property name="sessionFactory" ref="sessionFactory"></property> </bean> </beans>Die von Spring bereitgestellte Session Factory (sessionFactory) wertet die Annotationenin der User-Klasse aus. An diese Session Factory, die für das Erzeugen einer Hibernate-Session zuständig ist, wird ein Transaktionsmanager angebunden. Die Anweisung<tx:annotation-driven> veranlasst Spring dazu, die Annotationen in den Zugriffs-klassen auszuwerten und für die saubere Transaktionsbehandlung Proxy-Klassen für die 37
  • 3 Mit Leben füllen Zugriffsklassen bereitzustellen. Das Attribut proxy-target-class=true zwingt Spring dabei, auf die Verwendung von dynamischen Proxies zu verzichten und Proxies mithilfe des cglib Frameworks bereitzustellen. Dadurch ist es nicht mehr notwendig, ein spezielles Interface pro Klasse zu implementieren. Das erzeugte Zwischenobjekt ruft die Funktionen der eigentlichen Klasse auf und führt dabei vorher und nachher die notwendigen Trans- aktionsfunktionen aus. Wir haben jetzt die Persistenzschicht konfiguriert, aber noch keine Datenbankschnittstelle angebunden. Dazu erstellen wir eine weitere Konfigurationsdatei im selben Verzeichnis. Listing 3.10 persistence.xml <?xml version="1.0" encoding="UTF-8"?> <beans ...> <import resource="classpath:/de/wicketpraxis/db/config/dbconfig.xml"/> <import resource="bean-config.xml"/> </beans> In dieser Datei greifen wir auf die Datenbankkonfiguration für die Produktivdatenbank zu und referenzieren die Konfiguration der Persistenzschicht. 3.3.5 Persistenz-Tests Wie bereits erwähnt, kann man mit Hibernate relativ einfach Unit-Tests mit einer temporä- ren Datenbank durchführen. Für die Tests legen wir eine entsprechende Konfiguration an. Diese Konfigurationsdatei legen wir aber im Verzeichnis src/test/resources/de/ wicketpraxis/persistence/ ab, damit diese nur für die Tests benutzt wird und nicht im fertigen Projektarchiv auftaucht. Listing 3.11 persistence-test.xml <?xml version="1.0" encoding="UTF-8"?> <beans ...> <import resource="classpath:/de/wicketpraxis/db/ config/dbconfig-test.xml"/> <import resource="bean-config.xml"/> </beans> Wir benutzen dieselbe bean-config.xml jetzt mit der Datenbankkonfiguration für die Testdatenbank. In den Unit-Tests müssen wir daher die Konfigurationsdatei persisten- ce-test.xml verwenden. Als Basisklasse für alle weiteren Tests leiten wir eine eigene Klasse von einer Unit-Test-Hilfsklasse des Spring-Frameworks ab. Wir erweitern die Klasse ein wenig und leiten alle konkreten Testklassen von dieser Klasse ab: Listing 3.12 AbstractTest.java package de.wicketpraxis.persistence; import org.springframework.test. 38
  • 3.3 Persistenz AbstractTransactionalDataSourceSpringContextTests; public abstract class AbstractTest extends AbstractTransactionalDataSourceSpringContextTests { @Override protected String[] getConfigLocations() { return new String[] { "classpath:/de/wicketpraxis/persistence/persistence-test.xml" }; } protected <T> T getBean(String name,Class<T> requiredType) { return (T) getApplicationContext().getBean(name, requiredType); } protected <T> T getBean(Class<T> requiredType) { Map beansOfType = getApplicationContext(). getBeansOfType(requiredType); if (beansOfType.size()==1) { return (T) new ArrayList(beansOfType.values()).get(0); } return null; } }Der Unit-Test für die User-Klasse gestaltet sich recht übersichtlich. Das liegt sicher auchdaran, dass wir bisher nicht allzu viele Funktionen realisiert haben.Listing 3.13 TestUserDao.java package de.wicketpraxis.persistence.dao; import junit.framework.Assert; ... public class TestUserDao extends AbstractTest { public void testKeinNutzer() { UserDao userDao = getBean(UserDao.BEAN_ID,UserDao.class); User user = userDao.get(1); Assert.assertNull("Kein Nutzer",user); } public void testEinNutzer() { UserDao userDao = getBean(UserDao.BEAN_ID,UserDao.class); User nutzer=new User(); String email = "klaus@test.de"; nutzer.setEMail(email); nutzer.setName("Klaus"); userDao.save(nutzer); nutzer=userDao.get(nutzer.getId()); Assert.assertNotNull("User",nutzer); Assert.assertEquals("Email",email,nutzer.getEMail()); nutzer=userDao.getByEMail(email); Assert.assertNotNull("User",nutzer); Assert.assertEquals("Email",email,nutzer.getEMail()); } } 39
  • 3 Mit Leben füllen Der erste Test prüft, ob ein Nutzer mit der ID=1 bereits in der Datenbank ist. Das sollte nicht der Fall sein, weil bei jedem Test die Datenbank wieder gelöscht wird. Im zweiten Test wird geprüft, ob der Nutzer angelegt, gelesen und anhand der E-Mail gefunden wer- den kann. Diesen Test kann man mit mvn test ausführen. Wenn keine Fehler aufgetreten sind, kann man dieses Teilprojekt mit mvn install erstellen lassen. Die Unit-Tests wer- den dabei jedes Mal ausgeführt. Als einfacher Test, ob die Unit-Tests tatsächlich ausge- führt werden, kann man z.B. den Namen in setName auf einen anderen Wert ändern. Der Test sollte dann fehlschlagen. 3.3.6 Schema-Update Hibernate prüft je nach Einstellung vor dem ersten Datenbankzugriff, ob an der in der Da- tenbank vorhandenen Tabellenstruktur Anpassungen notwendig sind. Je nach Einstellung führt Hibernate dann die entsprechenden Anpassungen durch. Gerade wer wenig Erfahrun- gen in Bezug auf Datenbanken hat, ist meist besser damit beraten, auf diese Funktion von Hibernate zurückzugreifen. Das Erzeugen und Ändern von Tabellen und Spalten sollte aber nicht jedem Datenbanknutzer erlaubt sein. Daher empfiehlt es sich, die Schema- Update-Funktion nicht automatisch auszuführen, und dafür einen gesonderten Datenbank- nutzer mit erweiterten Rechten zu nehmen. Diese Einstellungen haben wir in die dbcon- fig-schema-update.xml einfließen lassen. Um die Funktion auszuführen, legen wir erst eine eigene Konfigurationsdatei im Ver- zeichnis src/test/resources/de/wicketpraxis/persistence/ an. Listing 3.14 persistence-schema-update.xml <?xml version="1.0" encoding="UTF-8"?> <beans ...> <import resource="classpath:/de/wicketpraxis/db/ config/dbconfig-schema-update.xml"/> <import resource="bean-config.xml"/> </beans> Diese Datei unterscheidet sich von persistence-test.xml nur an der Stelle, an der wir die Datenbankkonfiguration auswählen. Wir benutzen für den Start von Hibernate mit der entsprechenden Konfiguration dasselbe Verfahren wie bei den Persistenz-Tests. Damit dieser „Test“ aber nur bei Bedarf ausgeführt wird, sollte im Klassenname das Wort „Test“ nicht vorkommen. Listing 3.15 SchemaUpdate.java package de.wicketpraxis.persistence; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SchemaUpdate extends AbstractTest { private static final Logger _logger = LoggerFactory.getLogger(SchemaUpdate.class);40
  • 3.4 Anwendungsschicht @Override protected String[] getConfigLocations() { return new String[]{ "classpath:/de/wicketpraxis/ persistence/persistence-schema-update.xml" }; } public void testSchemaUpdate() { _logger.error("Schema Update"); } } Die Testfunktion hat dann eigentlich keine Funktion. Im Vorfeld wird Hibernate mit der referenzierten Datenbankkonfiguration gestartet und das Schema angepasst. Um den Test und damit das Schema-Update zu starten, rufen wir Maven wie folgt auf: $mvn –Dtest=de/wicketpraxis/persistence/SchemaUpdate test Das Teilprojekt ist jetzt soweit fertig. Wir haben durch Datenbanktests die Funktion getes- tet, wir können per Schema-Update die Datenbanktabellen anlegen lassen und mit der Standardkonfiguration aus der Anwendung heraus dann darauf zugreifen.3.4 Anwendungsschicht Wir wechseln in das Teilprojekt app. Da wir in dieser Schicht im Moment keine Funktio- nen implementieren, reicht es aus, wenn wir eine Konfigurationsdatei im Verzeichnis src/main/resources/de/wicketpraxis/app/ erstellen. Listing 3.16 app.xml <?xml version="1.0" encoding="UTF-8"?> <beans ...> <import resource="classpath:/de/wicketpraxis/ persistence/persistence.xml"/> </beans> In dieser Datei wird die Konfiguration aus der Persistenzschicht eingebunden. Mehr ist an dieser Stelle nicht notwendig.3.5 Präsentationsschicht Wir haben jetzt alle untergeordneten Anwendungsschichten erstellt. Als letztes Teilprojekt vervollständigen wir die Präsentationsschicht. 3.5.1 Hilfsklasse für Maven-Projekte Wicket lädt im Entwicklungsmodus die Markup-Dateien für die Komponenten bei jeder Veränderung neu. Dabei wird aber auf Dateien in target zugegriffen, also auf eine Kopie 41
  • 3 Mit Leben füllen von src/main/resources. Damit im richtigen Verzeichnis gesucht wird, müsste man Anpassungen in der Projektbeschreibung (pom.xml) vornehmen. Leider funktioniert diese Methode nicht zuverlässig, sodass ich nach Alternativen gesucht habe. Glücklicherweise bietet Wicket die Möglichkeit, die Anwendung in diesem Aspekt anzupassen. Die folgende Klasse sucht die Ressourcen, die Wicket benötigt, zuerst in dem in Maven-Projekten defi- nierten Standard-Ressourcenpfad (src/main/resources). Listing 3.17 MavenDevResourceStreamLocator.java package de.wicketpraxis.wicket.util.resource; ... import org.apache.wicket.util.resource.*; import org.apache.wicket.util.resource.locator.ResourceStreamLocator; public class MavenDevResourceStreamLocator extends ResourceStreamLocator { String _prefix="src/main/resources/"; @Override public IResourceStream locate(Class<?> clazz, String path) { IResourceStream located=getFileSysResourceStream(path); if (located != null) return located; return super.locate(clazz, path); } private IResourceStream getFileSysResourceStream(String path) { File f=new File(_prefix+path); if ((f.exists()) && (f.isFile())) { return new FileResourceStream(f); } return null; } } 3.5.2 Wicket Web Application Die kleinste Wicket-Anwendung besteht aus mindestens einer Seite und einer von We- bApplication abgeleiteten Klasse. Da unsere kleine Anwendung aber z.B. bereits über eine Datenbankanbindung verfügt, wird sie an dieser Stelle etwas komplexer. Natürlich müssen wir daher auf Funktionen zurückgreifen, die bisher nicht erklärt wurden, die aber bereits einen guten Eindruck vermitteln können, wie eine Webanwendung mit Wicket funktioniert. Dieses Grundgerüst kann dann als Basis für alle folgenden Beispiele benutzt werden. Zuerst legen wir eine Seite an, die dann in der Applikationsklasse als Startseite definiert werden kann. Listing 3.18 Start.java package de.wicketpraxis.web.pages; ... public class Start extends WebPage42
  • 3.5 Präsentationsschicht { @SpringBean(name=UserDao.BEAN_ID) UserDao _userDao; public Start() { LoadableDetachableModel<List<User>> userModel= new LoadableDetachableModel<List<User>>() { @Override protected List<User> load() { return _userDao.findAll(0, 10); } }; ListView<User> userList=new ListView<User>("userList",userModel) { @Override protected void populateItem(ListItem<User> item) { item.add(new Label("name",item.getModelObject().getName())); } }; add(userList); } }Die Annotation @SpringBean definiert, wie das Feld initialisiert werden soll. Dabei wirdüber einen optional zu definierenden Namen auf die Spring-Konfiguration zurückgegriffenund nach einer Spring-Bean vom Typ UserDao und der optionalen ID mit dem Wert ausdem Attribut name gesucht. Dieses Verhalten muss noch in der Applikation definiert wer-den.Das Objekt userModel sorgt dafür, dass bei Bedarf die Liste der ersten 10 Nutzer aus derDatenbank geladen werden. Das Objekt userList ist für die Anzeige zuständig und fügtfür jeden Eintrag der Liste ein Label mit dem Nutzernamen hinzu.Jetzt benötigen wir noch eine passende Markup-Datei für die Seite und legen eine Datei imVerzeichnis src/main/resources/de/wicketpraxis/web/pages/ an.Listing 3.19 Start.html <html> <head> <title>WicketPraxis</title> </head> <body> <h1>WicketPraxis</h1> <table> <thead> <tr> <th>Name</th> </tr> </thead> <tbody> <tr wicket:id="userList"> <td><span wicket:id="name"></span></td> </tr> </tbody> </table> </body> </html> 43
  • 3 Mit Leben füllen Die Attribute wicket:id referenzieren dabei die Komponenten in unserer Start-Klasse. Als Nächstes müssen wir unsere eigene Applikationsklasse erstellen. Listing 3.20 WicketPraxisApplication.java package de.wicketpraxis.web; ... public class WicketPraxisApplication extends WebApplication { @Override protected void init() { super.init(); addComponentInstantiationListener(new SpringComponentInjector(this)); if (DEVELOPMENT.equalsIgnoreCase(getConfigurationType())) { getResourceSettings(). setResourceStreamLocator(new MavenDevResourceStreamLocator()); } } @Override public Class<? extends Page> getHomePage() { return Start.class; } } Beim Start der Webanwendung wird von dieser Klasse genau eine Instanz erzeugt. Dabei wird die init-Methode nur einmal aufgerufen. In dieser Methode werden alle Einstellun- gen für diese Anwendung durchgeführt. Als Erstes wird der SpringComponentInjector eingebunden, der für die Auswertung der SpringBean-Annotation sorgt. Wenn Wicket im Entwicklungsmodus ist, wird der MavenDevResourceStreamLocator eingebunden und lädt während der Entwicklung zuverlässig alle veränderten Markup-Dateien neu. Start wird als die Startseite für die Anwendung definiert. 3.5.3 Servlet-Konfiguration Damit die Anwendung auch gestartet wird, müssen wir die Servlet-Konfiguration anpas- sen. Wir ändern unsere bisher leere web.xml im Verzeichnis src/main/webapp/WEB- INF/. Hinweis Windows unterscheidet bei Datei- und Verzeichnisnamen nicht zwischen Groß- und Klein- schreibung. Das kann dazu führen, dass es beim Bereitstellen auf einem Unix-Server zu Problemen kommen kann, wenn der Verzeichnisname nicht richtig geschrieben wurde. Der Servlet-Container erwartet die web.xml aber im Verzeichnis WEB-INF. Listing 3.21 web.xml <?xml version="1.0" encoding="ISO-8859-1"?> <!--DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.4//EN"44
  • 3.5 Präsentationsschicht "http://java.sun.com/j2ee/dtds/web-app_2_4.dtd"--> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <display-name>wicketpraxis.com</display-name> <description> WicketPraxis Webapp </description> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <filter> <filter-name>de.wicketpraxis.webapp</filter-name> <filter-class>org.apache.wicket.protocol.http.WicketFilter </filter-class> <init-param> <param-name>applicationClassName</param-name> <param-value>de.wicketpraxis.web.WicketPraxisApplication </param-value> </init-param> <init-param> <param-name>configuration</param-name> <param-value>deployment</param-value> </init-param> </filter> <filter> <filter-name>de.wicketpraxis.hibernate.osv</filter-name> <filter-class> org.springframework.orm.hibernate3.support.OpenSessionInViewFilter </filter-class> </filter> <filter-mapping> <filter-name>de.wicketpraxis.hibernate.osv</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>de.wicketpraxis.webapp</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>An erster Stelle wird eine Spring-Konfiguration im selben Verzeichnis angegeben (/WEB-INF/applicationContext.xml). Auf diese Konfiguration wird sowohl von Wicket alsauch durch den OpenSessionInViewFilter zugegriffen. Der ContextLoaderListenerstellt einen aus der Konfiguration ermittelten ApplicationContext bereit und sorgt beimBeenden des Servers für das Herunterfahren des Context. Im WicketFilter geben wirnun unsere eigene Wicket-Anwendung an und setzen den Startmodus auf deployment.Damit wird das Anwendungsarchiv standardmäßig mit diesem Modus erstellt. Das Öffnenund Schließen der Session für den Datenbankzugriff über Hibernate übernimmt der Open-SessionInViewFilter. Die Reihenfolge der Filter im Bereich filter-mapping ist daher 45
  • 3 Mit Leben füllen wichtig. Die Anfrage durchläuft den erstgenannten Filter vor dem nächsten. Damit ist sichergestellt, dass die Datenbankverbindung geöffnet ist, wenn die Anfrage durch Wicket behandelt wird. 3.5.4 Spring-Konfiguration Wir erstellen jetzt im selben Verzeichnis die referenzierte Konfigurationsdatei für den Ap- plicationContext. Dabei könnten wir zwar direkt unsere app.xml einbinden. Ich emp- fehle aber einen Umweg, sodass a) jede Schicht die Konfigurationsdatei an einer nachvoll- ziehbaren Position ablegt und b) in der Konfigurationsdatei für den Servlet-Container kei- ne zusätzlichen Einstellungen abgelegt werden. Für den Fall, das Unit-Tests für die Präsen- tationsschicht durchgeführt werden müssten, wäre es sehr schwierig (wenn nicht unmög- lich), an diese Datei heranzukommen. Listing 3.22 applicationContext.xml <?xml version="1.0" encoding="UTF-8"?> <beans ...> <import resource="classpath:/de/wicketpraxis/web/webapp.xml"/> </beans> In diesem binden wir die noch zu erstellende Datei webapp.xml aus dem Ressourcenpfad ein. Danach erstellen wir diese Datei im Verzeichnis src/main/resources/de/wicket- praxis/web/. Listing 3.23 webapp.xml <?xml version="1.0" encoding="UTF-8"?> <beans ... > <import resource="classpath:/de/wicketpraxis/app/app.xml"/> </beans> Die Anwendung ist damit vollständig konfiguriert und kann jetzt gestartet werden. 3.5.5 Start der Anwendung Wir haben jetzt alles zusammen. Alle Teilprojekte sollten, wenn noch nicht geschehen, über das ParentPom-Projekt erstellt (mvn install) werden. Dann wechseln wir in das Webapp-Projekt und starten die Webanwendung mit: $ mvn jetty:run –Dwicket.configuration=development Der eingebettete Webserver sollte starten und den erfolgreichen Start mit folgender Mel- dung quittieren: ... INFO: [WicketPraxisApplication] Started Wicket version 1.4 in development mode ******************************************************************** *** WARNING: Wicket is running in DEVELOPMENT mode. *** *** ^^^^^^^^^^^ *** *** Do NOT deploy to your live server(s) without changing this. *** *** See Application#getConfigurationType() for more information. *** ******************************************************************** 2009-01-13 19:18:17.161::INFO: Started SelectChannelConnector@0.0.0.0:8080 [INFO] Started Jetty Server46
  • 3.5 PräsentationsschichtDamit wurde der Server erfolgreich gestartet. Man öffnet nun mit dem Browser die Seitehttp://localhost:8080/. Dort erscheint eine Fehlermeldung mit einem Link, der auf unsereWebanwendung verweist. Nach einem Klick sollte man ein Ergebnis wie in Abbildung 3.1sehen.Abbildung 3.1 Startseite mit Daten aus der DatenbankWenn man wie ich schon einen Eintrag in die Datenbank getätigt hat, sieht man in der Er-gebnistabelle auch schon einen Eintrag. Damit ist die Anwendung funktionsfähig, und jededer notwendigen Schichten ist erstellt und eingebunden. Hinweis Wicket unterscheidet zwischen den Modi Deployment und Development. Alle erweiterten Informationen, die im Development-Modus angezeigt werden, werden durch den Modus Deployment deaktiviert. Für die Entwicklung sollte der Development-Modus gewählt werden, weil das die Fehlersuche erheblich vereinfacht. 47
  • 3 Mit Leben füllen48
  • 4 4 Die Wicket-Architektur Nachdem wir nun unsere erste kleine Webanwendung erstellt haben, möchte ich etwas genauer auf die Architektur von Wicket eingehen, bevor wir uns in den nächsten Kapiteln mit den einzelnen Komponenten beschäftigen.4.1 Wicket und das HTTP-Protokoll Eine Webanwendung benutzt das HTTP-Protokoll, um mit dem Browser zu kommunizie- ren. Über dieses Protokoll wird eine Anfrage an den Server geschickt und die Antwort an den Browser übermittelt. Das Protokoll dient dem Transport der Daten und ist selbst zustandslos. Das bedeutet, dass eine Anfrage an den Server in keiner Beziehung zu einer vorangegangenen Antwort stehen muss. Diese Zustandslosigkeit des Protokolls verursacht eine Reihe von Problemen, die in den verschiedenen Webframeworks unterschiedlich gelöst werden. Wicket orientiert sich dabei stark an einer Desktop-Anwendung, sodass man als Entwickler nur selten damit konfron- tiert wird, dass es sich immer noch um eine Webanwendung handelt. Dennoch unterstützt Wicket den Entwickler mit einer Reihe von Schnittstellen, die weiterhin den Zugriff auf das Transportprotokoll ermöglichen oder davon abstrahiert bestimmte Funktionen zur Ver- fügung stellt.4.2 Struktur Wicket besitzt verschiedene Elemente, die in einer Webanwendung zusammenspielen. Ich werde jedes im Folgenden etwas eingehender erläutern, sodass sich ein Gesamtbild ergibt, was für das Verständnis von Wicket hilfreich ist. 49
  • 4 Die Wicket-Architektur 4.2.1 WebApplication Wie wir bereits gesehen haben, müssen wir für unsere eigene Anwendung eine von Web- Application abgeleitete Klasse erstellen. Von dieser Klasse wird auf dem Server nur eine Instanz pro Anwendung erzeugt. Die init-Methode wird beim Start nur einmal ausge- führt, sodass an dieser Stelle alle Einstellungen vorgenommen werden können, die für die Anwendung gelten sollen. Außerdem gibt es verschiedene Methoden, die man mit eigenen Implementierungen überladen und so die Anwendung an die eigenen Bedürfnisse anpassen kann. 4.2.2 Session Jeder Nutzer, der auf die Anwendung zugreift bekommt eine Session zugewiesen. Das ist notwendig, da Wicket alle Informationen, die für eine Nutzerinteraktion notwendig sind, in dieser Session speichert. Während einige andere Frameworks versuchen, den Zustand einer Anwendung in URL-Parametern abzulegen, überträgt Wicket in dieser Phase nur die vom Nutzer durchgeführte Aktion. Die Daten einer Session werden in einem SessionStore gespeichert. Normalerweise wer- den die Daten in einer HttpSession aus dem javax.servlet-Paket gespeichert. Man könnte allerdings auch seinen eigenen SessionStore implementieren. 4.2.3 PageMap Jede Session hat mindestens eine PageMap. In dieser PageMap sind die Seiten (Page) ab- gelegt, die der Nutzer aufgerufen hat. Neben der zuletzt besuchten Version einer Seite fin- den sich in der PageMap auch ältere Versionen der Seite wieder. Dadurch kann der Nutzer im Browser auf die letzten Seiten zurückspringen, ohne dass es zu Fehlern kommt, weil Wicket den Zustand der Anwendung zu diesem Zeitpunkt wiederherstellen kann. Wenn ein Nutzer mehr als ein Browserfenster geöffnet hat (z.B. mit einem Popup), legt Wicket mehr als eine PageMap an. Auch wenn diese Vorgehensweise dafür sorgt, dass jederzeit die korrekten Funktionen aufgerufen werden, muss man sich als Entwickler nicht mit solchen Details beschäftigen, denn dieser Vorgang geschieht automatisch und vollkommen transparent. 4.2.4 Page Eine Seite (Page) ist eine Komponente, die sich von anderen Wicket-Komponenten nur dadurch unterscheidet, dass alle anderen Komponenten immer zu einer Seite gehören. Die Seite ist damit die oberste Komponente im Komponentenbaum. Man kann sich eine Page auch als Browserfenster vorstellen. Die Darstellung einer Seite liefert als Ergebnis die HTML-Daten, die der Browser dann darstellt.50
  • 4.3 Request-Behandlung 4.2.5 PageStore Wicket hält die aktuellste Seite der PageMap im Speicher vor. Alle anderen Seiten werden im PageStore abgelegt. Im Standardfall wird dazu die Seite serialisiert und in einem DiskPageStore, also auf der Festplatte abgelegt. Bei Bedarf wird die Seite wieder aus dem PageStore geladen. Auf diese Weise hält sich der Speicherverbrauch in Grenzen, ohne an Funktionalität einzubüßen. 4.2.6 Component Eine Komponente ist die Basiseinheit einer Wicket-Anwendung. Alle Wicket-Komponen- ten sind von dieser Klasse abgeleitet.4.3 Request-Behandlung Wicket kapselt die bei einer Webanwendung beteiligten HttpRequest- und HttpRespon- se-Klassen. Diese werden durch einen RequestCycle abgearbeitet, wobei die Behandlung der verschiedenen Phasen an einen RequestCycleProcessor übergeben werden. Im Re- questCycle werden folgende Phasen durchlaufen: PREPARE_REQUEST: Startet die Request-Verarbeitung. RESOLVE_TARGET: Ermittelt das Ziel dieser Abfrage (RequestTarget) durch den Re- questCycleProcessor. PROCESS_EVENTS: Die Event-Behandlung durch den RequestCycleProcessor wird gestartet. RESPOND: Der Response wird durch den RequestCycleProcessor erstellt. DETACH_REQUEST: Alle temporären Daten werden gelöscht, indem für jede Komponen- te die Methode detach() aufgerufen wird. Danach wird die Seite in der PageMap ab- gelegt. Zusätzlich wird die Seite serialisiert, damit sie im PageStore abgelegt werden kann. DONE: Die Abarbeitung ist abgeschlossen, der nächste Request kann verarbeitet werden. Hinweis Wenn an dieser Stelle eine Komponente noch Referenzen auf Objekte hat, die nicht serialisiert werden können, kann Wicket die ganze Seite nicht serialisieren und somit nicht in den PageStore schreiben. Das kann zu Problemen führen, wenn man später auf diese Seite zugreifen möchte. Wenn zu einem späteren Zeitpunkt auf diese Seite zugegriffen wird, kann Wicket diese nicht im PageStore finden und gibt eine Fehlermeldung aus. Der RequestCycleProcessor stellt dabei folgende Funktionen bereit: resolve(RequestCycle, RequestParameters): Die URL und die URL-Parameter werden dekodiert und das RequestTarget ermittelt. 51
  • 4 Die Wicket-Architektur processEvents(RequestCycle): Wenn das RequestTarget ermittelt wurde, werden die Events verarbeitet. Dabei werden dann Events wie der Klick auf einen Link oder das Abschicken eines Formulars ausgewertet (IRequestListener z.B. Ilink- Listener). respond(RequestCycle): Nachdem die Events verarbeitet wurden, wird das Ergebnis gerendert und an den Browser zurückgeschickt. respond(RuntimeException, RequestCycle): Wenn ein Fehler aufgetreten ist, der nicht abgefangen wurde, wird diese Methode aufgerufen, um auf diesen Zustand geeig- net zu reagieren. Im Entwicklungsmodus wird z.B. eine andere, mit mehr Informatio- nen versehene Fehlerseite dargestellt. Was dabei an den Browser zurückgeschickt wird, hängt natürlich davon ab, ob die Anfrage eine Seite oder ein Bild zurückliefern soll, oder ob das Ergebnis für eine Ajax-Anfrage aufbereitet werden muss. 4.3.1 Komponentenphasen Jede Komponente hat neben dem Lebenszyklus, der mit dem Erstellen der Komponente über einen Konstruktor beginnt und mit dem Bereinigen durch den Garbage Collector en- det, auch einen Request-Zyklus. Dabei sind die wesentlichen Phasen: Request-Behandlung: Die durch den Request beschriebene Aktion wird durchgeführt (Abschnitt 4.3.1.1). onBeforeRender: Wenn die Komponente sichtbar ist, dann wird, bevor die Kompo- nente dargestellt wird, die onBeforeRender-Methode aufgerufen. Dabei kann z.B. die Sichtbarkeit manipuliert werden, was Einfluss auf diesen und den nächsten Schritt hat. onRender: wird aufgerufen, wenn die Komponente dargestellt wird. onAfterRender: wird immer aufgerufen, auch wenn die Komponente unsichtbar ist. onDetach: wird danach aufgerufen und sorgt dafür, dass die temporären Daten ge- löscht werden können, damit die Session nur soviel Platz wie nötig belegt. 4.3.1.1 Request-Behandlung Die durch den Request ausgelösten Aktionen werden durchgeführt. Dabei werden zuerst die Komponenten gesucht, bei der eine Aktion ausgelöst wurde. Wenn die Komponente gefunden wurde, wird die Aktion ausgelöst, die dann z.B. im Fall eines Links dazu führt, dass irgendwann onClick() aufgerufen wird. Bei Formularen werden die durch den Re- quest übergebenen Werte der Formularkomponenten verarbeitet. 4.3.2 Nebenläufigkeit – Threads Pro Request wird ein Thread ausgeführt, sodass man sich normalerweise keine Gedanken um dieses Thema machen muss – mit einer wichtigen Ausnahme: die Session. Da ein Nut-52
  • 4.4 Komponenten, Modelle, Markup zer einer Session zugewiesen ist, kann es passieren, dass der Zugriff auf die Session gleichzeitig erfolgt. Daher muss man selbst sicherstellen, dass der Zugriff synchronisiert erfolgt.4.4 Komponenten, Modelle, Markup Wicket ist ein MVC-Framework. MVC steht dabei für Model, View und Controller. Das Model stellt die Daten bereit, die durch den Controller verändert werden können und die durch eine View dargestellt werden. 4.4.1 Komponenten Die kleinste Einheit einer Wicket-Anwendung ist die Komponente. Eine Komponente über- nimmt dabei die Funktion eines Controllers, wobei das Framework dafür sorgt, dass die Aktion des Nutzers der richtigen Komponente zugeordnet werden kann und die Komplexi- tät dieser Verarbeitung vollständig kapselt. 4.4.2 Modelle Wicket orientiert sich dabei an dem Programmiermodell einer Desktop-Anwendung. In diesem Modell informiert der Controller die View darüber, ob sich etwas geändert hat und die Komponente neu gezeichnet werden muss. Da bei Webanwendungen immer (es sei denn, Ajax kommt zum Einsatz) die ganze Seite dargestellt werden muss, entfällt das Be- nachrichtigen der View über die Veränderung. Es werden immer alle Komponenten mit den zugehörigen Daten dargestellt. Wenn Wicket über die Modelländerungen informiert wird, zieht Wicket diese Informationen heran, um für diesen neuen Zustand eine neue Ver- sion der Seite anzulegen. Die alte Version mit alten Daten wird im PageStore abgelegt. 4.4.3 Markup Eine Wicket-Komponente besteht aus einer Java-Klasse, die von einer Wicket-Basisklasse geerbt hat, und einer dazugehörigen Markup-Datei, die denselben Namen besitzt, in src/main/resources im selben Unterverzeichnis wie die Klasse abgelegt werden muss und die Endung html besitzt. Das bedeutet, dass eine Klasse StartPage im Package de.wicketpraxis.pages von der Klasse WebPage abgeleitet ist und eine Markup-Datei im Verzeichnis de/wicketpraxis/pages/ innerhalb des Ressourcenverzeichnisses src/main/resources mit den Dateinamen StartPage.html benötigt. 53
  • 5 5 Modelle Es ist sicher ungewöhnlich, dass man bei einem MVC-Framework mit der Erklärung der Modelle anfängt. Dass ich es trotzdem tue, hat zwei Gründe: Einerseits gibt es kaum Kom- ponenten, die nicht auf ein Modell zugreifen, um etwas darzustellen. Andererseits ist die häufigste Frage in Bezug auf Wicket: Wie stelle ich etwas dar? Und bevor man diese Frage beantworten kann, muss man erst klären, woher dieses Etwas kommt. Die meisten Komponenten greifen auf ein Modell zu, um die Modelldaten direkt oder in- direkt darzustellen. Dazu greifen die Komponenten über sehr einfache Methoden auf die Daten zu. Die einfachste Komponente Label konvertiert die Daten des Modells in einen String und stellt diesen dann dar. Das IModel<T>-Interface, das alle Modelle implementieren müssen, definiert nur drei Me- thoden: setObject(T): setzt die Daten. setObject(T): gibt die Daten zurück. detach(): Wenn ein Modell einer Komponente zugeordnet wurde, wird bei einem Aufruf der detach()-Methode der Komponente auch die detach()-Methode des Mo- dells aufgerufen. Das bedeutet, dass Modelle beliebige Daten enthalten können.5.1 Konverter Die Daten, die ein Modell zurückliefern kann, können beliebigen Typs sein. Damit diese Daten sinnvoll dargestellt werden können, liefert Wicket für die wichtigsten Datentypen Konverter mit, welche die Daten in Text und Text in Daten umwandeln können, wie es z.B. bei Formularen benötigt wird. Auch wenn Wicket für die wichtigsten Datentypen passende Konverter mitbringt, kann es notwendig sein, eigene Konverter für unbekannte Datentypen oder die bestehenden Kon- verter für einen oder mehrere Datentypen bereitzustellen. Dazu überschreibt man am bes- ten die newConverterLocator()-Methode der WebApplication-Klasse. 55
  • 5 Modelle Listing 5.1 WicketPraxisApplication.java ... @Override protected IConverterLocator newConverterLocator() { return new CustomConverterLocator(super.newConverterLocator()); } ... Da wir nicht alle Konverter neu definieren möchten, sondern bei fast allen Typen auf die bewährten Konverter zurückgreifen wollen, sollte unsere ConverterLocator-Klasse auf die Konverter der Standardklasse zurückgreifen. Listing 5.2 CustomConverterLocator.java package de.wicketpraxis.web.converter; ... public class CustomConverterLocator implements IconverterLocator { IConverterLocator _fallback; Map<Class<?>,IConverter> _customMap=new HashMap<Class<?>, IConverter>(); { _customMap.put(Some.class, new SomeClassConverter()); } public CustomConverterLocator(IConverterLocator fallback) { _fallback=fallback; } public IConverter getConverter(Class<?> type) { IConverter ret=_customMap.get(type); if (ret==null) ret=_fallback.getConverter(type); return ret; } } Das IConverter-Interface ist ähnlich einfach wie das IModel-Interface. Die Methode convertToString ist für die Umwandlung zur Darstellung verantwortlich. Die Methode convertToObject konvertiert im besten Fall einen String in den gewünschten Typ. Inte- ressant ist hierbei, dass mit Locale auch die gewünschte Spracheinstellung übergeben wird. Listing 5.3 IConverter.java package org.apache.wicket.util.convert; ... public interface IConverter extends IClusterable { Object convertToObject(String value, Locale locale); String convertToString(Object value, Locale locale); }56
  • 5.2 Einfache Modelle5.2 Einfache Modelle Genug der Theorie. Schauen wir uns an einem ersten Beispiel an, wie sich das alles zu- sammenfügt. Dass dabei noch nicht alles bekannt ist, liegt in der Natur der Sache. In die- sem ersten Beispiel möchten wir die aktuelle Uhrzeit auf der Seite anzeigen. Dazu erstellen wir eine Seite (das bedeutet: eine neue Klasse, die von WebPage abgeleitet wird). Listing 5.4 SimpleModelPage.java package de.wicketpraxis.web.thema.models; ... public class SimpleModelPage extends WebPage { public SimpleModelPage() { IModel<String> message=Model.of("Initialwert"); message.setObject("Jetzt ist "+new Date()); add(new Label("message",message)); } } Im Konstruktor der Seite erzeugen wir ein Modell mit einem String als Inhalt. Dabei gibt es mehr als eine Variante des Aufrufs. Die im Quelltext benutzte Variante ist eine Kurz- form für new Model<String>("Initialwert"). Die Kurzform erspart einiges an Schreibarbeit, sodass man gut beraten ist, nur diese Variante zu benutzen. In der nächsten Zeile werden die Daten im Modell überschrieben und ein Label erstellt, das zur Anzeige das Modell als Parameter übergeben bekommt. Damit die Seite angezeigt werden kann, müssen wir jetzt noch ein passendes Markup an- legen. Dazu legen wir im Ressourcenpfad (src/main/resources) ein Verzeichnis de/wicketpraxis/web/thema/models an. Das Verzeichnis entspricht der Struktur, wie Java Pakete im Verzeichnisbaum abbildet. Tipp Um ein Verzeichnis im Ressourcenpfad eines Projekts anzulegen, kann man unter Eclipse auch einfach ein Package mit demselben Namen wie das Package der Klasse erzeugen. Eclipse erzeugt dann wie für den Quelltext der Klasse auch das entsprechende Verzeichnis. Das geht zum einen schneller, und zum anderen kann man auch in den Ressourcen wie in den Quelltexten über die Hierarchieansicht navigieren. In dem Fall lautet der Name des Pakets de.wicketpraxis.web.thema.models. In die- sem Verzeichnis/Paket legen wir nun eine Datei an. Die Markup-Datei einer Klasse muss sich immer im selben Pfad oder Paket wie die dazugehörige Komponentenklasse befinden und denselben Namen tragen. Nur die Endung lautet „.html“. 57
  • 5 Modelle Listing 5.5 SimpleModelPage.html <html> <head> <title>Simple Model Page</title> </head> <body> <span wicket:id="message">Das wird ersetzt.</span> </body> </html> Wichtig ist, dass der Wert in wicket:id derselbe ist wie der erste Parameter der Label- Komponente, damit Wicket weiß, welche Komponente er darstellen soll. Der erste Para- meter einer Komponente ist meist die ID der Komponente, hier kurz Wicket-ID. Jetzt müssen wir noch in unserer WebApplication-Klasse den Rückgabewert von get- HomePage() anpassen, damit wir beim Start der Anwendung auf unserer Seite landen, und schon sehen wir folgendes Ergebnis (natürlich mit einem anderen Datum): Jetzt ist Wed Mar 25 20:57:11 CET 2009 Wenn wir jetzt im Browser die Seite neu laden, wird immer wieder der Konstruktor aufge- rufen und eine neue Seite mit einem neuen Datum erstellt. Dadurch ändert sich natürlich das Ergebnis. 5.2.1 Modelle verändern Wenn man die Daten eines Modells verändern möchte, kann man setObject() des Mo- dells aufrufen oder über die Komponentenmethode setDefaultModelObject() die Daten des Modells der Komponente aktualisieren. Jede Komponente hat ein Modell. Entweder wird das konkrete Modell als Parameter beim Erzeugen der Komponente übergeben oder ein leeres Modell benutzt. Das Modell einer Komponente kann mit setDefaultModel neu gesetzt werden. Wenn man das Modell direkt verändert (also die Methode setObject des Modells be- nutzt), dann kann Wicket unter Umständen nicht feststellen, ob sich Daten geändert haben. In diesem Fall wird Wicket keine neue Version einer Seite erstellen. Was bedeutet das konkret? Wenn der Nutzer auf einer Seite etwas ändert, dann erzeugt Wicket für den Fall, dass sich Daten geändert haben, eine neue Version der Seite. Wenn der Nutzer dann über den Browser auf eine ältere Seite zurück navigiert, kann Wicket auf diese Seite in diesem älteren Zustand zurückgreifen. Der Nutzer sieht nicht nur alte Daten, sondern die Interakti- on, die der Nutzer dann durchführt, basiert auch auf diesen Daten. Wenn Wicket nun keine neue Version einer Seite anlegt, dann navigiert der Nutzer auf die Seite im ursprünglichen Zustand. Auch wenn man dieses Verhalten deaktivieren kann, eröffnet es doch interessante Anwendungsmöglichkeiten. Wenn man nicht verhindern kann, dass man die Daten eines Models direkt ändern muss, kann man Wicket trotzdem mitteilen, dass sich der Zustand eines Modells geändert hat. Dazu ruft man für die Komponente, die sich dadurch verändert, die Methode modelChan- ging() vor der Veränderung und modelChanged() nach der Veränderung auf. Diese Funk-58
  • 5.2 Einfache Modelletionen werden auch in setDefaultModelObject() aufgerufen. Das folgende Beispiel sollden Unterschied veranschaulichen.Listing 5.6 ModelChangePage.java package de.wicketpraxis.web.thema.models; ... public class ModelChangePage extends WebPage { public ModelChangePage() { final IModel<Integer> message = Model.of(0); add(new Label("message", message)); add(new Link<Integer>("changeModel",message) { @Override public void onClick() { setModelObject(getModelObject()+1); } }); add(new Link<Integer>("changeModelDirect",message) { @Override public void onClick() { getModel().setObject(getModel().getObject()+1); } }); add(new Link("doNothing") { @Override public void onClick() { } }); } }Es wird wieder ein Modell angelegt, das durch ein Label zur Darstellung gebracht wird.Gleichzeitig wird ein Link angelegt, bei dem nach einem Klick der Wert des Modells, daszusätzlich an den Link gebunden ist, aktualisiert wird. In diesem Fall erhöhen wir denWert um 1. Der zweite Link führt dieselbe Aktion aus, ohne Wicket davon in Kenntnis zusetzen. Der dritte Link führt keinerlei Aktionen aus. Die Seite muss trotzdem neu geladenwerden, sodass der aktuelle Inhalt des Modells durch das Label dargestellt wird.Das Markup für diese Seite sieht ähnlich einfach aus wie für die Seite im Beispiel davorund muss im passenden Verzeichnis abgelegt werden.Listing 5.7 ModelChangePage.html <html> <head> <title>Model Change Page</title> </head> <body> 59
  • 5 Modelle Aktueller Wert: <span wicket:id="message">Das wird ersetzt.</span><br> <a wicket:id="changeModel">ändern</a><br> <a wicket:id="changeModelDirect">direkt ändern</a><br> <a wicket:id="doNothing">nicht ändern</a> </body> </html> Wie zu erwarten, ändern wir den Rückgabewert der getHomePage-Methode auf diese Seite und starten die Anwendung neu. Je nachdem, welchen Link wir anklicken, wird der Wert jeweils um eins erhöht. Wenn wir auf den Link „direkt ändern“ klicken, dann ändert sich ebenfalls der Wert. Wenn wir dann im Browser auf die Seite davor springen, sehen wir nicht eine Seite mit dem vorletzten Wert, sondern die Seite, die als Letztes versioniert werden konnte. Zusammenfassung Jetzt sind wir bereits in der Lage, Informationen anzuzeigen und durch Aktionen zu verän- dern. Diese einfache Möglichkeit ist nicht besonders elegant, veranschaulicht aber sehr schön das Prinzip.5.3 Modell-Hilfsklassen Wicket bietet für bestimmte Anwendungen bereits vordefinierte Hilfsklassen, um aus z.B. aus einer Liste ein Modell zu erzeugen. Das folgende Beispiel dient nur zur Darstellung der verschiedenen Klassen und Funktionsaufrufe. Unter den auskommentierten Funktions- aufrufen findet sich der gleichwertige kürzere Aufruf. Listing 5.8 ModelTypesPage.java package de.wicketpraxis.web.thema.models; ... public class ModelTypesPage extends WebPage { public ModelTypesPage() { List<String> liste = Arrays.asList("Das", "ist", "ne", "Liste"); Map<String, String> map = new HashMap<String, String>(); IModel<Collection<String>> collectionModel = new CollectionModel<String>(liste); IModel<List<String>> listModel = new ListModel<String>(liste); IModel<Set<String>> setModel = new SetModel<String>(map.keySet()); // IModel<Map<String, String>> mapModel = // new MapModel<String, String>(map); IModel<Map<String, String>> mapModel = Model.ofMap(map); // IModel<Collection<? extends String>> wildcardCollectionModel = // new WildcardCollectionModel<String>(liste); IModel<Collection<? extends String>> wildcardCollectionModel = Model.of((Collection<String>)liste); // IModel<List<? extends String>> wildcardListModel = // new WildcardListModel<String>(liste); IModel<List<? extends String>> wildcardListModel = Model.of(liste);60
  • 5.4 Modelle und Serialisierung // IModel<Set<? extends String>> wildcardSetModel = // new WildcardSetModel<String>(map.keySet()); IModel<Set<? extends String>> wildcardSetModel = Model.of(map.keySet()); } }5.4 Modelle und Serialisierung Wicket merkt sich den Zustand der letzten Seite in der Session, sodass alle Aktionen, die auf der Seite ausgeführt werden, auf diesen Zustand zurückgreifen können. Wicket seriali- siert außerdem die Seite und legt diese in einem PageStore ab. Der Nutzer kann dann im Browser zurück navigieren, und Wicket kennt trotzdem den Zustand der Seite und kann die durch den Nutzer ausgeübten Aktionen korrekt zuordnen. Damit Wicket die Seite serialisieren kann, muss Wicket alle Komponenten der Seite seria- lisieren. Da Komponenten auf Modelle und deren Daten zugreifen, müssen diese Modelle und Daten ebenfalls serialisiert werden. Bei bestimmten Daten ist das aber nicht möglich oder nicht gewünscht. Wenn Wicket z.B. die Daten eines persistierten Objekts darstellen soll, kann sich der Zustand des Objekts ändern, weil die Daten in der Datenbank geändert wurden. Wenn aber die Daten aus der serialisierten Version der Seite wieder hergestellt werden, kann es passieren, dass die Dar- stellung nicht mehr korrekt ist. Für alle Daten, die immer wieder aktualisiert werden müssen, die also nur für die Anzeige benötigt werden, kann auf die detach()-Methode zurückgegriffen werden. Damit man sich aber nicht selbst die Mühe machen muss, sich einen Mechanismus auszudenken, der bei Bedarf die Daten ermittelt und beim Aufruf der detach()-Methode wieder vergisst, gibt es bereits Modellklassen, die für diesen Fall geschaffen wurden. 5.4.1 DetachableModel – Dynamische Modelldaten Als Beispiel wählen wir eine Abwandlung der vorherigen Beispiele. Ein Label soll immer die aktuelle Uhrzeit anzeigen, ohne dass der Nutzer durch eine Aktion die Anwendung dazu veranlassen muss oder der Nutzer die ganze Seite über den Browser neu lädt. Listing 5.9 DetachedModelPage.java package de.wicketpraxis.web.thema.models; ... public class DetachedModelPage extends WebPage { public DetachedModelPage() { IModel<String> message = new LoadableDetachableModel<String>() { @Override protected String load() { 61
  • 5 Modelle return "Jetzt ist " + new Date(); } }; add(new Label("message", message)); add(new Link("doNothing") { @Override public void onClick() { // hier wir keine Aktion ausgeführt } }); } } Wie man sieht, wird für das Modell von der Klasse LoadableDetachableModel abgelei- tet. Dabei muss man die Methode load() implementieren. Dabei prüft das Modell beim Aufruf der Funktion getObject() der IModel-Schnittstelle, ob die Daten bereits über load() erzeugt wurden. Die Daten werden innerhalb der Klasse in einem Feld gespeichert, das mit transient markiert ist und somit nicht serialisiert wird. Wenn dann die Methode detach() aufgerufen wird, werden dieser Wert und die Information, dass die Daten bereits geladen wurden, zurückgesetzt. Die Modelldaten werden nicht gespeichert. Damit dieser Prozess funktioniert, muss das Modell an eine Komponente gebunden sein. Die Komponente ist dafür verantwortlich, die detach()-Methode für alle relevanten Modelle aufzurufen. Wenn das nächste Mal die Seite durch Wicket dargestellt wird (z.B. durch einen Klick auf den Link), wird die Methode load() wieder aufgerufen und erzeugt einen String mit dem aktuellen Datum. Listing 5.10 DetachedModelPage.html <html> <head> <title>DetachedModel Page</title> </head> <body> <span wicket:id="message">Das wird ersetzt.</span><br> <a wicket:id="doNothing">nicht ändern</a> </body> </html> 5.4.2 Kaskadierung von Modellen Stellen wir uns folgendes Szenario vor: Eine Funktion liefert eine Liste von Ergebnissen. Auf der Seite sollen aber nur die ersten fünf Ergebnisse angezeigt werden. Außerdem soll aber auch die Anzahl aller Ergebnisse dargestellt werden. Damit wir vermeiden, dass für die beiden Darstellungen zweimal dieselbe Funktion aufge- rufen wird, ermitteln wir zuerst nur die Ergebnisliste. In einem weiteren Modell greifen wir dann auf das Modell mit der Ergebnisliste zu und können dabei die entsprechenden Daten extrahieren. Dabei wird die Funktion nur einmal aufgerufen, da beim Zugriff durch ein zweites Modell die Daten bereits geladen wurden.62
  • 5.4 Modelle und SerialisierungJetzt ist allerdings das erste Modell nicht mehr mit einer Komponente verbunden und wirdsomit auch nicht per detach() zurückgesetzt. Die Daten können also unter Umständennoch geladen sein, was dazu führen kann, dass beim nächsten Seitenaufruf die Anzeigeveraltete Ergebnisse präsentiert.Im folgenden Beispiel kann das Problem demonstriert werden:Listing 5.11 DetachedDetachedModelPage.java package de.wicketpraxis.web.thema.models; ... public class DetachedDetachedModelPage extends WebPage { public DetachedDetachedModelPage() { final IModel<Date> dateModel= new LoadableDetachableModel<Date>() { @Override protected Date load() { return new Date(); } }; IModel<String> message = new LoadableDetachableModel<String>() { @Override protected String load() { return "Jetzt ist " + dateModel.getObject(); } }; add(new Label("message", message)); add(new Link("doNothing") { @Override public void onClick(){} }); add(new Link("detach") { @Override public void onClick() { dateModel.detach(); } }); }Das dateModel-Modell liefert immer das aktuelle Datum zurück. Das message-Modellgreift auf dateModel zurück und bekommt das Datum. Daraus wird ein String erstellt undan das Label ausgeliefert. Wenn die Seite fertig dargestellt wurde, wird für message diedetach()-Methode aufgerufen. Das dateModel bleibt davon unberührt. Damit gibt eskeine Veränderung auf der Seite, wenn der Link mit der ID „doNothing“ das zweite Malangeklickt wird. Erst wenn man den „detach“-Link anklickt, wird dateModel zurückge-setzt und das Datum für die Darstellung neu erzeugt. 63
  • 5 Modelle Listing 5.12 DetachedDetachedModelPage.html <html> <head> <title>DetachedDetachedModel Page</title> </head> <body> <span wicket:id="message">Das wird ersetzt.</span><br> <a wicket:id="doNothing">nicht ändern</a><br> <a wicket:id="detach">detach() aufrufen</a> </body> </html> Dass der Nutzer dafür sorgen soll, dass die aktuellen Daten angezeigt werden, ist keine gute Idee. Um dieses Problem zu umgehen, sollte deshalb innerhalb eines Modells, das an eine Komponente gebunden ist, beim Aufruf von detach() die detach()-Methode des ersten Modells aufgerufen werden. Listing 5.13 CascadingDetachedModelPage.java package de.wicketpraxis.web.thema.models; ... public class CascadingDetachedModelPage extends WebPage { public CascadingDetachedModelPage() { final IModel<Date> dateModel= new LoadableDetachableModel<Date>() { @Override protected Date load() { return new Date(); } }; IModel<String> message = new LoadableDetachableModel<String>() { @Override protected String load() { return "Jetzt ist " + dateModel.getObject(); } @Override protected void onDetach() { dateModel.detach(); } }; add(new Label("message", message)); add(new Link("doNothing") { @Override public void onClick() { } }); } } Die Klasse LoadableDetachableModel ruft die Methode onDetach() auf, wenn für das Modell die Methode detach() aufgerufen wurde. Dabei wird onDetach() nur aufgerufen, wenn das Modell noch nicht zurückgesetzt wurde. Sollte aus irgendeinem Grund (z.B. weil64
  • 5.4 Modelle und Serialisierungdas Modell von zwei Komponenten referenziert wird) die detach()-Methode ein zweitesMal aufgerufen werden, dann wird onDetach() nicht mehr aufgerufen. Die Anzeige än-dert sich jetzt bei jeder Interaktion.Listing 5.14 CascadingDetachedModelPage.html <html> <head> <title>CascadingDetachedModel Page</title> </head> <body> <span wicket:id="message">Das wird ersetzt.</span><br> <a wicket:id="doNothing">nicht ändern</a><br> </body> </html>5.4.3 Automatische Kaskadierung von ModellenDie detach()-Methode immer selbst weiterzureichen, ist aufwendig und fehleranfällig.Als Lösung bietet sich eine sehr einfache Klasse an, die diese Funktion bereitstellt. DieseKlasse ist nicht Teil von Wicket, aber so nützlich, dass man sie in allen weiteren Projektengut gebrauchen kann. Es empfiehlt sich also, diese Klasse in eine eigene Bibliothek auszu-lagern.Listing 5.15 CascadingLoadableDetachableModel.java package de.wicketpraxis.web.model; ... public abstract class CascadingLoadableDetachableModel<M,P> extends LoadableDetachableModel<M> { private IModel<? extends P> _parent; public CascadingLoadableDetachableModel(IModel<? extends P> parent) { super(); _parent=parent; } @Override protected void onDetach() { _parent.detach(); } @Override final protected M load() { P result=_parent.getObject(); M ret=load(result); return ret; } protected abstract M load(P p); }Dabei passiert Folgendes: Wenn für das Modell die getObject()-Methode aufgerufenwird, ruft die Basisklasse bei Bedarf die load()-Methode auf. Diese holt sich aus demreferenzierten Modell die Daten. Diese Daten werden an die Methode load(P) übergeben. 65
  • 5 Modelle Diese Methode muss anstelle der load()-Methode implementiert werden. Das Ergebnis der Methode dient dann als Rückgabewert für die load()-Methode. Außerdem wird on- Detach() entsprechend überschrieben. Damit holt die Klasse automatisch die notwen- digen Daten aus dem angegebenen Modell und setzt es nach Aufforderung entsprechend zurück. Listing 5.16 CascadingDetachedModelModelPage.java package de.wicketpraxis.web.thema.models; ... public class CascadingDetachedModelModelPage extends WebPage { public CascadingDetachedModelModelPage() { IModel<Date> dateModel= new LoadableDetachableModel<Date>() { @Override protected Date load() { return new Date(); } }; IModel<String> message = new CascadingLoadableDetachableModel<String,Date>(dateModel) { @Override protected String load(Date p) { return "Jetzt ist " + p; } }; add(new Label("message", message)); add(new Link("doNothing") { @Override public void onClick() { } }); } } 5.4.4 Datenbankzugriffsmodelle Es gibt verschiedene Gründe, warum Datenbankobjekte nicht serialisiert werden dürfen. Neben technologischen Aspekten (z.B. der Verwendung von Proxy-Klassen für persistierte Objekte) ist ein Aspekt besonders wichtig: Der Nutzer sollte die aktuellen Daten angezeigt bekommen. Damit wir diese Aufgabe nicht immer wieder von Neuem lösen, implementieren wir eine Modellklasse, die diese Aufgabe für alle Datenzugriffsklassen übernehmen kann. Da wir bereits eine sehr mächtige Datenzugriffsklasse haben, beschränkt sich die Klasse auf die Funktionen, die für das Funktionieren des Modells notwendig sind.66
  • 5.4 Modelle und SerialisierungListing 5.17 DaoModel.java package de.wicketpraxis.web.model; ... public class DaoModel<K extends Serializable,T extends DoInterface<K>> implements IModel<T> { DaoInterface<K, T> _dao; private K _id; private transient T _object; private transient boolean _attached=false; public DaoModel(DaoInterface<K, T> dao) { _dao=dao; } public DaoModel(DaoInterface<K, T> dao,T object) { this(dao); _object=object; _id=_object.getId(); _attached=true; } public DaoModel(DaoInterface<K, T> dao,K id) { this(dao); _id=id; } protected T load(K id) { if (id==null) return _dao.getNew(); return _dao.get(id); } public T getObject() { if (!_attached) { _object=load(_id); _attached=true; } return _object; } public void setObject(T object) { _attached=false; _id=null; _object=object; if (_object!=null) { _id=_object.getId(); _attached=true; } } public void detach() { _object=null; _attached=false; } } 67
  • 5 Modelle Das Prinzip dieser Modellklasse ist einfach, bedarf aber trotzdem einiger Erklärungen. Die Klasse kennt zwei Zustände in Bezug auf das Datenbankobjekt: 1. Das Objekt wurde bereits in der Datenbank abgelegt. Daher hat es eine ID und kann anhand dieser immer wieder aus der Datenbank geholt werden. 2. Das Objekt hat noch keine ID, weil es noch nicht in der Datenbank abgelegt wurde. Dann wird immer ein neues Objekt der entsprechenden Klasse erzeugt. Das hat folgende Bewandtnis: Wenn in einem Formular (was wir später in Beispielen se- hen werden) das Modell mit einem Objekt benutzt wird, werden die vorhandenen Objekt- informationen im Formular angezeigt. Wird das Formular dann abgeschickt, wird das Ob- jekt aus der Datenbank gelesen oder neu erzeugt. Dann werden die Werte aus dem Formu- lar gesetzt, und man kann mit dem nun so veränderten Objekt arbeiten (es z.B. speichern oder aktualisieren). Es ist also nicht nötig, sich das Objekt zu merken, da die Eingabedaten durch das Formular bereitgestellt werden. Doch dazu später mehr. Wie man erkennen kann, gibt es drei Konstruktoren. Während der erste für den Fall ge- wählt wird, dass noch kein Objekt in der Datenbank existiert, unterscheiden sich die ande- ren beiden in einem nicht unwesentlichen Detail: Wird der zweite Konstruktor mit einem bereits aus der Datenbank geladenen Objekt aufgerufen, wird dieses Objekt als Modell- objekt benutzt, bis die detach()-Methode aufgerufen wird. Danach verhalten sich beide Varianten wieder gleich. Listing 5.18 SimpleDaoModelPage.java package de.wicketpraxis.web.thema.models; ... public class SimpleDaoModelPage extends WebPage { @SpringBean UserDao _userDao; public SimpleDaoModelPage() { User user = _userDao.getByEMail("test@wicket-praxis.de"); DaoModel<Integer, User> model= new DaoModel<Integer,User>(_userDao,user!=null ? user.getId() : null); IModel<String> nameModel = new CascadingLoadableDetachableModel<String, User>(model) { @Override protected String load(User p) { if (p!=null) return p.getName(); return null; } }; add(new Label("name", nameModel)); add(new Link("doNothing") { @Override public void onClick() {68
  • 5.5 Komplexe Modellklassen } }); } } Damit das Beispiel Sinn macht, sollte man in der Datenbank einen Eintrag mit entspre- chender E-Mail anlegen. Dann wird der Nutzer aus der Datenbank ausgelesen. Die ID un- serer Klasse wird als Parameter übergeben. Darauf aufbauend holt sich das nächste Modell den Datensatz und gibt, wenn vorhanden, den Namen zurück. Diese Daten werden dann durch das Label zur Anzeige gebracht. Listing 5.19 SimpleDaoModelPage.html <html> <head> <title>SimpleDaoModel Page</title> </head> <body> <span wicket:id="name">Das wird ersetzt.</span><br> <a wicket:id="doNothing">nicht ändern</a> </body> </html> Ändert man in der Datenbank jetzt den Namen, wird diese Änderung beim erneuten Laden der Seite angezeigt (z.B. durch einen Klick auf den Link).5.5 Komplexe Modellklassen Wenn es nur darum geht, Daten aus Modellen direkt anzuzeigen, reichen die bisher behan- delten Modellklassen aus. Allerdings kann es schon sehr aufwendig werden, wenn man ein Datenbankobjekt mit vielen Attributen hat und dieses anzeigen möchte. 5.5.1 Zugriff auf Bean-Properties Um den Zugriff auf die verschiedenen Attribute einer Klasse zu ermöglichen, reicht die Funktionalität der Modellschnittstelle nicht aus. Und für jedes Attribut eine eigene Klasse zu schreiben, ist aufwendig und nicht besonders effektiv. Damit man das Grundprinzip besser verstehen und einordnen kann, zeige ich an dieser Stelle den ersten offensichtlichen Weg, um am Ende die elegante und schlanke Lösung zu zeigen. Das sollte für das Ver- ständnis nützlich sein. Doch zuerst benötigen wir eine Klasse mit einigen Attributen. Listing 5.20 SubBean.java (gekürzt) package de.wicketpraxis.web.thema.models; ... public class SubBean implements Serializable { Date _datum; setDatum(),getDatum()... 69
  • 5 Modelle @Override public String toString() { return "Datum: " + _datum; } } Die Klasse hat nur ein Attribut und ist serialisierbar. Wir benutzen diese Klasse als Attri- but einer weiteren Klasse. Listing 5.21 DummyBean.java (gekürzt) package de.wicketpraxis.web.thema.models; import java.io.Serializable; public class DummyBean implements Serializable { String _name; int _alter; SubBean _sub; getName(),setName(),getAlter(),... @Override public String toString() { return "Name: "+_name+", Alter: "+_alter+" ("+_sub+")"; } } Jetzt erstellen wir eine Modellklasse, die den Zugriff auf ein Attribut der Klasse gewähr- leistet. Listing 5.22 DummyBeanPropertyModel.java package de.wicketpraxis.web.thema.models; ... public class DummyBeanPropertyModel<T extends Serializable> implements IModel<T> { public enum PropertySelector { NAME, ALTER, }; DummyBean _bean; PropertySelector _selector; public DummyBeanPropertyModel(DummyBean bean,PropertySelector selector) { _bean=bean; _selector=selector; } public T getObject() { switch (_selector) { case NAME:return (T) _bean.getName(); case ALTER: return (T) new Integer(_bean.getAlter()); } return null; } public void setObject(T object) { switch (_selector) {70
  • 5.5 Komplexe Modellklassen case NAME: _bean.setName((String) object); break; case ALTER: Integer alter=(Integer) object; _bean.setAlter(alter.intValue()); break; } } public void detach() {} }Die Auswahl des Attributs erfolgt über den Parameter selector, der dann in der setOb-ject()- und der getObject()-Methode zur Unterscheidung herangezogen wird. Für je-des neue Attribut muss man dann mehrere Zeilen Code schreiben. Außerdem kann mandiese Modellklasse nur für Objekte der DummyBean-Klasse benutzen.Listing 5.23 HandmadePropertyModelPage.java package de.wicketpraxis.web.thema.models; ... public class HandmadePropertyModelPage extends WebPage { public HandmadePropertyModelPage() { DummyBean bean=new DummyBean(); DummyBeanPropertyModel<String> nameModel= new DummyBeanPropertyModel<String>(bean,PropertySelector.NAME); DummyBeanPropertyModel<Integer> alterModel= new DummyBeanPropertyModel<Integer>(bean,PropertySelector.ALTER); nameModel.setObject("Klaus"); alterModel.setObject(28); add(new Label("name",nameModel)); add(new Label("alter",alterModel)); } }Nach dem Erzeugen der Modellklassen werden Name und Alter über die Modellfunktio-nen gesetzt und später dann zur Anzeige gebracht.Listing 5.24 HandmadePropertyModelPage.html <html> <head> <title>HandmadePropertyModel Page</title> </head> <body> Name: <span wicket:id="name">Name</span><br> Alter: <span wicket:id="alter">Alter</span><br> </body> </html>Während es durchaus Situationen geben kann, wo dieses Vorgehen sinnvoll ist, reicht fürdie meisten Anwendungsfälle ein sehr viel einfacheres Modell vollkommen aus. 71
  • 5 Modelle 5.5.2 Die Klasse PropertyModel Die Klasse PropertyModel ermöglicht den Zugriff auf ein Attribut einer Klasse über eine Property-Expression. Im einfachsten Fall gibt man den Attributnamen des Objektes an. Listing 5.25 PropertyModelPage.java package de.wicketpraxis.web.thema.models; ... public class PropertyModelPage extends WebPage { public PropertyModelPage() { DummyBean bean=new DummyBean(); PropertyModel<String> nameModel= new PropertyModel<String>(bean,"name"); PropertyModel<Integer> alterModel= new PropertyModel<Integer>(bean,"alter"); PropertyModel<Date> datumModel= new PropertyModel<Date>(bean,"sub.datum"); nameModel.setObject("Klaus"); alterModel.setObject(28); datumModel.setObject(new Date()); add(new Label("name",nameModel)); add(new Label("alter",alterModel)); add(new Label("datum",datumModel)); add(new Label("toString",bean.toString())); } } Als Erstes erzeugen wir eine Instanz von DummyBean. Als Nächstes definieren wir drei Modelle, wobei jeweils die Attribute Name, Alter und Sub.Datum angesprochen werden. Anschließend werden die Modellfunktionen aufgerufen und entsprechende Werte gesetzt. Dabei wird beim Aufruf von setObject() beim Modell datumModel eine neue Instanz der Klasse SubBean erzeugt, das Attribut Sub der Klasse DummyBean mit diesem neuen Objekt gesetzt und zum Schluss das Attribut Datum der Klasse SubBean mit dem überge- benen Wert gesetzt. Dieses Verhalten funktioniert nur, wenn der vom PropertyModel ermittelte Typ einen Konstruktor ohne Parameter besitzt. Als Kontrolle, dass die über die IModel-Schnittstelle gesetzten Daten auch in der Dummy- Bean-Instanz gesetzt werden, wird das Ergebnis von toString() angezeigt. Listing 5.26 PropertyModelPage.html <html> <head> <title>Property Model Page</title> </head> <body> Name: <span wicket:id="name">Name</span><br> Alter: <span wicket:id="alter">Alter</span><br> Datum: <span wicket:id="datum">datum</span><br> toString: <span wicket:id="toString"></span><br> </body> </html>72
  • 5.5 Komplexe ModellklassenListing 5.27 Ergebnis Name: Klaus Alter: 28 Datum: 28.02.09 toString: Name: Klaus, Alter: 28 (Datum: Sat Feb 28 15:20:40 CET 2009)Die PropertyModel-Klasse unterstützt verschiedene Möglichkeiten, wie auf Attribute undFelder einer Klasse zugegriffen werden kann.5.5.2.1 Zugriff auf AttributeDie einfachste Möglichkeit, auf Attribute einer Klasse zuzugreifen, ist die Angabe des Att-ributnamens. Dabei kann man durch die Aneinanderreihung von Attributnamen auf denObjektbaum zugreifen, wie wir das bei datumModel gesehen haben. Die Attributnamenwerden durch einen Punkt getrennt: new PropertyModel<String>(bean,"person.anschrift.strasse“);Wenn in diesem Beispiel das Attribut anschrift aber leer ist, gibt es trotzdem keine Feh-lermeldung. Es wird dann kein Wert zurückgeliefert.5.5.2.2 Zugriff auf Listen und ArraysWenn ein Attribut vom Typ Liste oder Array ist, kann über einen Index auf ein Elementzugegriffen werden: new PropertyModel<String>(bean,"person.anschrift.2.strasse"); new PropertyModel<String>(bean,"person.anschrift[2].strasse");Im Gegensatz zum Array, bei dem es im schreibenden Zugriff (setObject()) dazu kom-men kann, dass der Index außerhalb der Grenzen liegt, wird bei vorhandener Liste ein Ein-trag an angegebener Stelle angelegt. Die erste Zeile gibt die Standardschreibweise an,beide Schreibweisen sind aber in diesem Fall erlaubt. Ich persönlich halte die zweiteSchreibweise für nachvollziehbarer.5.5.2.3 Zugriff auf indizierte PropertiesDer Zugriff auf indizierte Properties ähnelt dem Zugriff auf Listen. Die Klasse TestBeandefiniert ein indiziertes Attribut „Names“.Listing 5.28 TestBean.java public static class TestBean { Map<Integer,String> _names=new HashMap<Integer, String>(); public void setNames(int idx, String name) { _names.put(idx, name); } public String getNames(int idx) { return _names.get(idx); } } 73
  • 5 Modelle Dann kann mit folgender Zeile auf das Attribut zugegriffen werden: new PropertyModel<String>(bean,"names.4"); Im Unterschied zu Listen und Feldern wird nur die Angabe ohne eckige Klammern unter- stützt. 5.5.2.4 Felder Wenn kein passendes Attribut mit dem Namen gefunden wurde, wird versucht, ein Feld der Klasse mit entsprechendem Namen zu finden. Ich würde aber von der Verwendung dieser Methode abraten. 5.5.2.5 Maps Wenn das Attribut vom Typ Map ist, dann kann man auf die Elemente der Map über die Angabe eines Keys zugreifen: new PropertyModel<String>(bean,"colormap[red].rgb"); new PropertyModel<String>(bean,"colormap.red.rgb"); Auch hier sind wieder beide Schreibweisen möglich. Möglichkeiten und Risiken So interessant die Möglichkeiten der PropertyModel-Klasse auch sind, birgt die Verwen- dung doch gewisse Risiken. Das größte Risiko besteht darin, dass bestimmte Fehler erst zur Laufzeit auftreten, weil die Informationen ja per Reflektion ermittelt werden. Diesem Problem kann man mit einer hohen Testabdeckung begegnen. Das hilft auch in den Fällen, wo sich Attributnamen geändert haben. 5.5.3 CompoundPropertyModel Das CompoundPropertyModel ist eine Modellklasse, die den Aufwand im Umgang mit Objektattributen stark vereinfacht. Dazu implementiert die Klasse das IcomponentIn- heritedModel-Interface. Wenn eine Komponente ein Modell benutzt, das dieses Interface implementiert, dann rufen alle Kindkomponenten ohne eigenes Modell die Methode wrapOnInheritance mit sich selbst als Parameter auf. CompoundPropertyModel erzeugt bei diesem Aufruf eine eigene PropertyModel-Instanz, welche die Komponenten-ID als Property Expression benutzt. Listing 5.29 CompoundPropertyModelPage.java package de.wicketpraxis.web.thema.models; ... import org.apache.wicket.model.*; public class CompoundPropertyModelPage extends WebPage { public CompoundPropertyModelPage() {74
  • 5.5 Komplexe Modellklassen DummyBean bean=new DummyBean(); bean.setSub(new SubBean()); bean.setName("Achim"); bean.getSub().setDatum(new Date()); CompoundPropertyModel<DummyBean> model= new CompoundPropertyModel<DummyBean>(bean); setDefaultModel(model); add(new Label("name")); add(new Label("sub.datum")); add(new Label("toString",bean.toString())); } }Da hier die Wicket-ID der Komponente gleichzeitig als Property Expression genutzt wird,muss man bei einer Anpassung des Attributnamens sowohl im Code als auch im MarkupAnpassungen vornehmen.Listing 5.30 CompoundPropertyModelPage.html <html> <head> <title>Compound Property Model Page</title> </head> <body> Name: <span wicket:id="name"></span><br> Datum: <span wicket:id="sub.datum"></span><br> toString: <span wicket:id="toString">nr</span><br> </body> </html>Da die Klasse PropertyModel ebenso wie die Klasse CompoundPropertyModel auchModelle als Parameter akzeptiert, ergeben sich daraus sehr interessante Anwendungen.Listing 5.31 DaoModelPage.java package de.wicketpraxis.web.thema.models; ... public class DaoModelPage extends WebPage { @SpringBean UserDao _userDao; public DaoModelPage() { User user = _userDao.getByEMail("test@wicket-praxis.de"); DaoModel<Integer, User> model= new DaoModel<Integer, User>(_userDao,user); setDefaultModel(new CompoundPropertyModel<User>(model)); add(new Label("eMail")); add(new Label("name")); } }In diesem Beispiel wird zuerst ein Objekt aus der Datenbank geladen, das für das Datenob-jektmodell benutzt wird. Da dann das Modell der Seite auf das CompoundPropertyModelgesetzt wird, kann der Zugriff auf die Attribute des Datenbankeintrags über die ID derLabel-Komponenten erfolgen. 75
  • 5 Modelle5.6 Ausgelagerte Informationen Fehlermeldungen und Textrückmeldungen im Programmcode anpassen zu müssen, ist mühsam und fehleranfällig. Wenn dann auch noch eine mehrsprachige Anwendung gefor- dert wird, kann es schnell kompliziert werden. Wicket bietet an verschiedenen Stellen die Möglichkeit, Mehrsprachigkeit zu unterstützen. 5.6.1 Einfacher Zugriff auf Ressourcen Jede Komponente kann über die Methode getString() auf die Ressourcen zugreifen. Im folgenden Beispiel erstellen wir eine neue Seite, eine Markup-Datei und im selben Ver- zeichnis wie die Markup-Datei eine Property-Datei. Listing 5.32 SimpleResourcePage.java package de.wicketpraxis.web.thema.models; ... public class SimpleResourcePage extends WebPage { public SimpleResourcePage() { add(new Label("text",getString("text", Model.of(12), "Nicht gefunden"))); } } Listing 5.33 SimpleResourcePage.html <html> <head> <title>Simple Resource Page</title> </head> <body> <span wicket:id="text">Text</span><br> </body> </html> Die Property-Datei hat folgenden Inhalt: Listing 5.34 SimpleResourcePage.properties text=Das ist ein Text und das Model ist eine Zahl: ${} (als Fließkommazahl ${doubleValue}). Allerdings liefert die Methode getString() einen String und kein Modell zurück, was bedeutet, dass die Anzeige des Labels sich auch dann nicht verändert, wenn das beim getString-Aufruf übergebene Modell verändert wird. 5.6.2 ResourceModel Die ResourceModel-Klasse bietet eine sehr einfache Möglichkeit, Texte in Property-Da- teien auslagern zu können. Der Zugriff ist sehr einfach und mehrsprachenfähig. Dadurch entstehen sehr übersichtliche Anwendungen, die ohne Programmieraufwand weitere Spra-76
  • 5.6 Ausgelagerte Informationenchen unterstützen können. Die Funktionsweise können wir an einem einfachen Beispieltesten.Listing 5.35 ResourceModelPage.java package de.wicketpraxis.web.thema.models; ... public class ResourceModelPage extends WebPage { public ResourceModelPage() { add(new Label("text1",new ResourceModel("text1"))); add(new Label("text2",new ResourceModel("text2","Nicht gefunden"))); add(new Label("text3",new ResourceModel("text3"))); } }Listing 5.36 ResourceModelPage.html <html> <head> <title>Resource Model Page</title> </head> <body> <span wicket:id="text1">Text1</span><br> <span wicket:id="text2">Text1</span><br> <span wicket:id="text3">Text1</span><br> </body> </html>Dazu legen wir noch mindestens zwei Property-Dateien in src/main/resource und dempassenden Verzeichnis an. Der Dateiname entspricht dem Klassennamen mit einem ange-hängten .properties.Listing 5.37 ResourceModelPage.properties text1=This is text for text1. text3=This is some text for text3.Die erste, die sprachunabhängige Property-Datei sollte alle notwendigen Definitionen ent-halten, da auf diese Datei immer dann zurückgegriffen wird, wenn in den anderen Dateienkein entsprechender Wert gefunden werden konnte. Die zweite Datei im selben Verzeich-nis trägt den Namen ResourceModelPage_de_DE.properties.Listing 5.38 ResourceModelPage_de_DE.properties text3=Das ist die deutsche Version von text3.Wenn man die Seite mit einem Browser aufruft, der als bevorzugte Sprache Deutsch ein-gestellt hat, wird man folgendes Ergebnis sehen: This is text for text1. Nicht gefunden Das ist die deutsche Version von text3.Ändert man nun die Spracheinstellung, bekommt man folgende Anzeige: This is text for text1. Nicht gefunden This is some text for text3. 77
  • 5 Modelle In diesem Beispiel haben wir in keiner Property-Datei etwas für den Eintrag text2 defi- niert. Wenn ein ResourceModel den gewünschten Eintrag nicht findet (hier text2), wird entweder auf den zweiten Parameter zurückgegriffen oder ein entsprechender Fehler er- zeugt. Man muss also sicherstellen, dass dieser Eintrag mindestens einmal definiert wurde. Auf diese Weise haben wir bereits eine kleine mehrsprachige Anwendung geschrieben. 5.6.3 StringResourceModel Oft reicht es nicht aus, nur unterschiedliche Texte darzustellen, wenn die Sprache gewech- selt wird. Informationen, die erst zur Laufzeit entstehen, müssen entsprechend eingebun- den werden. Eine sehr umfangreiche Funktionspalette bietet die Klasse StringResource- Model. Dabei spielen folgende Parameter eine wichtige Rolle: resourceKey: Dieser Parameter definiert wie im ResourceModel den Namen des Ein- trags in der Property-Datei. Allerdings besteht hier die Möglichkeit, Platzhalter zu de- finieren, die durch Attribute des Modells (model) ersetzt werden können. component: Die Komponente, auf die sich das Modell bezieht. Aus dieser Information ergibt sich der Name für die Resource-Datei. model: Das Modell ist optional und kann mit Platzhalter sowohl im resourceKey als auch in den Einträgen in der Property-Datei angesprochen werden. parameters: Optionale Parameter, die über eine Positionierung in den Einträgen ange- sprochen werden kann. defaultValue: Wenn der Eintrag nicht gefunden werden konnte, wird dieser Wert benutzt. Eine Vorstellung von den Möglichkeiten bekommt man am besten anhand eines Beispiels. Listing 5.39 StringResourceModelPage.java package de.wicketpraxis.web.thema.models; ... public class StringResourceModelPage extends WebPage { public StringResourceModelPage() { add(new Label("text",new ResourceModel("test.text"))); DummyBean bean=new DummyBean(); bean.setName("textA"); add(new Label("textA", new StringResourceModel("test.${name}",Model.of(bean)))); add(new Label("textB", new StringResourceModel("test.textB",Model.of(bean)))); Object[] parameter= { new Date(), Model.of(1.23456789) }; add(new Label("textC", new StringResourceModel("test.textC",Model.of(bean),parameter))); } }78
  • 5.6 Ausgelagerte InformationenAls Erstes benutzen wir ein normales ResourceModel, das auf dieselbe Properties-Dateiwie die StringResourceModel-Klassen zugreift.Für „textA“ erzeugen wir eine Instanz der Klasse DummyBean, wobei wir name mit „textA“initialisieren. Dieses Objekt benutzen wir als Modell für das SpringResourceModel.Dadurch kann der resourceKey auf test.textA aufgelöst werden.Listing 5.40 StringResourceModelPage.html <html> <head> <title>Simple Resource Model Page</title> </head> <body> <span wicket:id="text">Das wird ersetzt.</span><br> <span wicket:id="textA">Das wird auch ersetzt.</span><br> <span wicket:id="textB">Das wird auch...</span><br> <span wicket:id="textC">Das wird auch...</span><br> </body> </html>Listing 5.41 StringResourceModelPage.properties test.text=Das ist der erste Text test.textA=Das ist der zweite Text (Name=${name}) test.textB=Das ist der dritte Text (Name=${name}) test.textC=Das ist der vierte Text (Name=${name}) und Parameter 0={0,date} oder 0={0,time} und 1={1,number,###.##} order 1={1,number,###0.00##}Wenn wir diese Seite im Browser öffnen, hat Wicket die Platzhalter durch die Werte er-setzt, und man bekommt folgende Anzeige: Das ist der erste Text Das ist der zweite Text (Name=textA) Das ist der dritte Text (Name=textA) Das ist der vierte Text (Name=textA) und Parameter 0=28.03.2009 oder 0=22:24:51 und 1=1,23 order 1=1,2346In der letzten Zeile sieht man die Möglichkeiten dieses Modells. In den Texten kann aufAttribute des Modellobjektes (${name}) zurückgegriffen werden. Auf das Objekt selbstkann man mit ${} zugreifen, wobei dann die toString()-Methode aufgerufen wird. Aufdie Einträge des parameter-Arrays wird über einen Index zugegriffen. Dabei kommt dieKlasse MessageFormat (java.text) zum Einsatz.ZusammenfassungModelle sind der Grundstein jeder Wicket-Anwendung. Das breite Spektrum an vorhande-nen Klassen und die einfache Erweiterbarkeit ermöglichen kompakte, funktionale Anwen-dungsmodule. 79
  • 6 6 Komponenten In den bisherigen Beispielen kamen schon einige Komponenten zum Einsatz. Auf den fol- genden Seiten werden wir die verschiedenen Komponenten, die bereits in Wicket enthalten sind, eingehender betrachten.6.1 Basisklasse Component In Wicket wird jede Komponente von einer Basisklasse Component abgeleitet. Diese Ba- sisklasse verfügt bereits über sehr vielfältige Funktionen, die eine ausführliche Betrach- tung notwendig machen. 6.1.1 Komponentenbaum Eine Komponente hat immer eine ID. Diese ID muss beim Erzeugen der Komponente de- finiert werden. Die ID kann nicht mehr verändert werden. Sie muss auf derselben Hierar- chiestufe eindeutig sein und darf daher nicht mehr als einmal vorkommen. Wenn eine Komponente zu einer anderen hinzugefügt wurde, kann sie die übergeordnete Komponente durch den Aufruf von getParent() ermitteln. Gleichzeitig wird eine Liste aller Kindkomponenten verwaltet, sodass man auf diese Weise alle Komponenten einer Seite ermitteln kann. Listing 6.1 RandomNumberPage.java package de.wicketpraxis.web.thema.komponenten.tree; ... public class RandomNumberPage extends WebPage { private static final List<Integer> NUMBERS = Arrays.asList(1,2,3,4,5,6,7,8,9); public RandomNumberPage() { List<Integer> list = getNumbers(); 81
  • 6 Komponenten for (int i=0;i<9;i++) { add(new Label("l"+i,Model.of(list.get(i)))); } add(new Link("link") { @Override public void onClick() { VisitLabels visitor = new VisitLabels(); RandomNumberPage.this.visitChildren(visitor); } }); add(new BookmarkablePageLink<RandomNumberPage>( "reset",RandomNumberPage.class)); } // 1..9 in zufälliger Reihenfolge private List<Integer> getNumbers() { ... } static class VisitLabels implements IVisitor<Component> { public Object component(Component component) { if (component instanceof Label) { Object data=component.getDefaultModelObject(); if (data instanceof Integer) { int val=(Integer) data; if ((val % 3)==0) component.setVisible(!component.isVisible()); } } return IVisitor.CONTINUE_TRAVERSAL; }; } } Wenn die Seite das erste Mal aufgerufen wird, erzeugt die Funktion getNumbers() eine Liste der Zahlen 1 bis 9 in zufälliger Reigenfolge. Dann werden 9 Labels erzeugt, die je- weils eine Zahl aus dieser Liste anzeigen. Beim Klicken auf den Link mit der ID „link“ werden über den Visitor alle Elemente der Seite durchsucht. Wenn der Visitor ein Label findet, bei dem der Wert des hinterlegten Modells ein Integer und außerdem durch 3 teilbar ist, dann wird die Sichtbarkeit der Komponente umgedreht. Der Link mit der ID „reset“ ruft die Seite noch einmal auf, und die Zahlen werden daher neu verteilt. Listing 6.2 RandomNumberPage.html <html> <head> <title>RandomNumber Page</title> <style> td { border:1px solid #aaa; background-color:#ccc; font-size:30px; } </style> </head>82
  • 6.1 Basisklasse Component <body> <table> <tr> <td><span wicket:id="l0"></span></td> <td><span wicket:id="l1"></span></td> <td><span wicket:id="l2"></span></td> </tr> <tr> <td><span wicket:id="l3"></span></td> <td><span wicket:id="l4"></span></td> <td><span wicket:id="l5"></span></td> </tr> <tr> <td><span wicket:id="l6"></span></td> <td><span wicket:id="l7"></span></td> <td><span wicket:id="l8"></span></td> </tr> </table> <a wicket:id="link">An/Aus</a> <a wicket:id="reset">Neu</a> </body> </html>Am Anfang werden die 9 Kästchen mit je einer Zahl wie in Abbildung 6.1 angezeigt. Nacheinem Klick auf den Link „An/Aus“ werden die durch 3 teilbaren Zahlen ausgeblendet. Abbildung 6.1 Zufallszahlen ein- und aus- geblendet6.1.2 DarstellungsphasenFür jede Komponente wird vor der Darstellung die Methode onBeforeRender() ausge-führt. In dieser Methode kann die Sichtbarkeit verändert werden, Komponenten erzeugenKindelemente, Daten werden für die Darstellung aufbereitet. Hinweis Wenn eine Komponente unsichtbar ist, kann es sein, dass diese Methode nicht aufgerufen wird. Wenn das aus einem Grund notwendig sein sollte, kann man die Methode callOn- BeforeRenderIfNotVisible() überschreiben, die dann true zurückliefern sollte.Wenn für alle Komponenten diese Methode ausgeführt wurde, dann wird die Methoderender() aufgerufen, welche die notwendigen Grundlagen schafft und danach die Metho-de onRender() ausführt. In diesem Schritt werden die Modelle und Daten in eine Ergeb-nisseite überführt. Wenn die Komponente nicht sichtbar ist, dann wird diese Methode nichtausgeführt.Nachdem dieser Schritt abgeschlossen wurde, wird noch für alle Komponenten onAfter-Render() ausgeführt. Im Abschluss wird über den Aufruf von detach() alles Temporäreaus den Komponenten entfernt. Während dieses Prozesses wird onDetach() aufgerufen. 83
  • 6 Komponenten Wenn man in einer der verschiedenen Komponentenphasen eigene Verarbeitungsroutinen einbinden möchte, überschreibt man eine der folgenden Methoden: onBeforeRender() onRender() onAfterRender() onDetach() 6.1.3 Page, Session und Application Die Klasse Component stellt häufig benötigte Funktionen bereit: getPage() – die Seite, auf der die Komponente eingebunden ist. getSession() – die Session (entspricht getPage().getSession()). getApplication() – die Applikation (... getSession().getApplication()). getLocale() – das eingestellte Gebietsschema (... getSession().getLocale()). Die aktuelle Session kann man auch über die statische Methode Session.get() ermitteln, Application.get() liefert die aktuelle Instanz der Anwendung. 6.1.4 Komponentenpfad Für jede Komponente auf einer Seite lässt sich über die Methoden getPath() und get- RelativePath() der Hierarchiepfad ermitteln. Dabei werden die Komponenten-IDs an- einandergehängt und mit „:“ abgetrennt (z.B. „liste:box:link“). 6.1.5 Modelle Jede Komponente kann ein Modell haben. Die Zugriffsmethoden lauten: setDefaultModel() getDefaultModel() Um Änderungen an Modelldaten vorzunehmen, sollte man diese Funktionen benutzen: getDefaultModelObject() setDefaultModelObject() Für die Umwandlung in einen String (z.B. für die Darstellung auf der Seite) wird die fol- gende Methode benutzt. Dabei werden die eingestellten Konverter für die Umwandlung benutzt. Ebenso wird geprüft, ob im Ergebnis Zeichen, welche die Darstellung der Seite beeinflussen, ersetzt werden müssen. Das sorgt dafür, dass Daten aus dem Modell nicht zu Veränderungen auf der Seite führen, da alle Zeichen, die der Browser als HTML-Code werten könnte, ungewandelt werden. Aus einem „<“ wird so ein „&lt;“. getDefaultModelObjectAsString()84
  • 6.2 Grundlagen der Vererbung 6.1.6 Feedback Jede Komponente hat die Methoden info(), warn() und error(), die als Parameter einen String erwarten. Dabei wird für die Komponente in der aktuellen Session eine Feed- backMessage vom passenden Typ angelegt. Diese Nachrichten kann man über eine Kom- ponente auf der Seite anzeigen lassen, was somit eine Rückmeldung an den Nutzer ermög- licht (siehe Abschnitt 9.2).6.2 Grundlagen der Vererbung Komponenten sind alle direkt oder indirekt von der Klasse Component abgeleitet. Dabei unterscheidet man zwischen Komponenten mit und ohne assoziierte Markup-Datei. Wenn Komponenten von anderen Komponenten abgeleitet werden, die mit einer Markup-Datei assoziiert werden müssen, hat man die Möglichkeit, durch das Anlegen einer eigenen Mar- kup-Datei die Darstellung der Komponente zu beeinflussen. Dabei müssen alle Kompo- nenten, die von der Basisklasse benutzt werden, auch in der Markup-Datei der abgeleiteten Klasse definiert werden. Allerdings kann Wicket auf Markup-Ebene auch auf einfache Vererbungsfunktionen zurückgreifen, sodass nicht nur die Funktionen, sondern auch das Aussehen einer Komponente durch Vererbung beeinflusst werden kann. Ein Beispiel soll das veranschaulichen. 6.2.1 Eine Seite mit eigenen Komponenten Im folgenden Beispiel erstellen wir eigene Komponenten und leiten von diesen ab. Zum Schluss erstellen wir eine Seite, die alle Komponenten einbindet. 6.2.1.1 Einfache Vererbung Die einfachste Möglichkeit, eine eigene Komponente zu erstellen, ist die Ableitung einer eigenen Klasse von der Klasse Panel. Listing 6.3 BasePanel.java package de.wicketpraxis.web.thema.komponenten.vererbung.extension; ... public class BasePanel extends Panel { public BasePanel(String id) { super(id); add(new Label("message",getMessage())); } protected String getMessage() { return "Base"; } } 85
  • 6 Komponenten Da die Klasse Panel, ihrerseits von Component abgeleitet, keine eigene Markup-Datei mitbringt, müssen wir ein eigenes Markup erstellen. Listing 6.4 BasePanel.html <html> <head> <title>Nicht sichtbar</title> <wicket:head> <!-- BasePanel Header --> </wicket:head> </head> <body> <wicket:panel> <div style="background-color:#ffeeee; padding:4px;"> <span wicket:id="message">Message</span> <wicket:child/> </div> </wicket:panel> </body> </html> Der Unterschied zu den bisher erstellten Markup-Dateien liegt darin, dass hier nicht mehr eine vollständige HTML-Seite definiert werden muss, sondern nur noch das Fragment einer Seite. Komponenten, die von Panel ableiten, müssen im Markup den darzustellenden Teil mit <wicket:panel> umschließen. Alles, was sich außerhalb dieser Tags befindet, wird nicht mit dargestellt. Dennoch kann es hilfreich sein, eine vollständige HTML-Seite zu definieren, wenn man dadurch eine bessere Vorschau der Ergebnisseite im Editor er- möglichen kann. Wenn eine Komponente Informationen bereitstellen muss, die im Kopf der Seite (<head>) abgelegt werden sollten, kann man durch die Verwendung von <wicket:head> entspre- chende Angaben im Kopf platzieren. Dabei achtet Wicket darauf, dass diese Information nur einmal dargestellt wird. Als Alternative kann man solche Informationen auch pro- grammatisch hinzufügen (siehe Abschnitt 7.1.1.8). 6.2.1.2 Vererbung ohne eigenes Markup Von unserer ersten eigenen Komponente leiten wir eine weitere ab. Listing 6.5 ExtendedOnlyPanel.java package de.wicketpraxis.web.thema.komponenten.vererbung.extension; public class ExtendedOnlyPanel extends BasePanel { public ExtendedOnlyPanel(String id) { super(id); } @Override protected String getMessage() { return "Extends ("+super.getMessage()+")"; } }86
  • 6.2 Grundlagen der VererbungIn diesem Fall verzichten wir auf das Anlegen einer eigenen Markup-Datei. Wicket greiftdann auf das Markup der Basisklasse zurück. Unsere Anpassung bezieht sich nur auf denRückgabewert von getMessage(). Hinweis Die Methode getMessage() wird im Konstruktor der Basisklasse aufgerufen. Die eigene Klasse ist zu diesem Zeitpunkt noch nicht vollständig initialisiert.6.2.1.3 Vererbung mit eigenem MarkupWir leiten ein weiteres Mal eine Klasse ab. Wir überschreiben allerdings keine Methode,sondern definieren ein neues eigenes Markup.Listing 6.6 OverrideWithMarkupPanel.java package de.wicketpraxis.web.thema.komponenten.vererbung.extension; public class OverrideWithMarkupPanel extends BasePanel { public OverrideWithMarkupPanel (String id) { super(id); } }Listing 6.7 OverrideWithMarkupPanel.html <wicket:head> <!-- OverrideWithMarkup Header --> </wicket:head> <wicket:panel> <div style="border:1px solid blue; background-color:#eeeeff; padding:4px;"> <div style="font-weight: bold;"> Override (<i><span wicket:id="message">Message</span></i>) </div> </div> </wicket:panel>In dem Markup müssen nun alle eigenen oder durch die Basisklasse hinzugefügten Ele-mente referenziert werden. Dabei kann das Aussehen der Komponente angepasst werden.Einen ähnlichen Effekt kann man erzielen, indem man von der getVariation()-MethodeGebrauch macht (siehe Abschnitt 6.3.1).Man sollte sich diese Möglichkeit für den Fall merken, wenn man an der Basisklasse keineVeränderungen vornehmen kann. Man sollte beachten, dass bei einer Veränderung der Ba-sisklasse Elemente gelöscht oder hinzugefügt werden könnten, was zur Folge hat, dassauch das eigene Markup angepasst werden muss.6.2.1.4 Markup-VererbungBisher haben wir gesehen, dass Komponenten eigene Markup-Dateien benutzen können,die vorhandene Markup-Definitionen der Basiskomponente überschreiben können, odernur die Funktion der Komponente verändern, sodass das Markup der Basiskomponente 87
  • 6 Komponenten übernommen wird. In der letzten Variante betrachten wir die Möglichkeit, dass die Mar- kup-Definition der Basisklasse durch die der abgeleiteten Klasse ergänzt wird. Dazu leiten wir wiederum eine Klasse ab. Listing 6.8 ExtendedWithMarkupPanel.html package de.wicketpraxis.web.thema.komponenten.vererbung.extension; import org.apache.wicket.markup.html.basic.Label; public class ExtendedWithMarkupPanel extends BasePanel { public ExtendedWithMarkupPanel(String id) { super(id); add(new Label("message2","Extends With Markup")); } } Hinweis Wenn in einer abgeleiteten Komponente Elemente hinzugefügt werden, dann befinden sich diese Elemente auf derselben Hierarchie wie die Elemente der Basisklasse. Wenn nun dieselbe ID benutzt wird, gibt es daher eine Kollision, weil diese ID schon benutzt wurde. Wir haben in dieser Komponente ein weiteres Label hinzugefügt. Das Markup gibt nun an, ob das Markup der Basisklasse überschrieben oder erweitert werden soll. Da wir in diesem Fall das Markup erweitern wollen, erstellen wir eine neue Datei. Listing 6.9 ExtendedWithMarkupPanel.html <wicket:head> <!-- ExtendedWithMarkup Header --> </wicket:head> <wicket:extend> <div style="background-color:#ffcccc; padding:4px;"> <span wicket:id="message2">Extended</span> <wicket:child/> </div> </wicket:extend> Das Tag <wicket:extend> ist dafür verantwortlich, dass Wicket das Markup der Basis- klasse nicht überschreibt. Wicket nimmt nun dieses Fragment und fügt es in das Basis- Template ein. Der Platzhalter wird dabei durch das Tag <wicket:child/> definiert, das wir bereits in Markup der Klasse BasePanel definiert haben. 6.2.1.5 Ergebnis Damit wir die verschiedenen Varianten und damit die verschiedenen Darstellungen beur- teilen können, erstellen wir eine Seite, auf der wir diese Komponenten einbauen. Listing 6.10 SimpleExtensionPage.java package de.wicketpraxis.web.thema.komponenten.vererbung.extension; import org.apache.wicket.markup.html.WebPage;88
  • 6.2 Grundlagen der Vererbung public class SimpleExtensionPage extends WebPage { public SimpleExtensionPage() { add(new BasePanel("base")); add(new ExtendedOnlyPanel("extended")); add(new OverrideWithMarkupPanel("override")); add(new ExtendedWithMarkupPanel("withMarkup")); } }Listing 6.11 SimpleExtensionPage.html <html> <head> <title>Simple Extension Page</title> </head> <body> <wicket:container wicket:id="base"></wicket:container><br> <wicket:container wicket:id="extended"></wicket:container><br> <wicket:container wicket:id="override"></wicket:container><br> <wicket:container wicket:id="withMarkup"></wicket:container><br> </body> </html>Bisher haben wir Elemente direkt an ein HTML-Tag gebunden. Wenn Komponenten mit<wicket:container> eingebunden werden, wird das umschließende Tag von Wicketnicht dargestellt. Abbildung 6.2 Darstellung der abgeleiteten KomponentenIm Quelltext der Ergebnisseite (Abbildung 6.2) können wir gut erkennen, wie die Seiteentstanden ist.Listing 6.12 Ergebnis.html <html> <head> <title>Simple Extension Page</title> <!-- BasePanel Header --> <!-- OverrideWithMarkup Header --> <!-- ExtendedWithMarkup Header --> </head> <body> <wicket:container wicket:id="base"><wicket:panel> <div style="background-color:#ffeeee; padding:4px;"> <span wicket:id="message">Base</span> <wicket:child/> </div> </wicket:panel></wicket:container><br> <wicket:container wicket:id="extended"><wicket:panel> 89
  • 6 Komponenten <div style="background-color:#ffeeee; padding:4px;"> <span wicket:id="message">Extends (Base)</span> <wicket:child/> </div> </wicket:panel></wicket:container><br> <wicket:container wicket:id="override"><wicket:panel> <div style="border:1px solid blue; background-color:#eeeeff; padding:4px;"> <div style="font-weight: bold;"> Override (<i><span wicket:id="message">Base</span></i>) </div> </div> </wicket:panel></wicket:container><br> <wicket:container wicket:id="withMarkup"><wicket:panel> <div style="background-color:#ffeeee; padding:4px;"> <span wicket:id="message">Base</span> <wicket:child><wicket:extend> <div style="background-color:#ffcccc; padding:4px;"> <span wicket:id="message2">Extends With Markup</span> <wicket:child/> </div> </wicket:extend></wicket:child> </div> </wicket:panel></wicket:container><br> </body> </html> An diesem Beispiel kann man gut nachvollziehen, wie Wicket die Ergebnisseite erzeugt hat. Ich habe zur besseren Übersicht die Wicket-Tags hervorgehoben. Dabei kann man gut erkennen, dass das äußere wicket:container-Tag mit der ID der Komponente das Er- gebnis-Markup der Komponente einschließt. Man kann ebenso gut erkennen, wie das Markup der Komponente ohne den Anteil, der für den Kopf bestimmt ist, eingebettet wird. Im Fall der Komponente mit der ID withMarkup wird dann innerhalb von wicket:child das Markup der abgeleiteten Komponente eingebunden. Im Deployment-Modus werden die Wicket-eigenen Tags nicht mehr dargestellt. Listing 6.13 Deployment-Ergebnis.html <html> <head> <title>Simple Extension Page</title> <!-- BasePanel Header --> <!-- OverrideWithMarkup Header --> <!-- ExtendedWithMarkup Header --> </head> <body> <div style="background-color:#ffeeee; padding:4px;"> <span>Base</span> </div><br> <div style="background-color:#ffeeee; padding:4px;"> <span>Extends (Base)</span> </div><br> <div style="border:1px solid blue; background-color:#eeeeff; padding:4px;"> <div style="font-weight: bold;"> Override (<i><span>Base</span></i>) </div> </div><br> <div style="background-color:#ffeeee; padding:4px;">90
  • 6.2 Grundlagen der Vererbung <span>Base</span> <div style="background-color:#ffcccc; padding:4px;"> <span>Extends With Markup</span> </div> </div><br> </body> </html>6.2.2 Vererbung für FortgeschritteneWenn Komponenten von abstrakten Klassen ableiten, kann Wicket trotzdem auf das Mar-kup der abstrakten Klasse zugreifen. Welche Möglichkeiten sich daraus ergeben, zeigt dasnachfolgende Beispiel. Als Erstes erstellen wir eine Klasse, bei der die ID der Komponentenicht verändert werden kann, weil wir eine Konstante als ID benutzen.Listing 6.14 BasePanelLabel.java package de.wicketpraxis.web.thema.komponenten.vererbung.extension3; import org.apache.wicket.markup.html.basic.Label; public class BasePanelLabel extends Label { public static final String MESSAGE_ID="message"; public BasePanelLabel(String message) { super(MESSAGE_ID,message); } }Wie man sieht, wird die ID der Komponente unveränderlich auf „message“ gesetzt. AlleKlassen, die von dieser Klasse ableiten, können die ID nicht verändern. Auf diese Weisekann man sicherstellen, dass auch jede abgeleitete Klasse dieselbe ID benutzt. Wie wirdiese Konstruktion anwenden, wird im weiteren Verlauf sichtbar.Da unsere Klasse von Label abgeleitet ist, kommen wir an dieser Stelle ohne eine Markup-Datei aus. Wir erstellen als Nächstes eine abstrakte Klasse, die wir dann auf unserer Seitebenutzen werden.Listing 6.15 AbstractBasePanel.java package de.wicketpraxis.web.thema.komponenten.vererbung.extension3; import org.apache.wicket.markup.html.panel.Panel; public abstract class AbstractBasePanel extends Panel { public AbstractBasePanel(String id) { super(id); add(getMessage()); add(getChild("child")); } protected abstract BasePanelLabel getMessage(); protected abstract Panel getChild(String id); } 91
  • 6 Komponenten Die Klasse definiert zwei abstrakte Methoden, die von jeder abgeleiteten Klasse über- schrieben werden müssen. Die erste Methode muss eine Instanz der Klasse BasePanel- Label zurückliefern. Da die ID in der Klasse festgelegt wurde, ist es unmöglich, eine zweite Instanz hinzuzufügen, weil die ID innerhalb der Komponente bereits benutzt wird. Die Methode getChild liefert eine Instanz der Klasse Panel zurück. Die ID wird erst durch den Aufruf festgelegt. Beide Varianten erreichen in etwa dasselbe Ergebnis. Welcher Variante der Vorzug gegeben werden sollte, hängt sehr stark vom Anwendungsfall ab. Für diese Klasse definieren wir ein Markup, das dann in den abgeleiteten Klassen nicht überschrieben oder erweitert wird. Listing 6.16 AbstractBasePanel.html <wicket:panel> <span wicket:id="message">Message</span><br> <div style="border:1px dotted black; padding: 4px; margin:4px;"> <wicket:container wicket:id="child"></wicket:container> </div> </wicket:panel> Damit wir den Unterschied im Ergebnis sehen können, erstellen wir eine Klasse, die ein- fach nur eine Box darstellt. Das Markup wird damit fast komplizierter als die Klassendefi- nition. Listing 6.17 BoxPanel.java package de.wicketpraxis.web.thema.komponenten.vererbung.extension3; import org.apache.wicket.markup.html.panel.Panel; public class BoxPanel extends Panel { public BoxPanel(String id) { super(id); } } Listing 6.18 BoxPanel.html <wicket:panel> <div style="width:100px; height:100px; background-color:white; border:1px solid black;"> Box </div> </wicket:panel> Jetzt erstellen wir noch eine Komponente, die von AbstractBasePanel ableitet und die zwei geforderten Methoden implementiert. Listing 6.19 HasBoxPanel.java package de.wicketpraxis.web.thema.komponenten.vererbung.extension3; import org.apache.wicket.markup.html.panel.Panel; public class HasBoxPanel extends AbstractBasePanel { public HasBoxPanel(String id)92
  • 6.2 Grundlagen der Vererbung { super(id); } @Override protected Panel getChild(String id) { return new BoxPanel(id); } @Override protected BasePanelLabel getMessage() { return new HalloLabel(); } static class HasBoxLabel extends BasePanelLabel { public HasBoxLabel() { super("Hab ne Box"); } } }Auch wenn dasselbe Ergebnis mit sehr viel weniger Aufwand erreichbar wäre, wollen wires an dieser Stelle so kompliziert machen. In der Klasse HasBoxLabel kann man sehen,wie man von einer Komponente wie BasePanelLabel ableiten kann. Die Methode get-Child liefert eine Instanz der Klasse BoxPanel zurück, könnte aber natürlich jede beliebi-ge Komponente zurückliefern, die von Panel abgeleitet ist. Wir erstellen eine Seite undkombinieren die verschiedenen Möglichkeiten, die diese Komponenten bereits jetzt bieten.Listing 6.20 ExtendedExtensionPage.java package de.wicketpraxis.web.thema.komponenten.vererbung.extension3; ... public class ExtendedExtensionPage extends WebPage { public ExtendedExtensionPage() { add(new HasBoxPanel("hasBox")); add(new AbstractBasePanel("inline") { @Override protected BasePanelLabel getMessage() { return new BasePanelLabel("Ich hab keine Box"); } @Override protected Panel getChild(String id) { return new EmptyPanel(id); } }); } }Listing 6.21 ExtendedExtensionPage.html <html> <head> <title>Extended Extension Page</title> 93
  • 6 Komponenten </head> <body> <div wicket:id="hasBox" style="background-color:#ffeeee;"></div> <div wicket:id="inline" style="background-color:#ccccff;"></div> </body> </html> In diesem Beispiel habe ich auf die Verwendung von wicket:container verzichtet. Die div-Tags werden in dem Fall auch nicht im Deployment-Modus gelöscht. Das Ergebnis sehen wir in Abbildung 6.3. Abbildung 6.3 Zwei abgeleitete Komponenten6.3 Style, Locale und Variation In mehrsprachige Anwendungen werden oft nicht nur die Texte in der gewählten Landes- sprache dargestellt, sondern oft auch Anpassungen am Layout und der Gestaltung vorge- nommen. Wicket bietet über diese Anforderungen hinaus auch die Möglichkeit, mehrere Variationen einer Komponente zu erstellen, ohne dass Anpassungen am Programmcode der Komponente notwendig werden. 6.3.1 Markup-Variationen Wenn eine Komponente unterschiedlich dargestellt werden soll, ohne dass sich dabei die Funktion verändert, wäre das ein zu großer Aufwand, wenn man dieses Problem über Ver- erbung lösen würde. Wesentlich einfacher wäre es, wenn man für eine Komponente ver- schiedene Markup-Dateien bereitstellen könnte. Durch das Überschreiben der Methode getVariation() können wir eine andere Markup-Datei für die Darstellung heranziehen. Dabei bewirkt nicht nur der Rückgabewert der Funktion, dass eine andere Markup-Datei zu Einsatz kommt. Vielmehr ergibt sich der Dateiname aus der Spracheinstellung und dem Rückgabewert der Methode getStyle() der aktuellen Session-Instanz. Listing 6.22 SimplePanel.java package de.wicketpraxis.web.thema.komponenten.variations; ... public class SimplePanel extends Panel { String _variation; Locale _locale;94
  • 6.3 Style, Locale und Variation public SimplePanel(String id) { super(id); } public SimplePanel(String id,String variation) { this(id); _variation=variation; } public SimplePanel setLocale(Locale locale) { _locale = locale; return this; } @Override public String getVariation() { return _variation; } @Override public Locale getLocale() { if (_locale!=null) return _locale; return super.getLocale(); } }In dieser Komponente überschreiben die Methoden getLocale() und getVariation().Dann legen wir die folgenden Markup-Dateien an. Die Dateinamen sind in jedem Fall re-levant und sollten auch in Bezug auf Groß- und Kleinschreibung genau wie angegebenangelegt werden. Außerdem muss darauf geachtet werden, dass die Markup-Dateien inner-halb des Resource-Verzeichnisses im selben Paket bzw. im selben Verzeichnis wie dieKomponentenklasse angelegt werden.Listing 6.23 SimplePanel.html <wicket:panel> <div style="background-color:#eee; border:1px solid black; margin:4px;"> Standard </div> </wicket:panel>Listing 6.24 SimplePanel_v1.html <wicket:panel> <div style="background-color:#00e; color:white; margin:4px; padding:10px;"> Variante 1 </div> </wicket:panel>Listing 6.25 SimplePanel_v1_rot.html <wicket:panel> <div style="background-color:#f00; margin:4px; padding:10px; color:white"> Variante 1 in Rot </div> </wicket:panel> 95
  • 6 Komponenten Listing 6.26 SimplePanel_v2_de.html <wicket:panel> <div style="background-color:#ff0; margin:4px; padding:10px;"> Variante 2 </div> </wicket:panel> Listing 6.27 SimplePanel_v2_rot_de_CH.html <wicket:panel> <div style="background-color:#f00; margin:4px; padding:10px; color:white"> <span style="font-size: 30px;">+</span><br> Variante 2 für die Schweiz in Rot </div> </wicket:panel> Wie man erkennen kann, unterscheiden sich diese verschiedenen Markup-Dateien nur un- wesentlich. Außerdem benutzten wir an dieser Stelle keine Kindkomponenten, sodass wir nicht darauf achten müssen, dass alle Komponenten in der richtigen Hierarchie referenziert wurden. Da die Reihenfolge innerhalb derselben Hierarchiestufe irrelevant ist, könnte man auf diese Weise recht einfach die Anordnung von Kindkomponenten verändern. Hinweis Es ist natürlich möglich, auch in Abhängigkeit vom benutzten Markup (also dem Rückgabe- wert von getVariation) Kindkomponenten wegzulassen. Allerdings empfehle ich, in einem solchen Fall eher die Sichtbarkeit von Komponenten anzupassen. So vermeidet man Fehler, die darauf zurückzuführen sind, dass die Komponentenstruktur nicht zu dem verwen- deten Markup passt, weil man z.B. in einem besonderen Fall diese Kindkomponenten ver- gessen oder fälschlicherweise hinzugefügt hat. Listing 6.28 StyleLocaleVariationPage.java package de.wicketpraxis.web.thema.komponenten.variations; ... public class StyleLocaleVariationPage extends WebPage { public StyleLocaleVariationPage(PageParameters pageParameters) { String changeStyle = pageParameters.getString("style"); if ((changeStyle!=null) && (changeStyle.length()>0)) { getSession().setStyle(changeStyle); } else { getSession().setStyle(null); } add(new SimplePanel("pure")); add(new SimplePanel("withVariation","v1")); add(new SimplePanel("withCustomLocale","v2") .setLocale(new Locale("de","CH"))); add(new BookmarkablePageLink<StyleLocaleVariationPage>( "rot",StyleLocaleVariationPage.class, new PageParameters("style=rot")));96
  • 6.3 Style, Locale und Variation add(new BookmarkablePageLink<StyleLocaleVariationPage>( "normal",StyleLocaleVariationPage.class)); } }Listing 6.29 StyleLocaleVariationPage.html <html> <head> <title>StyleLocalVariation Page</title> </head> <body style="width:300px;"> <wicket:container wicket:id="pure"></wicket:container> <wicket:container wicket:id="withVariation"></wicket:container> <wicket:container wicket:id="withCustomLocale"></wicket:container> <br> <a wicket:id="rot">in Rot</a> <a wicket:id="normal">Normal</a> </body> </html>Die Seite erfüllt folgende Funktionen: Die Parameter des Seitenaufrufs werden als Parameter im Konstruktor übergeben. Ab- hängig vom Inhalt des Parameters „style“ wird das Attribut Style in der Session gesetzt oder gelöscht. Zuerst wird die Komponente ohne Anpassungen erstellt. Im zweiten Fall wird das Att- ribut Variation mit „v1“ gesetzt. Danach wird die Komponente mit dem Attribut Varia- tion=„v2“ gesetzt und das Attribut Locale auf „de“ und „CH“ gesetzt. Es werden zwei Links eingebunden, welche die Seite einmal mit dem Parameter „sty- le=rot“ und das andere Mal ohne den Parameter aufrufen.Wenn die Seite aufgerufen wird, erhält man ein Ergebnis wie in Abbildung 6.4. Wenn manauf den Link „in Rot“ klickt, dann wechselt die Darstellung wie in Abbildung 6.5. Abbildung 6.4 Normale Darstellung der Komponenten Abbildung 6.5 Alternative Darstellung 97
  • 6 Komponenten Wicket sucht zu jeder Komponente die passende Markup-Datei. Dabei ergibt sich der Ver- zeichnisname aus dem Paketnamen der Klasse (z.B. wird aus „de.wicketpraxis.pages“ „de/wicketpraxis/pages“) und der Dateiname für das Markup nach folgendem Such- muster: Klassenname_variation_style_language_COUNTRY.html Klassenname_variation_style_language.html Klassenname_variation_style.html Klassenname_language_COUNTRY.html Klassenname_language.html Klassenname.html Wenn Variation und Style nicht gesetzt sind, verkürzt sich die Suche entsprechend. Wenn man für eine abgeleitete Klasse kein Markup angelegt hat, dann versucht Wicket, ein Mark- up mit der Namen der Basisklasse zu finden. Dabei wird nach demselben Muster vorge- gangen. Wenn man auf Vererbung auch im Markup zurückgreift, wird zusätzlich das Mark- up der Basisklasse ebenfalls anhand dieses Musters ermittelt. Der Wert aus getVariati- on() und getStyle() gilt dann ebenso. Resource- und Property-Dateien Die drei Attribute haben auch Auswirkungen auf die Auflösung von Resource- und Proper- ty-Dateien. Letztere werden nach einem stark erweiterten Muster aufgelöst. Dabei wird nicht nur die Property-Datei der Klasse und aller Basisklassen durchsucht, sondern auch innerhalb des Pakets, in dem sich die Klasse befindet, dann aller übergeordneten Pakete sowie zum Schluss noch in der Hierarchie der Applikationsklasse und aller Basisklassen der Applikationsklasse. Klassenname_variation_style_language_COUNTRY.properties Klassenname_variation_style_language_COUNTRY.xml ... Klassenname.properties Klassenname.xml Basisklasse_variation_style_language_COUNTRY.properties Basisklasse_variation_style_language_COUNTRY.xml ... Basisklasse.properties Basisklasse.xml de.wicketpraxis.klasse.package_variation_style_language_COUNTRY.properties de.wicketpraxis.klasse.package_variation_style_language_COUNTRY.xml ...98
  • 6.4 Sichtbarkeit de.wicketpraxis.package_variation_style_language_COUNTRY.properties ... de. package_variation_style_language_COUNTRY.properties ... WicketPraxisApplication_variation_style_language_COUNTRY.properties ... Application_variation_style_language_COUNTRY.properties Application.properties Application.xml Das muss man sich dann so vorstellen, dass die Definitionen in den Properties, die in die- ser Liste ganz oben stehen, die Definitionen weiter unten überschreiben. So ist es möglich, bestimmte Werte applikationsweit zu setzen und diese im Einzelfall zu überschreiben. Hinweis Wicket kann dabei bereits Property-Dateien im XML-Format verarbeiten, die z.B. nicht auf die Zeichenkodierung ISO-8859-1 festgelegt sind. Das wirkt sich besonders auf den Um- gang mit Zeichen in anderen Sprachen aus. Für die nach ISO-8859-1 kodierten Property- Dateien mussten diese bisher aufwendig umgewandelt werden.6.4 Sichtbarkeit Wenn Komponenten unsichtbar sind, dann gibt es in der Ergebnisseite nichts, was darauf hindeuten könnte, dass es diese Komponente überhaupt gibt und auf der Seite benutzt wird. Ob eine Komponente sichtbar ist, wird in zwei Stufen ermittelt: Ist isVisible()=false, ist die Komponente unsichtbar. Dieser Wert kann durch Set- zen oder Überschreiben der Methode verändert werden. Gibt die Methode isVisibilityAllowed() false zurück, wird die Komponente nicht dargestellt. Diese Methode kann nicht überschrieben werden. Ist isRenderAllowed()=false, wird die Komponente ebenfalls nicht dargestellt. Das hat folgenden Hintergrund: Die Methode isVisible() kann von einer Komponente überladen werden. Dann hat der Aufruf von setVisible() nicht den gewünschten Effekt. Die Methode isVisibilityAllowed() kann nicht überschrieben werden. Der Rückgabe- wert kann über setVisibilityAllowed() verändert werden. Der Rückgabewert von is- RenderAllowed() kann durch Sicherheitseinstellungen beeinflusst werden, sodass sich die Sichtbarkeit der Komponenten aus allen drei Informationen ergibt. Ob eine Komponen- te sichtbar ist, kann über die Methode determineVisibility() ermittelt werden. Was auf den ersten Blick verwirrend klingt, wird sinnvoll und nachvollziehbar, wenn man sich folgenden Anwendungsfall vorstellt: 99
  • 6 Komponenten Die Komponente kann durch den Nutzer ein- und ausgeblendet werden. Dafür wird entweder setVisible() aufgerufen oder die Methode isVisible() überschrieben. Wenn der Nutzer aber nicht die entsprechenden Rechte hat, kann die Anwendung die Komponente unabhängig vom Nutzer ausblenden. Soll der Nutzer gar keinen Zugriff auf die Komponente erhalten, kann die Komponente durch Sicherheitseinstellungen für z.B. unangemeldete Benutzer ausgeblendet werden (siehe Abschnitt 10.3). 6.4.1 wicket:enclosure Mit dem wicket:enclosure-Tag kann man Markup-Bereiche im Einzugsbereich der Komponente in Abhängigkeit zur Sichtbarkeit der Komponente ausblenden. Listing 6.30 EnclosurePage.java package de.wicketpraxis.web.thema.komponenten.visibility; ... public class EnclosurePage extends WebPage { public EnclosurePage() { add(new Label("view","Hallo")); add(new Label("hide","unsichtbar").setVisible(false)); add(new Label("view2","Das zweite Mal")); add(new Label("hide2","wieder unsichtbar").setVisible(false)); } } Listing 6.31 EnclosurePage.html <html> <head> <title>Enclosure Page</title> </head> <body> <wicket:enclosure> <div style="border:1px dotted black;"> <span wicket:id="view"></span> </div> </wicket:enclosure> <wicket:enclosure> <div style="border:1px solid black;"> <span wicket:id="hide"></span> </div> </wicket:enclosure> <wicket:enclosure child="hide2"> <div style="border:2px solid green;"> <span wicket:id="view2"></span><br> <span wicket:id="hide2"></span> </div> </wicket:enclosure> </body> </html> Die Komponente, die für die Sichtbarkeit herangezogen wird, muss dazu innerhalb des wicket:enclosure-Tags liegen. Befindet sich mehr als eine Komponente innerhalb des Tags, muss diese über das Attribut child adressiert werden. Dazu wird die Wicket-ID der100
  • 6.4 SichtbarkeitKomponente angegeben. Befindet sich eine Komponente mit der ID „kind“ innerhalb eineranderen Komponente mit der ID „vater“, lautet der Wert für das Attribut „vater:kind“. Hinweis Wenn die Sichtbarkeit der Komponente per AJAX geändert wird, wirkt sich das nicht auf den durch wicket:enclosure definierten Block aus. In diesem Fall sollte eine Elternkomponente aktualisiert werden, die das wicket:closure-Tag enthält.6.4.2 Empfehlung zur AnwendungEs ist naheliegend, dass man die Methode isVisible() überschreibt und so die Sichtbar-keit der Komponente steuert. Allerdings ist dabei zu beachten, dass diese Methode mehr-mals aufgerufen werden kann. Wenn das Ermitteln der Sichtbarkeit aufwendig ist, sollteman zwischen zwei Alternativen wählen: Das Ergebnis wird gepuffert und innerhalb der zu überschreibenden Methode onDe- tach() gelöscht. Die Methode onBeforeRender() wird überschrieben. Innerhalb der Methode erfolgt dann der Aufruf von setVisible().Ich bevorzuge die zweite Variante. Allerdings ist dabei zu beachten, dass die Methode on-BeforeRender() nicht aufgerufen wird, wenn eine Komponente unsichtbar ist. Um diesesVerhalten abzuändern, muss man die Methode callOnBeforeRenderIfNotVisible()überschreiben. Die Implementierung folgt dann diesem Muster:Listing 6.32 EineKomponente.java ... protected boolean isVisibleCalledOnlyOneTime() { ... } @Override protected void onBeforeRender() { super.onBeforeRender(); setVisible(isVisibleCalledOnlyOneTime()); } @Override protected boolean callOnBeforeRenderIfNotVisible() { return true; } ... 101
  • 6 Komponenten6.5 Ajax Dass sich Ajax so durchsetzen konnte, lag nicht daran, dass der Nutzer die Technik dahin- ter so spannend fand, sondern dass dadurch die Geschwindigkeit und die Interaktivität einer Webanwendung wesentlich gesteigert werden konnte. Außerdem konnten so neue Interaktionsformen entwickelt werden. Dabei ändert sich aber nicht die Geschwindigkeit der Anwendung an sich, sondern nur die Reaktionszeit auf die Nutzeraktion, da nicht mehr die vollständige Seite, sondern nur noch ein Teil der Seite über das Internet übertragen werden muss. Auch in Zeiten von breitbandigen Internetanschlüssen ist dieser Unterschied relevant. In Wicket gibt es vielfältige Möglichkeiten, Teile der Webseite per Ajax zu aktualisieren. Damit Wicket weiß, welche Komponenten neu gezeichnet werden sollen, muss jede Kom- ponente, die aktualisiert werden muss, in eine Liste eingetragen werden. Wicket kümmert sich dann um die Aktualisierung der Darstellung im Browser. Listing 6.33 SimpleAjaxPage.java package de.wicketpraxis.web.thema.komponenten.ajax; ... public class SimpleAjaxPage extends WebPage { private Label _labelRefresh; private Label _labelNoRefresh; public SimpleAjaxPage() { _labelRefresh = new Label("refresh","Bin gleich weg."); _labelRefresh.setOutputMarkupId(true); add(_labelRefresh); _labelNoRefresh = new Label("noRefresh","Bin noch da."); add(_labelNoRefresh); add(new AjaxLink("link") { @Override public void onClick(AjaxRequestTarget target) { _labelRefresh.setDefaultModelObject("neuer Text"); _labelNoRefresh.setDefaultModelObject("...auch hier neuer Text"); target.addComponent(_labelRefresh); } }); } } Es werden zwei Labels erstellt. Wenn der Link angeklickt wird, ruft Wicket die on- Click()-Methode mit dem AjaxRequestTarget als Parameter auf. Innerhalb der Metho- de wird für jedes Label ein neuer Text für die Anzeige hinterlegt. Alle Komponenten wer- den durch den Aufruf von target.addComponent() aktualisiert. Wir fügen in diesem Beispiel allerdings nur eins der beiden Label zur Komponentenliste des AjaxRequestTar- get hinzu. Als Resultat wird nur das Label mit der ID „refresh“ aktualisiert, obwohl sich auch der Inhalt des anderen Labels verändert hat.102
  • 6.5 Ajax Hinweis Für eine Komponente, die man per Ajax aktualisieren möchte, muss Wicket dafür sorgen, dass innerhalb der Ergebnisseite auch ein HTML-Tag mit der passenden ID vorhanden ist. Dazu muss man Wicket mitteilen, welche Komponente ein potentieller Kandidat ist, und ruft die Methode setOutputMarkupId(true) auf.Innerhalb einer Komponente ist diese Vorgehensweise ausreichend. Wenn die Komponen-ten, die sich durch einen AjaxRequest verändern sollen, aber in anderen Komponentenliegen und man nur mit sehr viel Aufwand Zugriff auf diese Komponente erlangen kann,empfiehlt sich ein alternativer Ansatz.6.5.1 Ajax-EventsDurch eine geschickte Kombination von Funktionen, die Wicket bereits mitbringt, kannman bei der Verwendung von Ajax die sonst notwendige starke Koppelung von Kompo-nenten aufweichen.Listing 6.34 AjaxEventListenerInterface.java package de.wicketpraxis.web.thema.komponenten.ajax; public interface AjaxEventListenerInterface { public void notifyAjaxEvent(AbstractAjaxEvent event); }Einer Komponente, die das Interface implementiert, wird durch den Aufruf von notify-AjaxEvent() mitgeteilt, dass ein Event ausgelöst wurde.Listing 6.35 AbstractAjaxEvent.java package de.wicketpraxis.web.thema.komponenten.ajax; ... public abstract class AbstractAjaxEvent { Component _source; AjaxRequestTarget _requestTarget; protected AbstractAjaxEvent(Component source,AjaxRequestTarget requestTarget) { _source=source; _requestTarget=requestTarget; } public Component getSource() { return _source; } public void fire() { _source.getPage().visitChildren( AjaxEventListenerInterface.class, new AjaxEventVisitor(this)); } public void update(Component component) 103
  • 6 Komponenten { _requestTarget.addComponent(component); } protected static class AjaxEventVisitor implements IVisitor<Component> { AbstractAjaxEvent _event; protected AjaxEventVisitor(AbstractAjaxEvent event) { _event=event; } public Object component(Component component) { ((AjaxEventListenerInterface) component).notifyAjaxEvent(_event); return IVisitor.CONTINUE_TRAVERSAL; } } } Die Klasse AbstractAjaxEvent nimmt als Parameter die Komponente entgegen, die den Event auslöste. Außerdem muss das AjaxRequestTarget übergeben werden. Der Event wird dann über die fire()-Methode ausgelöst. Dabei wird jede Komponente der Seite, die AjaxEventListenerInterface implementiert, mit einem Visitor besucht. Dieser ruft dann die Methode notifyAjaxEvent() auf. 6.5.2 Einfache Event-Behandlung Angenommen, wir möchten den Text eines Labels austauschen, wenn der Nutzer auf einen Link klickt. Listing 6.36 SimpleAjaxEventPage.java package de.wicketpraxis.web.thema.komponenten.ajax; import java.util.Date; ... public class SimpleAjaxEventPage extends WebPage { public SimpleAjaxEventPage() { add(new EventLabel("message","Text")); add(new AjaxLink("link") { @Override public void onClick(AjaxRequestTarget target) { new SomeEvent(this,target).fire(); } }); } static class EventLabel extends Label implements AjaxEventListenerInterface { public EventLabel(String id, String label) { super(id, label); setOutputMarkupId(true); }104
  • 6.5 Ajax public void notifyAjaxEvent(AbstractAjaxEvent event) { if (event instanceof SomeEvent) { setDefaultModelObject("Event ausgelöst durch "+ event.getSource().getId()+ "("+new Date()+")"); event.update(this); } } } static class SomeEvent extends AbstractAjaxEvent { protected SomeEvent(Component source, AjaxRequestTarget requestTarget) { super(source, requestTarget); } } }Wir erstellen die Klasse EventLabel, die das Interface implementiert. Wenn ein Eventausgelöst wird, prüft die Klasse innerhalb der Methode notifyAjaxEvent() ob der Typdes Events passt. Im Erfolgsfall wird der Text geändert und die Komponente über die up-date()-Methode der Event-Klasse der Komponentenliste von AjaxRequestTarget hin-zugefügt.Ausgelöst wird der Event durch den Aufruf in der onClick()-Methode des Links. Wieman gut sehen kann, haben die zwei Komponenten (Link und Label) keine Beziehung mit-einander. Die Kommunikation erfolgt über den Event, den die eine Komponente auslöstund auf den die andere Komponente reagiert.Natürlich ist der Aufwand kaum zu rechtfertigen, wenn sich beide Komponenten wie indiesem Beispiel innerhalb einer Komponente befinden. In diesem Fall empfiehlt es sich,genau abzuwägen, ob die Nutzeraktion nur Veränderungen innerhalb der übergeordnetenKomponente (hier eine Seite) verursacht, oder ob andere Komponenten betroffen wären.6.5.3 Automatische Event-BehandlungEin häufiger Anwendungsfall besteht darin, eine Gruppe von Komponenten zu aktualisie-ren. Jede Komponente zu überschreiben, wäre ein zu großer Aufwand. Wenn eine Kompo-nente durch Ajax aktualisiert wird, werden natürlich auch die eingebetteten Komponentenaktualisiert. Wir schreiben uns daher eine Komponente, die genau das macht:Listing 6.37 AjaxEventListener.java package de.wicketpraxis.web.thema.komponenten.ajax; import org.apache.wicket.markup.html.WebMarkupContainer; public class AjaxEventListener<T extends AbstractAjaxEvent> extends WebMarkupContainer implements AjaxEventListenerInterface { Class<? extends T> _eventType; protected AjaxEventListener(String id,Class<? extends T> eventType) { 105
  • 6 Komponenten super(id); setOutputMarkupPlaceholderTag(true); _eventType=eventType; } public void notifyAjaxEvent(AbstractAjaxEvent event) { if (_eventType.isInstance(event)) { ajaxEvent((T) event); } } protected void ajaxEvent(T event) { if (onAjaxEvent(event)) event.update(this); } protected boolean onAjaxEvent(T event) { return true; } } Die Komponente wird von WebMarkupContainer abgeleitet. Die Klasse WebMarkupCon- tainer besitzt kein eigenes Markup, kann aber Kindelemente enthalten. Damit eignet sich die Klasse hervorragend für unsere Aufgabenstellung. Der Aufruf von setOutputMark- upPlaceholderTag() führt dazu, dass Wicket für den Fall, dass die Komponente nicht sichtbar ist, ein HTML-Tag mit der notwendigen ID einfügt, das zwar nicht sichtbar, aber auf der Seite vorhanden ist. Damit ist gewährleistet, dass Wicket den Inhalt des HTML- Tags ersetzen kann. Beim Aufruf von setOutputMarkupPlaceholderTag() wird gleich- zeitig setOutputMarkupId() aufgerufen, sodass man sich diesen Aufruf sparen kann. Wenn ein Event ausgelöst wurde, wird in der Methode notifyAjaxEvent() geprüft, ob der Event-Typ passt. Wenn das der Fall ist, wird die Methode ajaxEvent() aufgerufen. Diese prüft, ob der Rückgabewert von onAjaxEvent() true ist, und schlägt in diesem Fall die Komponente zur Aktualisierung vor. Man kann das Verhalten der Komponente beeinflussen, indem man entweder onAjax- Event() überschreibt und den Rückgabewert ändert oder ajaxEvent() überschreibt und das Verhalten selbst definiert. Listing 6.38 AjaxEventPage.java package de.wicketpraxis.web.thema.komponenten.ajax; ... public class AjaxEventPage extends WebPage { private Label _someLabel; public AjaxEventPage() { AjaxEventListener<Event> event = new AjaxEventListener<Event>("event",Event.class); _someLabel = new Label("message","Text"); event.add(_someLabel); add(event);106
  • 6.5 Ajax add(new AjaxLink("link") { @Override public void onClick(AjaxRequestTarget target) { _someLabel.setDefaultModelObject("Link geklickt ("+ new Date()+")"); new Event(this,target).fire(); } }); } static class Event extends AbstractAjaxEvent { protected Event(Component source, AjaxRequestTarget requestTarget) { super(source, requestTarget); } } }In onClick() wird der Text des Labels direkt geändert. Durch das Auslösen von fire()wird die Komponente mit der ID „event“ und damit alle Kindkomponenten aktualisiert.Listing 6.39 AjaxEventPage.html <html> <head> <title>AjaxEvent Page</title> </head> <body> <div wicket:id="event"> <span wicket:id="message"></span> </div> <a wicket:id="link">Klick mich</a> </body> </html>Durch das Ableiten von der Klasse AbstractAjaxEvent kann man dem Event weitereInformationen hinzufügen, die man dann beim Empfänger auswerten kann:Listing 6.40 AjaxEvent2Page.java package de.wicketpraxis.web.thema.komponenten.ajax; ... public class AjaxEvent2Page extends WebPage { private Label _message; public AjaxEvent2Page() { AjaxEventListener<Event> event = new AjaxEventListener<Event>("event",Event.class) { @Override protected boolean onAjaxEvent(Event event) { _message.setDefaultModelObject(event.getMessage()); return true; } }; _message = new Label("message","Text"); event.add(_message); add(event); 107
  • 6 Komponenten add(new AjaxLink("link") { @Override public void onClick(AjaxRequestTarget target) { new Event(this,target,"Link geklickt ("+new Date()+")").fire(); } }); } static class Event extends AbstractAjaxEvent { String _message; protected Event(Component source, AjaxRequestTarget requestTarget, String message) { super(source, requestTarget); _message=message; } public String getMessage() { return _message; } } } In diesem Beispiel bekommt die Event-Klasse ein Attribut Message, das in der überlade- nen Methode onAjaxEvent() ausgelesen wird. Der Text des Labels wird mit diesem Wert neu gesetzt. Zusammenfassung Die aufgeführten Beispiele demonstrieren gut, wie flexibel Wicket im Umgang mit Ajax ist. Da Anwendungen im Laufe der Zeit komplexer werden, sollte man in den meisten Fäl- len von Anfang an mehr Aufwand in die Ajax-Behandlung stecken. Der vorgestellte An- satz kann dabei als Anhaltspunkt dienen. Je nach Anwendung kann es sinnvoll sein, auch einen Rückkanal zuzulassen, oder z.B. neben dem Ajax-Event-System ein allgemeines Nachrichtensystem zu benutzen.108
  • 7 7 Basiskomponenten Wie wir bereits sehen konnten, ist es recht einfach, eine eigene Komponente zu erstellen. Die einfachsten Komponenten entstehen, indem man bereits vorhandene innerhalb der eigenen Komponente benutzt und so aus vielen kleinen einfachen Elemente etwas Größe- res und Komplexeres erschafft. Die Komplexität der Komponenten wird dabei gut ver- steckt, sodass es unerheblich ist, ob man ein einfaches Panel oder eine Komponente benutzt, die eine ganze Anwendung bereitstellt. Auf den folgenden Seiten erkläre ich die wichtigsten Komponenten, die bereits in Wicket enthalten sind.7.1 Gruppierende Komponenten Die beiden wichtigsten Komponenten sind uns bereits begegnet. Das wären die Klassen WebPage und Panel. Beide Komponenten sind von der Klasse MarkupContainer abgelei- tet. Diese Klasse stellt die Grundfunktionen bereit, die dafür verantwortlich sind, dass in- nerhalb einer Komponente andere Komponenten eingebunden werden können. Doch mit diesen beiden Komponenten sind die Möglichkeiten, andere Elemente einzubinden und zu gruppieren, noch nicht erschöpft. 7.1.1 Seiten Die Klasse WebPage ist die oberste Komponente und eine Analogie zur Webseite, die durch eine URL gekennzeichnet wird. Man benötigt in einer Wicket-Anwendung mindes- tens eine Klasse, die von WebPage abgeleitet ist. Auch wenn man, wie wir später sehen werden, eine Anwendung mit nur einer Seite entwickeln könnte, empfiehlt es sich aber schon aus Gründen der Ordnung, seine Anwendung auch über unterschiedliche Seiten zu strukturieren. 109
  • 7 Basiskomponenten 7.1.1.1 WebPage und Bookmarkable Pages Die Klasse WebPage bietet verschiedene Konstruktoren, von denen man mindestens einen überschreiben muss. Neben dem Konstruktor ohne Parameter ist noch ein Konstruktor auf- fällig, der einen Parameter vom Typ PageParameter erwartet. Wie eine Instanz der Klasse WebPage von Wicket erzeugt wird, erschließt sich durch folgendes Beispiel: Listing 7.1 SimplePage.java package de.wicketpraxis.web.thema.komponenten.basis.pages; ... public class SimplePage extends WebPage { public SimplePage() { initPage("Default Constructor"); } public SimplePage(PageParameters pageParameters) { initPage("Constructor with PageParameter: " + pageParameters); } public SimplePage(String message) { initPage("Constructor with Parameter: " + message + " (not bookmarkable)"); } protected void initPage(String message) { add(new Label("message", message)); add(new Link("link") { @Override public void onClick() { setResponsePage(new SimplePage("direkt")); } }); add(new BookmarkablePageLink<SimplePage>("linkBookmarkable", SimplePage.class)); add(new BookmarkablePageLink<SimplePage>("linkBookmarkableParameter", SimplePage.class,new PageParameters("p=1"))); } } In der Klasse habe ich drei Konstruktoren definiert. Der erste Konstruktor kommt zum Einsatz, wenn die Seite ohne irgendwelche Parameter aufgerufen wird. Das kann durch das Betätigen eines Links sein oder indem die Seite die Startseite der Anwendung ist. Der zweite Konstruktor wird aufgerufen, wenn die Seite mit Seitenparametern aufgerufen wur- de. In beiden Fällen kann der Nutzer ein Lesezeichen auf diese Seite setzen. Der dritte Konstruktor erwartet als Parameter einen einfachen String. Der Parameter kann natürlich durch jedes beliebige Objekt ersetzt werden. Eine Seite, die so erstellt wurde, kann nicht über eine fest definierte URL angesprochen werden. Damit ist die URL dieser Seite nicht für ein Lesezeichen geeignet.110
  • 7.1 Gruppierende KomponentenIn der Methode initPage() werden drei verschiedenen Link-Typen definiert, welche dieFunktionsweise veranschaulichen sollen. Der erste Link setzt als neue Zielseite die SeiteSimplePage mit entsprechendem Objekt als Parameter. Das ist ein Beispiel für den Ver-weis auf eine Seite, bei dem Informationen, die den Zustand der Seite beeinflussen können,innerhalb von Wicket über Parameter des Konstruktors weitergegeben werden. Der zweiteund der dritte Link verweisen auf die Seite, auf der die Klasse der Seite als Parameter be-nutzt wird. Auf diese Weise wird einer der beiden anderen Konstruktoren aufgerufen,wenn der Nutzer auf den Link geklickt hat.Listing 7.2 SimplePage.html <html> <head> <title>Simple Page</title> </head> <body> <span wicket:id="message"></span><br> <a wicket:id="link">direct to Page</a><br> <a wicket:id="linkBookmarkable">Page without PageParameter</a><br> <a wicket:id="linkBookmarkableParameter">Page with PageParameter</a><br> </body> </html>Beim initialen Aufruf wird folgender Text durch die Komponente mit der ID „message“dargestellt: Default ConstructorEin Klick auf den ersten Link ändert den Text in: Constructor with Parameter: direkt (not bookmarkable)Beim zweiten Link wird dasselbe wie beim initialen Seitenaufruf angezeigt, und beimKlick auf den dritten Link erscheint: Constructor with PageParameter: p = "[1]"Wenn der Konstruktor ohne Parameter weggelassen wird, wird automatisch der Konstruk-tor mit dem Parameter vom Typ PageParameter aufgerufen. Man sollte daher besser nureinen der ersten beiden Konstruktoren verwenden. Eine Anpassung ist jederzeit ohne Kon-sequenzen möglich.Wicket kann die URL, unter der die Seiten erreicht werden können, frei definieren. DieseURLs kommen aber nur zum Zuge, wenn die Seiten nicht über interne Parameter aufgeru-fen werden. Die URL muss man beim Applikationsstart definieren. Die einfachste Mög-lichkeit ist der Aufruf von mountBookmarkablePage(final String path, final Class<T> bookmarkablePageClass)innerhalb der init()-Methode der WebApplication-Klasse. Ausführlicher wird dasThema in Abschnitt 11.6 behandelt. 111
  • 7 Basiskomponenten 7.1.1.2 RedirectPage Es gibt verschiedenen Möglichkeiten, auf externe Seiten zu verlinken. Man könnte im Markup direkt einen Link definieren, der ohne Zuhilfenahme von Wicket funktioniert. Eine andere Möglichkeit ist der Aufruf einer Weiterleitungsseite, welche die Weiterleitung durchführt. Die einfachste Variante ist die RedirectPage, die als Parameter die URL und die Zeit in Sekunden erwartet, nach der die Zielseite aufgerufen werden soll. Listing 7.3 RedirectExternalPage.java package de.wicketpraxis.web.thema.komponenten.basis.pages; ... public class RedirectExternalPage extends RedirectPage { public RedirectExternalPage() { super("http://www.google.com",10); } } Auf diese Weise kann ich innerhalb meiner Anwendung die konkrete Klasse als Linkziel benutzen und somit sicherstellen, dass ich nur an einer Stelle die URL anpassen muss, wenn sich daran etwas verändern sollte. 7.1.1.3 HTTP Temporary Redirect Neben der Weiterleitung per MetaTag kann auch einer Weiterleitung per HTTP erfolgen. Dafür gibt es zwar keine vorgefertigte Klasse, aber der Aufwand für eine eigene Klasse ist sehr gering. Listing 7.4 RedirectHttpTemporaryPage.java package de.wicketpraxis.web.thema.komponenten.basis.pages; ... public class RedirectHttpTemporaryPage extends WebPage { public RedirectHttpTemporaryPage() { getRequestCycle().setRequestTarget( new RedirectRequestTarget("http://www.google.de")); } } Da wir im Konstruktor die Weiterleitung vornehmen, können wir auch von WebPage ablei- ten. Wenn die Seite aufgerufen wird, sendet Wicket eine Serverantwort mit dem HTTP- Statuscode 302 (Temporary Redirect). Diese Art der Weiterleitung eignet sich gut, wenn das Ziel sich immer wieder ändert. Suchmaschinen und Browser gehen dann davon aus, dass die Zielseite sich bei jedem Aufruf ändern könnte. 7.1.1.4 HTTP Permanent Redirect Angenommen, man hat eine neue Version einer Anwendung erstellt. Da die Anwendung anders funktioniert, hat sich vereinzelt auch die URL geändert. In den Suchmaschinen wird aber immer noch die alte Adresse gefunden, sodass Nutzer immer wieder auf den alten112
  • 7.1 Gruppierende KomponentenSeiten landen. Eine Weiterleitung lenkt zwar die Nutzer auf die neuen Seiten, aber dieSuchmaschinen gehen davon aus, dass die Weiterleitung nur temporär ist.Dabei ist die Lösung recht einfach. Statt eines HTTP-Statuscode 302 muss der Server nurmit dem Statuscode 301 (Moved Permanently) antworten. Leider ist das nicht ganz so ein-fach umzusetzen, da Wicket für den Fall leider keine entsprechende Klasse mitbringt.Dazu müssen wir eine eigene Implementierung von IRequestTarget erstellen. Das Inter-face RequestTarget gibt, wie der Name schon sagt, Auskunft darüber, wohin die Reisegehen soll, was also das Ziel dieses HttpRequests sein soll.Listing 7.5 RedirectPermanentRequestTarget.java package de.wicketpraxis.web.thema.komponenten.basis.pages; import javax.servlet.http.HttpServletResponse; ... public class RedirectPermanentRequestTarget implements IRequestTarget { String _redirectUrl; public RedirectPermanentRequestTarget(String url) { _redirectUrl=url; } public void respond(RequestCycle requestCycle) { WebResponse response = (WebResponse) requestCycle.getResponse(); response.reset(); if (_redirectUrl.startsWith("/")) { RequestContext rc = RequestContext.get(); if (rc.isPortletRequest() && ((PortletRequestContext)rc).isEmbedded()) { redirect(response, _redirectUrl); } else { String location = RequestCycle.get() .getProcessor() .getRequestCodingStrategy() .rewriteStaticRelativeUrl(_redirectUrl.substring(1)); if (location.startsWith("./") && location.length() > 2) { location = location.substring(2); } redirect(response, location); } } else if (_redirectUrl.contains("://")) { redirect(response, _redirectUrl); } else { redirect(response,RequestCycle.get() .getRequest() .getRelativePathPrefixToWicketHandler() + _redirectUrl); } 113
  • 7 Basiskomponenten } private void redirect(WebResponse response, String toUrl) { response.setHeader("Location", response.encodeURL(toUrl).toString()); response.getHttpServletResponse(). setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); } public void detach(RequestCycle requestCycle) {} } Als Vorlage diente die Klasse RedirectRequestTarget, die entsprechend umgeändert wurde. An der Stelle, an der in RedirectRequestTarget response.redirect() aufge- rufen wurde, setzen wir im HttpHeader den Parameter Location mit der Ziel-URL und setzen den Status der Serverantwort auf den gewünschten Wert. Man muss die Funktionsweise dieser Klasse an dieser Stelle noch nicht verstehen. Wichtig ist nur, dass der Statuscode entsprechend gesetzt wird. Die Benutzung ist dann wieder recht einfach. Listing 7.6 RedirectHttpPermanentPage.java package de.wicketpraxis.web.thema.komponenten.basis.pages; import org.apache.wicket.markup.html.WebPage; public class RedirectHttpPermanentPage extends WebPage { public RedirectHttpPermanentPage() { getRequestCycle().setRequestTarget( new RedirectPermanentRequestTarget("http://www.google.de")); } } 7.1.1.5 Migration mithilfe von Weiterleitungsseiten Bevor ich Webanwendungen mit Wicket entwickelt habe, entstanden bereits einige An- wendungen mit anderen Frameworks, die ich natürlich auf Wicket migrieren wollte. Da aber für eine komplette Umstellung der Aufwand viel zu hoch war, habe ich mich dafür entschieden, mit den Anwendungen Schritt für Schritt auf Wicket umzuziehen. Davon soll- te der Nutzer aber so wenig wie möglich mitbekommen. Das bedeutete, dass Verweise auf die alten Anwendungsteile eingebaut werden mussten. Es war schnell klar, dass die Verweise, also Links, nur an einer Stelle gepflegt werden soll- ten. Die Lösung sollte nicht nur auf die alte Seite weiterleiten, sondern auch später, wenn dieser Teil in Wicket realisiert wird, keine großen Anpassungen in den bereits implemen- tierten Anwendungen nach sich ziehen. Wie das erreicht werden kann, soll folgendes Bei- spiel demonstrieren. Listing 7.7 MigrationPage.java package de.wicketpraxis.web.thema.komponenten.basis.pages; ... public class MigrationPage extends WebPage114
  • 7.1 Gruppierende Komponenten { public MigrationPage(PageParameters pageParameters) { getRequestCycle().setRequestTarget( new RedirectRequestTarget( "/alteAnwendung?Nr="+pageParameters.getString("Nr"))); } public static AbstractLink getLink(String id,int nr) { return new BookmarkablePageLink<MigrationPage>( id,MigrationPage.class, new PageParameters("Nr="+nr)); } }Die Seite selbst folgt dem Beispiel für die temporäre Weiterleitungsseite. Die Einbindungin andere Seiten erfolgt über den Aufruf der statischen Methode getLink(), die neben derID für den Link außerdem die Parameter für die Seite entgegennimmt. Der Rückgabewertist vom Typ AbstractLink, sodass man jeden beliebigen Link benutzen kann. In demBeispiel benutze ich einen BookmarkablePageLink, mit dem ich die Seite mit den ent-sprechenden Parametern aufrufe. Wenn die Seite geladen wird, wird dann im Konstruktorauf die alte Seite weitergeleitet.Um dann in einer Komponente oder Seite einen Link auf diese Seite zu setzen, reicht dannfolgender Funktionsaufruf: add(MigrationPage.getLink("link",7));7.1.1.6 URL der aktuellen SeiteImmer wieder kommt es vor, dass man die URL der aktuellen Seite benötigt. Wenn manE-Mails verschicken muss, in denen ein Registrierungslink enthalten ist, benötigt man dieURL von einer anderen Seite.Listing 7.8 GetCurrentUrlPage.java package de.wicketpraxis.web.thema.komponenten.basis.pages; ... public class GetCurrentUrlPage extends WebPage { public GetCurrentUrlPage(PageParameters pageParameters) { CharSequence currentUrl = urlFor(getRequestCycle().getRequestTarget()); add(new Label("currentUrl",currentUrl.toString())); add(new Label("currentUrlAbs", RequestUtils.toAbsolutePath(currentUrl.toString()))); CharSequence customUrl = urlFor(GetCurrentUrlPage.class, new PageParameters("a=1,b=2")); add(new Label("customUrl",customUrl.toString())); add(new BookmarkablePageLink<GetCurrentUrlPage>( "link",GetCurrentUrlPage.class,new PageParameters("c=3"))); } } 115
  • 7 Basiskomponenten Listing 7.9 GetCurrentUrlPage.html ... <body> Aktuelle URL: <span wicket:id="currentUrl"></span><br> Aktuelle URL(absolut): <span wicket:id="currentUrlAbs"></span><br> URL <span wicket:id="customUrl"></span><br><br> <a wicket:id="link">Link mit Parametern</a><br> </body> ... Auf der Ergebnisseite wird Folgendes angezeigt (an den durch ... markierten Stellen wurde gekürzt): Aktuelle URL: ?wicket:bookmarkablePage=:....GetCurrentUrlPage Aktuelle URL(absolut): http://192.168.2.96:8080/de.wicketpraxis--webapp/ ?wicket:bookmarkablePage=:....GetCurrentUrlPage URL: ?wicket:bookmarkablePage=:....GetCurrentUrlPage&a=1&b=2 Auf diese Weise ist es möglich, die benötigten URLs zur Laufzeit zu ermitteln. Wenn die- se Seite z.B. mit mountBookmarkablePage() an den Pfad /testUrlSeite gebunden wäre, würde statt ?wicket: bookmarkablePage=:....GetCurrentUrlPage einfach nur http://server/testUrlSeite stehen. Die Parameter der Seite werden dann an die URL angehängt. 7.1.1.7 ResourceReference Mit der Klasse ResourceReference kann man auf Ressourcen im Klassenpfad der An- wendung zugreifen. Dabei stellt die Klasse sicher, dass es eine zur konfigurierten Sprach- einstellung, dem Stil (Session.getStyle()) und dem Wert aus Component.getVaria- tion() passende Ressource auswählt. Der erste Parameter gibt die Referenzklasse an. Die Datei wird dann innerhalb des Verzeichnisses, des Pakets der Klasse gesucht. Diese Klasse wird z.B. zum Einbinden von Grafiken (siehe Abschnitt 7.2.5.1), JavaScript- und CSS- Dateien benutzt. 7.1.1.8 JavaScript und CSS Eine Webanwendung ohne eine Stylesheet-Datei, in der das Erscheinungsbild der Anwen- dung definiert wird, ist kaum vorstellbar. Um die Datei im Kopf einzubinden, genügt ein einfacher Aufruf. Da es nicht unüblich ist, dass man auch einige JavaScript-Funktionen benutzt, zeige ich im nächsten Beispiel, wie man die nötigen Verweise im Kopfbereich erscheinen lässt. Listing 7.10 HeaderReferencesPage.java package de.wicketpraxis.web.thema.komponenten.basis.pages; ... public class HeaderReferencesPage extends WebPage { public HeaderReferencesPage()116
  • 7.1 Gruppierende Komponenten { add(CSSPackageResource.getHeaderContribution( HeaderReferencesPage.class, "styles/standard.css")); add(CSSPackageResource.getHeaderContribution( new ResourceReference( HeaderReferencesPage.class, "styles/locale.css",getLocale(),getStyle()))); add(JavascriptPackageResource.getHeaderContribution( HeaderReferencesPage.class, "js/test.js")); add(new HeaderContributor(new IHeaderContributor() { public void renderHead(IHeaderResponse response) { response.renderString("<!-- mein Beitrag -->"); } })); } }In der ersten Zeile fügen wir eine CSS-Datei ein. In der zweiten Zeile ergänzen wir einesprachabhängige CSS-Datei. Mit dem dritten Aufruf binden wir eine JavaScript-Datei ein,und der vierte Block zeigt, wie man z.B. einen Kommentar im Kopfbereich der Seite hin-zufügt. Hinweis Als Basisverzeichnis für die JavaScript- und CSS-Dateien wird das Paketverzeichnis der angegebenen Klasse verwendet. Wenn man statt der aktuellen Klasse z.B. die Applikations- klasse benutzt, werden die Referenzen in Paketverzeichnis der Applikationsklasse gesucht. Da sich diese Klasse viel seltener ändert als alle anderen Klassen und die Klasse eine ver- gleichsweise zentrale Rolle spielt, empfiehlt sich dieses Vorgehen immer dann, wenn für die Anwendung eine globales CSS-Datei verwendet werden soll.Nicht nur Seiten, sondern jede Komponente kann auf diese Weise JavaScript-, CSS- oderandere Referenzen einbinden. Dabei sorgt Wicket dafür, dass dieselbe JavaScript-Dateinicht mehrfach eingebunden wird.7.1.2 PanelDie einfachste Komponente ist das Panel. Innerhalb eines Panels kann man beliebige ande-re Komponenten verwenden und so die innere Komplexität von außen verstecken.Listing 7.11 SimplePanel.java package de.wicketpraxis.web.thema.komponenten.basis.panels; ... public class SimplePanel extends Panel { public SimplePanel(String id,String message) { super(id); add(new Label("message",message)); } } 117
  • 7 Basiskomponenten Listing 7.12 SimplePanel.html <wicket:panel> <div style="background-color: #ffcccc; border:1px solid #cc8888;"> <span wicket:id="message">Message</span> </div> </wicket:panel> Um auch an dieser Stelle noch einmal zu demonstrieren, dass jede Komponente Informa- tionen zum Kopfbereich einer Seite hinzufügen kann, leiten wir von unserer Panel-Kompo- nente eine weitere ab. Listing 7.13 SimpleWithHeaderPanel.java package de.wicketpraxis.web.thema.komponenten.basis.panels; import org.apache.wicket.markup.html.CSSPackageResource; public class SimpleWithHeaderPanel extends SimplePanel { public SimpleWithHeaderPanel(String id, String message) { super(id, message); add(CSSPackageResource.getHeaderContribution( getClass(), "styles/standard.css")); } } Diese Komponente bekommt kein eigenes Markup. Wir müssen aber die verwendete CSS- Datei erstellen. Dazu legen wir das Verzeichnis styles an und erstellen eine CSS-Datei. Listing 7.14 standard.css span { color:red; } div { margin:2px; } Auf der folgenden Seite benutzen wir diese Komponenten, um das Ergebnis zu überprüfen. Listing 7.15 SimplePanelPage.java package de.wicketpraxis.web.thema.komponenten.basis.panels; ... public class SimplePanelPage extends WebPage { public SimplePanelPage() { add(new SimplePanel("p1","Panel1")); add(new SimpleWithHeaderPanel("p2","Panel2")); add(new SimpleWithHeaderPanel("p3","Panel3")); add(new EmptyPanel("p4")); } } Listing 7.16 SimplePanelPage.html <html> <head> <title>Simple Panel Page</title>118
  • 7.1 Gruppierende Komponenten </head> <body> <wicket:container wicket:id="p2"></wicket:container> <wicket:container wicket:id="p1"></wicket:container> <wicket:container wicket:id="p3"></wicket:container> <wicket:container wicket:id="p4"></wicket:container> </body> </html>Ich habe in diesem Beispiel absichtlich die Reihenfolge der Komponenten im Markup ver-tauscht, da die Positionen der Komponenten erst im Markup definiert werden. Im Quelltextkönnen wir erkennen, dass der Verweis auf die CSS-Datei genau einmal im Kopfbereichaufgeführt wird, obwohl die Komponente mehrfach verwendet wurde.Listing 7.17 Ergebnis.html <html> <head> <title>Simple Panel Page</title> <link rel="stylesheet" type="text/css" href="resources/...SimpleWithHeaderPanel/styles/standard.css" /> </head> <body> ... </body> </html>7.1.3 FragmentWenn man innerhalb einer Komponente viele kleine, einfache Komponenten benutzt, kannes sich recht aufwendig gestalten, wenn man für jede von Panel abgeleitete Komponenteeine eigene Markup-Datei anlegen müsste. Viel einfacher wäre es, wenn man diese weni-gen Zeilen im Markup der Hauptkomponenten unterbringen könnte. Genau für diesen An-wendungsfall gibt es die Klasse Fragment. Die der Panel-Klasse sehr ähnliche Kompo-nente unterscheidet sich dadurch, dass das Markup der Klasse in einem anderen Markupeingebunden ist. Dabei muss man nicht notwendigerweise von Fragment ableiten, sondernkann die Klasse auch direkt verwenden.Listing 7.18 SimpleFragmentPage.java package de.wicketpraxis.web.thema.komponenten.basis.panels; ... public class SimpleFragmentPage extends WebPage { public SimpleFragmentPage() { add(new Fragment("p1","fragment1",this).add( new Label("message","p1"))); add(new Fragment("p2","fragment2",this).add( new Label("message","p2"))); add(new Fragment("p3","fragment1",this).add( new Label("message","p3"))); } } 119
  • 7 Basiskomponenten Der erste Parameter der Klasse ist die Wicket-ID, der zweite Parameter gibt die Fragment- Markup-ID an und der dritte Parameter einen MarkupContainer, also eine Komponente mit assoziiertem Markup, in der das Markup für das Fragment enthalten ist. In diesem Fall ist das die Seite. Das Markup fällt damit etwas komplizierter aus und ist erklärungsbedürf- tig. Listing 7.19 SimpleFragmentPage.html ... <body> <wicket:fragment wicket:id="fragment1"> <div style="background-color: #ffcccc; border:1px solid #cc8888;"> <span wicket:id="message">Message</span> </div> </wicket:fragment> <wicket:fragment wicket:id="fragment2"> <div style="background-color: #ccffcc; border:1px solid #88cc88;"> <span wicket:id="message">Message</span> </div> </wicket:fragment> <wicket:container wicket:id="p1"></wicket:container> <wicket:container wicket:id="p2"></wicket:container> <wicket:container wicket:id="p3"></wicket:container> </body> ... Am Anfang der Datei kann man das Markup für die Fragmente erkennen. Das wi- cket:fragment-Tag mit der Fragment-ID umschließt das Markup, das dann für die Frag- mente benutzt wird. Die Position für das Fragment-Markup ist nicht festgelegt. In Abbil- dung 7.1 kann man sehen, dass beide Fragment-Markups benutzt werden. Abbildung 7.1 Fragmente Fragmente bieten sich immer dann an, wenn man innerhalb einer Komponente bestimmte Funktionen herauslösen möchte. Für ein Fragment muss dann keine eigene Markup-Datei erstellt werden. Es ist auf der anderen Seite allerdings sehr einfach, aus einem Fragment eine eigenständige Komponente zu erstellen. Dazu kopiert man den Fragment-Block in eine eigenständige Markup-Datei und leitet die Klasse von einem Panel ab. Fragmente bieten sich daher immer als Zwischenschritt in einem Refactoring-Prozess an. 7.1.4 Border Die Funktion der Border-Klasse erschließt sich schon aus ihrem Namen: Eine Border- Klasse kann um die enthaltenen Elemente einen Rahmen ziehen. In unserem Beispiel (Ab- bildung 7.2) leiten wir zwei Klassen von der Border-Klasse ab. Dabei ergibt sich der Name der Markup-Dateien aus dem Klassennamen der umschließenden Klasse und dem Klassennamen der abgeleiteten Klassen.120
  • 7.1 Gruppierende Komponenten Abbildung 7.2 Zwei unterschied- liche Border-KomponentenListing 7.20 SimpleBorderPage.java package de.wicketpraxis.web.thema.komponenten.basis.panels; ... public class SimpleBorderPage extends WebPage { public SimpleBorderPage() { RedBorder redBorder = new RedBorder("red"); redBorder.add(new Label("message","in red border")); add(redBorder); BlueBorder blueBorder = new BlueBorder("blue"); blueBorder.add(new Label("message","in blue border")); add(blueBorder); } static class RedBorder extends Border { public RedBorder(String id) { super(id); } } static class BlueBorder extends Border { public BlueBorder(String id) { super(id); } } }Listing 7.21 SimpleBorderPage.html ... <body> <wicket:container wicket:id="red"> <span wicket:id="message"></span> </wicket:container> <wicket:container wicket:id="blue"> <span wicket:id="message"></span> </wicket:container> </body> ...Listing 7.22 SimpleBorderPage$RedBorder.html <wicket:border> <div style="background-color: #ffcccc; border:1px solid #cc8888;"> <i>before</i> <wicket:body/> <i>after</i> </div> </wicket:border>Listing 7.23 SimpleBorderPage$BlueBorder.html <wicket:border> <div style="background-color: #ccccff; border:1px solid #8888cc;"> [<wicket:body/>] </div> </wicket:border> 121
  • 7 Basiskomponenten Das Markup für eine Border wird von wicket:border umschlossen. Das wicket:body- Tag gibt an, an welcher Stelle die Kindelemente der Border eingefügt werden sollen. 7.1.4.1 Border-Klasse mit Komponenten Eine Border-Komponente kann auch eigene Komponenten enthalten. Dabei ist zu beach- ten, dass die verwendeten Komponenten-IDs nicht mit den IDs der Elemente in derselben Hierarchiestufe kollidieren. Listing 7.24 ComplexBorderPage.java package de.wicketpraxis.web.thema.komponenten.basis.panels; ... public class ComplexBorderPage extends WebPage { public ComplexBorderPage() { ComplexBorder complexBorder = new ComplexBorder("border", "Its a border."); complexBorder.add(new Label("message","in a border")); add(complexBorder); ComplexBorder complexBorder2 = new ComplexBorder("border2","Its an other border."); complexBorder2.setTransparentResolver(true); add(complexBorder2); add(new Label("message","again in a border?")); } static class ComplexBorder extends Border { public ComplexBorder(String id,String message) { super(id); add(new Label("border_message",message)); } } } Wir fügen der Seite zwei Border-Komponenten hinzu (Abbildung 7.3). Allerdings fügen wir das zweite Label mit der ID „message“ zur Seite und nicht unterhalb der zweiten Bor- der-Komponente hinzu. Listing 7.25 ComplexBorderPage.html ... <body style="width: 300px;"> <wicket:container wicket:id="border"> <span wicket:id="message"></span> </wicket:container> <br> <wicket:container wicket:id="border2"> <span wicket:id="message"></span> </wicket:container> </body> ... Dabei fällt auf, dass entgegen den Ausführungen in der Seitenklasse das zweite Label mit der ID message doch von der zweiten Instanz unserer Border-Klasse umschlossen wird.122
  • 7.1 Gruppierende Komponenten Abbildung 7.3 Border-Komponente mit KindelementDamit Wicket die Komponente trotzdem findet, wird durch den Aufruf von setTranspa-rentResolver(true) der Suchpfad entsprechend erweitert. Es kann vorkommen, dass soeine Konstruktion notwendig ist. Aber um sich die Fehlersuche zu vereinfachen, sollte manauf eine saubere Hierarchie achten und auf solche „Tricks“ verzichten. Im Markup für dieBorder-Komponente können wir erkennen, an welcher Stelle das eingebettete Elementverwendet wird.Listing 7.26 ComplexBorderPage$ComplexBorder.html <wicket:border> <div style="background-color: #ccccff; border:1px solid #8888cc; padding:4px;"> <span wicket:id="border_message"></span> <div style="background-color: white; margin:4px; padding:4px; border:1px dotted black;"> [<wicket:body/>] </div> </div> </wicket:border>7.1.4.2 SichtbarkeitsteuerungEine Border-Komponente eignet sich ausgezeichnet, um Komponenten ein- und auszu-blenden, da die unsichtbare Border-Komponente automatisch alle Kindelemente unsicht-bar macht. In dem folgenden Beispiel ist die Sichtbarkeit von dem Wert eines Modells ab-hängig. Da dieser Anwendungsfall relativ häufig vorkommt, empfiehlt es sich, dafür eineallgemeine Border-Komponente zu erstellen.Listing 7.27 AbstractVisibleBorder.java package de.wicketpraxis.web.thema.komponenten.basis.panels; ... public abstract class AbstractVisibleBorder<T> extends Border { public AbstractVisibleBorder(String id, IModel<T> model) { super(id, model); } public AbstractVisibleBorder<T> getInvers(String id) { final AbstractVisibleBorder<T> _this=this; return new AbstractVisibleBorder<T>(id,(IModel<T>) getDefaultModel()) { protected boolean isVisibleWith(T object) { 123
  • 7 Basiskomponenten return !_this.isVisibleWith(object); }; }; } @Override protected void onBeforeRender() { super.onBeforeRender(); IModel<T> model = (IModel<T>) getDefaultModel(); setVisible(isVisibleWith(model.getObject())); } @Override protected boolean callOnBeforeRenderIfNotVisible() { return true; } protected abstract boolean isVisibleWith(T object); } Die Komponente prüft in der Methode onBeforeRender() die Sichtbarkeit über die zu überschreibende Methode isVisibleWith() mit den Daten des Modells als Parameter. Die Methode callOnBeforeRenderIfNotVisible() wurde so überschrieben, dass diese Prüfung auch durchgeführt wird, wenn die Komponente unsichtbar ist. Die getInvers()- Methode liefert eine Border zurück, die sich genau entgegengesetzt verhält. Listing 7.28 AbstractVisibleBorder.htm <wicket:border><wicket:body/></wicket:border> Das Markup ist sehr kurz, denn es enthält keine zusätzlichen Informationen oder Kompo- nenten. Das folgende Beispiel veranschaulicht die Funktion. Listing 7.29 VisibleBorderPage.java package de.wicketpraxis.web.thema.komponenten.basis.panels; ... public class VisibleBorderPage extends WebPage { public VisibleBorderPage(PageParameters pageParameters) { AbstractVisibleBorder<String> border= new AbstractVisibleBorder<String>( "border",Model.of(pageParameters.getString("border"))) { @Override protected boolean isVisibleWith(String object) { if ((object==null) || (object.equals("show"))) { return true; } return false; } }; border.add(new Label("message","in a border")); add(border); AbstractVisibleBorder<String> invers = border.getInvers("invers");124
  • 7.1 Gruppierende Komponenten invers.add(new Label("message","second in a border")); add(invers); add(new BookmarkablePageLink<VisibleBorderPage>( "show",VisibleBorderPage.class)); add(new BookmarkablePageLink<VisibleBorderPage>( "hide",VisibleBorderPage.class,new PageParameters("border=hide"))); } }Listing 7.30 VisibleBorderPage.html ... <body style="width: 300px;"> <wicket:container wicket:id="border"> <span wicket:id="message"></span> </wicket:container> <br> <wicket:container wicket:id="invers"> <span wicket:id="message"></span> </wicket:container> <br> <a wicket:id="show">1. zeigen, 2. verstecken</a><br> <a wicket:id="hide">1. verstecken, 2. zeigen</a> </body> ...Wenn die Seite geladen wird, wird das erste Label angezeigt. Wenn man auf den zweitenLink klickt, dann verschwindet das erste Label, und das zweite wird angezeigt.7.1.5 ComponentBorderDie einfachste Möglichkeit, um ein Element einen Rahmen zu ziehen, besteht darin, fürdas Element eine Instanz der Klasse IComponentBorder zu setzen. Da diese Klasse nichtvon Component ableitet, kann sie selbst keine Komponenten einbinden. Das bedeutet auch,dass man keine ID vergeben kann und muss. Deshalb ist dieses Element auch nicht imMarkup zu sehen. Der Einsatz der ComponentBorder hat damit nur Einfluss auf die Dar-stellung der Komponente (siehe Abbildung 7.4) und keine wirkliche gruppierende Funk-tion.Listing 7.31 ComponentBorderPage.java package de.wicketpraxis.web.thema.komponenten.basis.panels; ... public class ComponentBorderPage extends WebPage { public ComponentBorderPage() { add(new Label("l1","in red border").setComponentBorder( new RedBorder())); add(new Label("l2","in blue border").setComponentBorder( new BlueBorder())); } static class RedBorder implements IComponentBorder { public void renderBefore(Component component) { 125
  • 7 Basiskomponenten component.getResponse().write( "<div style="border:1px solid red;">"); } public void renderAfter(Component component) { component.getResponse().write("</div>"); } } static class BlueBorder extends MarkupComponentBorder { } } Die Klasse RedBorder implementiert die Schnittstelle direkt und schreibt den gewünsch- ten HTML-Code direkt in die Ergebnisseite. Die Klasse BlueBorder wird von Markup- ComponentBorder abgeleitet. Diese Klasse ist mit einer Markup-Datei assoziiert, die von der Struktur dem Markup für eine Border-Komponente gleicht. Der Unterschied besteht darin, dass keine Elemente eingebettet werden können, da die Klasse nicht von Component erbt. Listing 7.32 ComponentBorderPage.html ... <body style="width: 300px;"> <span wicket:id="l1"></span><br> <span wicket:id="l2"></span><br> </body> ... Listing 7.33 ComponentBorderPage$BlueBorder.html <wicket:border> <div style="background-color: #ccccff; border:1px solid #8888cc; padding:4px;"> [<wicket:body/>] </div> </wicket:border> Abbildung 7.4 Komponenten mit gesetzter ComponentBorder Da eine Komponente nur einen Komponentenrahmen haben kann, sind die Einsatzmög- lichkeiten begrenzt. Dass man diese Funktion verwenden kann, ohne dass man das Mark- up der Komponente anpassen muss, macht es gerade im Hinblick auf eine schnelle und schlanke Möglichkeit, eine Komponente mit einem Rahmen versehen zu können, interes- sant. 7.1.6 WebMarkupContainer Die einfachste Art und Weise, die Hierarchie der Elemente innerhalb einer Komponente zu strukturieren, ist die Verwendung der Klasse WebMarkupContainer. Die Klasse benutzt kein eigenes Markup, sondern wird üblicherweise an ein HTML-Tag innerhalb der Kom- ponente gebunden. Auf diese Weise kann man die Sichtbarkeit des Tags und der eventuell126
  • 7.2 Inhaltselemente vorhandenen Kindelemente beeinträchtigen. Mann kann auch die Darstellung des Tags verändern, indem man über noch zu behandelnde Funktionen Attribute hinzufügt oder ma- nipuliert. In diesem Beispiel möchten wir uns aber auf die Verwendung als gruppierendes Element beschränken. Listing 7.34 SimpleWebMarkupContainerPage.java package de.wicketpraxis.web.thema.komponenten.basis.panels; ... public class SimpleWebMarkupContainerPage extends WebPage { public SimpleWebMarkupContainerPage() { add(new WebMarkupContainer("p1").add(new Label("message","p1"))); add(new WebMarkupContainer("p2").add(new Label("message","p2"))); } } Listing 7.35 SimpleWebMarkupContainerPage.html ... <body> <div wicket:id="p1" style="background-color: #ffcccc; border:1px solid #cc8888;"> <span wicket:id="message">Message</span> </div> <div wicket:id="p2" style="background-color: #ccffcc; border:1px solid #88cc88;"> <span wicket:id="message">Message</span> </div> </body> ... Wie man sehen kann, gibt es keine Kollision mit der ID message, weil die Elemente je- weils Kinder der WebMarkupContainer-Instanzen sind.7.2 Inhaltselemente In vielen Beispielen haben wir bereits gesehen, wie man Texte darstellt. Dabei haben wir bisher die einfachste Form der Textdarstellung kennengelernt. Webseiten bestehen nicht nur aus Text. Wie man Grafiken einbindet und welche Möglichkeiten Wicket dabei bietet, werden wir auf den nächsten Seiten behandeln. 7.2.1 Label und MultiLineLabel Die Klasse Label ist die einfachste Komponente für die Darstellung von Textinformatio- nen. Es gibt aber auch hier eine Besonderheit zu beachten: Wicket sorgt dafür, dass für Inhalte, die durch die Klasse dargestellt werden, alle Buchstaben, die eine Bedeutung in- nerhalb der HTML-Seite haben könnten, umgeschrieben werden. Wenn z.B. der darzustel- lende Text aus einem Datenbankeintrag stammt, der durch einen Nutzer erzeugt werden konnte, bestünde sonst das Risiko, dass über diesen Weg das Aussehen und das Verhalten 127
  • 7 Basiskomponenten der angezeigten Seite beeinflusst werden könnten. Auch wenn dieses Verhalten abschaltbar ist, sollte man sicherstellen, dass man dadurch die Anwendung nicht gefährdet. Listing 7.36 SimpleLabelPage.java package de.wicketpraxis.web.thema.komponenten.basis.content; ... public class SimpleLabelPage extends WebPage { public SimpleLabelPage() { IModel<String> text=Model.of("Text und <strong>Html</strong><br>n wird unterschiedlich dargestellt."); add(new Label("text",text)); add(new Label("textMitHtml",text).setEscapeModelStrings(false)); IModel<String> texte=Model.of("Text und <strong>Html</strong>n wird unterschiedlich dargestellt.nnZweiter Absatz."); add(new MultiLineLabel("texte",texte)); add(new MultiLineLabel("texteMitHtml",texte). setEscapeModelStrings(false)); } } Listing 7.37 SimpleLabelPage.html ... <body> <span wicket:id="text"></span> <hr> <span wicket:id="textMitHtml"></span> <hr> <span wicket:id="texte"></span> <hr> <span wicket:id="texteMitHtml"></span> <hr> </body> ... In diesem Beispiel ist der Quelltext zur Abbildung 7.5 interessant. Man kann sehr gut er- kennen, wie in den Fällen, wo setEscapeModelStrings(false) nicht aufgerufen wurde, die „gefährlichen Elemente“ entschärft wurden. Die Klasse MultiLineLabel wandelt da- bei einen vorhandenen Zeilenumbruch in ein br-Tag und zwei aufeinanderfolgende Zei- lenumbrüche in ein p-Tag um. Listing 7.38 Ergebnis.html ... <body> <span>Text und &lt;strong&gt;Html&lt;/strong&gt;&lt;br&gt; wird unterschiedlich dargestellt.</span> <hr> <span>Text und <strong>Html</strong><br> wird unterschiedlich dargestellt.</span> <hr> <span><p>Text und &lt;strong&gt;Html&lt;/strong&gt;<br/> wird unterschiedlich dargestellt.</p><p>Zweiter Absatz.</p></span> <hr> <span><p>Text und <strong>Html</strong><br/> wird unterschiedlich dargestellt.</p><p>Zweiter Absatz.</p></span> <hr> </body> ...128
  • 7.2 Inhaltselemente Abbildung 7.5 Verschiedene Label- Komponenten7.2.2 Lokaler KonverterWenn ein Label ein Objekt auf der Seite darstellen soll, muss es für dieses Objekt eineTextdarstellung ermitteln. Für diese Umwandlung benutzt die Label-Klasse einen Konver-ter, der diese Umwandlung für den Objekttyp vornehmen kann. Wenn man nicht auf den inder Anwendung definierten Konverter zurückgreifen möchte, erstellt man einfach eineneigenen.Listing 7.39 ConverterWithLabelPage.java package de.wicketpraxis.web.thema.komponenten.basis.content; ... public class ConverterWithLabelPage extends WebPage { public ConverterWithLabelPage() { IModel<Color> colorModel=Model.of(new Color(255,128,64)); add(new Label("standard",colorModel)); add(new Label("custom",colorModel) { @Override public IConverter getConverter(Class<?> type) { if (type==Color.class) { return new ColorConverter(); } return super.getConverter(type); } }); } static class ColorConverter implements IConverter { public Object convertToObject(String value, Locale locale) { return null; 129
  • 7 Basiskomponenten } public String convertToString(Object value, Locale locale) { Color color=(Color) value; return "("+color.getRed()+","+ color.getGreen()+","+color.getBlue()+")"; } } } Listing 7.40 ConverterWithLabelPage.html ... <body> <span wicket:id="standard"></span><br> <span wicket:id="custom"></span><br> </body> ... Wir erhalten dann folgendes Ergebnis: java.awt.Color[r=255,g=128,b=64] (255,128,64) Die erste Darstellung entspricht dem Aufruf der Methode toString() der Color-Klasse, die zweite Darstellung zeigt, dass unser Konverter eine Instanz der Klasse Color erfolg- reich in einen String umgewandelt hat. 7.2.3 XML Wenn man mit Wicket statt einer HTML-Seite XML ausgeben möchte, dann muss man eigentlich nur eine Anpassung vornehmen. Man muss die Methode getMarkupType() überschreiben. Listing 7.41 XmlPage.java package de.wicketpraxis.web.thema.komponenten.basis.content; ... public class XmlPage extends WebPage { public XmlPage() { add(new Label("name","Klaus")); } @Override public String getMarkupType() { return "xml"; } } Die Markup-Datei sucht Wicket jetzt allerdings nicht mehr mit der Endung „html“, son- dern mit der Dateierweiterung „xml“. Auch wenn es geeignetere Möglichkeiten gibt, wie man Daten als XML ausgibt, sollte diese Variante als schlanke Alternative im Hinterkopf behalten.130
  • 7.2 InhaltselementeListing 7.42 XmlPage.xml <?xml version=1.0 encoding=utf-8?> <someXml xmlns:wicket="http://wicket.apache.org/"> <person> <name wicket:id="name"></name> </person> </someXml>7.2.4 Das wicket:message-TagKeine Komponente, aber eine Markup-Funktionalität stellt das wicket:message-Tag be-reit. Dieses Tag führt dazu, dass Inhalte der Seite durch Texte aus Property-Dateien ersetztwerden können. Dabei stellt dieses Tag dieselben Funktionen wie das StringResource-Model (siehe Abschnitt 5.6.3) bereit, ohne dass man eine Zeile Code dafür schreiben muss.Die Klasse für die Seite sieht daher sehr einfach aus.Listing 7.43 WicketMessageTagPage.java package de.wicketpraxis.web.thema.komponenten.basis.content; import org.apache.wicket.markup.html.WebPage; public class WicketMessageTagPage extends WebPage { public WicketMessageTagPage() { } public String getHello() { return "I am a Wicket Message Page"; } }Listing 7.44 WicketMessageTagPage.html ... <body> <span wicket:message="style:message.style"> <wicket:message key="message.text"> from properties </wicket:message> </span><br> <span wicket:message="style:message2.style"> <wicket:message key="message2.text"> from properties </wicket:message> </span><br> <br> <wicket:message key="message.textMitProperty">from properties</wicket:message> </body> ...Man kann wicket:message als eigenständiges Tag benutzen. Dann wird der Inhalt desTags und im Deployment-Modus auch das Tag selbst mit dem Inhalt des passenden Ein-trags aus der Property-Datei ersetzt. Wenn man wicket:message als Attribut innerhalbeines Tags benutzt, dann gibt der Teil vor dem Doppelpunkt den zu ersetzenden Attribut-namen an. 131
  • 7 Basiskomponenten Listing 7.45 WicketMessageTagPage.properties message.text=This is some text message.style=color:red; message2.text=This is some other text message2.style=color:yellow; message.textMitProperty=Text WicketMessageTagPage.getHello(): <strong>${hello}</strong> Die zweite Property-Datei legen wir im XML-Format an. Wicket unterstützt beide Versio- nen. Listing 7.46 WicketMessageTagPage_de.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <entry key="message.text"><![CDATA[Das ist der Text in <strong>Deutsch</strong>]]></entry> <entry key="message.style">color:blue</entry> <entry key="message2.style">color:green</entry> </properties> Um in der XML-Variante der Property-Datei ein eingebettetes HTML-Tag schreiben zu können, muss der ganze Eintrag in einer CDATA-Definition stehen. Da die normale Pro- perty-Datei immer die ISO-8859-1-Kodierung benutzt, ist die Benutzung von Sonder- zeichen und Umlauten sehr erschwert. Das Eurozeichen müsste in dem Fall als u20ac umschrieben werden. Die XML-Variante benutzt, wenn nicht anders definiert, die UTF-8- Kodierung. In Abbildung 7.6 können wir sehen, dass die HTML-Tags aus den Property- Dateien ungefiltert dargestellt wurden. Abbildung 7.6 Das wicket:message-Tag 7.2.5 Image Einfache Grafiken werden in Wicket mit der Image-Komponente eingebunden. Dabei un- terscheidet sich die Image-Komponente in Bezug auf die Anpassbarkeit an Spracheinstel- lung, Style und Variation nicht von anderen Komponenten. 7.2.5.1 Grafiken direkt Einbinden Das nachfolgende Beispiel zeigt unterschiedliche Möglichkeiten, wie man Grafiken ein- binden kann. Gerade in Zusammenhang mit der Auswahl der passenden Grafik in Abhän- gigkeit zur Lokalisation und des eingestellten Stils (Session.getStyle()) gibt es ein paar Besonderheiten zu beachten.132
  • 7.2 InhaltselementeListing 7.47 SimpleImagePage.java package de.wicketpraxis.web.thema.komponenten.basis.content; ... public class SimpleImagePage extends WebPage { public SimpleImagePage() { add(new Label("locale",Model.of(getLocale()))); add(new Label("style",getStyle())); // test1.gif add(new Image("image1","test1.gif")); // test2.gif, test2_de_DE.gif add(new Image("image2","test2.gif")); add(new Image("image2ResourceRef",new ResourceReference( SimpleImagePage.class,"test2.gif",getLocale(),getStyle()))); add(new Image("image2ResourceRefOhne",new ResourceReference( SimpleImagePage.class,"test2.gif"))); add(new Image("image2ResourceRefMit",new ResourceReference( SimpleImagePage.class,"test2.gif",null,null))); // test3.gif, test3_rot.gif add(new Image("image3","test3.gif")); add(new Image("image3rot","test3.gif") { @Override public String getVariation() { return "rot"; } }); // test4.gif nicht vorhanden add(new Image("image4","test4.gif")); } }Listing 7.48 SimpleImagePage.html ... <body> Locale: <span wicket:id="locale">Locale</span>, Style: <span wicket:id="style">Style</span> <br> <img wicket:id="image1"> <wicket:link> <img src="test1.gif"> </wicket:link> <br> <img wicket:id="image2"> <wicket:link> <img src="test2.gif"> </wicket:link> <br> <img wicket:id="image2ResourceRef"> <img wicket:id="image2ResourceRefOhne"> <img wicket:id="image2ResourceRefMit"> <br> <img wicket:id="image3"> <img wicket:id="image3rot"> 133
  • 7 Basiskomponenten <br> <img wicket:id="image4"> </body> ... Zusätzlich benötigen wir noch folgende Grafikdateien, die sich im selben Verzeichnis wie die Markup-Datei befinden: test1.gif test2.gif und test2_de_DE.gif test3.gif und test3_rot.gif Die referenzierte Grafik test4.gif legen wir nicht an. Die Grafiken sollten sich unterschei- den, damit man das Ergebnis besser beurteilen kann. Abbildung 7.7 Darstellung der eingebundenen Grafiken In der ersten Reihe von Abbildung 7.7 sehen wir zweimal das Bild test1.gif. Das erste ha- ben wir als Komponente eingebunden und das zweite mit dem wicket:link-Tag. Bei der Komponente image2ResourceRef wird die Grafik über eine ResourceReference adres- siert. Die auf den ersten Blick recht unterschiedlichen Möglichkeiten führen im Hinter- grund zu ein und demselben Resultat. Mit dem String wird eine ResourceReference er- zeugt, die als Basisklasse die in der Hierarchie der Image-Komponente übergeordnete Komponentenklasse benutzt. Das wicket:link-Tag erzeugt automatisch eine Komponen- te, die den Wert aus einem „src“- oder „href“-Attribut benutzt und mit der Klasse der Komponente eine ResourceReference erzeugt. Es gibt allerdings einen wichtigen Unterschied: Wenn man eine ResourceReference mit einer Klasse als Referenz für den Pfad und einem String als Referenz für den Dateinamen benutzt, dann werden die Werte aus Session.getLocale() und Session.getStyle() nicht mit übernommen. Deshalb unterscheiden sich image2ResourceRefOhne und image- 2ResourceRefMit von den anderen Grafiken in der Reihe. In den ersten drei Fällen konnte Wicket die richtige (test2_de_DE.gif) für die Anzeige benutzen. Die Komponente image- 3rot zeigt, dass auch der Rückgabewert getVariation()Einfluss auf das Ergebnis hat. Interessant wird es bei image4. Da Wicket die Grafik nicht finden kann, erscheint im Att- ribut „src“ der Wert „.../test4_de_DE.gif“. Entgegen der Erwartung erscheint nur dann der längstmögliche Pfad, wenn Wicket eine entsprechende Datei gefunden hat oder Wicket keine passende Datei finden konnte. In jedem anderen Fall wird das Attribut so134
  • 7.2 Inhaltselementegesetzt, dass es auf eine vorhandene Datei passt. Man sollte sich also wundern, wennWicket einen Pfad zu einer Datei benutzt, die man so nicht angelegt hat. Dann findetWicket vermutlich die eigentliche Datei nicht.7.2.5.2 Grafiken zentral verwaltenDass Grafiken wesentlicher Bestandteil einer Komponente sein können, deckt nur einenTeil der Anforderungen an Webanwendungen ab. Oft möchte man alle Grafiken an einemzentralen Ort bündeln. Eine Möglichkeit bestünde darin, eine andere Referenzklasse füreine ResourceReference zu benutzen. Um zu vermeiden, dass sich die Aufrufe in derganzen Anwendung verteilen, sollte man die Image-Komponente durch die Referenzklasseerzeugen lassen. Für das folgende Beispiel erstellen wir alle Klassen im selben Paket.Listing 7.49 Images.java package de.wicketpraxis.web.thema.komponenten.basis.content; ... public enum Images { TEST1("test1.gif"),TEST2("test2.gif"),TEST3("test3.gif"); String _name; private Images(String name) { _name=name; } public Image newImage(String id) { Session session = Session.get(); return new Image(id,new ResourceReference( getClass(),_name,session.getLocale(),session.getStyle())); } }Listing 7.50 ImagesAsEnumPage.java package de.wicketpraxis.web.thema.komponenten.basis.content; ... public class ImageAsEnumPage extends WebPage { public ImageAsEnumPage() { add(TEST1.newImage("i1")); add(TEST2.newImage("i2")); add(TEST3.newImage("i3")); } }Listing 7.51 ImageAsEnumPage.html ... <body> <img wicket:id="i1"><img wicket:id="i2"><img wicket:id="i3"> </body> ... 135
  • 7 Basiskomponenten Abbildung 7.8 Zentral verwaltete Grafiken In diesem Beispiel kann man erkennen, wie man die Angabe von Basisklassen und Pfad- angaben vermeidet (Abbildung 7.8). Außerdem ist diese Variante erfreulich kompakt. Sprechende Namen (also bessere als in unserem Beispiel) sorgen zusätzlich für Übersicht- lichkeit. 7.2.5.3 Dynamisch generierte Grafiken Wicket kann nicht nur statische Grafiken anzeigen. Wicket bietet auch alle Möglichkeiten, Grafiken dynamisch zu erzeugen. In diesem einfachen Beispiel greifen wir auf eine Kom- ponente zurück, die einen grafischen Button erzeugt (Abbildung 7.9). Abbildung 7.9 Dynamisch erzeugte Grafiken Listing 7.52 SimpleDynamicImages.java package de.wicketpraxis.web.thema.komponenten.basis.content; ... public class SimpleDynamicImages extends WebPage { public SimpleDynamicImages() { add(new Image("ok",new DefaultButtonImageResource("Ok"))); add(new Image("hallo",new DefaultButtonImageResource(122,16,"Hallo"))); DefaultButtonImageResource res = new DefaultButtonImageResource("Wicket"); res.setWidth(140); res.setArcHeight(20); res.setArcWidth(20); res.setColor(new Color(10,128,250)); res.setTextColor(new Color(0,0,0)); res.setFont(new Font("Helvetica", Font.BOLD, 32)); add(new Image("wicket",res)); } } Listing 7.53 SimpleDynamicImages.html ... <body> <img wicket:id="ok"> <img wicket:id="hallo"> <img wicket:id="wicket"> </body> ... Auch wenn der praktische Wert dieser Klasse gering sein mag, kann man sich bei Eigen- entwicklungen an der Implementierung von DefaultButtonImageResource orientieren.136
  • 7.3 Links7.3 Links Eine Webanwendung ohne Links ist schwer vorstellbar. Welche Link-Klassen es gibt und wie man sie einsetzt, zeigen die folgenden Beispiele. Hinweis Die Basisklasse aller Links ist nicht die Klasse Link, sondern die Klasse AbstractLink. Möchte man eine Liste von Link-Komponenten verwalten oder soll der Rückgabewert einer Funktion eine Link-Komponente sein, kann es notwendig werden, AbstractLink als Rückgabetyp zu verwenden. 7.3.1 Von A nach B Um den Nutzer von Seite A auf Seite B zu lenken, gibt es mehr als eine Möglichkeit. Da- bei gibt es eventuell kein Unterschied im Resultat, aber in der Art und Weise, wie dieses Ziel erreicht wird. Listing 7.54 SimpleLinkPage.java package de.wicketpraxis.web.thema.komponenten.basis.links; ... public class SimpleLinkPage extends WebPage { private Label _message; public SimpleLinkPage() { Model<String> messageModel = Model.of("Jetzt ist es "+new Date()); _message = new Label("message",messageModel); add(_message); add(new BookmarkablePageLink<AllLinkTypesPage>( "bookmarkLink",SimpleLinkPage.class,new PageParameters("a=1"))); add(new Link("noBookmarkLink") { @Override public void onClick() { setResponsePage(SimpleLinkPage.class,new PageParameters("b=2")); } }); add(new Link<String>("link") { @Override public void onClick() { _message.setDefaultModelObject("Link geklickt ("+new Date()+")"); } }); add(new Link<String>("linkModel",messageModel) { @Override public void onClick() { setModelObject("Link mit Model geklickt ("+new Date()+")"); } }); } } 137
  • 7 Basiskomponenten Der erste Link ist ein BookmarkablePageLink. Der Link-Typ erwartet als Parameter eine Klasse vom Typ Page. Außerdem ist es möglich, eine Parameterliste zu übergeben, die an den Link angehängt wird. Wicket erzeugt daraus einen Link, den sich der Nutzer als Lese- zeichen abspeichern könnte: http://server/de.wicketpraxis--webapp/?wicket:bookmarkablePage= :de.wicketpraxis.web.thema.komponenten.basis.links.SimpleLinkPage&a=1 Ein Klick auf den Link springt dann zu dieser Seite. Der zweite Link springt ebenfalls eine Seite über die Klasse der Seite und einer optionalen Parameterliste an. Da diese Information aber erst zur Verfügung steht, wenn der Nutzer bereits auf den Link geklickt hat und die onClick()-Methode aufgerufen wurde, unter- scheidet sich der Link wesentlich: http://server/de.wicketpraxis--webapp/? wicket:interface=:1:noBookmarkLink::ILinkListener:: Wenn der Nutzer auf den Link klickt, dann landet er auf der Seite mit folgender URL: http://server/de.wicketpraxis--webapp/?wicket:bookmarkablePage= :de.wicketpraxis.web.thema.komponenten.basis.links.SimpleLinkPage&b=2 Auch wenn das Ergebnis für den Nutzer an sich gleich bleibt, gibt es Unterschiede z.B. im Bezug auf die Suchmaschinentauglichkeit dieser Navigationsform. Im ersten Link ist keine sitzungsabhängige (Session) Information kodiert, sodass dieser Seitenaufruf unabhängig von vorangegangenen Aktionen erlaubt ist. Der Link ist also nicht nur als Lesezeichen verwendbar, sondern kann auch durch Suchmaschinen verarbeitet werden. Wie man Wi- cket-Anwendungen für Suchmaschinen optimiert, behandelt Abschnitt 11.6. Der Link mit der ID „link“ demonstriert, dass durch einen Klick auf einen Link nicht au- tomatisch ein Seitenwechsel durchgeführt wird. Es wird einfach eine Aktion ausgeführt und die Seite neu geladen. Der Link mit der ID „linkModel“ unterscheidet sich vom Link „link“ dadurch, dass er dasselbe Modell wie das Label „message“ benutzt und daher den Modellwert direkt und damit typsicher setzen kann. Listing 7.55 SimpleLinkPage.html ... <body> <span wicket:id="message"></span><br><br> <a wicket:id="bookmarkLink">Bookmarkable Link</a><br> <a wicket:id="noBookmarkLink">No Bookmarkable Link</a><br> <a wicket:id="link">Link</a><br> </body> ... 7.3.2 Ajax und Links Wicket bietet im Zusammenhang mit Ajax einen sehr komfortablen Weg, Ajax in einer Seite zu benutzen, ohne dass man Nutzer, die JavaScript deaktiviert haben, daran hindert, die Webanwendung benutzen zu können.138
  • 7.3 LinksListing 7.56 AjaxFallbackLinkPage.java package de.wicketpraxis.web.thema.komponenten.basis.links; ... public class AjaxFallbackLinkPage extends WebPage { private Label _message; public AjaxFallbackLinkPage() { _message = new Label("message","Jetzt ist es "+new Date()); _message.setOutputMarkupId(true); add(_message); add(new AjaxFallbackLink("ajax") { @Override public void onClick(AjaxRequestTarget target) { _message.setDefaultModelObject("Link geklickt ("+new Date()+")"); if (target!=null) { _message.setDefaultModelObject("Ajax Link geklickt ("+ new Date()+")"); target.addComponent(_message); } } }); add(new IndicatingAjaxFallbackLink("ajax2") { @Override public void onClick(AjaxRequestTarget target) { timeConsumingTask(); _message.setDefaultModelObject("Link geklickt ("+new Date()+")"); if (target!=null) { _message.setDefaultModelObject("Ajax Link geklickt ("+ new Date()+")"); target.addComponent(_message); } } }); } private void timeConsumingTask() { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }Die Klasse AjaxFallbackLink unterscheidet sich von der Link-Klasse darin, dass derMethode onClick() ein AjaxRequestTarget übergeben wird. Wenn der Parameter nichtgesetzt wurde, dann wurde der Link nicht per Ajax aufgerufen. Man muss also prüfen, obder Link per Ajax aufgerufen wurde, und kann nur dann die Komponenten zur Aktualisie-rung vorschlagen. 139
  • 7 Basiskomponenten Die Klasse IndicatingAjaxFallbackLink zeigt nach dem Klick ein Symbol an, das dem Nutzer darstellen soll, dass die Antwort noch auf sich warten lässt. Sobald die Antwort vom Server geliefert wurde, wird das Symbol entfernt. Damit das in diesem Beispiel funk- tioniert, wartet die Methode timeConsumingTask() einfach nur eine halbe Sekunde, be- vor es dann weitergeht. Listing 7.57 AjaxFallbackLinkPage.html ... <body> <span wicket:id="message"></span><br><br> <a wicket:id="ajax">AjaxFallbackLink</a><br> <a wicket:id="ajax2">AjaxFallbackLink mit Indikator</a><br> </body> ... Für den Fall, dass man Links benutzen möchte, die nur den Ajax-Aufruf tätigen, gibt es die Klassen AjaxLink und IndicatingAjaxLink. In der onClick()-Methode ist der Parame- ter AjaxRequestTarget dann immer gesetzt und muss daher nicht mehr geprüft werden. 7.3.3 Link-Tricks Normalerweise erwartet ein Link ein entsprechendes Link-Tag im Markup. Wenn man die Komponente aber an ein anderes Tag bindet, wird der Link in einen JavaScript-Aufruf um- gewandelt. Listing 7.58 LinkTrickPage.java package de.wicketpraxis.web.thema.komponenten.basis.links; ... public class LinkTrickPage extends WebPage { public LinkTrickPage() { Model<String> messageModel = Model.of(""); add(new Label("message",messageModel)); add(new Link<String>("span",messageModel) { @Override public void onClick() { setModelObject("Trotzdem geklickt"); } }); } } Listing 7.59 LinkTrickPage.html ... <body> <span wicket:id="message"></span><br><br> Der <span wicket:id="span"><i>Link</i></span> versteckt sich im Text. </body> ...140
  • 7.3 LinksWenn man die Seite aufruft, sieht man im Quelltext, wie Wicket den zweiten Link umge-wandelt hat: ... <body> <span wicket:id="message"></span><br><br> Der <span wicket:id="span" onclick="var win = this.ownerDocument.defaultView || this.ownerDocument.parentWindow; if (win == window) { window.location.href=?wicket:interface=:0:span::ILinkListener::;} ;return false"><i>Link</i></span> versteckt sich im Text. </body> ...Der Nachteil dieser Methode liegt darin, dass sich der Mauszeiger nicht ändert und derNutzer nicht sieht, dass er auf den Text klicken kann. Es ist ja auch kein Link.7.3.4 Externe LinksEs ist jederzeit möglich, im Markup einen Link einzubinden, ohne dass Wicket diesenLink anpassen würde. Man könnte z.B. auf diese Weise einen Link zu einer anderen Web-seite einbinden. Doch ich empfehle, auch in diesem Fall eine passende Komponente zubenutzen, auch wenn das für das Ergebnis keinen Unterschied macht. Die ExternalLink-Klasse schreibt den Wert des zweiten Parameters direkt in das Attribut href.Listing 7.60 ExternalLinkPage.java package de.wicketpraxis.web.thema.komponenten.basis.links; ... public class ExternalLinkPage extends WebPage { public ExternalLinkPage() { add(new ExternalLink("external","http://www.wicket-praxis.de")); add(new ExternalLink("mail","mailto:michael@mosmann.de")); } }Listing 7.61 ExternalLinkPage.html ... <body> <a wicket:id="external">wicket-praxis.de</a><br> <a wicket:id="mail">michael@mosmann.de</a><br> </body> ...7.3.5 PopupsUm mit Wicket ein Popup zu öffnen, muss man den JavaScript-Aufruf nicht selbst erstel-len. Ob ein Link in einem Popup geöffnet werden soll (Abbildung 7.10), reduziert sich aufdas Setzen der notwendigen Einstellungen. 141
  • 7 Basiskomponenten Abbildung 7.10 Verschiedene Popup-Fenster Listing 7.62 PopupLinkPage.java package de.wicketpraxis.web.thema.komponenten.basis.links; ... public class PopupLinkPage extends WebPage { public PopupLinkPage() { PopupSettings popupSettings = new PopupSettings(PopupSettings.RESIZABLE|PopupSettings.TOOL_BAR); popupSettings.setHeight(300); popupSettings.setWidth(300); popupSettings.setWindowName("Popup"); BookmarkablePageLink<PopupPage> popupLink = new BookmarkablePageLink<PopupPage>("popup",PopupPage.class); popupLink.setPopupSettings(popupSettings); add(popupLink); Link popupLink2 = new Link("popup2") { @Override public void onClick() { setResponsePage(PopupPage.class); } }; popupLink2.setPopupSettings(popupSettings); add(popupLink2); BookmarkablePageLink<PopupPage> newWindowLink = new BookmarkablePageLink<PopupPage>("newWindow",PopupPage.class); add(newWindowLink); } } Listing 7.63 PopupLinkPage.html ... <body> <a wicket:id="popup">Popup</a><br> <a wicket:id="popup2">Popup2</a><br> <a wicket:id="newWindow" target="_blank">newWindow</a><br> </body> ... Wie man an diesem Beispiel sieht, kann man ein Popup-Fenster sowohl mit einem Book- markablePageLink als auch mit einem normalen Link öffnen. Man hat dabei Zugriff auf142
  • 7.3 Linksalle Einstellungen, die man auch per JavaScript ansprechen kann, nur ist es so natürlichviel einfacher.Der letzte Link zeigt, dass die Zielseite eine ganz normale Wicket-Seite ist, die über einenLink auch einfach in einem neuen Fenster (target="_blank") geöffnet werden kann(Abbildung 7.10). Auf der Seite, die dann als Popup geöffnet wird, binden wir einen Linkein, der dieses Popup wieder schließt.Listing 7.64 PopupPage.java package de.wicketpraxis.web.thema.komponenten.basis.links; ... public class PopupPage extends WebPage { public PopupPage() { add(new PopupCloseLink("close")); } }Listing 7.65 PopupPage.html ... <body> <a wicket:id="close">Schliessen</a><br> <div style="width:280px; height:240px; border:1px solid black; background-color:#ffff88; font-size:50px;"> Popup </div> </body> ...7.3.6 ResourceLinkWenn man dem Nutzer die Möglichkeit geben möchte, dass er sich Informationen z.B. alsPDF herunterladen kann, kann man auf zwei Komponenten zurückgreifen.Listing 7.66 ResourceLinksPage.java package de.wicketpraxis.web.thema.komponenten.basis.links; ... public class ResourceLinksPage extends WebPage { public ResourceLinksPage() throws URISyntaxException { add(new ResourceLink("resource", new ResourceReference(ResourceLinksPage.class,"images/test.gif"))); URL resourceURI = getClass().getResource( "/"+ getClass().getPackage().getName().replace(., File.separatorChar)+ "/images/test.gif"); add(new DownloadLink("file", new File(resourceURI.toURI()),"test.gif")); } } 143
  • 7 Basiskomponenten Listing 7.67 ResourceLinkPage.html ... <body> <a wicket:id="resource">Resource Link</a><br> <a wicket:id="file">Download Link</a><br> </body> ... Der ResourceLink setzt das Attribut „href“ des Links einfach mit der URL einer Res- source, die z.B. auch als Image eingebunden werden könnte. Ein Klick auf den Link zeigt dann die Ressource an. Der DownloadLink übermittelt die Datei in derselben Anfrage, in welcher der Klick aus- gewertet wird. Dadurch ist die PageMap blockiert, was zur Folge hat, dass z.B. kein weite- rer Download auf der Seite gestartet werden kann, solange der andere Download noch nicht fertig ist. Außerdem benötigt der DownloadLink eine Datei, die man evtl. erst erzeu- gen müsste. Daher empfiehlt es sich, immer einen ResourceLink zu benutzen, da diese einfacher zu benutzen sind und die Zugriffe die Anwendungen nicht blockieren. Außerdem sind sie fle- xibler in der Anwendung, da man Ressourcen auch zur Laufzeit erzeugen kann (siehe Ab- schnitt 11.7). 7.3.7 Formularlinks Die zwei Linkklassen SubmitLink und AjaxSubmitLink werden in Abschnitt 9.4 erklärt, da sie sinnvollerweise in Formularen benutzt werden.7.4 Behavior Wenn sich zwei Komponenten nur dadurch unterscheiden, dass ein Attribut im HTML-Tag einen anderen Wert hat, kann man das auf verschiedenen Wegen lösen. Entweder erzeugt man eine Variante der Markup-Datei oder überschreibt die Methode onComponentTag(). Spätestens wenn man dieses Attribut bei einer ganzen Reihe von Komponenten anpassen müsste, steht der Aufwand in keinem Verhältnis zum Nutzen. In Wicket kann eine Komponente durch ein (oder mehrere) Behavior verändert werden. Dazu muss ein Behavior mehrere Methoden implementieren, die während des Verarbei- tungsprozesses aufgerufen werden. So ist es möglich, vor und nach der Komponente oder beim Darstellen des HTML-Tags einzugreifen. 7.4.1 Darf es etwas JavaScript sein? Im ersten Beispiel machen wir nur von der Möglichkeit Gebrauch, dem HTML-Tag der Komponente ein weiteres Attribut hinzuzufügen:144
  • 7.4 BehaviorListing 7.68 SimpleBehaviorPage.java package de.wicketpraxis.web.thema.komponenten.behaviors; ... public class SimpleBehaviorPage extends WebPage { public SimpleBehaviorPage() { add(new Label("message","Text"). add(new OnMouseUpInnerHtmlBehavior("neuer Text"))); } static class OnMouseUpInnerHtmlBehavior extends AbstractBehavior { String _content; public OnMouseUpInnerHtmlBehavior(String content) { _content=content; } @Override public void onComponentTag(Component component, ComponentTag tag) { tag.put("onmouseup", "this.innerHTML = "+_content+""); } } }Dem Label wird unser OnMouseUpInnerHtmlBehavior hinzugefügt. Wenn die Kompo-nente dargestellt wird, wird in unserem Behavior unter anderem die Methode onCompo-nentTag() aufgerufen. In der Methode setzen wir das onmouseup-Attribut auf eine ZeileJavaScript-Code, der den Inhalt des Labels mit einem neuen Text versieht. Beim Klickenund Loslassen der Maustaste (onmouseup) wird dann die JavaScript-Funktion ausgeführtund der Text ersetzt.Listing 7.69 SimpleBehaviorPage.html ... <body> <span wicket:id="message"></span> </body> ...Listing 7.70 Ergebnis.html ... <body> <span wicket:id="message" onmouseup="this.innerHTML = neuer Text">Text</span> </body> ...7.4.2 Attribute anpassenOft muss man, wie in der Einleitung bereits geschildert, einfach nur ein Attribut im HTML-Tag der Komponente anpassen. Welche vorgefertigten Möglichkeiten es gibt, demonstriertdieses Beispiel: 145
  • 7 Basiskomponenten Listing 7.71 AttributeModifierPage.java package de.wicketpraxis.web.thema.komponenten.behaviors; ... public class AttributeModifierPage extends WebPage { public AttributeModifierPage() { add(new WebMarkupContainer("div1").add(new AttributeModifier( "style",true,Model.of("border:2px solid red;")))); add(new WebMarkupContainer("div2").add(new AttributeAppender( "style",true,Model.of("border-left:2px solid red"),";"))); add(new WebMarkupContainer("div3").add(new SimpleAttributeModifier( "style","border-right:2px solid red;"))); } } Um zu veranschaulichen, wo der Unterschied der drei Varianten liegt, betrachten wir das Markup der Seite: Listing 7.72 AttributeModifierPage.html <html> <head> <title>SimpleBehavior Page</title> <style>div { margin:4px; }</style> </head> <body> <div wicket:id="div1" style="color:red; border:1px dotted black"> Das ist der erste Block </div> <div wicket:id="div2" style="color:red; border:1px dotted black"> Das ist der zweite Block </div> <div wicket:id="div3" style="color:red; border:2px solid black"> Das ist der dritte Block </div> </body> </html> Die Klasse AttributeModifier ersetzt den Wert des angegebenen Attributs durch den neuen Wert. Die Klasse AttributeAppender kann an ein bereits vorhandenes Attribut eigene Daten anhängen. Dazu muss man ein Trennzeichen definieren, das zwischen dem alten und dem neuen Wert eingefügt wird. Wenn man z.B. das Attribut „style“ anpasst, ist das Trennzeichen ein „;“ beim Attribut „class“ ist es ein einfaches Leerzeichen. Abbildung 7.11 Komponenten mit angepassten Attributen Die Klasse SimpleAttributeModifier macht es sich einfach und ersetzt den Wert eines Attributs durch einen neuen. Die Klasse SimpleAttributeModifier verzichtet darauf, den Wert aus einem Modell zu ermitteln. Daher macht diese Klasse dann Sinn, wenn man schon im Vorfeld weiß, welche Anpassung durchzuführen ist. In Abbildung 7.11, aber auch im Quelltext der Seite kann man erkennen, was verändert wurde:146
  • 7.4 BehaviorListing 7.73 Ergebnis.html ... <div wicket:id="div1" style="border:2px solid red;"> Das ist der erste Block </div> <div wicket:id="div2" style="color:red; border:1px dotted black;border-left:2px solid red"> Das ist der zweite Block </div> <div wicket:id="div3" style="border-right:2px solid red;"> Das ist der dritte Block </div> ...7.4.3 Attribute erweiternWenn man mehr als ein Behavior für eine Komponente benutzt, dann kann es passieren,dass das gleiche Attribut mehrfach verändert wird. Damit alle Veränderungen in das End-ergebnis einfließen, sollte man in solchen Fällen die Klasse AttributeAppender benut-zen. Dabei werden nicht nur die bestehenden Werte aus dem Markup berücksichtigt, son-dern es wird ebenso auf die letzte Änderung zurückgegriffen. Somit kann man unabhängi-ge Behavior-Klassen entwickeln, sodass Kollisionen ausgeschlossen werden können.Listing 7.74 AttributeAppenderPage.java package de.wicketpraxis.web.thema.komponenten.behaviors; ... public class AttributeAppenderPage extends WebPage { public AttributeAppenderPage() { WebMarkupContainer div = new WebMarkupContainer("div"); div.add(new AttributeAppender("style",true,Model.of( "border-left:2px solid red"),";")); div.add(new AttributeAppender("style",true,Model.of( "border-right:2px solid green"),";")); div.add(new AttributeAppender("style",true,Model.of( "border-top:2px solid yellow"),";")); div.add(new AttributeAppender("style",true,Model.of( "border-bottom:2px solid blue"),";")); add(div); } }Vier AttributeAppender modifizieren nacheinander das Attribut style und hängen je-weils für eine Seite eine CSS-Definition für den Rahmen an.Listing 7.75 AttributeAppenderPage.html ... <body> <div wicket:id="div"> 147
  • 7 Basiskomponenten Das ist der Block </div> </body> ... Im Ergebnis sehen wir, dass jede der CSS-Definitionen korrekt eingebunden wurde. Listing 7.76 Ergebnis.html ... <div wicket:id="div" style="border-left:2px solid red;border-right:2px solid green;border-top:2px solid yellow;border-bottom:2px solid blue"> Das ist der Block </div> ... 7.4.4 Ajax und Formulare Gerade im Zusammenhang mit Formularen stehen Funktionen bereit, die den Umgang mit Formularen vereinfachen und die Anwenderfreundlichkeit (Usability) verbessern können (siehe Abschnitt 9.13). Um dem nicht vorwegzugreifen, möchten wir an dieser Stelle nur noch eine Klasse betrachten, die eine Komponente nach einer festen Zeitspanne per Ajax aktualisiert. Wenn man die Seite aufruft, wird ohne eigenes Zutun die Uhrzeit aktualisiert. Listing 7.77 AjaxUpdatingPage.java package de.wicketpraxis.web.thema.komponenten.behaviors; ... public class AjaxUpdatingPage extends WebPage { public AjaxUpdatingPage() { LoadableDetachableModel<String> uhrModel= new LoadableDetachableModel<String>() { @Override protected String load() { return "Mit dem Zeitzeichen ist es genau "+new Date(); } }; Label uhr=new Label("uhr",uhrModel); uhr.setOutputMarkupId(true); uhr.add(new AjaxSelfUpdatingTimerBehavior(Duration.ONE_SECOND)); add(uhr); } } Listing 7.78 AjaxUpdatingPage.html ... <body> <span wicket:id="uhr" style="font-size:20px;"></span> </body> ...148
  • 8 8 Listen und Tabellen Listen kommen in Webanwendungen häufiger vor, als man denkt. Dabei geht es nicht nur um Ergebnislisten, sondern um alles, was mehr als einmal vorhanden ist und sich z.B. durch den Inhalt, aber nicht durch die Struktur unterscheidet. Das können Menüeinträge, Prozessschritte oder eine Liste von Eingabefeldern in Formularen sein. Bisher waren alle Komponenten vorher bekannt. Für jede Komponente gab es die passende ID und die Stelle, wo sie im Markup referenziert wurde. Wenn man eine Liste von Werten oder Elementen darstellen möchte, dann funktioniert dieser Ansatz nicht, da die Anzahl der Elemente meist unbekannt ist. Auf den folgenden Seiten zeige ich unterschiedliche Ansätze, wie wir diese Aufgabenstel- lung mit Bordmitteln lösen können. Im ersten Abschnitt geht es nur darum, alle Elemente einer Liste darzustellen. Im zweiten Teil zeigen wir einen veränderlichen Ausschnitt einer Liste an.8.1 Darstellung von Listen Von den unterschiedlichen Möglichkeiten, die Wicket für die Darstellung von Listen bie- tet, wird man in der Praxis nur wenige, diese aber um so intensiver einsetzen. Dabei hängt es oft von der zu lösenden Aufgabe ab, welcher Komponente der Vorzug zu gewähren ist. 8.1.1 RepeatingView Die einfachste Komponente für die Listendarstellung ist die RepeatingView. Dieser Komponente fügt man die darzustellenden Kindelemente mit einer durch die Komponente erzeugten ID hinzu. Listing 8.1 RepeatingViewPage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... public class RepeatingViewPage extends WebPage 149
  • 8 Listen und Tabellen { public RepeatingViewPage() { RepeatingView repeatingView = new RepeatingView("list"); for (int i=0;i<10;i++) { repeatingView.add(new Label(repeatingView.newChildId(), "Zeile "+i)); } add(repeatingView); } } Die dynamisch erzeugte ID für die Kindkomponente wird im Markup nicht referenziert. Vielmehr wird anstelle der RepeatingView das Kindelement mit dem Markup der Repea- tingView dargestellt. Im Quelltext der Ergebnisseite können wir alle 10 Kindelemente erkennen. Listing 8.2 RepeatingViewPage.html ... <body> <ul> <li wicket:id="list"></li> </ul> </body> ... Listing 8.3 Ergebnis.html ... <body> <ul> <li wicket:id="list">Zeile 0</li><li wicket:id="list">Zeile 1</li> <li wicket:id="list">Zeile 2</li><li wicket:id="list">Zeile 3</li> <li wicket:id="list">Zeile 4</li><li wicket:id="list">Zeile 5</li> <li wicket:id="list">Zeile 6</li><li wicket:id="list">Zeile 7</li> <li wicket:id="list">Zeile 8</li><li wicket:id="list">Zeile 9</li> </ul> </body> ... 8.1.2 RefreshingView Die Elemente der RepeatingView werden einmal erzeugt, wenn die Komponente erzeugt wird. Daher ändert sich die Anzeige nicht, wenn zugrunde liegende Daten geändert wur- den. Die RefreshingView-Komponente erzeugt daher die Kindkomponenten bei jeder Darstellung neu. Veränderungen in den Ursprungsdaten führen dann natürlich zu Verände- rungen in der Darstellung. Listing 8.4 RefreshingViewPage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... public class RefreshingViewPage extends WebPage { List<String> _texte = Arrays.asList("Das","sind","Textteile"); public RefreshingViewPage() {150
  • 8.1 Darstellung von Listen add(new RefreshingView<String>("list") { @Override protected Iterator<IModel<String>> getItemModels() { return new ModelIteratorAdapter<String>(_texte.iterator()) { @Override protected IModel<String> model(String object) { return Model.of(object); } }; } @Override protected void populateItem(Item<String> item) { item.add(new Label("label",item.getModelObject())); } }); add(new Link("link") { @Override public void onClick() { _texte=Arrays.asList("Das","ist","ein","anderer","Text"); } }); } }In der Methode getItemModels() wird die Liste der Elemente erzeugt, und in der Metho-de populateItem() werden die Kindkomponenten erzeugt. Ein Klick auf den Linktauscht die Daten für die Darstellung aus.Listing 8.5 RefreshingViewPage.html ... <body> <table> <tr wicket:id="list"> <td> <span wicket:id="label"></span> </td> </tr> </table> <a wicket:id="link">Textliste ändern</a> </body> ...Im Gegensatz zur RepeatingView muss hier die Kindkomponente im Markup referenziertwerden, da die RepeatingView-Komponente eine Hilfskomponente (Item) einfügt, an diewir die eigenen Komponenten anbinden. Das bedeutet gleichzeitig, dass man mehr als eineKomponente hinzufügen kann.Listing 8.6 Ergebnis.html ... <body> <table> <tr wicket:id="list"> <td> <span wicket:id="label">Das</span> 151
  • 8 Listen und Tabellen </td> </tr><tr wicket:id="list"> <td> <span wicket:id="label">sind</span> </td> </tr><tr wicket:id="list"> <td> <span wicket:id="label">Textteile</span> </td> </tr> </table> <a wicket:id="link" href="?wicket:interface=:1:link::ILinkListener::"> Textliste ändern</a> </body> ... Klickt man dann auf den Link, dann werden die neuen Texte angezeigt. 8.1.3 ListView Die ListView-Komponente unterscheidet sich von der RepeatingView vor allem da- durch, dass man die Daten schon als Liste übergibt. Die Komponente erwartet als zweiten Parameter ein Objekt vom Typ IModel<List> oder List, wobei ein Objekt vom Typ List intern in ein IModel<List> umgewandelt wird. Listing 8.7 ListViewPage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... public class ListViewPage extends WebPage { public ListViewPage() { add(new ListView<String>("list", Arrays.asList("Das", "ist", "eine", "Textliste")) { @Override protected void populateItem(ListItem<String> item) { String text = item.getModelObject(); item.add(new Label("part1",text.substring(0, 1))); item.add(new Label("part2",text.substring(1))); } }); } } Für jedes Element der Liste wird die Methode populateItem() mit einem neuen List- Item aufgerufen. Auf diese Weise kapselt die Komponente den Zugriff auf die darunter- liegende Liste, sodass wir über item.getModelObject() direkt auf das Element der Liste zugreifen können. Man kann den Eintrag der Liste auch als Modell an die Kindkomponen- ten weiterreichen, indem man nur item.getModel() aufruft und diesen Wert als Aufruf- parameter benutzt. In diesem Beispiel schneiden wir von jedem Element den ersten Buchstaben ab und erzeu- gen mit diesem ersten Buchstaben und dem Rest jeweils eine Komponente.152
  • 8.1 Darstellung von ListenListing 8.8 ListViewPage.html ... <body> <table> <tr wicket:id="list"> <td> <strong><span wicket:id="part1"></span></strong> <span wicket:id="part2"></span> </td> </tr> </table> </body> </html>Da wir den ersten Buchstaben in Fettdruck dargestellt haben, erhalten wir folgendes Er-gebnis.Listing 8.9 Ergebnis Das ist eine Textliste8.1.4 PropertyListViewDie Komponente PropertyListView ist von ListView abgeleitet und ersetzt das Modell,das an das ListItem gebunden ist, durch ein CompoundPropertyModel. Dadurch kanndie Angabe eines expliziten Modells entfallen, wenn die Komponenten-ID gleichzeitig einvorhandenes Attribut des Listenelements adressiert.Für dieses und folgende Beispiele erstellen wir eine einfache Klasse Kunde, welche dieAttribute Name, Vorname und Alter besitzt.Listing 8.10 Kunde.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; import java.io.Serializable; public class Kunde implements Serializable { String _name; String _vorname; int _geburtsjahr; public Kunde(String vorname, String name,int geburtsjahr) { _vorname = vorname; _name = name; _geburtsjahr=geburtsjahr; } getVorname(),getName(),getGeburtsjahr()... }Wir fügen in unserem Beispiel zwei Elemente vom Typ Kunde in eine Liste und kapselndiese Liste in einem Modell. 153
  • 8 Listen und Tabellen Listing 8.11 PropertyListViewPage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... public class PropertyListViewPage extends WebPage { public PropertyListViewPage() { List<Kunde> liste = Arrays.asList(new Kunde("Klaus","Müller",1973), new Kunde("Hans","Meier",1967)); add(new PropertyListView<Kunde>("list", Model.of(liste)) { @Override protected void populateItem(ListItem<Kunde> item) { item.add(new Label("vorname")); item.add(new Label("name", new PropertyModel<String>(item.getModel(),"name"))); } }); } } Um den Unterschied deutlich zu machen, erzeuge ich pro Eintrag zwei Komponenten. Das erste Label bekommt kein Modell als Parameter. Dabei entspricht das Resultat dieser Konstellation dem zweiten Aufruf, wo ein explizites PropertyModel benutzt wird. Kurz: Solange man mit einfachen Zugriffen auf Attribute eines Listenelements auskommt, kann der Einsatz einer PropertyListView-Komponente Schreibarbeit reduzieren. Listing 8.12 PropertyListViewPage.html ... <body> <table> <tr wicket:id="list"> <td> <span wicket:id="vorname"></span> </td> <td> <span wicket:id="name"></span> </td> </tr> </table> </body> ... 8.1.5 ColumnListView Elemente einer Liste darzustellen, ist also recht einfach. Das folgende Beispiel soll veran- schaulichen, wie man einfach eine Tabelle darstellt, indem man zwei Listen kombiniert. Dazu leiten wir eine eigene Komponente von ListView ab und ergänzen diese entspre- chend. Listing 8.13 ColumnListView.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... public abstract class ColumnListView<R,C> extends ListView<R> { IModel<? extends List<? extends C>> _columns;154
  • 8.1 Darstellung von Listen String _columnId; public ColumnListView(String id, IModel<? extends List<? extends R>> rows, String columnId, IModel<? extends List<? extends C>> columns) { super(id, rows); _columnId=columnId; _columns=columns; } @Override protected void populateItem(final ListItem<R> row) { row.add(new ListView<C>(_columnId,_columns) { @Override protected void populateItem(ListItem<C> column) { ColumnListView.this.populateItem(row,column); } }); } protected abstract void populateItem(ListItem<R> row, ListItem<C> column); }Wir benutzen als Parameter neben der Komponenten-ID und dem Modell für die List-View-Komponente zwei weitere Parameter, die wir dann in der überschriebenen popu-lateItem-Methode benutzen. Wir fügen für jede Zeile eine eigene ListView-Kompo-nente ein und überschreiben auch hier die populateItem-Methode. In dieser Methoderufen wir dann die zu überschreibende Methode populateItem auf, die aber in dem Fallbeide ListItem-Komponenten als Parameter übergeben bekommt. Wichtig ist dabei, dassman Komponenten nur zum ListItem der Spaltenliste hinzufügt, da die Methode für jedeSpalte in jeder Zeile aufgerufen wird. Würde man eine Komponente in der Zeile hinzufü-gen, müsste man sicherstellen, dass dieser Aufruf nur einmal pro Zeile erfolgt. Diese Kom-ponentenklasse bietet also noch reichlich Potential für Verbesserungen.Die Anwendung ist dann wieder recht einfach: Ich habe zwei Listen und möchte für jedeZeile und Spalte ein Ergebnis anzeigen. In dem Fall fügen wir zwei Label hinzu, die je-weils den Wert der Zeile und den Wert der Spalte anzeigen.Listing 8.14 ColumnListViewPage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... public class ColumnListViewPage extends WebPage { public ColumnListViewPage() { IModel<List<? extends Integer>> rows=Model.of(Arrays.asList(1,2,3)); IModel<List<? extends String>> columns= Model.of(Arrays.asList("A","B","C")); add(new ColumnListView<Integer,String>("rows",rows, "columns",columns) { @Override protected void populateItem(ListItem<Integer> row, ListItem<String> column) { 155
  • 8 Listen und Tabellen column.add(new Label("row",row.getModel())); column.add(new Label("column",column.getModel())); } }); } } Listing 8.15 ColumnListViewPage.html ... <body> <table> <tr wicket:id="rows"> <td wicket:id="columns"> <span wicket:id="row"></span><span wicket:id="column"></span> </td> </tr> </table> </body> ... Dabei ist zu erkennen, dass die Liste für die Zeilen und die Spalten an das passende HTML-Tag gebunden werden. Dieses Beispiel veranschaulicht sehr eindrucksvoll, wie man aus einfachen Komponenten ohne großen Aufwand wesentlich komplexere Darstel- lungsmöglichkeiten schaffen kann.8.2 DataProvider Selten möchte man alle Daten auf einmal darstellen. Zum einen leidet die Übersichtlich- keit, zum anderen kann die Menge unter Umständen eine Herausforderung darstellen. Das IDataProvider-Interface definiert eine Abstraktion, die es einer Komponente ermöglicht, einen Ausschnitt der Gesamtliste abzufragen und die Länge der Liste zu ermitteln. 8.2.1 DataView Die DataView-Komponente ähnelt der ListView-Komponente. Anstelle einer Liste wird allerdings ein DataProvider benutzt. Wir erzeugen bei unserer Implementierung der IDa- taProvider-Schnittstelle die Daten generisch und legen die Anzahl der Einträge auf die willkürliche Größe von 124 fest. Die Methode model() muss überschrieben werden und ein Modell für das Listenelement zurückgeben. Listing 8.16 DataViewPage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... public class DataViewPage extends WebPage { public DataViewPage() { IDataProvider<String> data=new IDataProvider<String>() { public Iterator<? extends String> iterator(int first, int count) { List<String> tempList=new ArrayList<String>(); for (int i=0;i<count;i++)156
  • 8.2 DataProvider { tempList.add("Position "+(i+first)); } return tempList.iterator(); } public IModel<String> model(String object) { return Model.of(object); } public int size() { return 124; } public void detach() { /* hier nicht nötig */ } }; final DataView<String> dataView = new DataView<String>("list",data,10) { @Override protected void populateItem(Item<String> item) { item.add(new Label("label",item.getModel())); } }; add(dataView); add(new Link("link") { @Override public void onClick() { int page = dataView.getCurrentPage()+1; if (page<dataView.getPageCount()) dataView.setCurrentPage(page); } }); } }Die DataView-Komponente stellt in dem Beispiel die ersten 10 Einträge dar. Wenn mandie nächsten 10 Einträge darstellen möchte, kann man die Seitenzahl der Komponente er-höhen. In unserem Beispiel erhöht ein Klick auf den Link die Seitenzahl um 1. Die Kom-ponente zeigt daraufhin die nächsten 10 Datensätze an.Listing 8.17 DataViewPage.html ... <body> <table> <tr wicket:id="list"> <td> <span wicket:id="label"></span> </td> </tr> </table> <a wicket:id="link">Nächste Seite</a> </body> ... 157
  • 8 Listen und Tabellen 8.2.2 GridView Wie einfach man dieselben Daten in einer anderen Art und Weise darstellen kann, zeigt die GridView-Komponente (Abbildung 8.1). Dabei werden die Daten in einer Tabelle darge- stellt. Die Anzahl der Spalten und der Zeilen setzen wir auf 3, sodass 9 Elemente darge- stellt werden können. Wie im vorherigen Beispiel erhöht der Klick auf den Link den Sei- tenindex, und die nächsten 9 Elemente werden dargestellt. Listing 8.18 GridViewPage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... public class GridViewPage extends WebPage { public GridViewPage() { IDataProvider<String> data=new IDataProvider<String>() { public Iterator<? extends String> iterator(int first, int count) { List<String> tempList=new ArrayList<String>(); for (int i=0;i<count;i++) { tempList.add("Position "+(i+first)); } return tempList.iterator(); } public IModel<String> model(String object) { return Model.of(object); } public int size() { return 25; } public void detach() { /* hier nicht nötig */ } }; final GridView<String> gridView = new GridView<String>("list",data) { @Override protected void populateItem(Item<String> item) { item.add(new Label("label",item.getModelObject())); } @Override protected void populateEmptyItem(Item<String> item) { item.add(new Label("label","Leer")); } }; gridView.setColumns(3); gridView.setRows(3); add(gridView); add(new Link("link") { @Override public void onClick() {158
  • 8.2 DataProvider int page = gridView.getCurrentPage()+1; if (page<gridView.getPageCount()) gridView.setCurrentPage(page); } }); } }Die ID der Spaltenkomponente, bei der die GridView-Komponente auf eine Repeating-View zurückgreift, ist fest mit „cols“ definiert und kann nicht geändert werden. Wenn fürdie Darstellung einer Spalte kein Datenelement zur Verfügung steht, kann man popula-teEmptyItem überschreiben, um z.B. eine alternative Darstellung zu ermöglichen.Listing 8.19 GridViewPage.html ... <body> <table> <tr wicket:id="list"> <td wicket:id="cols"> <span wicket:id="label"></span> </td> </tr> </table> <a wicket:id="link">Nächste Seite</a> </body> ... Abbildung 8.1 GridView8.2.3 DataGridViewBisher mussten wir die Anzeige der Daten immer innerhalb einer populateItem-Methodeimplementieren. Der Ansatz ist für die tabellarische Darstellung von Datensätzen nichtbesonders gut geeignet. Ähnlich unserer ColumnListView-Komponente benutzt die Da-taGridView-Komponente eine Liste von Spalten, die in die Darstellung einfließen sollen(Abbildung 8.2). Doch anders als bei unseren Komponente besteht die Liste von Spaltennicht aus Daten, sondern aus Hilfsklassen, welche die Komponenten für die Darstellungder Spalten erzeugen.Listing 8.20 DataGridViewPage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... import org.apache.wicket.extensions.markup.html.repeater.data.grid.*; public class DataGridViewPage extends WebPage { public DataGridViewPage() { 159
  • 8 Listen und Tabellen List<ICellPopulator<Kunde>> cells=new ArrayList<ICellPopulator<Kunde>>(); cells.add(new PropertyPopulator("Vorname")); cells.add(new PropertyPopulator("Name")); cells.add(new ICellPopulator<Kunde>() { public void populateItem(Item<ICellPopulator<Kunde>> cellItem, String componentId, IModel<Kunde> rowModel) { Kunde kunde = rowModel.getObject(); cellItem.add(new Label(componentId, Model.of(kunde.getGeburtsjahr()))); } public void detach() {} }); List<Kunde> liste = new ArrayList<Kunde>(); for (int i=1;i<6;i++) { liste.add(new Kunde("Albrecht","von Klausewitz der "+i+".", 1381+i*69)); } ListDataProvider<Kunde> data=new ListDataProvider<Kunde>(liste); final DataGridView<Kunde> datagrid = new DataGridView<Kunde>("list",cells,data); datagrid.setRowsPerPage(3); add(datagrid); add(new Link("link") { @Override public void onClick() { int page = datagrid.getCurrentPage()+1; if (page<datagrid.getPageCount()) datagrid.setCurrentPage(page); } }); } } Daher erzeugen wir als Erstes eine Liste von Spaltendefinitionen, die alle das Interface ICellPopulator implementieren. Auch hier gibt es vorgefertigte Implementierungen, die z.B. ein Attribut des Elements referenzieren. Wir erstellen außerdem eine eigene Imple- mentierung, die ein Label mit dem Geburtsjahr des Kundendatensatzes erstellt. Die Kom- ponenten-ID für die Spalten und den Spalteninhalt ist mit „cells“ und „cell“ ebenfalls fest innerhalb der Komponente verankert. Listing 8.21 DataGridViewPage.html ... <body> <table> <thead> <tr> <th>Vorname</th> <th>Name</th> <th>Geburtsjahr</th> </tr> </thead> <tbody> <tr wicket:id="list">160
  • 8.2 DataProvider <td wicket:id="cells"> <wicket:container wicket:id="cell"></wicket:container> </td> </tr> </tbody> </table> <a wicket:id="link">Nächste Seite</a> </body> ... Abbildung 8.2 DataGridViewEs fällt auf, dass die Überschriften über den Spalten nicht automatisch mit den Spalten-definitionen korrespondieren. Außerdem ist es sehr aufwendig, wenn man aus diesem Bei-spiel eine vollwertige Anzeige mit Seitennavigation machen wollte. Genau dafür setzenwir die nächste Komponente ein.8.2.4 DataTableWie bereits im letzten Beispiel verwendet die DataTable-Komponente eine Spaltendefini-tion. Der Unterschied besteht darin, dass diese Definition auch für die Darstellung derSpaltenüberschriften benutzt wird. Außerdem gibt es bereits eine vorgefertigte Komponen-te für die Seitennavigation, sodass ohne großen Aufwand eine paginierbare Datensatzan-sicht erzeugt werden kann (Abbildung 8.3).Listing 8.22 DataTablePage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... import org.apache.wicket.extensions.markup.html.repeater.data.table.*; public class DataTablePage extends WebPage { public DataTablePage() { List<Kunde> liste=new ArrayList<Kunde>(); for (int i=1;i<7;i++) { liste.add(new Kunde("Hansi","Klapper der "+i+".",1381+i*69)); } IDataProvider<Kunde> data=new ListDataProvider<Kunde>(liste); IColumn[] columns= { new PropertyColumn<Kunde>(Model.of("Vorname"),"vorname"), new PropertyColumn<Kunde>(Model.of("Name"),"name"), new PropertyColumn<Kunde>(Model.of("J"),"geburtsjahr"), }; DataTable<Kunde> dataTable = new DataTable<Kunde>("list", 161
  • 8 Listen und Tabellen columns,data,3); dataTable.addTopToolbar(new NavigationToolbar(dataTable)); add(dataTable); } } Listing 8.23 DataTablePage.html ... <body> <table wicket:id="list"></table> </body> ... Abbildung 8.3 DataTable 8.2.5 DefaultDataTable Anhand der DefaultDataTable-Komponente möchte ich zeigen, wie man die verschiede- nen Teile zusammenfügt, um z.B. Datensätze aus einer Datenbank anzeigen, sortieren und löschen zu können. Damit aus den Teilen ein Ganzes wird, müssen wir allerdings noch etwas Vorarbeit leisten. Da die meisten Komponenten und Klassen wiederverwendbar sind, hält sich der Aufwand für jedes weitere, ähnlich gelagerte Problem in Grenzen. Auf dieses Weise kann man dann sehr schnell komplexe Anwendungen erstellen. 8.2.5.1 Ergänzungen der Datenbankschicht Zuerst müssen wir unsere Datenbankschicht um ein paar Funktionen ergänzen. Dazu schreiben wir uns eine abstrakte Hilfsklasse, welche die benötigten Funktionen unabhängig von der persistenten Klasse bereitstellt. Listing 8.24 CriteriaFilterInterface.java package de.wicketpraxis.persistence; import org.hibernate.Criteria; public interface CriteriaFilterInterface { public void applyFilter(Criteria criteria); } Das CriteriaFilterInterface implementiert genau eine Methode. Diese wird sowohl bei der Erzeugung der Abfrage für die Liste als auch beim Ermitteln der Größe des Ge- samtergebnisses aufgerufen.162
  • 8.2 DataProviderListing 8.25 AbstractDaoList.java package de.wicketpraxis.persistence; ... import org.hibernate.Criteria; import org.hibernate.criterion.*; public class AbstractDaoList<K extends Serializable,T extends DoInterface<K>,R> { AbstractDao<K, T> _dao; protected AbstractDaoList(AbstractDao<K, T> dao) { _dao=dao; } public List<R> getList(Integer offset,Integer max) { List<R> ret=null; Criteria criteria = _dao.getCriteria(); if (offset!=null) criteria.setFirstResult(offset); if (max!=null) criteria.setMaxResults(max); for (Order order : getOrder()) criteria.addOrder(order); for (CriteriaFilterInterface filter: getFilter()) filter.applyFilter(criteria); ret=criteria.list(); return ret; } public int getSize() { int ret=0; Criteria criteria = _dao.getCriteria(); for (CriteriaFilterInterface filter: getFilter()) filter.applyFilter(criteria); criteria.setProjection(Projections.rowCount()); ret=(Integer) criteria.uniqueResult(); return ret; } protected List<Order> getOrder() { return new ArrayList<Order>(); } protected List<CriteriaFilterInterface> getFilter() { return new ArrayList<CriteriaFilterInterface>(); } }Die Klasse AbstractDaoList liefert immer einen Ausschnitt einer Liste von Objektenzurück, die sortiert und gefiltert werden kann. Um die Größe der Ergebnisliste zu ermit-teln, werden dieselben Filter angewendet, aber auf die Sortierung verzichtet. Auf dieseWeise kann gewährleistet werden, dass die Größe der Liste korrekt ermittelt wird.Nachdem wir diese Klasse fertiggestellt haben, müssen wir unsere Datenzugriffsklasseetwas ergänzen. Wir leiten eine eigene Klasse ab, die alle Elemente der Tabelle zurücklie-fert. 163
  • 8 Listen und Tabellen Listing 8.26 UserDao.java package de.wicketpraxis.persistence.dao; ... import org.hibernate.Criteria; import org.hibernate.criterion.*; public class UserDao extends AbstractDao<Integer, User> { public static final String BEAN_ID="userDao"; public UserDao() { super(User.class); } public User getByEMail(String email) { return (User) getCriteria(). add(Property.forName("EMail").eq(email)).uniqueResult(); } public All getAll() { return new All(this); } public static class All extends AbstractDaoList<Integer, User, User> { String _sort; boolean _asc; protected All(UserDao userDao) { super(userDao); } @Override protected List<Order> getOrder() { ArrayList<Order> ret = new ArrayList<Order>(); if (_sort!=null) { ret.add(_asc ? Order.asc(_sort) : Order.desc(_sort)); } return ret; } public void setOrder(String column,boolean asc) { _sort=column; _asc=asc; } } } In diesem Fall verzichten wir auf den Einsatz von Filtern, sondern ermöglichen nur das Sortieren des Ergebnisses. Der Einfachheit halber reduzieren wir die Sortiermöglichkeit auf ein Attribut. 8.2.5.2 DataProvider Damit die Komponenten mit den Daten umgehen können, implementieren wir einen Adap- ter, der zwischen der Datenbank und den Wicket-Komponenten vermittelt. Damit die Liste164
  • 8.2 DataProviderauch sortiert werden kann, implementieren wir nicht IDataProvider, sondern Isort-ableDataProvider.Listing 8.27 UserList.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... import org.apache.wicket.extensions.markup.html.repeater.data.sort.*; import org.apache.wicket.extensions.markup.html.repeater.data.table.*; import org.apache.wicket.extensions.markup.html.repeater.util.SingleSortState; public class UserList implements ISortableDataProvider<User> { UserDao _userDao; transient All _all; boolean _attached=false; ISortState _sortState=new SingleSortState(); public UserList(UserDao userDao) { _userDao=userDao; } protected All load() { if (!_attached) { _all=_userDao.getAll(); if (_sortState!=null) { _all.setOrder(null, true); String[] properties={"id","EMail"}; for (String s : properties) { int propertySortOrder = _sortState.getPropertySortOrder(s); switch (propertySortOrder) { case ISortState.ASCENDING: _all.setOrder(s, true); break; case ISortState.DESCENDING: _all.setOrder(s, false); break; } } } } _attached=true; return _all; } public Iterator<? extends User> iterator(int first, int count) { return load().getList(first, count).iterator(); } public IModel<User> model(User object) { return new DaoModel<Integer,User>(_userDao,object); } public int size() { 165
  • 8 Listen und Tabellen return load().getSize(); } public void detach() { _all=null; _attached=false; } public ISortState getSortState() { return _sortState; } public void setSortState(ISortState state) { _sortState=state; detach(); } } Es ist wichtig, darauf hinzuweisen, dass die Klasse All nicht serialisiert werden darf. Da- her wird das Feld _all beim Aufruf von detach() auch zurückgesetzt. Wenn über eine der Methoden auf diese Klasse zugegriffen werden muss, wird sie entsprechend geladen. In der load()-Methode wird dann die Sortierung, die durch die Tabellenkomponente ge- setzt wurde, an die Datenbankschicht weitergereicht. Was in diesem Beispiel noch auf die UserDao-Klasse zugeschnitten ist, kann man natürlich auch wesentlich allgemeiner umset- zen. 8.2.5.3 Zusammenfügen Nachdem alle notwendigen Hilfsklassen geschrieben sind, ist das Zusammenfügen der Tei- le gewohnt einfach. Listing 8.28 DefaultDataTablePage.java package de.wicketpraxis.web.thema.komponenten.basis.repeater; ... import org.apache.wicket.extensions.markup.html.repeater.data.grid.*; import org.apache.wicket.extensions.markup.html.repeater.data.table.*; public class DefaultDataTablePage extends WebPage { @SpringBean UserDao _userDao; public DefaultDataTablePage() { UserList data=new UserList(_userDao); List<IColumn<User>> columns=new ArrayList<IColumn<User>>(); columns.add(new PropertyColumn<User>(Model.of("Id"),"id","Id")); columns.add(new PropertyColumn<User>(Model.of("EMail"), "EMail","EMail")); columns.add(new AbstractColumn<User>(Model.of("")) { public void populateItem(Item<ICellPopulator<User>> cellItem, String componentId, IModel<User> rowModel) { Fragment fragment = new Fragment(componentId,"deleteFragment", DefaultDataTablePage.this);166
  • 8.2 DataProvider fragment.add(new Link<User>("link",rowModel) { @Override public void onClick() { User user= getModelObject(); _userDao.delete(user); } }); cellItem.add(fragment); } }); DefaultDataTable<User> table = new DefaultDataTable<User>("list",columns,data,3); add(table); } }Wir erstellen drei Spalten, wobei die letzte Spalte einen Link enthält, der den Datensatzlöscht. Die DefaultDataTable-Komponente hat bereits die Kindkomponente für die Sei-tennavigation, die Anzeige der Spalten und für die Anzeige bei leerer Tabelle hinzugefügt.Damit die Struktur der Komponente etwas besser sichtbar wird, habe ich in diesem Bei-spiel ein paar CSS-Definitionen zum Markup hinzugefügt.Listing 8.29 DefaultDataTable.html <html> <head> <title>DefaultDataTable Page</title> <style> table.liste { border-collapse: collapse; width:400px; } table.liste tbody td { border:1px solid #a0a0a0; } table.liste tbody tr.even td { background-color:#f0f0f0; } .wicket_orderUp { border-top: 2px solid red; } .wicket_orderDown { border-bottom: 2px solid red; } </style> </head> <body> <wicket:fragment wicket:id="deleteFragment"> <a wicket:id="link">löschen</a> </wicket:fragment> <table wicket:id="list" class="liste"> </table> </body> </html>In Abbildung 8.4 sehen wir dann das Ergebnis unserer Mühen. Zu beachten ist, dass durcheinen Klick auf die Kopfzeile der Spalte das Ergebnis sortiert werden kann. Abbildung 8.4 DefaultDataTable mit Daten aus der Datenbank 167
  • 8 Listen und Tabellen Zusammenfassung Während einfache Listen am besten mit der ListView-Komponente dargestellt werden können, empfiehlt sich für Datensätze aller Art eine Variante der DataTable-Komponente. Wenn man die Klassen für den Zugriff auf die Daten aus einer Persistenzschicht etwas allgemeiner implementiert und die DataTable-Komponente noch um ein paar Hilfsfunk- tionen ergänzt, kann man sehr schnell komplexe Datenbankanwendungen schreiben. Wicket bietet dabei ausreichend Flexibilität, um die Lösung den eigenen Bedürfnissen entspre- chend anpassen zu können.168
  • 9 Formulare Formulare sind ein wesentlicher Bestandteil einer Webanwendung. Im folgenden Kapitel werden wir uns mit den unterschiedlichen Formularkomponenten befassen. Ich werde an- hand von Beispielen demonstrieren, wie man Formulareingaben prüft und dem Nutzer ein hilfreiches Feedback gibt. Außerdem werden wir uns damit beschäftigen, wie man mithilfe von Ajax die Nutzbarkeit eines Formulars weiter verbessern kann.9.1 Voraussetzungen Für die folgenden Beispiele ist es hilfreich, wenn wir durch ein wenig CSS die Darstellung von Formularen und Komponenten etwas verändern. Dazu binden wir eine CSS-Datei in jede Beispielseite ein. Listing 9.1 Forms.java package de.wicketpraxis.web.thema.komponenten.forms; ... public class Forms { public static HeaderContributor getCss() { return CSSPackageResource.getHeaderContribution( Forms.class, "form.css"); } } Der Aufruf der Methode getCSS() liefert einen HeaderContributor, der die Datei form.css in die Seite einbindet. Diese Datei müssen wir im selben Paket wie die Forms- Klasse anlegen, allerdings in src/main/resources. Listing 9.2 form.css ul.feedbackPanel { padding-left:0px; } li.feedbackPanelINFO { border:1px dashed green; background-color:#ddffdd; list-style-type: none; } li.feedbackPanelWARNING { 169
  • 9 Formulare border:1px dotted #cccc00; background-color:#ffffcc; list-style-type: none; } li.feedbackPanelERROR { border:1px solid red; background-color:#ffcccc; list-style-type: none; color:red; font-weight:bold; } div.ERROR { border:1px solid red; background-color:#ffcccc; } div.ERROR input { border:1px solid red; margin:4px; }9.2 Feedback Für Rückmeldungen an den Nutzer steht in Wicket die FeedbackPanel-Komponente zur Verfügung. Dabei werden die Nachrichten in der Session abgelegt, sodass auf der Ergeb- nisseite darauf zugegriffen werden kann. Um zu veranschaulichen, dass das FeedbackPanel unabhängig von Formularen funktio- niert und dass man mehr als nur Fehlermeldungen anzeigen kann, verwenden wir im fol- genden Beispiel drei Links, die jeweils eine Nachricht an den Nutzer erzeugen. Listing 9.3 FeedbackPanelPage.java package de.wicketpraxis.web.thema.komponenten.forms.basics; ... public class FeedbackPanelPage extends WebPage { public FeedbackPanelPage() { add(Forms.getCss()); add(new FeedbackPanel("feedback")); add(new Link("link") { @Override public void onClick() { info("Geklickt"); } }); add(new Link("linkWarnung") { @Override public void onClick() { warn("Achtung"); } }); add(new Link("linkFehler") { @Override public void onClick() { error("Irren ist menschlich."); } }); } } Listing 9.4 FeedbackPanelPage.html <html> <head> <title>FeedbackPanel Page</title> </head> <body> <div wicket:id="feedback"></div>170
  • 9.3 Basisklasse für alle Beispiele <a wicket:id="link">Klick mich</a><br> <a wicket:id="linkWarnung">Vorsicht</a><br> <a wicket:id="linkFehler">besser nicht klicken</a><br> </body> </html> Je nachdem, auf welchen Link man klickt, erhält man eine passende Rückmeldung (Abbil- dung 9.1). Abbildung 9.1 FeedbackPanel in Aktion9.3 Basisklasse für alle Beispiele Für Formulare ist es wichtig, dass mindestens ein FeedbackPanel auf der Seite eingebun- den ist, denn alle Fehlermeldungen, die bei der Abarbeitung von Formularen auftreten, werden über diese Komponente angezeigt. Wir erstellen daher eine abstrakte Klasse, von der wir alle weiteren Beispiele ableiten. Da- bei fügen wir zur Seite eine FeedbackPanel-Komponente und die Referenz auf die CSS- Datei hinzu. Für die FeedbackPanel-Komponente aktivieren wir die Ajax-Unterstützung (setOutputMarkupId(true)). Listing 9.5 AbstractFormPage.java package de.wicketpraxis.web.thema.komponenten.forms; ... public abstract class AbstractFormPage extends WebPage { private FeedbackPanel _feedbackPanel; public AbstractFormPage() { add(Forms.getCss()); _feedbackPanel = new FeedbackPanel("feedback"); _feedbackPanel.setOutputMarkupId(true); add(_feedbackPanel); } protected FeedbackPanel getFeedbackPanel() { return _feedbackPanel; } protected void updateFeedbackPanel(AjaxRequestTarget target) { target.addComponent(_feedbackPanel); } } 171
  • 9 Formulare Für den Zugriff auf das FeedbackPanel definieren wir noch zwei Methoden. Die zweite Methode ist wichtig, wenn das FeedbackPanel per Ajax aktualisiert werden muss. Da wir alle weiteren Beispielseiten von dieser Seite ableiten, muss die Stelle definiert werden, wo die abgeleitete Seite ihre Informationen einblendet (<wicket:child/>). Listing 9.6 AbstractFormPage.html <html> <head><title>Form Page</title></head> <body> <div wicket:id="feedback"></div> <wicket:child/> </body> </html>9.4 Formulare absenden Wenn der Nutzer Daten in das Formular eingetragen hat, können die Daten übermittelt und das Formular abgesendet werden. Dieser einfache Vorgang kann auf sehr unterschiedli- chen Wegen ausgelöst werden. 9.4.1 Absenden mit Submit-Button Im folgenden Beispiel erzeugen wir ein einfaches Formular ohne jede Eingabemöglichkeit. Wir überschreiben die onSubmit-Methode des Formulars und erzeugen eine Nachricht an den Nutzer. Listing 9.7 StandardFormPage.java package de.wicketpraxis.web.thema.komponenten.forms.basics; ... public class StandardFormPage extends AbstractFormPage { public StandardFormPage() { add(new Form("form") { @Override protected void onSubmit() { info("Formular abgeschickt"); } }); } } Listing 9.8 StandardFormPage.html <wicket:extend> <form wicket:id="form"> <button>Abschicken</button> </form> </wicket:extend> Das Formular ist die einzige Komponente. Das button-Tag sorgt dafür, dass der Browser die Datenübertragung startet.172
  • 9.4 Formulare absenden9.4.2 Button-KomponenteIm letzten Beispiel wurde das Formular abgeschickt und verarbeitet, ohne dass der Submit-Button eine Komponente war. Dabei ist gerade diese Möglichkeit besonders interessant.Deshalb ergänzen wir das letzte Beispiel um zwei Button- und zwei SubmitLink-Kom-ponenten.Listing 9.9 FormSubmitButtonPage.java package de.wicketpraxis.web.thema.komponenten.forms.basics; ... public class FormSubmitButtonsPage extends AbstractFormPage { public FormSubmitButtonsPage() { Form form = new Form("form") { @Override protected void onSubmit() { info("Formular abgeschickt"); } }; form.setOutputMarkupId(true); form.add(new Button("button1") { @Override public void onSubmit() { warn("Button 1 geklickt"); } }); form.add(new Button("button2") { @Override public void onSubmit() { warn("Button 2 geklickt"); } }); form.add(new SubmitLink("button3")); add(form); add(new SubmitLink("button4",form)); } }Für die beiden Button-Komponenten überschreiben wir die onSubmit-Methode, sodasseine Nachricht erzeugt wird, welcher Button angeklickt wurde. Dass man ein Formularauch durch Klick auf einen Link abschicken kann, kann man an den SubmitLink-Komponenten sehen. Dabei ist es möglich, Formulare auch durch Links abzuschicken, diesich außerhalb des Formulars befinden. Dazu kann man das entsprechende Formular alsParameter angeben. Wicket erzeugt bei beiden Links den notwendigen JavaScript-Aufruf.Listing 9.10 FormSubmitButtonPage.html <wicket:extend> <form wicket:id="form"> <button wicket:id="button1">Button1</button><br> <input type="submit" wicket:id="button2" value="Button2"><br> <a wicket:id="button3">Button3</a> </form> <a wicket:id="button4">Button4</a> </wicket:extend>In Abbildung 9.2 können wir sehen, welches Ergebnis wir erhalten, wenn wir auf einenButton klicken. 173
  • 9 Formulare Abbildung 9.2 Formulare absenden durch Buttons und Links 9.4.3 Submit per Ajax Wicket bietet auch eine gute Ajax-Unterstützung für Formulare. Das folgende Beispiel soll veranschaulichen, wie man ein Formular per Ajax abschickt. Dabei unterstützt uns Wicket mit drei verschiedenen Komponenten, die je nach Anwendungsfall eingesetzt werden kön- nen. Besonders interessant ist dabei die AjaxFallbackButton-Komponente, die wie bei der analogen Link-Komponente dafür sorgt, dass das Formular auch abgeschickt wird, wenn der Nutzer JavaScript und damit die Ajax-Unterstützung deaktiviert hat. Listing 9.11 FormAjaxSubmitPage.java package de.wicketpraxis.web.thema.komponenten.forms.basics; ... public class FormAjaxSubmitButtonsPage extends AbstractFormPage { public FormAjaxSubmitButtonsPage() { Form form = new Form("form") { @Override protected void onSubmit() { info("Formular abgeschickt"); } }; form.setOutputMarkupId(true); form.add(new AjaxFallbackButton("button1",form) { @Override protected void onSubmit(AjaxRequestTarget target, Form<?> form) { warn("Button 1 geklickt"); if (target!=null) { target.addComponent(form); target.addComponent(getFeedbackPanel()); } } }); form.add(new AjaxButton("button2",form) { @Override protected void onSubmit(AjaxRequestTarget target, Form<?> form) { warn("Button 2 geklickt"); target.addComponent(form); target.addComponent(getFeedbackPanel());174
  • 9.4 Formulare absenden } }); add(form); add(new AjaxSubmitLink("button3",form) { @Override protected void onSubmit(AjaxRequestTarget target, Form<?> form) { warn("Button 3 geklickt"); target.addComponent(form); target.addComponent(getFeedbackPanel()); } }); } }In allen drei Fällen müssen wir die zu aktualisierenden Komponenten zum AjaxRe-questTarget hinzufügen. Zu beachten ist dabei Folgendes: Hat der Nutzer die Ajax-Unterstützung deaktiviert, dann schickt die AjaxFallbackButton-Komponente das For-mular ab, und es werden beide onSubmit-Methoden (Formular und Button) aufgerufen.Die AjaxButton-Komponente schickt auch das Formular ab, allerdings wird nicht die on-Submit-Methode der Button-Komponente aufgerufen. Zu guter Letzt passiert beim Klickauf die AjaxSubmitLink-Komponente einfach gar nichts, das Formular wird nicht abge-schickt. Daher empfiehlt sich im Regelfall die AjaxFallbackButton-Komponente, da siein beiden Fällen zum gleichen Ergebnis führt.Listing 9.12 FormAjaxSubmitPage.html <wicket:extend> <form wicket:id="form"> <button wicket:id="button1">Button1</button><br> <input type="submit" wicket:id="button2" value="Button2"><br> </form> <a wicket:id="button3">Button3</a> </wicket:extend>9.4.4 POST und GETFormulare sollten immer per HTTP-POST-Methode übertragen werden. Trotzdem kann esnotwendig sein, in bestimmten Fällen Formulare per HTTP-GET, also wie einen normalenSeitenaufruf mit in der URL kodierten Parametern, zu übertragen. Das zweite Formularüberschreibt dazu die Methode getMethod().Listing 9.13 FormMethodPage.java package de.wicketpraxis.web.thema.komponenten.forms.basics; ... public class FormMethodPage extends AbstractFormPage { public FormMethodPage() { add(new Form("formPost") { @Override protected void onSubmit() { info("Form mit POST"); 175
  • 9 Formulare } }); Form formGet = new Form("formGet") { @Override protected void onSubmit() { info("Form mit GET"); setRedirect(false); } @Override protected String getMethod() { return Form.METHOD_GET; } }; add(formGet); } } Ansonsten gleichen sich die beiden Formulare, und die onSubmit-Methode wird wie er- wartet aufgerufen. In der Grundkonfiguration wird der Nutzer nach Absenden eines For- mulars auf eine neue Zielseite weitergeleitet, auf der das Ergebnis angezeigt wird. Das ge- schieht transparent und hat den Vorteil, dass beim Aktualisieren der Seite durch den Nut- zer das Formular nicht noch einmal abgeschickt wird (DoubleSubmit-Problem). Möchte man dieses Verhalten ändern, kann man das wie in unserem Beispiel durch den Aufruf von setRedirect(false) unterbinden. Listing 9.14 FormMethodPage.html <wicket:extend> <form wicket:id="formPost"> <input type="text" name="Text"> <button>Abschicken(Post)</button> </form> <form wicket:id="formGet"> <input type="text" name="Text"> <button>Abschicken(Get)</button> </form> </wicket:extend>9.5 Textfelder Die einfachste Formularkomponente ist das Textfeld. Um an die Daten zu gelangen, die in das Textfeld eingegeben werden, muss man der TextField-Komponente ein Modell zu- weisen, das gelesen und geschrieben werden kann. Ein LoadableDetachedModel ginge an dieser Stelle also nicht. Doch was auf den ersten Blick einfach und naheliegend aussieht, birgt eine kleine Überraschung. Listing 9.15 FormTextFieldTypePage.java package de.wicketpraxis.web.thema.komponenten.forms. komponenten.textfield; ... public class FormTextFieldTypePage extends AbstractFormPage176
  • 9.5 Textfelder { public FormTextFieldTypePage() { final Model<String> modelText = new Model<String>(); final Model<Integer> modelNumber = new Model<Integer>(); Form form = new Form("form") { @Override protected void onSubmit() { Object valueText=modelText.getObject(); Object valueNumber=modelNumber.getObject(); info("TextType: "+valueText.getClass()); info("NumberType: "+valueNumber.getClass()); } }; TextField<String> textField = new TextField<String>("text", modelText); textField.setRequired(true); form.add(textField); TextField<Integer> numberField = new TextField<Integer>("zahl", modelNumber); numberField.setRequired(true); form.add(numberField); form.add(new Button("submit")); add(form); } }Wir haben zwei Textfelder, denen wir jeweils ein Modell zugewiesen haben. Dabei solltein dem einen Textfeld eine Zahl eingegeben werden können. Wenn wir das Formular ab-schicken und die Werte aus den Modellen auslesen, werden wir feststellen, dass in beidenFällen ein String und kein Integer zurückgegeben wird. Das erklärt sich daraus, dass Wi-cket auf den generischen Typ des Modells nicht zurückgreifen kann und daher davon aus-geht, dass die gewünschten Daten vom Typ String sind.Durch den Aufruf von setRequired() machen wir beide Textfelder zu Pflichtfeldern. DieFehlermeldungen, die ausgegeben werden, wenn wir das Formular abschicken, ohne dieTextfelder gefüllt zu haben, sind in verschiedenen Sprachen bereits hinterlegt.Listing 9.16 FormTextFieldTypePage.html <wicket:extend> <form wicket:id="form"> Text: <input wicket:id="text"><br> Zahl: <input wicket:id="zahl"><br> <button wicket:id="submit">Abschicken</button> </form> </wicket:extend>Schicken wir das Formular ab, dann erhalten wir folgende Ausgabe: TextType: class java.lang.String NumberType: class java.lang.String 177
  • 9 Formulare 9.5.1 Typangabe Die Probleme des letzten Beispiels lassen sich relativ einfach umschiffen. Dazu benötigt Wicket nur eine verlässliche Typinformation, um die Eingabe in den passenden Typ kon- vertieren zu können. Wir ergänzen daher das eine Textfeld um einen Typparameter, auf den die Komponenten zurückgreifen können. Listing 9.17 FormWithTextFieldPage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.textfield; ... public class FormWithTextFieldPage extends AbstractFormPage { public FormWithTextFieldPage() { Form form = new Form("form") { @Override protected void onSubmit() { info("Hat geklappt"); } @Override protected void onError() { warn("Es gab da einen Fehler."); } }; TextField<String> textField = new TextField<String>("text",new Model<String>()); textField.setRequired(true); form.add(textField); TextField<Integer> textField2 = new TextField<Integer>("text2",new Model<Integer>(),Integer.class); textField2.setRequired(true); form.add(textField2); form.add(new Button("submit")); add(form); } } Wenn man nun in das Textfeld, in dem Wicket eine Zahl erwartet, etwas anderes eingibt, dann wird automatisch ein Fehler erzeugt. Die Standardfehlermeldung möchten wir in die- sem Beispiel abändern. Dazu legen wir neben der Markup-Datei im selben Verzeichnis eine Property-Datei an. Listing 9.18 FormWithTextFieldPage.html <wicket:extend> <form wicket:id="form"> Text: <input wicket:id="text"><br> Zahl: <input wicket:id="text2"><br> <button wicket:id="submit">Abschicken</button> </form> </wicket:extend> Da Wicket die Property-Dateien rekursiv verarbeitet, ist es nicht nötig, für alle Komponen- ten und alle zu erwartenden Fehlermeldungen neue Definitionen in dieser Datei abzulegen. Wir ändern nur die Fehlermeldungen für die zweite TextField-Komponente. Dabei ermit- telt Wicket den passenden Eintrag anhand der Komponenten-ID. Ändert sich die ID, muss man auch die ID in der Property-Datei anpassen. Beim Formulieren der Texte kann man auf verschiedene Variablen zugreifen und diese im Text durch die Angabe eines Platzhal- ters in der Form ${name} ersetzen lassen.178
  • 9.5 TextfelderListing 9.19 FormWithTextFieldPage.properties form.text2.Required=Bitte was in ${label} reinschreiben form.text2.IConverter=Irgendwie kann ich aus ${input} in ${label} keine Zahl machen.Wenn wir nichts in das Formular eintragen, dann erscheinen die passenden Fehlermeldun-gen (Abbildung 9.3). Tragen wir für Text und Zahl ein „hallo“ ein, dann erscheint eineFehlermeldung, die darüber Auskunft erteilt, dass die Eingabe „hallo“ in keine Zahl um-gewandelt werden konnte (Abbildung 9.4). Abbildung 9.3 Formular mit fehlerhafter Eingabe Abbildung 9.4 Fehlerhaft ausgefülltes FormularWenn für Text ein „hallo“ und für Zahl eine 123 eingetragen wird, dann kann das Formu-lar erfolgreich abgeschickt werden.9.5.2 Automatische TypermittlungSelbst für einfache Formulare ist der Aufwand schon recht groß. Auch hier kann mandurch die geschickte Kombination von verschiedenen Komponenten zu einem wesentlicheinfacheren Ansatz kommen. Dabei spielt die PropertyModel-Klasse eine wesentlicheRolle.Wir legen dazu eine neue Klasse an, die verschiedene Attribute bereitstellt. Auf dieseKlasse greifen wir auch in den folgenden Beispielen zurück, sodass wir an dieser Stellealle auch später notwendigen Attribute definieren.Listing 9.20 StandardTypesBean.java (gekürzt) package de.wicketpraxis.web.thema.komponenten.forms.beans; ... public class StandardTypesBean implements Serializable { int _zahl; double _kommazahl; String _text; String _text2; Date _datum; 179
  • 9 Formulare getZahl(),setZahl(),getKommazahl(),... } Dabei haben wir drei geläufige Datentypen definiert, die wir im folgenden Beispiel ver- wenden möchten. Dazu binden wir ein Modell an das Formular. In diesem Fall benutzen wir ein CompoundPropertyModel. Die TextField-Komponenten bekommen das passen- de Modell über ihre Komponenten-ID. Dabei kann auch der Typ ermittelt werden, sodass die Eingaben automatisch in die richtigen Typen konvertiert werden können. Listing 9.21 TextFieldsPage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.textfield; ... public class TextFieldsPage extends AbstractFormPage { public TextFieldsPage() { StandardTypesBean bean = new StandardTypesBean(); bean.setDatum(new Date()); Form<StandardTypesBean> form=new Form<StandardTypesBean>("form", new CompoundPropertyModel<StandardTypesBean>(bean)) { @Override protected void onSubmit() { StandardTypesBean bean = getModelObject(); info("Text: "+bean.getText()+", Zahl: "+bean.getZahl()+ ", Datum: "+bean.getDatum()); } @Override protected void onError() { StandardTypesBean bean = getModelObject(); error("Text: "+bean.getText()+", Zahl: "+bean.getZahl()+ ", Datum: "+bean.getDatum()); } }; form.add(new TextField<String>("Text")); form.add(new TextField<Double>("Zahl")); form.add(new DateTextField("Datum","dd.MM.yyyy um HH:mm:ss")); add(form); } } Wir füllen die Felder aus und schicken das Formular ab. Anschließend werden Zahl und Datum aus dem Inhalt des Eingabefeldes in den entsprechenden Datentyp konvertiert. Beim Datumsfeld haben wir einen Format-String angegeben, der dafür verantwortlich ist, wie die Eingabe interpretiert wird. Außerdem wird dieser Format-String für die Darstel- lung des Eingabefeldes herangezogen. Wichtiger Hinweis Wenn das Formular abgeschickt wird, werden die Werte nur dann in die Bean übertragen, wenn kein Fehler aufgetreten ist.180
  • 9.6 Label Listing 9.22 TextFieldPage.html <wicket:extend> <form wicket:id="form"> Text: <input wicket:id="Text"><br> Zahl: <input wicket:id="Zahl"><br> Datum: <input wicket:id="Datum"><br> <button>Abschicken</button> </form> </wicket:extend> Wenn wir das erste Mal die Felder mit korrekten Werten füllen und beim zweiten Mal ei- nen kleinen Fehler einbauen, erhalten wir ein Ergebnis wie in Abbildung 9.5. Abbildung 9.5 Formular mit Fehler- meldungen Empfehlung In den meisten Fällen ist der Schreibaufwand für ein Formular geringer, wenn man eine JavaBean als Modell benutzt. Außerdem sind dann alle Eingabedaten bereits in einem Objekt zusammengefasst und können so als Parameter einfach weitergegeben werden. Das Auslesen der einzelnen Komponentenmodelle entfällt.9.6 Label Jedes Eingabefeld hat üblicherweise einen Bezeichner. Dafür gibt es sogar ein entspre- chendes HTML-Tag. Das ermöglicht unter anderem, dass das Eingabefeld aktiviert wird, wenn der Nutzer auf den Bezeichner klickt. Listing 9.23 SimpleFormComponentLabelPage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.formcomplabel; ... public class SimpleFormCompLabelPage extends AbstractFormPage { public SimpleFormCompLabelPage() { Form form = new Form("form"); TextField<String> textField = new TextField<String>("text", Model.of("")); textField.setLabel(Model.of("etwas Text")); form.add(new SimpleFormComponentLabel("label",textField)); form.add(textField); add(form); } } 181
  • 9 Formulare Listing 9.24 SimpleFormComponentLabelPage.html <wicket:extend> <form wicket:id="form"> <label wicket:id="label">Text</label>: <input wicket:id="text"><br> <button>Abschicken</button> </form> </wicket:extend> Die SimpleFormComponentLabel-Komponente setzt das Attribut „for“ der Komponente und ersetzt den Inhalt des HTML-Tags durch den Rückgabewert aus getLabel() der TextField-Komponente. Ein Klick auf das Label aktiviert nun das Eingabefeld. Hinweis Das Label der TextField-Komponente entspricht der Komponenten-ID, solange man es nicht auf einen anderen Wert setzt. In eigenen Fehlermeldungen kann man dann auf das Label, den Namen und den Inhalt der Eingabe über ${label}, ${name} und ${input} zurück- greifen. Wenn man aber nur das Attribut ersetzen möchte, ohne dass auch der Inhalt des label- Tags ersetzt wird, benutzt man einfach die FormComponentLabel-Klasse. Anhand des Quelltextes der Ergebnisseite kann man gut erkennen, wie das Ganze zusammenspielt. Listing 9.25 Ergebnis.html ... <body> <div wicket:id="feedback" id="feedback4"><wicket:panel> </wicket:panel></div> <wicket:child><wicket:extend> <form wicket:id="form" id="form5" method="post" action="?wicket:interface=:1:form::IFormSubmitListener::"> ... <label wicket:id="label" for="texta">etwas Text</label>: <input wicket:id="text" value="" name="text" id="texta"><br> <button>Abschicken</button> </form> </wicket:extend></wicket:child> </body> ...9.7 CheckBox Die CheckBox-Komponente erwartet als Datentyp ein Boolean. Daher erstellen wir uns für dieses Beispiel eine neue Bean-Klasse, die entsprechende Attribute besitzt. Listing 9.26 CheckBoxBean.java (gekürzt) package de.wicketpraxis.web.thema.komponenten.forms.beans; ... public class CheckBoxBean implements Serializable { boolean _check; ArrayList<String> _liste=new ArrayList<String>(); isCheck(),setCheck(),getListe(),... }182
  • 9.7 CheckBoxDass diese Klasse ein Attribut vom Typ ArrayList<String> besitzt, liegt daran, dass wirin diesem Beispiel neben der einfachen CheckBox-Komponente die darauf aufbauendeCheckBoxMultipleChoice-Komponente einsetzen möchten.Listing 9.27 CheckBoxPage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.check; ... public class CheckBoxPage extends AbstractFormPage { public CheckBoxPage() { Form<CheckBoxBean> form = new Form<CheckBoxBean>("form", new CompoundPropertyModel<CheckBoxBean>(new CheckBoxBean())) { @Override protected void onSubmit() { CheckBoxBean bean = getModelObject(); info("Check: " + bean.isCheck()); info("Liste: " + bean.getListe()); } }; form.add(new CheckBox("Check")); form.add(new CheckBoxMultipleChoice<String>("Liste", Arrays.asList("Handtuch", "Seife", "Stöpsel"))); add(form); } }Wir übergeben der CheckBoxMultipleChoice-Komponente eine Liste mit möglichenWerten. Für jedes Element erzeugt die Komponente dann eine CheckBox (Abbildung 9.6).Das Attribut wird mit einer Liste gesetzt, die alle Elemente enthält, für die man die Check-Box aktiviert hat. Abbildung 9.6 Die verschiedenen CheckBox- KomponentenListing 9.28 CheckBoxPage.html <wicket:extend> <form wicket:id="form"> Checkbox: <input wicket:id="Check" type="checkbox"><br> <span wicket:id="Liste"></span> <button>Abschicken</button> </form> </wicket:extend>Möchten wir dem Nutzer die Möglichkeit geben, alle Einträge auf einmal zu selektieren,lässt sich das einfach bewerkstelligen. Dazu benutzen wir dann aber die CheckGroup-Kom-ponente und fügen jeweils eine Check-Komponente für jeden gewünschten Eintrag hinzu 183
  • 9 Formulare Abbildung 9.7 Die CheckGroup- Komponente (Abbildung 9.7). An dieser Stelle könnte man auf eine ListView-Komponente zurückgrei- fen. Hinweis Wenn man in Formularen eine ListView-Komponente benutzt, um dynamisch Formular- elemente hinzuzufügen, muss man für die ListView-Komponente die Methode set- ReuseItems(true) aufrufen, da sonst die Validierung nicht korrekt funktioniert. Listing 9.29 CheckGroupPage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.check; ... public class CheckGroupPage extends AbstractFormPage { public CheckGroupPage() { Form<CheckBoxBean> form = new Form<CheckBoxBean>("form", new CompoundPropertyModel<CheckBoxBean>(new CheckBoxBean())) { @Override protected void onSubmit() { CheckBoxBean bean = getModelObject(); info("Liste: " + bean.getListe()); } }; CheckGroup<String> checkGroup = new CheckGroup<String>("Liste"); checkGroup.add(new Check<String>("handtuch",Model.of("Handtuch"))); checkGroup.add(new Check<String>("seife",Model.of("Seife"))); checkGroup.add(new Check<String>("stoepsel",Model.of("Stöpsel"))); checkGroup.add(new CheckGroupSelector("alles",checkGroup)); form.add(checkGroup); add(form); } } Die CheckGroupSelector-Komponente aktiviert alle Check-Komponenten per Java- Script. Die CheckGroup-Komponente setzt das Attribut anhand der Werte in den Check- Komponenten. Listing 9.30 CheckGroupPage.html <wicket:extend> <form wicket:id="form"> <span wicket:id="Liste"> Handtuch: <input wicket:id="handtuch" type="checkbox"><br> Seife: <input wicket:id="seife" type="checkbox"><br> Stöpsel: <input wicket:id="stoepsel" type="checkbox"><br>184
  • 9.8 RadioButton <br> Alles? <input wicket:id="alles" type="checkbox"><br> </span> <button>Abschicken</button> </form> </wicket:extend>9.8 RadioButton Aus einer Gruppe von Radio-Buttons kann der Nutzer immer nur einen auswählen. Das be- deutet, dass das Ergebnis aus nur einem Wert und keiner Liste von Werten besteht. Listing 9.31 RadioButtonPage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.radio; ... public class RadioButtonPage extends AbstractFormPage { public RadioButtonPage() { Form<StandardTypesBean> form = new Form<StandardTypesBean>("form", new CompoundPropertyModel<StandardTypesBean>( new StandardTypesBean())) { @Override protected void onSubmit() { StandardTypesBean bean = getModelObject(); info("Zahl: " + bean.getZahl()); info("Text: " + bean.getText()); } }; RadioGroup<Integer> radioGroup = new RadioGroup<Integer>("Zahl"); radioGroup.add(new Radio<Integer>("Zahl1",Model.of(1))); radioGroup.add(new Radio<Integer>("Zahl2",Model.of(2))); radioGroup.add(new Radio<Integer>("Zahl4",Model.of(4))); radioGroup.setRequired(true); form.add(radioGroup); form.add(new RadioChoice<String>("Text", Arrays.asList("Haus", "Baum", "Auto"))); add(form); } } Da das Attribut „Zahl“ der Bean nicht null sein darf (ist vom Typ int und nicht Integer), machen wir dieses Eingabefeld zum Pflichtfeld (setRequired(true)). Auch wenn die RadioChoice-Komponente einfacher zu benutzen ist, bietet die RadioGroup-Komponente wesentlich mehr Flexibilität, was sich gerade in der Darstellung niederschlägt (Abbildung 9.8). Listing 9.32 RadioButtonPage.html <wicket:extend> <form wicket:id="form"> Zahl: <span wicket:id="Zahl"> 1 <input wicket:id="Zahl1" type="radio">, 2 <input wicket:id="Zahl2" type="radio">, oder 4 <input wicket:id="Zahl4" type="radio"> </span> <br><br> 185
  • 9 Formulare Text:<br><span wicket:id="Text"></span><br> <button>Abschicken</button> </form> </wicket:extend> Abbildung 9.8 RadioButton und RadioGroup9.9 Auswahlfelder Das select-Tag bietet sehr viel Möglichkeiten, um die Funktion und die Darstellung nach eigenen Wünschen anzupassen. Das Wicket-Framework bietet für die verschiedenen Vari- anten jeweils eigene Komponenten. Auch in diesem Bereich hat man die Wahl zwischen Flexibilität und Aufwand. Für welchen Anwendungsfall welche Komponente am besten geeignet ist, sollen die folgenden Beispiele veranschaulichen. 9.9.1 Select Am einfachsten scheint es, wenn man jeden Eintrag des Menüs von Hand hinzufügt. In diesem Beispiel fügen wir eine Select-Komponente hinzu. Dabei tragen wir die ersten vier Auswahlmöglichkeiten von Hand ein. Das Aussehen jedes einzelnen Eintrags könnte ich auf diese Weise verändern, da jeder Eintrag eine eigenständige Komponente ist. Möchte man aber Listen unbekannter Länge als Option hinterlegen, bietet sich die Selec- tOptions-Komponente an. Dafür muss man aber einen IOptionRenderer implementie- ren, der für einen Wert die Anzeige und auch das Model erstellt. Hinweis Die SelectOption-Komponente ist nicht zu verwechseln mit der SelectOptions- Komponente, was recht einfach geschehen kann, da sich der Name der beiden Komponenten nur durch ein „s“ unterscheidet. Listing 9.33 SimpleSelectPage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.select; ... public class SimpleSelectPage extends AbstractFormPage186
  • 9.9 Auswahlfelder { public SimpleSelectPage() { Form<StandardTypesBean> form = new Form<StandardTypesBean>("form", new CompoundPropertyModel<StandardTypesBean>( new StandardTypesBean())) { @Override protected void onSubmit() { StandardTypesBean bean = getModelObject(); info("Zahl: " + bean.getZahl()); info("Text: " + bean.getText()); } }; Select select = new Select("Zahl"); select.add(new SelectOption<Integer>("das",Model.of(1))); select.add(new SelectOption<Integer>("ist",Model.of(2))); select.add(new SelectOption<Integer>("zu",Model.of(3))); select.add(new SelectOption<Integer>("schwierig",Model.of(4))); IOptionRenderer<Integer> renderer=new IOptionRenderer<Integer>() { public String getDisplayValue(Integer object) { return "Wähle "+object.toString(); } public IModel<Integer> getModel(Integer value) { return Model.of(value); } }; select.add(new SelectOptions<Integer>("options", Arrays.asList(5,6,7,8),renderer)); form.add(select); add(form); } }Die hohe Flexibilität erkaufen wir uns an dieser Stelle mit einem nicht unwesentlichenSchreibaufwand. Da es aber immer wieder vorkommt, dass für bestimmte Fälle nicht aufdie Standardkomponenten zurückgegriffen werden kann, lohnt es sich vielleicht, basierendauf diesen einfachen Komponenten eine an die eigenen Bedürfnisse angepasste Kompo-nente zu erstellen.Listing 9.34 SimpleSelectPage.html <wicket:extend> <form wicket:id="form"> <select wicket:id="Zahl"> <option wicket:id="das">1</option> <option wicket:id="ist">2</option> <option wicket:id="zu">3</option> <option wicket:id="schwierig">4</option> <wicket:container wicket:id="options"> <option wicket:id="option" style="color: red;"></option> </wicket:container> </select> <br> <button>Abschicken</button> </form> </wicket:extend> 187
  • 9 Formulare 9.9.2 DropDownChoice Die DropDownChoice-Komponente nimmt uns noch mehr Arbeit ab. Um die Möglichkei- ten zu zeigen, die diese Komponente bietet, erstellen wir für dieses Beispiel wieder eine passende JavaBean. Dabei ist darauf hinzuweisen, dass das Attribut „Tag1“ nicht vom Typ Integer, sondern vom Typ int ist. Damit ist dieses Attribut bereits mit 0 vorbelegt. Wel- che Auswirkungen das hat, werden wir später feststellen. Listing 9.35 WochentagBean.java (gekürzt) package de.wicketpraxis.web.thema.komponenten.forms.beans; ... public class WochentagBean implements Serializable { int _tag1; Integer _tag2; Integer _tag3; getTag1(),setTag1(),getTag2(),... } In unserem Beispiel erstellen wir eine Map, die alle Wochentage aufnimmt. Dabei benutzen wir die Konstanten der Calendar-Klasse als Schlüssel. Die Liste aller Auswahlmöglich- keiten ergibt sich aus der Liste der Key-Elemente der Map. Listing 9.36 DropDownChoicePage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.select; ... public class DropDownChoicePage extends AbstractFormPage { static LinkedHashMap<Integer,String> _tage= new LinkedHashMap<Integer, String>(); static { _tage.put(Calendar.MONDAY,"Montag"); _tage.put(Calendar.TUESDAY,"Dienstag"); _tage.put(Calendar.WEDNESDAY,"Mittwoch"); _tage.put(Calendar.THURSDAY,"Donnerstag"); _tage.put(Calendar.FRIDAY,"Freitag"); _tage.put(Calendar.SATURDAY,"Sonnabend"); _tage.put(Calendar.SUNDAY,"Sonntag"); } public DropDownChoicePage() { Form<WochentagBean> form=new Form<WochentagBean>("form", new CompoundPropertyModel<WochentagBean>(new WochentagBean())) { @Override protected void onSubmit() { WochentagBean bean = getModelObject(); info("Tag 1: "+bean.getTag1()); info("Tag 2: "+bean.getTag2()); info("Tag 3: "+bean.getTag3()); } }; IModel<List<? extends Integer>> choices = Model.of((List<Integer>) new ArrayList<Integer>(_tage.keySet())); IChoiceRenderer<Integer> renderer=new IChoiceRenderer<Integer>()188
  • 9.9 Auswahlfelder { public Object getDisplayValue(Integer object) { return _tage.get(object); } public String getIdValue(Integer object, int index) { return object.toString(); } }; form.add(new DropDownChoice<Integer>("Tag1",choices,renderer)); form.add(new DropDownChoice<Integer>("Tag2",choices,renderer)); form.add(new ListChoice<Integer>("Tag3",choices,renderer). setMaxRows(4)); add(form); } }Listing 9.37 DropDownChoicePage.html <wicket:extend> <form wicket:id="form"> Tag 1: <select wicket:id="Tag1"></select><br> Tag 2: <select wicket:id="Tag2"></select><br> Tag 3: <select wicket:id="Tag3"></select><br> <button>Abschicken</button> </form> </wicket:extend>Auch in diesem Beispiel muss ein entsprechender Renderer implementiert werden, möchteman für die Werte eine sinnvolle Anzeige bekommen. Es werden zwei DropDownChoice-und eine ListChoice-Komponente benutzt (Abbildung 9.9). Die erste DropDownChoice-Komponente benutzt als Attribut „Tag1“, was ja vom Typ int ist und bereits mit einemWert vorbelegt ist. Der Unterschied zwischen „Tag1“ und „Tag2“ wird sichtbar, wenn dieSeite das erste Mal aufgerufen wird. Dann wird für „Tag2“ ein Hinweis angezeigt, dassnoch nichts ausgewählt wurde. Diesen Hinweistext können wir einfach ersetzen, indem wirneben der Markup-Datei eine Property-Datei mit folgendem Inhalt anlegen.Listing 9.38 DropDownChoicePage.properties Tag2.null="Einen Tag wählen..." Abbildung 9.9 Die Komponenten DropDown- Choice und ListChoice 189
  • 9 Formulare 9.9.3 ListMultipleChoice Bisher können wir immer nur einen Eintrag aus der Liste auswählen. Für eine Mehrfach- auswahl gibt es ebenfalls passende Komponenten. Für dieses Beispiel benötigen wir wie- der eine JavaBean, welche die Auswahl im Attribut „Lieblingsfarben“ speichert. Listing 9.39 LieblingsfarbenBean.java (gekürzt) package de.wicketpraxis.web.thema.komponenten.forms.beans; ... public class LieblingsfarbenBean implements Serializable { List<String> _lieblingsfarben=new ArrayList<String>(); getLieblingsfarben(),setLieblingsfarben()... } Da die auszuwählenden Elemente auch automatisch für die Anzeige geeignet sind, benöti- gen wir keinen ChoiceRenderer zum Aufbereiten der Darstellung. Listing 9.40 ListMultipleChoicePage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.select; ... public class ListMultipleChoicePage extends AbstractFormPage { public ListMultipleChoicePage() { Form<LieblingsfarbenBean> form=new Form<LieblingsfarbenBean>("form", new CompoundPropertyModel<LieblingsfarbenBean>( new LieblingsfarbenBean())) { @Override protected void onSubmit() { LieblingsfarbenBean bean = getModelObject(); info("Lieblingsfarben: "+bean.getLieblingsfarben()); } }; List<String> farben = Arrays.asList("Rot","Grün","Gelb","Ocker", "Schwarz"); form.add(new ListMultipleChoice<String>("Lieblingsfarben",farben). setMaxRows(3)); add(form); } } Listing 9.41 ListMultipleChoicePage.html <wicket:extend> <form wicket:id="form"> Lieblingsfarben: <select wicket:id="Lieblingsfarben"></select><br> <button>Abschicken</button> </form> </wicket:extend> Die Komponente wird ebenfalls direkt an ein select-Tag gebunden. Nun kann man seine Lieblingsfarben auswählen und das Formular absenden.190
  • 9.9 AuswahlfelderPaletteAuch wenn die folgende Komponente eher keine Basiskomponente ist, möchte ich sie kurzvorstellen, weil sie sehr interessante Möglichkeiten bietet und nur mit Aufwand selbstentwickelt werden kann. Die Komponente gehört zum Paket Wicket-Extensions, das wir inunserem Projekt eingebunden haben.Die Komponente benutzt intern zwei ListMultipleChoice-Komponenten, die einmal dieausgewählten und auf der anderen Seite die zur Verfügung stehenden Einträge anzeigenkönnen (Abbildung 9.10). Außerdem können die Einträge in der Reihenfolge verändertwerden.Listing 9.42 PalettePage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.select; ... public class PalettePage extends AbstractFormPage { public PalettePage() { Form<LieblingsfarbenBean> form=new Form<LieblingsfarbenBean>("form", new CompoundPropertyModel<LieblingsfarbenBean>( new LieblingsfarbenBean())) { @Override protected void onSubmit() { LieblingsfarbenBean bean = getModelObject(); info("Lieblingsfarben: "+bean.getLieblingsfarben()); } }; CollectionModel<String> farben = new CollectionModel<String>( Arrays.asList("Rot","Grün","Gelb","Ocker","Schwarz")); IChoiceRenderer<String> renderer=new IChoiceRenderer<String>() { public Object getDisplayValue(String object) { return object; } public String getIdValue(String object, int index) { return object; } }; form.add(new Palette<String>("Lieblingsfarben", farben,renderer,10,true)); add(form); } }Die Komponente erwartet einen Renderer als Parameter, weshalb wir hier ebenfalls einenimplementieren. Der letzte Parameter beim Erzeugen der Palette-Komponente ist true underlaubt somit die Sortierung der Liste. 191
  • 9 Formulare Listing 9.43 PalettePage.html <wicket:extend> <form wicket:id="form"> Lieblingsfarben: <wicket:container wicket:id="Lieblingsfarben"> </wicket:container><br> <button>Abschicken</button> </form> </wicket:extend> Wir wählen ein paar Farben, und schon haben wir die Liste unserer Lieblingsfarben ermit- telt und in die richtige Reihenfolge gebracht (Abbildung 9.10). Abbildung 9.10 Die Interaktionsmöglichkeiten der Palette9.10 Dateien hochladen In manchen Fällen ist es notwendig, dass der Nutzer Daten und Dokumente in einer Web- anwendung zur Verfügung stellen muss. Wicket stellt dafür verschiedene Komponenten bereit, die die zugrundeliegende Komplexität, mit der man in anderen Frameworks unter Umständen noch in Berührung kommt, versteckt und gleichzeitig interessante Möglichkei- ten bieten. 9.10.1 FileUpload Die einfachste Möglichkeit, eine Datei hochzuladen, wird durch die FileUploadField- Komponente bereitgestellt. Listing 9.44 FileUploadPage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.upload; ... import org.apache.wicket.extensions.ajax.markup.html.form.upload.*; public class FileUploadPage extends AbstractFormPage { public FileUploadPage()192
  • 9.10 Dateien hochladen { final FileUploadField fileUploadField = new FileUploadField("Upload"); Form<Void> form=new Form<Void>("form") { @Override protected void onSubmit() { FileUpload upload = fileUploadField.getFileUpload(); if (upload!=null) { info(MessageFormat.format("Filename: {0}, Size: {1}, MimeType: {2}", upload.getClientFileName(), upload.getSize(),upload.getContentType())); } } }; form.setMultiPart(true); form.setMaxSize(Bytes.megabytes(100)); form.add(new UploadProgressBar("progressbar",form)); form.add(fileUploadField); add(form); } }Um den Upload nicht beliebig groß werden zu lassen, kann man die Größe begrenzen. Aufdie hochgeladenen Daten kann man in der onSubmit-Methode zugreifen und so Informa-tionen wie den Dateinamen, die Größe und den MimeType ermitteln.Um dem Nutzer etwas mehr Informationen darüber geben zu können, wie schnell die Da-ten gerade hochgeladen werden, kann man eine UploadProgressBar-Komponente hinzu-fügen. Damit diese Komponente aber etwas anzeigen kann, müssen wir in unserer Web-Application-Klasse eine Anpassung vornehmen.Listing 9.45 WicketPraxisApplication.java ... import javax.servlet.http.HttpServletRequest; import org.apache.wicket.extensions.ajax.markup.html.form. upload.UploadWebRequest; ... public class WicketPraxisApplication ... { ... @Override protected WebRequest newWebRequest(HttpServletRequest servletRequest) { return new UploadWebRequest(servletRequest); } ... }Nur der UploadWebRequest stellt die nötigen Daten bereit, die dann durch die Komponen-te angezeigt werden können.Listing 9.46 FileUploadPage.html <wicket:extend> <form wicket:id="form"> Datei: <input wicket:id="Upload" type="file"><br> <wicket:container wicket:id="progressbar"></wicket:container> 193
  • 9 Formulare <button>Abschicken</button> </form> </wicket:extend> 9.10.2 MultiFileUpload Möchte man mehr als eine Datei auf einmal hochladen, ist das ähnlich einfach wie das Hochladen einer Datei. Dabei kann man angeben, wie viele Dateien auf einmal hochgela- den werden können. Listing 9.47 MultiFileUploadPage.java package de.wicketpraxis.web.thema.komponenten.forms.komponenten.upload; ... public class MultiFileUploadPage extends AbstractFormPage { public MultiFileUploadPage() { final MultiFileUploadField fileUploadField = new MultiFileUploadField("Upload", new CollectionModel<FileUpload>(new ArrayList<FileUpload>()),5); Form<Void> form=new Form<Void>("form") { @Override protected void onSubmit() { Collection<FileUpload> uploads = fileUploadField.getModel().getObject(); for (FileUpload upload : uploads) { info(MessageFormat.format("Filename: {0}, Size: {1}, MimeType: {2}", upload.getClientFileName(), upload.getSize(),upload.getContentType())); } } }; form.setMultiPart(true); form.setMaxSize(Bytes.megabytes(100)); form.add(new UploadProgressBar("progressbar",form)); form.add(fileUploadField); add(form); } } Die Komponente manipuliert das Formular per JavaScript, sodass man vor dem Abschi- cken die zur Auswahl stehenden Dateien auch wieder aus der Liste entfernen kann. Für die UploadProgressBar-Komponente sind auch in diesem Beispiel dieselben Anpassungen an der Application-Klasse durchzuführen. Listing 9.48 MultiFileUploadPage.html <wicket:extend> <form wicket:id="form"> Datei: <wicket:container wicket:id="Upload"></wicket:container><br> <wicket:container wicket:id="progressbar"></wicket:container> <button>Abschicken</button> </form> </wicket:extend>194
  • 9.11 Gültigkeitsprüfung Wenn man dann Dateien auswählt und hochlädt, zeigt die UploadProgressBar-Kompo- nente Informationen über die Geschwindigkeit und den Fortschritt an (Abbildung 9.11). Nachdem alle Dateien hochgeladen wurden, wird wie in Abbildung 9.12 die Liste der Da- teien und Informationen über Größe, Dateiname und Dateityp angezeigt. Abbildung 9.11 Hochladen von Dateien Abbildung 9.12 Liste der hochgeladenen Dateien mit erweiterten Informationen9.11 Gültigkeitsprüfung Bisher konnten wir Eingabefelder zu Pflichtfeldern machen. Außerdem wurde geprüft, ob die Eingabe in den gewünschten Typ umgewandelt werden konnte. Doch das reicht bei weitem nicht aus, wenn es darum geht, die vom Nutzer eingegebenen Daten auf Richtig- keit zu prüfen. Wicket bringt daher eine Reihe vordefinierter Validatoren mit, mit denen man die Nutzereingabe prüfen kann. Außerdem ist es recht einfach, eigene Validatoren zu schreiben. Wie in den vorangegangenen Beispielen bereits zu erkennen war, gibt es einen wichtigen Aspekt, den man im Umgang mit Formularen beachten muss: Formulare schreiben die Da- ten erst dann in die Modelle, wenn kein einziger Fehler mehr vorhanden ist. Wenn die Me- thode onSubmit() aufgerufen wird, konnte alle Validatoren fehlerfrei durchlaufen werden. 195
  • 9 Formulare 9.11.1 StringValidator Für einfache Regeln bezüglich der Länge einer Eingabe bietet Wicket eine Reihe von ein- fachen Validatoren. Die Methodennamen lassen erkennen, welche Regel der entsprechende Validator prüft (Abbildung 9.13). Listing 9.49 StringValidatorPage.java package de.wicketpraxis.web.thema.komponenten.forms.validators; ... public class StringValidatorPage extends AbstractFormPage { public StringValidatorPage() { Form form=new Form("form"); form.add(new TextField<String>("Len4",Model.of("")).add( StringValidator.exactLength(4))); form.add(new TextField<String>("Len24",Model.of("")).add( StringValidator.lengthBetween(2, 4))); form.add(new TextField<String>("Max8",Model.of("")).add( StringValidator.maximumLength(8))); form.add(new TextField<String>("Min2",Model.of("")).add( StringValidator.minimumLength(2))); add(form); } } Dabei wird durch das Hinzufügen eines Validators das Eingabefeld nicht automatisch zum Pflichtfeld. Dafür muss diese Regel durch den Aufruf von setRequired(true) aktiviert werden. Listing 9.50 StringValidatorPage.html <wicket:extend> <form wicket:id="form"> Länge 4: <input wicket:id="Len4"><br> Länge 2-4: <input wicket:id="Len24"><br> Länge bis 8: <input wicket:id="Max8"><br> Länge ab 2: <input wicket:id="Min2"><br> <button>Abschicken</button> </form> </wicket:extend> Wenn wir in jedem Feld etwas eingeben, was die Regel verletzt, erhalten wir folgendes Ergebnis: Abbildung 9.13 Verschiedene Gültigkeits- prüfungen196
  • 9.11 Gültigkeitsprüfung9.11.2 Minimum und MaximumFür alle Datentypen, die das Comparable-Interface implementieren, kann ein Minimum-Validator, MaximumValidator und ein RangeValidator benutzt werden.Listing 9.51 RangeMinMaxValidatorPage.java package de.wicketpraxis.web.thema.komponenten.forms.validators; ... public class RangeMinMaxValidatorPage extends AbstractFormPage { public RangeMinMaxValidatorPage() { Form<StandardTypesBean> form=new Form<StandardTypesBean>("form", new CompoundPropertyModel<StandardTypesBean>( new StandardTypesBean())); form.add(new TextField<Integer>("Zahl").add( new MinimumValidator<Integer>(12))); form.add(new TextField<Double>("Kommazahl").add( new RangeValidator<Double>(Math.PI,42d))); form.add(new TextField<String>("Text").add( new MaximumValidator<String>("EDCBA"))); form.add(new TextField<Date>("Datum").add( new MaximumValidator<Date>(new Date()))); add(form); } }Dabei müssen die Validatoren nicht selbst die Eingaben in die entsprechenden Typen kon-vertieren. Die Validatoren werden erst aufgerufen, wenn die Konvertierung durchgeführtwurde. Dabei wandert das Ergebnis nicht in das Modell, sondern wird innerhalb der Kom-ponente gepuffert.Listing 9.52 RangeMinMaxValidatorPage.html <wicket:extend> <form wicket:id="form"> Zahl &gt; 12: <input wicket:id="Zahl"><br> Kommazahl > Pi &lt; 42 : <input wicket:id="Kommazahl"><br> Text &lt;= EDCBA: <input wicket:id="Text"><br> Datum &lt;= heute: <input wicket:id="Datum"><br> <button>Abschicken</button> </form> </wicket:extend>9.11.3 E-MailHäufig möchte man, dass der Nutzer eine gültige E-Mail-Adresse eingibt. Die Überprü-fung, ob diese E-Mail-Adresse existiert, ist der zweite Schritt. Als Erstes muss geprüftwerden, ob es die Adresse wenigstens theoretisch geben könnte.Für die folgenden Beispiele benötigen wir allerdings wieder einmal eine JavaBean, die dieAttribute EMail, Password und Password2 bereitstellt. 197
  • 9 Formulare Listing 9.53 LoginBean.java (gekürzt) package de.wicketpraxis.web.thema.komponenten.forms.beans; ... public class LoginBean implements Serializable { String _eMail; String _password; String _password2; getEMail(),setEMail(),getPassword(),... public String toString() { return MessageFormat.format("EMail: {0}, Password: {1}, Password2: {2}",_eMail,_password,_password2); } } Das Format einer gültigen E-Mail-Adresse ist kompliziert, und so ist auch die Überprüfung dieser Adresse eine komplizierte Angelegenheit. Das Wesentlichen dabei ist aber, die Feh- ler, die der Nutzer bei der Eingabe machen kann, zu erkennen und ihn darauf hinzuweisen. Wer es dann doch genau mag und sich an Standards halten möchte, der kann die E-Mail- Adressen gerne durch einen RFC-konformen Validator prüfen lassen. Die Fehlermeldung wird dem Nutzer allerdings weniger helfen, weil er sie nicht versteht. Ich habe daher in diesem Beispiel die „einfache“ Variante benutzt. Wer es komplizierter mag, benutzt den RfcCompliantEmailAddressValidator. Listing 9.54 EMailValidatorPage.java package de.wicketpraxis.web.thema.komponenten.forms.validators; ... public class EMailValidatorPage extends AbstractFormPage { public EMailValidatorPage() { Form<LoginBean> form=new Form<LoginBean>("form", new CompoundPropertyModel<LoginBean>(new LoginBean())) { @Override protected void onSubmit() { info("EMail: "+getModelObject().getEMail()); } }; TextField<String> emailTextField = new TextField<String>("EMail"); emailTextField.add(EmailAddressValidator.getInstance()); form.add(emailTextField); add(form); } } Listing 9.55 EmailValidatorPage.htmll <wicket:extend> <form wicket:id="form"> EMail: <input wicket:id="EMail"><br> <button>Abschicken</button> </form> </wicket:extend>198
  • 9.11 Gültigkeitsprüfung9.11.4 URLWenn der Nutzer eine URL eingeben soll, dann ist zumindest diese Prüfung wesentlicheinfacher als das Prüfen einer E-Mail-Adresse.Listing 9.56 UrlValidatorPage.java package de.wicketpraxis.web.thema.komponenten.forms.validators; ... public class UrlValidatorPage extends AbstractFormPage { public UrlValidatorPage() { Form form=new Form("form"); form.add(new TextField<String>("UrlAll",Model.of("")).add( new UrlValidator(UrlValidator.ALLOW_ALL_SCHEMES))); form.add(new TextField<String>("UrlStandard",Model.of("")).add( new UrlValidator())); form.add(new TextField<String>("UrlLimit",Model.of("")).add( new UrlValidator(new String[]{"http"},UrlValidator.NO_FRAGMENTS))); add(form); } }Dabei gibt es drei Varianten. Die erste Variante lässt alles zu, was wie eine gültige URLaussieht. Die zweite Variante erlaubt nur die Protokolle HTTPS, HTTP und FTP. Die letz-te Variante erlaubt nur das HTTP-Protokoll und lässt keine Fragmente zu (das „#unten“am Ende von http://wicket-praxis.de/blog/#unten).Listing 9.57 UrlValidatorPage.html <wicket:extend> <form wicket:id="form"> Alles geht: <input wicket:id="UrlAll"><br> Standard: <input wicket:id="UrlStandard"><br> Nur http ohne #: <input wicket:id="UrlLimit"><br> <button>Abschicken</button> </form> </wicket:extend>9.11.5 Eigene ValidatorenSo einfach wie das Einbinden ist auch das Erstellen von eigenen Validatoren. Für diesesBeispiel implementieren wir einen Palindrom-Validator. Ein Palindrom ist, kurz gesagt,ein Text, der vorwärts wie rückwärts gelesen das Gleiche ergibt.Listing 9.58 CustomValidatorPage.java package de.wicketpraxis.web.thema.komponenten.forms.validators; ... public class CustomValidatorPage extends AbstractFormPage { public CustomValidatorPage() { Form form=new Form("form"); 199
  • 9 Formulare getFeedbackPanel().setEscapeModelStrings(false); form.add(new TextField<String>("Palindrom",Model.of("")).add( new PalindromValidator())); add(form); } public static class PalindromValidator extends AbstractValidator<String> { @Override protected void onValidate(IValidatable<String> validatable) { String value = validatable.getValue().toLowerCase(); String reverse=new StringBuilder(value).reverse().toString(); int index=-1; for (int i=0;i<value.length();i++) { if (value.charAt(i)!=reverse.charAt(i)) { index=i; break; } } if (index>=0) { Map<String,Object> vars=new HashMap<String, Object>(); vars.put("index", index); vars.put("reverse", reverse); if (index>0) { vars.put("part", value.substring(0,index)); vars.put("leftInput", value.substring(index)); vars.put("leftReverse", reverse.substring(index)); error(validatable,"Partial",vars); } else { error(validatable,"NoMatch",vars); } } } } } Der Validator wird mit dem Validatable aufgerufen. Dabei handelt sich es um einen Adapter zum jeweiligen Eingabefeld. Wir wandeln den bereits konvertierten Wert, in die- sem Fall ist es einfach ein String, in Kleinbuchstaben um. Dann erstellen wir eine gespie- gelte Kopie und vergleichen Zeichen für Zeichen das Original mit dem Spiegelbild. Sollte der Vergleich keine Abweichung finden, gibt es nichts zu bemängeln, und es wird kein Fehler erzeugt. Kommt es jedoch zu Abweichungen, dann wird eine Map erzeugt und mit relevanten Infor- mationen gefüllt. Danach wird die error-Methode aufgerufen, die durch die Abstract- Validator-Klasse bereitgestellt wird. Dabei wird der Fehler mit dem zu prüfenden Ein- gabefeld verknüpft. Diese Information kann man für die Darstellung des Fehlers nutzen, worauf wir in Abschnitt 9.15.2 noch zu sprechen kommen werden.200
  • 9.12 FormValidator Listing 9.59 CustomValidatorPage.html <wicket:extend> <form wicket:id="form"> Palindrom: <input wicket:id="Palindrom"><br> <button>Abschicken</button> </form> </wicket:extend> Damit der Nutzer etwas mit der Fehlermeldung anfangen kann, legen wir eine Property- Datei mit den passenden Fehlermeldungen an. Wie man sieht, benutzen wir in der Proper- ty-Datei HTML-Tags, die Wicket normalerweise entsprechend umwandelt, sodass sie im Fehlertext als Text und nicht als Tag auftauchen. Deshalb habe ich in diesem Beispiel die- ses Verhalten für das FeedbackPanel mit dem Aufruf setEscapeModelStrings(false) deaktiviert. Warnung Da der Inhalt des Eingabefeldes in einer Fehlermeldung dargestellt wird und die Fehlermel- dung den Wert aus dem Eingabefeld ungefiltert übernimmt, kann der Nutzer unerwünschten HTML-Code in die Seite injizieren. Die Eingabe von „<script>alert(‚Sicherheitslücke’)</script>“ ruft eine unerwünschte Ja- vaScript-Funktion auf. Listing 9.60 CustomValidatorPage$PalindromValidator.properties Partial=Bei <i>${part}</i>${leftInput} stimmen nur die ersten ${index} Zeichen überein: <i>${part}</i>${leftReverse}. NoMatch=${input} ist kein Palindrom. Das erste und letzte Zeichen stimmen schon nicht überein. Wenn wir jetzt „Lagerregal“ eingeben, ist alles in Ordnung. Geben wir „Lagerrgal“ ein, erhalten wir eine Fehlermeldung wie in Abbildung 9.14. Abbildung 9.14 Der PalindromValidator in Aktion9.12 FormValidator Bisher haben wir immer nur ein Eingabefeld geprüft. Um mehr als ein Eingabefeld prüfen zu können, müssen wir einen FormValidator implementieren. 9.12.1 Passwortprüfung Das beste Beispiel für einen FormValidator kommt aus der Praxis. Wenn der Nutzer zweimal das gleiche Passwort eingeben muss, dann kann man das mit einem einfachen Validator nicht prüfen. Wenn man die Prüfung in der onSubmit-Methode durchführt und 201
  • 9 Formulare dann einen Fehler erzeugt, sind die Daten bereits im Modell gelandet. Man muss also beide Eingabefelder gleichzeitig prüfen. Listing 9.61 FormValidatorPage.java package de.wicketpraxis.web.thema.komponenten.forms.validators; ... public class FormValidatorPage extends AbstractFormPage { public FormValidatorPage() { Form<LoginBean> form=new Form<LoginBean>("form", new CompoundPropertyModel<LoginBean>(new LoginBean())) { @Override protected void onSubmit() { LoginBean bean = getModelObject(); info("Passwort: "+bean.getPassword()+ ", Passwort2: "+bean.getPassword2()); } }; PasswordTextField passwordTextField = new PasswordTextField("Password"); form.add(passwordTextField); PasswordTextField passwordTextField2 = new PasswordTextField("Password2"); form.add(passwordTextField2); form.add(new EqualPasswordInputValidator( passwordTextField,passwordTextField2)); add(form); } } Wie man sieht, wird der FormValidator, in diesem Fall der EqualPasswordInputVali- dator nicht zu einem der beiden Eingabefelder, sondern zur Form-Komponente hinzuge- fügt. Als Parameter erwartet der Validator die beiden Eingabefelder. Die Password- TextField-Komponente ist eine Erweiterung der TextField-Komponente, die dafür sorgt, dass die letzte Eingabe wieder gelöscht wird. Das Passwort bleibt also nicht stehen. Der EqualPasswordInputValidator wurde vom EqualInputValidator abgeleitet und sorgt dafür, dass bei Fehlermeldungen das eingegebene Passwort nicht im Klartext in der Fehlermeldung auftaucht. Listing 9.62 FormValidatorPage.html <wicket:extend> <form wicket:id="form"> Password: <input type="password" wicket:id="Password"><br> Password2: <input type="password" wicket:id="Password2"><br> <button>Abschicken</button> </form> </wicket:extend> Wenn wir nun das Passwort das zweite Mal etwas anders eingeben, erhalten wir eine Feh- lermeldung (Abbildung 9.15).202
  • 9.12 FormValidator Abbildung 9.15 Der PasswordValida- tor bei unterschiedlichen Passwörtern9.12.2 Eigene PrüfungAuch einen FormValidator kann man einfach selbst implementieren. Damit sich der Auf-wand in Grenzen hält und man besser erkennen kann, worauf es ankommt, beschäftigenwir uns in diesem Beispiel nur mit einem ganz kleinen Problem.Doch vorher erstellen wir eine JavaBean, die die Attribute „A“, „B“ und „Summe“ bereit-hält.Listing 9.63 CalcBean.java (gekürzt) package de.wicketpraxis.web.thema.komponenten.forms.beans; ... public class CalcBean implements Serializable { int _a; int _b; int _summe; getA(),setA(),getB(),... public String toString() { return MessageFormat.format("A: {0}, B: {1}, Summe: {2}", _a,_b,_summe); } }Unser FormValidator prüft, ob die Summe aus A und B der eingegebenen Summe ent-spricht.Listing 9.64 CustomFormValidatorPage.java package de.wicketpraxis.web.thema.komponenten.forms.validators; ... public class CustomFormValidatorPage extends AbstractFormPage { public CustomFormValidatorPage() { Form<CalcBean> form=new Form<CalcBean>("form", new CompoundPropertyModel<CalcBean>(new CalcBean())) { @Override protected void onSubmit() { info(getModelObject().toString()); } }; final TextField<Integer> a = new TextField<Integer>("A"); final TextField<Integer> b = new TextField<Integer>("B"); final TextField<Integer> summe = new TextField<Integer>("Summe"); a.setRequired(true); 203
  • 9 Formulare b.setRequired(true); summe.setRequired(true); form.add(a); form.add(b); form.add(summe); form.add(new AbstractFormValidator() { public FormComponent<?>[] getDependentFormComponents() { return new FormComponent<?>[]{a,b,summe}; } public void validate(Form<?> form) { int aInput = a.getConvertedInput(); int bInput = b.getConvertedInput(); int summeInput = summe.getConvertedInput(); if ((aInput+bInput)!=summeInput) { Map<String, Object> param=new HashMap<String, Object>(); param.put("a", aInput); param.put("b", bInput); param.put("summe", summeInput); param.put("result", aInput+bInput); error(summe, "summe",param); } } }); add(form); } } Im Gegensatz zum Validator bekommt der FormValidator das Formular übergeben, das geprüft werden soll. In diesem Beispiel sprechen wir die Eingabefelder direkt an. Da- mit Wicket weiß, welche Eingabefelder für die Prüfung herangezogen werden, gibt die Methode getDependentFormComponents() die drei Eingabefelder zurück. Die Prüfung wird dann erst aktiviert, wenn keine Fehler für die abhängigen Komponenten vorliegen. In der validate-Methode greifen wir nun auf die konvertierten Werte zurück und ermit- teln die Summe. Wenn die Summen nicht übereinstimmen, produzieren wir wie bereits beim Beispiel mit dem PalindromValidator eine Map mit Parametern und erzeugen ei- nen Fehler. Listing 9.65 CustomFormValidatorPage.html <wicket:extend> <form wicket:id="form"> <input wicket:id="A">+<input wicket:id="B">=<input wicket:id="Summe"><br> <button>Testen</button> </form> </wicket:extend> Für dieses Beispiel legen wir die Property-Datei nicht mit dem Klassennamen des Valida- tors an, sondern erzeugen eine Property-Datei für die Seite. Dabei veranschaulicht das Lis- ting noch einmal die verschiedenen Möglichkeiten, wie ein entsprechender Eintrag ange- legt werden kann. Entweder definiert man einen Eintrag nur zum ResourceKey (der zweite Parameter des error-Methoden-Aufrufs) oder man bezieht die Komponentenhierarchie mit ein.204
  • 9.13 Ajax Listing 9.66 CustomFormValidatorPage.properties summe=${a}+${b}=${result} und nicht ${summe} oder form.Summe.summe=${a}+${b}=${result} und nicht ${summe} Eine fehlerhafte Eingabe führt zu einer passenden Fehlermeldung (Abbildung 9.16). Abbildung 9.16 Der CustomFormValida- tor mit Fehlermeldung Zusammenfassung Eigene Validatoren zu schreiben, ist einfach. Man sollte versuchen, diese Klassen allge- mein zu halten und die Texte für die Fehlermeldungen als Property-Datei dieser Kompo- nente zu definieren. Schon erhält man im Laufe der Zeit ein mächtiges Grundgerüst, das man vielfältig einsetzen kann. Da die Fehlermeldungen jederzeit lokal (z.B. in der Proper- ty-Datei der Komponente, in der ein Validator verwendet wird) überschrieben werden können, kann man Fehlermeldungen in bestimmten Situationen immer noch gezielt anpas- sen.9.13 Ajax Gerade im Zusammenhang mit Formularen kann man sehr viele Verbesserungen im Be- reich der Nutzerinteraktion durch die Verwendung von Ajax erreichen. Die folgenden Bei- spiele zeigen nur einen Ausschnitt des Anwendungsbereichs, da es vielfältige Möglichkei- ten gibt, bei welcher Interaktion (z.B. Maus oder Tastatur) man z.B. ein Formular per Ajax abschickt. 9.13.1 AjaxFormSubmitBehavior In diesem Beispiel schicken wir das Formular immer dann ab, wenn der Nutzer das Einga- befeld verlässt. Listing 9.67 AjaxFormSubmitBehaviorPage.java package de.wicketpraxis.web.thema.komponenten.forms.ajax; ... public class AjaxFormSubmitBehaviorPage extends AbstractFormPage { public AjaxFormSubmitBehaviorPage() { Form<StandardTypesBean> form=new Form<StandardTypesBean>("form", new CompoundPropertyModel<StandardTypesBean>( new StandardTypesBean())) { 205
  • 9 Formulare @Override protected void onSubmit() { info("Text: "+getModelObject().getText()); } }; form.setOutputMarkupId(true); AjaxFormSubmitBehavior ajaxBehavior = new AjaxFormSubmitBehavior("onBlur") { @Override protected void onSubmit(AjaxRequestTarget target) { Form<StandardTypesBean> form=(Form<StandardTypesBean>) getForm(); info("Ajax.Submit: Text: "+form.getModelObject().getText()); updateFeedbackPanel(target); } @Override protected void onError(AjaxRequestTarget target) { Form<StandardTypesBean> form=(Form<StandardTypesBean>) getForm(); warn("Ajax.Error: Text: "+form.getModelObject().getText()); updateFeedbackPanel(target); } }; form.add(new TextField<String>("Text").setRequired(true). add(ajaxBehavior)); add(form); } } Dazu übergeben wir dem AjaxFormSubmitBehavior als Parameter „onBlur“, was den JavaScript-Event bezeichnet, der ausgelöst wird, wenn das Eingabefeld den Focus verliert. Damit man sehen kann, dass das Formular tatsächlich abgeschickt wird und auch alle ent- sprechenden Regeln geprüft werden, geben wir in der onSubmit- und der onError- Methode die Daten des Modells aus. Da die AjaxFormSubmitBehavior-Komponente dafür sorgt, dass das entsprechende For- mular neu gezeichnet wird, müssen wir das Formular zwar nicht dem AjaxRequestTar- get hinzufügen, aber die Ajax-Unterstützung durch den Aufruf von setOutputMarku- pId(true) aktivieren. Alternativ kann man die Behavior-Komponente auch mit dem Formular als Parameter instanziieren, wobei dann die Komponente diesen Aufruf durch- führt. Damit deutlich wird, dass das Formular ohne jeden Submit-Button abgeschickt wird, habe ich in diesem Beispiel den Button auskommentiert. Listing 9.68 AjaxFormSubmitBehaviorPage.html <wicket:extend> <form wicket:id="form"> Text: <input wicket:id="Text"><br> <!–- <button>Abschicken</button> –-> </form> </wicket:extend>206
  • 9.13 Ajax9.13.2 AjaxFormValidatingBehaviorAber es geht noch einfacher: Das AjaxFormValidatingBehavior ist von AjaxFormSub-mitBehavior abgeleitet und fügt automatisch alle FeedbackPanel oder besser, alle Kom-ponenten, die IFeedback implementiert haben, automatisch zum AjaxRequestTargethinzu. Dadurch gestaltet sich der Aufruf natürlich etwas einfacher.Listing 9.69 AjaxFormValidatingBehaviorPage.java package de.wicketpraxis.web.thema.komponenten.forms.ajax; ... public class AjaxFormValidatingBehaviorPage extends AbstractFormPage { public AjaxFormValidatingBehaviorPage() { Form<StandardTypesBean> form=new Form<StandardTypesBean>("form", new CompoundPropertyModel<StandardTypesBean>( new StandardTypesBean())) { @Override protected void onSubmit() { info("Text: "+getModelObject().getText()); } }; AjaxFormValidatingBehavior ajaxBehavior= new AjaxFormValidatingBehavior(form,"onBlur"); form.add(new TextField<String>("Text").setRequired(true). add(ajaxBehavior)); add(form); } } Tipp Die Klasse AjaxFormValidatingBehavior besitzt die statische Methode addToAll- FormComponents(), mit der man allen Komponenten eines Formulars diese Eigenschaft hinzufügen kann.Listing 9.70 AjaxFormValidatingBehaviorPage.html <wicket:extend> <form wicket:id="form"> Text: <input wicket:id="Text"><br> <!-- <button>Abschicken</button> --> </form> </wicket:extend>9.13.3 AjaxComponentUpdatingBehaviorIm Gegensatz zu den letzten beiden Implementierungen aktualisiert dieses Behavior nurdie entsprechende Komponente, ohne das ganze Formular zu übermitteln. Dabei werdenalle Validatoren dieser Komponente, aber keine des Formulars aufgerufen. 207
  • 9 Formulare Wenn für die Komponente kein Fehler erzeugt wurde, wird der Wert in das Modell über- tragen. Dieses Vorgehen bietet sich daher nur an, wenn das Formular später noch komplett übertragen wird. Listing 9.71 AjaxComponentUpdatingBehaviorPage.java package de.wicketpraxis.web.thema.komponenten.forms.ajax; ... public class AjaxComponentUpdatingBehaviorPage extends AbstractFormPage { public AjaxComponentUpdatingBehaviorPage() { final Form<StandardTypesBean> form= new Form<StandardTypesBean>("form", new CompoundPropertyModel<StandardTypesBean>( new StandardTypesBean())) { @Override protected void onSubmit() { StandardTypesBean bean = getModelObject(); info("Text: "+bean.getText()+", Text2: "+bean.getText2()); } @Override protected void onError() { StandardTypesBean bean = getModelObject(); warn("Text: "+bean.getText()+", Text2: "+bean.getText2()); } }; form.setOutputMarkupId(true); final TextField<String> textField = new TextField<String>("Text"); AjaxFormComponentUpdatingBehavior ajaxBehavior = new AjaxFormComponentUpdatingBehavior("onBlur") { @Override protected void onUpdate(AjaxRequestTarget target) { StandardTypesBean bean = form.getModelObject(); info("Ajax.Update: Text: "+bean.getText()+ ", Text2: "+bean.getText2()+ " Input: "+textField.getConvertedInput()); updateFeedbackPanel(target); } @Override protected void onError(AjaxRequestTarget target,RuntimeException e) { super.onError(target, e); StandardTypesBean bean = form.getModelObject(); error("Ajax.Error: Text: "+bean.getText()+ ", Text2: "+bean.getText2()+ " Input: "+textField.getConvertedInput()); updateFeedbackPanel(target); } @Override protected boolean getUpdateModel() { return true; } }; textField.setRequired(true).add(ajaxBehavior); TextField<String> textField2 = new TextField<String>("Text2");208
  • 9.13 Ajax textField2.setRequired(true); form.add(textField2); form.add(textField); add(form); } }Wenn man die Methode getUpdateModel() überschreibt und den Rückgabewert auf„false“ setzt, dann wird auch das Modell nicht aktualisiert. Die Methode wurde hier soüberschrieben, dass sie denselben Wert zurück liefert wie die Methode der Basisklasse unddamit das Setzen des Modells nicht verhindert.Listing 9.72 AjaxComponentUpdatingBehaviorPage.html <wicket:extend> <form wicket:id="form"> Text: <input wicket:id="Text"><br> Text2: <input wicket:id="Text2"><br> <button>Abschicken</button> </form> </wicket:extend>Das Ergebnis aus Abbildung 9.17 zeigt, dass zwar beide Textfelder gefüllt waren, aber nurder Wert des ersten Textfelds ins Modell übernommen wurde. Abbildung 9.17 AjaxComponentUpdatingBehavior9.13.4 OnChangeBehaviorDie OnChangeAjaxBehavior-Klasse erbt von der AjaxFormComponentUpdatingBehavi-or-Klasse und benutzt den JavaScript-Event „onChange“.Listing 9.73 AjaxOnChangeBehaviorPage.java package de.wicketpraxis.web.thema.komponenten.forms.ajax; ... public class AjaxOnChangeBehaviorPage extends AbstractFormPage { public AjaxOnChangeBehaviorPage() { final Form<StandardTypesBean> form= new Form<StandardTypesBean>("form", new CompoundPropertyModel<StandardTypesBean>( new StandardTypesBean())) { @Override protected void onSubmit() { info("Text: "+getModelObject().getText()); } }; form.setOutputMarkupId(true); final TextField<String> textField = new TextField<String>("Text"); 209
  • 9 Formulare OnChangeAjaxBehavior ajaxBehavior=new OnChangeAjaxBehavior() { @Override protected void onUpdate(AjaxRequestTarget target) { info("Ajax.Update: Text: "+form.getModelObject().getText()+ " Input: "+textField.getConvertedInput()); updateFeedbackPanel(target); } @Override protected boolean getUpdateModel() { return false; } }; form.add(textField.setRequired(true).add(ajaxBehavior)); add(form); } } In diesem Beispiel haben wir die Methode getUpdateModel() so überschrieben, dass das Modell nur aktualisiert wird, wenn das Textfeld korrekt ausgefüllt wurde. Während des Eintippens kann man erkennen, wie die Daten übermittelt werden (Abbildung 9.18). Listing 9.74 AjaxOnChangeBehaviorPage.html <wicket:extend> <form wicket:id="form"> Text: <input wicket:id="Text"><br> <button>Abschicken</button> </form> </wicket:extend> Abbildung 9.18 AjaxOnChangeBehavior 9.13.5 AutoCompleteTextField Eine besondere Komponente ist das AutoCompleteTextField. Wie der Name vermuten lässt, geht es darum, dem Nutzer in Abhängigkeit von der letzten Eingabe eine Liste von Vorschlägen zu unterbreiten, die er dann auswählen kann. Listing 9.75 AutoCompleteTextFieldPage.java package de.wicketpraxis.web.thema.komponenten.forms.ajax; ... public class AutoCompleteTextFieldPage extends AbstractFormPage { IModel<List<String>> _liste=new ListModel<String>( new ArrayList<String>()); IModel<String> _text=Model.of(""); public AutoCompleteTextFieldPage() { add(CSSPackageResource.getHeaderContribution( 210
  • 9.13 Ajax AutoCompleteTextFieldPage.class,"AutoCompleteTextFieldPage.css")); Form form=new Form("form") { @Override protected void onSubmit() { _liste.getObject().add(_text.getObject()); _text.setObject(null); } }; AutoCompleteSettings settings=new AutoCompleteSettings(); settings.setCssClassName("autocomplete"); settings.setPreselect(true); AutoCompleteTextField<String> autoCompleteTextField = new AutoCompleteTextField<String>("Text",_text,settings) { @Override protected Iterator<String> getChoices(String input) { List<String> result=new ArrayList<String>(); if ((input!=null) && (input.length()>0)) { for (String eintrag : _liste.getObject()) { if (eintrag.startsWith(input)) { result.add(eintrag); } } } return result.iterator(); } }; autoCompleteTextField.setRequired(true); form.add(autoCompleteTextField); add(form); add(new ListView<String>("liste",_liste) { @Override protected void populateItem(ListItem<String> item) { item.add(new Label("text",item.getModel())); } }); } }Alle Eingaben in dem Textfeld werden einer Liste hinzugefügt, die über die ListView-Komponente angezeigt werden. Tippt der Nutzer etwas in das Eingabefeld, dann sucht dieFunktion getChoices() nach passenden Alternativen, indem die vorhandene Liste aufEinträge überprüft wird, die mit dem bereits eingegebenen Text am Anfang übereinstim-men.Listing 9.76 AutoCompleteTextFieldPage.html <wicket:extend> <form wicket:id="form"> Text: <input wicket:id="Text"><br> <button>Abschicken</button> </form> <ol> 211
  • 9 Formulare <li wicket:id="liste"><span wicket:id="text"></span></li> </ol> </wicket:extend> Damit man sehen kann, welchen Eintrag man ausgewählt hat, muss man allerdings noch eine passende CSS-Datei angelegen, in der folgende Informationen enthalten sein sollten: Listing 9.77 AutoCompleteTextFieldPage.css div.autocomplete { background-color: white; border: 1px solid black; padding: 0px; margin: 0 0 0 0; text-align:left; } div.autocomplete ul { list-style:none; padding: 2px; margin:0px; } div.autocomplete ul li.selected { background-color: #6060ff; color:white; padding: 2px; margin:0px; } Nach ein paar Eingaben können wir bereits auf eine Liste von Vorschlägen zurückgreifen (Abbildung 9.19). Abbildung 9.19 AutoCompleteTextField9.14 AjaxEditableLabel Wie man aus einem Formular und etwas Ajax eine sehr nützliche Komponente bauen kann, zeigt das Beispiel der AjaxEditableLabel-Komponente. Dabei wird durch einen Klick auf einen Text (kein Link) dieser Text durch ein Formular ersetzt, in dem dieser Text dann verändert werden kann. Listing 9.78 AjaxEditLabelPage.java package de.wicketpraxis.web.thema.komponenten.forms.ajax; ... public class AjaxEditLabelPage extends AbstractFormPage { public AjaxEditLabelPage() { add(new AjaxEditableLabel<String>("ajaxLabel",Model.of("Label")) { @Override protected void onSubmit(AjaxRequestTarget target) { super.onSubmit(target);212
  • 9.14 AjaxEditableLabel info("Label: "+getDefaultModelObject()); updateFeedbackPanel(target); } }); add(new AjaxEditableMultiLineLabel<String>( "ajaxMultiLine",Model.of("Dasnistnmehrzeilig"))); final ListModel<String> choices = new ListModel<String>( Arrays.asList("der erste","der zweite","der dritte")); add(new AjaxEditableChoiceLabel<String>("ajaxChoice", Model.of("klicken und wählen"),choices) { @Override protected void onSubmit(AjaxRequestTarget target) { super.onSubmit(target); info("Choice: "+getDefaultModelObject()); updateFeedbackPanel(target); } }); } }Listing 9.79 AjaxEditLabelPage.html <wicket:extend> <span wicket:id="ajaxLabel"></span><hr> <span wicket:id="ajaxMultiLine"></span><hr> <span wicket:id="ajaxChoice"></span> </wicket:extend>Auch wenn man die Funktion der einzelnen Komponente aus dem Klassennamen herleitenkann, ist es einfacher, wenn man die Komponenten in Aktion sieht (Abbildung 9.20). DieDaten werden übermittelt, sobald das Formularfeld den Fokus verliert.Abbildung 9.20 AjaxEditLabel-Varianten 213
  • 9 Formulare9.15 Erweitertes Feedback Bisher haben wir nur ein FeedbackPanel für alle Informationen genutzt, die an den Nut- zer gerichtet waren. Wenn wir ein weiteres FeedbackPanel zur Seite hinzufügen würden (absichtlich oder unabsichtlich, weil sich z.B. ein weiteres innerhalb einer Komponente befindet), wäre der Nutzer sicher etwas verwirrt, wenn er eine Fehlermeldung an zwei Stel- len zu sehen bekäme. Wie man das Feedback an der richtigen Stelle anzeigt und es an die eigenen Bedürfnisse anpasst, soll Thema der folgenden Seiten sein. 9.15.1 Feedback zum Formular Das FeedbackPanel bietet die Möglichkeit, nur eine Auswahl von Nachrichten anzeigen zu lassen. Dazu wird als zweiter Parameter ein MessageFilter übergeben. Im folgenden Beispiel ist das ein ContainerFeedbackMessageFilter, der dafür sorgt, dass nur Nach- richten angezeigt werden, die zum jeweiligen Formular gehören. Listing 9.80 FormFeedbackPage.java package de.wicketpraxis.web.thema.komponenten.forms.feedback; ... public class FormFeedbackPage extends AbstractFormPage { public FormFeedbackPage() { Form<?> f1=new Form<Void>("form1"); f1.add(new TextField<String>("Text",new Model()).setRequired(true)); add(f1); Form<?> f2=new Form<Void>("form2"); f2.add(new TextField<String>("Text",new Model()).setRequired(true)); add(f2); add(new FeedbackPanel("feedback1", new ContainerFeedbackMessageFilter(f1))); add(new FeedbackPanel("feedback2", new ContainerFeedbackMessageFilter(f2))); } } Dem ContainerFeedbackMessageFilter kann man dabei jede beliebige Komponente als Parameter übergeben. Der Filter filtert dann alle Nachrichten heraus, die in einer belie- bigen Kindkomponente erzeugt wurden. Dass wir in unserem Beispiel die Formulare als Parameter übergeben haben, soll nicht darauf hindeuten, dass nur Formulare erlaubt wären. Listing 9.81 FormFeedbackPage.html <wicket:extend> <h2>Formular 1</h2> <div wicket:id="feedback1"></div> <form wicket:id="form1"> Text: <input wicket:id="Text"><br> <button>Abschicken</button> </form> <h2>Formular 2</h2> <div wicket:id="feedback2"></div>214
  • 9.15 Erweitertes Feedback <form wicket:id="form2"> Text: <input wicket:id="Text"><br> <button>Abschicken</button> </form> </wicket:extend>Auch wenn die Nachrichten jetzt wie in Abbildung 9.21 zweimal erscheinen, ist doch er-kennbar, dass die jeweiligen Feedback-Komponenten nur die Nachrichten der entspre-chenden Form-Komponente anzeigen. Abbildung 9.21 Auf das Formular eingegrenztes Feedback9.15.2 Feedback für die KomponenteMöchte man genau für eine Komponente die entsprechenden Nachrichten darstellen, be-nutzt man einfach anstelle des ContainerFeedbackMessageFilter einen Component-FeedbackMessageFilter. Wie der Name schon sagt, werden alle Nachrichten der Kom-ponente herausgefiltert.Listing 9.82 FormComponentFeedbackPage.java package de.wicketpraxis.web.thema.komponenten.forms.feedback; ... public class FormComponentFeedbackPage extends AbstractFormPage { public FormComponentFeedbackPage() { Form<?> f1=new Form<Void>("form"); TextField<String> textField = new TextField<String>("Text",new Model()); textField.setRequired(true); TextField<String> textField2 = new TextField<String>("Text2",new Model()); textField2.setRequired(true); f1.add(textField); f1.add(textField2); f1.add(new FeedbackPanel("feedback", new ComponentFeedbackMessageFilter(textField))); add(f1); } } 215
  • 9 Formulare Das zusätzliche FeedbackPanel zeigt nun alle Nachrichten zum ersten Textfeld an (Ab- bildung 9.22). Listing 9.83 FormComponentFeedbackPage.html <wicket:extend> <form wicket:id="form"> <div style="background-color:#f0f0f0;"> Text: <input wicket:id="Text"><br> <div wicket:id="feedback"></div> </div> <br> <div style="background-color:#f0f0f0;"> Text2: <input wicket:id="Text2"><br> </div> <button>Abschicken</button> </form> </wicket:extend> Abbildung 9.22 Auf die Komponente ein- gegrenztes Feedback 9.15.3 Feedback als Rahmen Die FormComponentFeedbackBorder-Komponente markiert die Formularkomponente, in der ein Fehler aufgetreten ist, mit einem roten Stern. Listing 9.84 FormFeedbackBorderPage.java package de.wicketpraxis.web.thema.komponenten.forms.feedback; ... public class FormFeedbackBorderPage extends AbstractFormPage { public FormFeedbackBorderPage() { Form<?> f1=new Form<Void>("form"); TextField<String> textField = new TextField<String>("Text",new Model()); textField.setRequired(true); TextField<String> textField2 = new TextField<String>("Text2",new Model()); textField2.setRequired(true); f1.add(new FormComponentFeedbackBorder("feedback").add(textField)); f1.add(textField2); add(f1); } }216
  • 9.15 Erweitertes FeedbackIntern benutzt die Komponente einen ContainerMessageFilter, sodass die Nachrichtender untergeordneten Elemente herausgefiltert werden können.Listing 9.85 FormFeedbackBorderPage.html <wicket:extend> <form wicket:id="form"> <div wicket:id="feedback"> Text: <input wicket:id="Text"><br> </div> Text2: <input wicket:id="Text2"><br> <button>Abschicken</button> </form> </wicket:extend>9.15.4 Feedback als IndikatorDen gleichen optischen Effekt erreicht man, in dem man eine FormComponentFeed-backIndicator-Komponente benutzt. Auch in diesem Fall wird bei einem Fehler ein roterStern angezeigt.Listing 9.86 FormFeedbackIndicatorPage.java package de.wicketpraxis.web.thema.komponenten.forms.feedback; ... public class FormFeedbackIndicatorPage extends AbstractFormPage { public FormFeedbackIndicatorPage() { Form<?> f1=new Form<Void>("form"); TextField<String> textField = new TextField<String>("Text",new Model()); textField.setRequired(true); FormComponentFeedbackIndicator feedback = new FormComponentFeedbackIndicator("feedback"); feedback.setIndicatorFor(textField); TextField<String> textField2 = new TextField<String>("Text2",new Model()); textField2.setRequired(true); FormComponentFeedbackIndicator feedback2 = new CustomIndicator("feedback2"); feedback.setIndicatorFor(textField2); f1.add(textField); f1.add(feedback); f1.add(textField2); f1.add(feedback2); add(f1); } static class CustomIndicator extends FormComponentFeedbackIndicator { public CustomIndicator(String id) { super(id); } } } 217
  • 9 Formulare Um das Aussehen etwas anzupassen, reicht es, eine eigene Klasse von der Klasse Form- ComponentFeedbackIndicator abzuleiten und in einem eigenen Markup die Darstellung anzupassen (Abbildung 9.23). Listing 9.87 FormFeedbackIndicatorPage.html <wicket:extend> <form wicket:id="form"> Text: <input wicket:id="Text"><span wicket:id="feedback"></span><br> Text2: <input wicket:id="Text2"><span wicket:id="feedback2"></span><br> <button>Abschicken</button> </form> </wicket:extend> Listing 9.88 FormFeedbackIndicatorPage$CustomIndicator.html <wicket:panel> <span style="color:red">Fehler</span> </wicket:panel> Abbildung 9.23 FormComponentFeedbackIndicator in zwei Varianten 9.15.5 Feedback per CSS Die beiden letzten Komponenten sind eine gute Ausgangsbasis für eine eigene Komponen- te. Unsere Komponente soll in Abhängigkeit davon, ob Nachrichten vorliegen, das Attribut class mit einem definierten Wert setzen. Listing 9.89 FormComponentCssFeedbackBorder.java package de.wicketpraxis.web.thema.komponenten.forms.feedback; ... public class FormComponentCssFeedbackBorder extends Border implements IFeedback { private static final Logger _logger = LoggerFactory.getLogger(FormComponentCssFeedbackBorder.class); boolean _hasErrors; private CharSequence _cssClass; public FormComponentCssFeedbackBorder(String id,String cssClass) { super(id); _cssClass=cssClass; } @Override protected void onBeforeRender() { super.onBeforeRender(); _hasErrors = Session.get().getFeedbackMessages().messages( getMessagesFilter()).size() != 0;218
  • 9.15 Erweitertes Feedback } @Override protected void onComponentTag(ComponentTag tag) { super.onComponentTag(tag); if (_hasErrors) tag.put("class", _cssClass); } protected IFeedbackMessageFilter getMessagesFilter() { return new ContainerFeedbackMessageFilter(this); } }Der Aufbau der Klasse orientiert sich stark an der FormComponentFeedbackBorder-Klasse. Der MessageFilter bezieht sich auf alle untergeordneten Komponenten. In deronBeforeRender-Methode wird geprüft, ob Nachrichten vorhanden sind, und ein entspre-chendes Feld gesetzt. Wenn die Komponente dargestellt wird, wird das Attribut gesetzt.Das Markup der Komponente muss erstellt werden und sorgt dafür, dass die Kindelementekorrekt eingebunden werden.Listing 9.90 FormComponentCssFeedbackBorder.html <wicket:border><wicket:body/></wicket:border>Unsere Komponente kommt im folgenden Beispiel zum Einsatz. Wir übergeben für dasAttribut class den Wert „error“. Für diese Style-Klasse haben wir in unserer für alleBeispiele geltenden CSS-Datei (form.css) entsprechende Regeln definiert.Listing 9.91 FormComponentCssFeedbackBorderPage.java package de.wicketpraxis.web.thema.komponenten.forms.feedback; ... public class FormComponentCssFeedbackBorderPage extends AbstractFormPage { public FormComponentCssFeedbackBorderPage() { Form<?> f1=new Form<Void>("form"); TextField<String> textField = new TextField<String>("Text",Model.of("")); textField.setRequired(true); TextField<String> textField2 = new TextField<String>("Text2",Model.of("")); textField2.setRequired(true); f1.add(new FormComponentCssFeedbackBorder("feedback","error"). add(textField)); f1.add(textField2); add(f1); } }Listing 9.92 FormComponentCssFeedbackBorderPage.html <wicket:extend> <form wicket:id="form"> <div wicket:id="feedback"> Text: <input wicket:id="Text"><br> </div> 219
  • 9 Formulare Text2: <input wicket:id="Text2"><br> <button>Abschicken</button> </form> </wicket:extend> Wenn ein Fehler auftritt, wird das Attribut gesetzt, wodurch sich die Darstellung der Kom- ponente wie in Abbildung 9.24 ändert. Abbildung 9.24 FormComponentCssFeedbackBorder im Fehlerfall Zusammenfassung Wicket bietet eine reiche Palette an Möglichkeiten, um Rückmeldungen auf Nutzeraktio- nen an geeigneter Stelle anzuzeigen. Es empfiehlt sich, in Anlehnung an die vorhandenen Komponenten eigene Varianten zu erstellen, die dann in der jeweiligen Anwendung an jeder Stelle genutzt werden können. Dabei ist zu beachten, dass in den letzten Beispielen darauf verzichtet wurde zu prüfen, ob die Nachricht einen Fehler, eine Warnung oder einen Hinweis enthielt. Diese Erweiterung sollte ohne große Schwierigkeiten umzusetzen sein.9.16 Generierte Formulare Bisher haben wir nur Formulare betrachtet, wo alle Eingabemöglichkeiten bekannt waren. Das folgende Beispiel soll veranschaulichen, wie Formulare aus veränderlichen Daten ge- neriert werden können (Abbildung 9.25). Dazu erstellen wir eine auf dieses Beispiel zuge- schnittene Modellklasse. Listing 9.93 MapPropertyModel.java package de.wicketpraxis.web.thema.komponenten.forms.dynamic; ... public class MapPropertyModel<V extends Serializable> implements IModel<V> { IModel<? extends Map<String,Serializable>> _model; String _property; public MapPropertyModel(IModel<? extends Map<String, Serializable>> model, String property) { _model = model; _property = property; } public V getObject() { return (V) _model.getObject().get(_property); }220
  • 9.16 Generierte Formulare public void setObject(V object) { _model.getObject().put(_property,object); } public void detach() { _model.detach(); } }Die Modellklasse sorgt dafür, dass der Wert aus dem Eingabefeld in der Map mit demSchlüssel aus der ID der Komponente abgelegt wird.Listing 9.94 DynFormPage.java package de.wicketpraxis.web.thema.komponenten.forms.dynamic; ... public class DynFormPage extends AbstractFormPage { public DynFormPage() { final IModel<Map<String, Serializable>> model = Model.ofMap(new HashMap<String,Serializable>()); IModel<? extends List<? extends String>> properties = Model.of(Arrays.asList("!Name","!Vorname","Straße","Hausnummer")); Form form=new Form("form"); form.add(new ListView<String>("inputs",properties) { @Override protected void populateItem(ListItem<String> item) { String id = item.getModelObject(); boolean required=false; if (id.startsWith("!")) { id=id.substring(1); required=true; } FormComponentCssFeedbackBorder feedbackBorder = new FormComponentCssFeedbackBorder("border","error"); TextField<String> textField = new TextField<String>("input", new MapPropertyModel<String>(model,id)); textField.setRequired(required); textField.setLabel(Model.of(id)); feedbackBorder.add(new Label("id",id)); feedbackBorder.add(textField); item.add(feedbackBorder); } }.setReuseItems(true)); add(form); } }Um das Beispiel einfach zu halten, wird ein Pflichtfeld mit einem Ausrufezeichen amAnfang markiert. Um jedes Eingabefeld kommt unsere FormComponentCssFeedbackBor-der-Komponente, sodass bei den zwei Pflichtfeldern der passende optische Hinweis er-scheint (Abbildung 9.25). Wichtig ist in diesem Zusammenhang auch der Aufruf vonsetReuseItems(true), da so verhindert werden kann, dass die ListView die Komponen-ten neu erstellt und so die Fehlermeldungen nicht zugeordnet werden können. 221
  • 9 Formulare Listing 9.95 DynFormPage.html <wicket:extend> <form wicket:id="form"> <wicket:container wicket:id="inputs"> <div wicket:id="border"> <span wicket:id="id"></span><input wicket:id="input"><br> </div> </wicket:container> <button>Abschicken</button> </form> </wicket:extend> Abbildung 9.25 Generiertes Formular9.17 Verschachtelte Formulare Was passiert, wenn man innerhalb eines Formulars eine Komponente benutzt, die selbst ein Formular benutzt? Normalerweise würden auch im Quelltext der Seite zwei form-Tags ausgegeben werden. Doch das würde zu Fehler führen. Wicket löst dieses Problem elegant, indem das innere form-Tag einfach in ein div-Tag umgewandelt wird. Das hat zur Folge, dass in so einem Fall alle Formulare korrekt abge- schickt werden, weil es nur ein übergeordnetes Formular gibt. Das bedeutet zwar, dass entweder das übergeordnete oder das untergeordnete Formular unabsichtlich abgeschickt wird (und onSubmit() aufgerufen wird), doch kann man dieses Problem elegant umgehen, indem man von Buttons Gebrauch macht und die notwendigen Aktionen in Abhängigkeit von deren Aktivierung auflöst. Listing 9.96 FormFormPage.java package de.wicketpraxis.web.thema.komponenten.forms.embedded; ... public class FormFormPage extends AbstractFormPage { private StandardTypesBean _bean; private StandardTypesBean _subBean; public FormFormPage() { _bean = new StandardTypesBean(); _subBean=new StandardTypesBean(); Form<StandardTypesBean> form=new Form<StandardTypesBean>("form", new CompoundPropertyModel<StandardTypesBean>(_bean)) { @Override222
  • 9.17 Verschachtelte Formulare protected void onSubmit() { showInfo("Main"); } }; form.add(new TextField<String>("Text")); Form<StandardTypesBean> subForm=new Form<StandardTypesBean>("sub", new CompoundPropertyModel<StandardTypesBean>(_subBean)) { @Override protected void onSubmit() { showInfo("Sub"); } }; subForm.add(new TextField<String>("Text")); form.add(subForm); add(form); } protected void showInfo(String source) { info("Beans: "+_bean.getText()+", "+_subBean.getText()+ ", Form: "+source); } }In diesem Fall befinden sich beide Formulare innerhalb einer Komponente. Da Wicketaber erkennen kann, ob irgendwo in der Komponentenhierarchie bereits ein Formular vor-handen ist, können Formulare beliebig in Komponenten verschachtelt werden. Wicketkann die Eingabedaten dabei korrekt zuordnen (Abbildung 9.26). Abbildung 9.26 Verschachtelte FormulareListing 9.97 FormFormPage.html <wicket:extend> <h2>Formular im Formular</h2> <form wicket:id="form" style="background-color:#f0f0f0;"> Text: <input wicket:id="Text"><br> <form wicket:id="sub" style="background-color:#eee; margin:4px;"> Text(sub): <input wicket:id="Text"><br> <button>Abschicken</button> </form> <button>Abschicken</button> </form> </wicket:extend>Listing 9.98 Ergebnis.html ... <body> <div wicket:id="feedback" id="feedback4"><wicket:panel> </wicket:panel></div> 223
  • 9 Formulare <wicket:child><wicket:extend> <h2>Formular im Formular</h2> <form wicket:id="form" style="background-color:#f0f0f0;" id="form5" method="post" action="..."> ... Text: <input wicket:id="Text" value="" name="Text"><br> <div wicket:id="sub" style="background-color:#eee;margin:4px;" id="sub6"> Text(sub):<input wicket:id="Text" value="" name="sub:Text"><br> <button>Abschicken</button> </div> <button>Abschicken</button> </form> </wicket:extend></wicket:child> </body> ... Die Hervorhebung markiert die Stelle, an der Wicket das form-Tag ausgetauscht hat. Zusammenfassung Wicket bietet interessante Möglichkeiten, um Formulare auf die eigenen Bedürfnisse an- zupassen. Dabei ist die Integration von Ajax derart einfach, dass nicht mehr die Frage nach dem „ob“, sondern nach dem „wie“ man die Handhabung der Webanwendung verbessert, gestellt werden sollte. Die unterschiedlichen Feedback-Komponenten dienen allenfalls als Ausgangspunkt für eigene Entwicklungen, die aber recht einfach umzusetzen sind. Im Laufe der Zeit entsteht so ein Portfolio an eigenen Komponenten, mit denen sich in kurzer Zeit selbst komplexe Formulare umsetzen lassen.224
  • 10 10 Sessions und Security Bisher haben wir die Session nur unter dem Gesichtspunkt betrachtet, dass Wicket den Zustand der Komponenten und Seiten in der Session hält. Auf den folgenden Seiten be- schäftigen wir uns damit, wie man in einer Session Anmeldedaten verwaltet und Seiten vor unberechtigtem Zugriff schützt. Damit die Beispiele einfacher nachzuvollziehen sind, erstellen wir eine neue Wicket-Applikation.10.1 Einfache Variante Die einfachste Variante besteht darin, den Zugriff auf bestimmte Seiten nur angemeldeten Benutzern zu erlauben. Doch zuerst müssen wir die Grundlage dafür schaffen, dass der Nutzer sich anmelden kann. 10.1.1 Eine eigene Session-Klasse Zuerst erstellen wir eine eigene Session-Klasse, die von der Klasse WebSession abgelei- tet wird. Damit klärt sich auch sofort, wie man eigene Daten in einer Session ablegt: indem man die abgeleitete Klasse erweitert. Dabei ist zu beachten, dass der Zugriff auf die Sessi- on durch mehrere Threads erfolgen kann. Listing 10.1 SecurePageSession.java package de.wicketpraxis.apps.session.session; ... public class SecurePageSession extends WebSession { @SpringBean(name=UserDao.BEAN_ID) UserDao _userDao; Integer _userId; public SecurePageSession(Request request) { super(request); InjectorHolder.getInjector().inject(this); } 225
  • 10 Sessions und Security public synchronized void setUser(User user) { _userId = user.getId(); dirty(); } public synchronized void clearUser() { _userId = null; dirty(); } public synchronized User getUser() { if (_userId!=null) return _userDao.get(_userId); return null; } public synchronized boolean isUserLogin() { return _userId!=null ? true : false; } public static SecurePageSession get() { return (SecurePageSession) Session.get(); } } Da die Session serialisiert werden könnte (wenn man z.B. Session-Replikation in einem Cluster betreibt), empfiehlt es sich, so wenig wie möglich Daten in der Session abzulegen. In diesem Beispiel wird in der Session nur die Datensatz-ID (_userId) abgelegt. 10.1.2 Geschützte Seiten Die einfachste Möglichkeit, den Zugriff auf Seiten zu reglementieren, besteht in einer ein- fachen Markierung durch ein Interface. Das Interface dient nur der Markierung, es müssen also keine Methoden implementiert werden. Generell ist zu empfehlen, dass man in die Information, welcher Nutzer auf welche Seite zugreifen kann, durch Marker-Interfaces (also ohne Methoden wie z.B. Serializable) oder Annotationen bereitstellt. Dadurch verhindert man, dass Programmcode zur Nutzerverwaltung in die Komponente einfließt. Listing 10.2 SecurePageInterface.java package de.wicketpraxis.apps.session.pages; public interface SecurePageInterface { } 10.1.3 Strategie Jetzt können wir Seiten markieren, auf die der Zugriff nur für angemeldete Nutzer möglich ist. Diese Entscheidung wird aber nicht durch die Seite selbst getroffen, sondern bereits bevor eine Instanz der Seite erstellt wird. Dazu leiten wir eine Klasse von der Klasse SimplePageAuthorizationStrategy ab. Die Basisklasse erwartet zwei Parameter: Der erste gibt die Klasse (oder in unserem Fall das Interface) an, das geschützte Seiten identifi- ziert. Der zweite Parameter gibt die Klasse der Seite an, auf die der Nutzer umgeleitet wird,226
  • 10.1 Einfache Variantewenn er noch nicht angemeldet ist. Das sollte daher auch eine Seite sein, auf der man sichanmelden kann.Die Entscheidung, ob ein Nutzer autorisiert ist, machen wir in dem Fall nur daran fest, obder Nutzer angemeldet ist oder nicht. Jeder Nutzer, der sich anmelden konnte, kann folg-lich auf alle Seiten zugreifen.Listing 10.3 UserLoginSimplePageAuthStrategy.java package de.wicketpraxis.apps.session.auth; ... public class UserLoginSimplePageAuthStrategy extends SimplePageAuthorizationStrategy { public UserLoginSimplePageAuthStrategy(Class<? extends Page> signInPageClass) { super(SecurePageInterface.class, signInPageClass); } @Override protected boolean isAuthorized() { return SecurePageSession.get().isUserLogin(); } }Wenn der Nutzer durch einen Link auf eine Seite gelangt, für die eine Anmeldung nötigist, springt er auf die Anmeldeseite, von der er nach erfolgreichem Login wieder zurückauf die ursprüngliche Seite geleitet werden kann.10.1.4 WebApplicationIn der WebApplication-Klasse sind zwei Anpassungen entscheidend. Zum einen setzenwir für die Applikation unsere gewünschte Autorisierungsstrategie. Zum andern über-schreiben wir die Methode, in der die Session erzeugt wird.Listing 10.4 SecurePageApplication.java package de.wicketpraxis.apps.session; ... public class SecurePageApplication extends WebApplication { @Override protected void init() { super.init(); addComponentInstantiationListener(new SpringComponentInjector(this)); ... getSecuritySettings().setAuthorizationStrategy( new UserLoginSimplePageAuthStrategy(LoginPage.class)); } @Override public Class<? extends Page> getHomePage() { return Start.class; } @Override 227
  • 10 Sessions und Security public Session newSession(Request request, Response response) { return new SecurePageSession(request); } } 10.1.5 Seiten Jetzt müssen wir natürlich noch die fehlenden Seiten erstellen. Damit die Beispiele schön kurz bleiben, erstellen wir eine abstrakte Basisklasse, in der wir das Markup für eine voll- ständige Seite ablegen. Alle anderen Seiten werden von dieser Seite abgeleitet. Listing 10.5 AbstractBasePage.java package de.wicketpraxis.apps.session.pages; import org.apache.wicket.markup.html.WebPage; public abstract class AbstractBasePage extends WebPage { } Listing 10.6 AbstractBasePage.html <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <title>Wicket Praxis - SecurePage</title> </head> <body> <wicket:child></wicket:child> </body> </html> Die erste Seite ist die Startseite. Auf dieser Seite fügen wir einen Link auf eine geschützte Seite ein. Die Seite ist natürlich noch für jeden Nutzer sichtbar. Listing 10.7 Start.java package de.wicketpraxis.apps.session.pages; import org.apache.wicket.markup.html.link.BookmarkablePageLink; public class Start extends AbstractBasePage { public Start() { add(new BookmarkablePageLink<ProtectedPage>("link", ProtectedPage.class)); } } Listing 10.8 Start.html <wicket:extend> <h1>Startseite</h1> <p>Weiter zur <a wicket:id="link">geschützten Seite</a></p> </wicket:extend>228
  • 10.1 Einfache VarianteAls Nächstes erstellen wir die geschützte Seite, die wir mit dem Interface entsprechendmarkieren. Außerdem fügen wir einen Link zur Seite ein, über die sich ein angemeldeterNutzer abmelden kann.Listing 10.9 ProtectedPage.java package de.wicketpraxis.apps.session.pages; ... public class ProtectedPage extends AbstractBasePage implements SecurePageInterface { public ProtectedPage() { add(new BookmarkablePageLink<LogoutPage>("logout",LogoutPage.class)); } }Listing 10.10 ProtectedPage.html <wicket:extend> <h1>geschützte Seite</h1> <p><a wicket:id="logout">Abmelden</a></p> </wicket:extend>Jetzt benötigen wir noch die Anmeldeseite, auf der sich ein Formular befindet, in das derNutzer seine Zugangsdaten eintragen muss. Für das Formular erstellen wir uns eine Hilfs-klasse, welche die Anmeldedaten aufnimmt.Listing 10.11 LoginBean.java (gekürzt) package de.wicketpraxis.apps.session.data; ... public class LoginBean implements Serializable { String _eMail; String _password; getEMail(),setEMail(),getPassword(),... }Wenn der Nutzer seine Daten eingibt, wird geprüft, ob ein passender Datensatz vorhandenist. Nach Prüfung des Passworts wird der Nutzer in der Session abgelegt. Durch den Auf-ruf von continueToOriginalDestination() wird geprüft, ob der Nutzer beim Zugriffauf eine geschützte Seite umgeleitet wurde. Sollte das der Fall sein, wird direkt zu dieserSeite zurückgesprungen. Ist das nicht der Fall, sollte man den Nutzer auf eine passendeSeite lenken.Listing 10.12 LoginPage.java package de.wicketpraxis.apps.session.pages; ... public class LoginPage extends AbstractBasePage { @SpringBean(name=UserDao.BEAN_ID) UserDao _userDao; public LoginPage() { add(new FeedbackPanel("feedback")); 229
  • 10 Sessions und Security Form<LoginBean> form=new Form<LoginBean>("form", new CompoundPropertyModel<LoginBean>(new LoginBean())) { @Override protected void onSubmit() { LoginBean loginBean = getModel().getObject(); User user = _userDao.getByEMail(loginBean.getEMail()); if (user!=null) { if (user.isPasswordValid(loginBean.getPassword())) { SecurePageSession.get().setUser(user); if (!continueToOriginalDestination()) { setResponsePage(SecurePageApplication.get().getHomePage()); } } else error("EMail oder Passwort falsch (Passwort natürlich)"); } else error("EMail oder Passwort falsch (EMail natürlich)"); } }; form.add(new TextField<String>("EMail").setRequired(true)); form.add(new PasswordTextField("Password")); add(form); } } Listing 10.13 LoginPage.html <wicket:extend> <h1>Login</h1> <div wicket:id="feedback"></div> <form wicket:id="form"> <table> <tr> <td>EMail</td><td><input wicket:id="EMail"></td> </tr> <tr> <td>Passwort</td> <td><input wicket:id="Password" type="password"></td> </tr> <tr> <td colspan="2"><button>Anmelden</button></td> </tr> </table> </form> </wicket:extend> Jetzt fehlt nur noch die Seite, die für die Abmeldung verantwortlich ist. Auch wenn man das Löschen des angemeldeten Nutzers und das Weiterleiten auf die Homepage direkt in jede Seite integrieren könnte, sollte man dem Umweg über eine gesonderte Abmeldeseite gehen. Auf diese Weise kann man später wesentlich einfacher Anpassungen vornehmen. Da wir in unserer Seite direkt zur Startseite weiterleiten, benötigen wir keine Markup- Datei für diese Seite. Listing 10.14 LogoutPage.java package de.wicketpraxis.apps.session.pages; ... public class LogoutPage extends AbstractBasePage { public LogoutPage() {230
  • 10.2 Marker an Komponenten SecurePageSession.get().clearUser(); setResponsePage(SecurePageApplication.get().getHomePage()); } } Der Anmeldeprozess sieht dann wie folgt aus (Abbildung 10.1): Der Nutzer beginnt auf der Startseite. Dann klickt er den Link zur geschützten Seite an. Daraufhin wird er, da er noch nicht angemeldet ist, auf die Anmeldeseite weitergeleitet. Wenn er dort die korrekten Zugangsdaten eingegeben hat, wird er auf sein ursprüngliches Ziel, die geschützte Seite, weitergeleitet. Wenn er auf den Link „Abmelden“ klickt, landet er wieder auf der Start- seite. Abbildung 10.1 Der Anmeldeprozess10.2 Marker an Komponenten Je nach Anwendung bilden Seiten nur den Rahmen, in dem verschiedene Komponenten ihren Platz finden. Da liegt es nahe, wenn man nicht die Seiten, sondern die Komponenten entsprechend markieren würde. Dann ist eine Anmeldung notwendig, sobald ein unange- meldeter Benutzer auf eine geschützte Komponente zugreifen möchte. Auf diese Weise befindet sich die Information über die Schutzbedürftigkeit der Informationen sehr viel dichter an der Quelle. Listing 10.15 SecureComponentInterface.java package de.wicketpraxis.apps.session.pages; public interface SecureComponentInterface { } Auch in diesem Fall benutzen wir ein Marker-Interface. Unsere Strategie leiten wir aller- dings nicht mehr von einer Basisstrategie ab, sondern implementieren das Iauthoriza- tionStrategy-Interface direkt. Listing 10.16 UserLoginSecureComponentAuthStrategy.java package de.wicketpraxis.apps.session.auth; ... public class UserLoginSecureComponentAuthStrategy implements IAuthorizationStrategy { Class<? extends Page> _signInPageClass; public UserLoginSecureComponentAuthStrategy( Class<? extends Page> signInPageClass) 231
  • 10 Sessions und Security { _signInPageClass=signInPageClass; } public boolean isActionAuthorized(Component component, Action action) { return true; } public <T extends Component> boolean isInstantiationAuthorized( Class<T> componentClass) { if (SecureComponentInterface.class.isAssignableFrom(componentClass)) { return SecurePageSession.get().isUserLogin(); } return true; } } Wir übergeben wieder die Klasse der Seite, auf der der Nutzer sich anmelden kann. Au- ßerdem müssen wir die Methode isActionAuthorized() implementieren (was das zu bedeuten hat, behandelt das nächste Beispiel). In der Methode isInstantiationAutho- rized() wird geprüft, ob eine Instanz der Komponente erzeugt werden darf. Wir prüfen, ob die Komponente einen angemeldeten Benutzer voraussetzt, und prüfen für diesen Fall, ob der Nutzer angemeldet ist. Wenn diese Methode „false“ zurückgibt, ist das Erzeugen der Instanz nicht gestattet. Normalerweise wird in so einem Fall eine passende Fehlermel- dung ausgelöst. Da der Nutzer aber die Chance bekommen soll, sich anzumelden, über- schreiben wir das Verhalten mit einer eigenen Variante des IUnauthorizedComponen- tInstantiationListener, der den Nutzer auf die Login-Seite weiterleitet. Listing 10.17 SecurePageApplication.java ... protected void init() { ... getSecuritySettings().setUnauthorizedComponentInstantiationListener( new IUnauthorizedComponentInstantiationListener() { public void onUnauthorizedInstantiation( final Component component) { throw new RestartResponseAtInterceptPageException( LoginPage.class); } }); getSecuritySettings().setAuthorizationStrategy( new UserLoginSecureComponentAuthStrategy(LoginPage.class)); ... } ... Wir fügen jetzt eine Komponente ein, die entsprechend markiert ist. Außerdem entfernen wir das SecurePageInterface von der Seite, da dieser Marker in diesem Beispiel keine Beachtung findet.232
  • 10.3 Elemente ausblenden Listing 10.18 ProtectedPage.java package de.wicketpraxis.apps.session.pages; ... public class ProtectedPage extends AbstractBasePage { public ProtectedPage() { add(new BookmarkablePageLink<LogoutPage>("logout",LogoutPage.class)); add(new SecureLabel("secureLabel",Model.of("Das darf nicht jeder sehen"))); } public static class SecureLabel extends Label implements SecureComponentInterface { public SecureLabel(String id, IModel<?> model) { super(id, model); } } } Listing 10.19 ProtectedPage.html <wicket:extend> <h1>geschütze Seite</h1> <p><a wicket:id="logout">Abmelden</a></p> <p><span wicket:id="secureLabel"></span></p> </wicket:extend> Die Anwendung verhält sich jetzt aus Nutzersicht genau wie im ersten Beispiel. Ob Kom- ponenten und damit Informationen vor unberechtigtem Zugriff zu schützen sind, befindet sich jetzt aber an der richtigen Stelle.10.3 Elemente ausblenden Eine Alternative zur Anmeldepflicht besteht darin, Komponenten, die nur ein angemelde- ter Benutzer sehen darf, einfach auszublenden. Dabei muss diese Funktion nicht in jeder Komponente implementiert werden, was vom Aufwand wohl nur sehr schwer zu rechtfer- tigen sein würde. Wir implementieren wieder das IAuthorizationStrategy-Interface und geben dieses Mal in der Methode isInstantiationAuthorized() immer true zurück, sodass jede Komponente erzeugt werden kann. Jetzt prüfen wir in der Methode isActionAuthori- zed(), ob der Nutzer angemeldet ist. Listing 10.20 UserLoginSecureComponentDisableAuthStrategy.java package de.wicketpraxis.apps.session.auth; ... public class UserLoginSecureComponentDisableAuthStrategy implements IAuthorizationStrategy { public boolean isActionAuthorized(Component component, Action action) { if (component instanceof SecureComponentInterface) { if (!SecurePageSession.get().isUserLogin()) 233
  • 10 Sessions und Security { return false; } } return true; } public <T extends Component> boolean isInstantiationAuthorized( Class<T> componentClass) { return true; } } Im diesem Beispiel ignorieren wir den Wert des Parameters „action“. Momentan benutzt Wicket nur zwei verschiedene Werte: RENDER und ENABLE. Dabei bezieht sich REN- DER auf die Darstellung der Komponente, während sich ENABLE auf den Rückgabewert von inEnabled() der Klasse Component bezieht. Würde man nur für die Action EN- ABLE prüfen, könnte man z.B. Links auf der Seite deaktivieren, da die Link-Klasse die Information auswertet und statt eines Links einfach nur den Text darstellt. Welche Konse- quenzen das für die Darstellung und Funktion der Komponente hat, liegt in der Verantwor- tung der Komponente selbst. Listing 10.21 SecurePageApplication.java ... protected void init() { ... getSecuritySettings().setAuthorizationStrategy( new UserLoginSecureComponentDisableAuthStrategy()); ... } ... Wenn ein unangemeldeter Benutzer die Seite lädt, werden die markierten Komponenten einfach nicht angezeigt. Zusammenfassung Wie wir in den Beispielen sehen konnten, gibt es verschiedene Ansatzpunkte für eine ge- eignete Strategie, wie man geschützte Informationen nur angemeldeten Benutzern zugäng- lich macht. Dabei hat die Strategie nur indirekt Einfluss auf die Komponenten. Außerdem muss sich die Komponente nicht selbst um die Darstellung oder den Sprung auf die An- meldeseite kümmern. Der Aufwand für einen Strategiewechsel ist somit gering.234
  • 11.1 Die Integration von Spring 11 11 Wicket in der Praxis Nachdem wir nun die wichtigsten Komponenten kennen und gelernt haben, wie man sie einsetzt, könnten wir so richtig loslegen. Meist stellt sich dann an dieser Stelle recht schnell die Frage: Wie fange ich an? Wie löse ich die alltäglichen Probleme? Wicket ist ein flexibles, anpassungsfähiges Framework. Das bedeutet aber auch, dass es für ein Problem nicht nur eine Lösung gibt. Komplexe Aufgabenstellungen wie z.B. das Thema Navigation können aber nicht mit nur einer Komponente gelöst werden. Auf den folgenden Seiten möchte ich anhand von ausgewählten Beispielen Anregungen geben, wie Sie solche und andere Probleme lösen und die ganzen Puzzleteile zu einem Ganzen zu- sammenfügen können. Quelltexte als Anregung Es ist immer hilfreich, wenn man von einer Sache so viel wie möglich versteht. Das hat sich nicht erst beim Schreiben dieses Buches gezeigt. Für das bessere Verständnis der Zu- sammenhänge habe ich mich z.B. intensiv in den Quelltexten von Wicket beschäftigt. Die Quelltexte sind sehr gut dokumentiert. Man kann daher anhand der Basiskomponenten gut erkennen, wie die verschiedenen Funktionen zusammenspielen und wie Wicket intern funktioniert. Fragen, die in diesem Buch nicht beantwortet wurden, können unter anderem durch einen Blick in den Code geklärt werden.11.1 Die Integration von Spring Bisher haben wir das eine oder andere Mal von der SpringBean-Annotation Gebrauch gemacht. Allerdings funktioniert diese Annotation nur für Klassen, die von Component abgeleitet wurden. Wichtig ist dabei, dass das Feld nicht initialisiert werden darf, da sonst der Wert, der durch Wicket gesetzt wurde, im Konstruktor der Klasse durch den deklarier- ten Wert (in dem Fall null) überschrieben würde. 235
  • 11 Wicket in der Praxis Listing 11.1 SimpleSpringIntegrationPage.java package de.wicketpraxis.web.thema.spring; ... public class SimpleSpringIntegrationPage extends WebPage { // geht auch ohne name, wenn es nur eine von dem Typ gibt @SpringBean(name = UserDao.BEAN_ID) UserDao _userDao; public SimpleSpringIntegrationPage() { add(new Label("message",_userDao.get(1)==null ? "kein Nutzer" : "ein Nutzer")); } } Auch wenn die Instanz, auf die dann in dem Beispiel das Feld _userDao zeigt, wie eine Instanz von UserDao aussieht, gibt es doch einen wesentlichen Unterschied: Die Instanz ist ein Proxy, der zuverlässig serialisiert werden kann. Dabei wird die eigentliche SpringBean nicht serialisiert, sondern mit den Informationen aus der Annotation temporär verfügbar gemacht und alle Methodenaufrufe an diese Instanz weitergeleitet. Es ist also möglich, einer anderen Komponente oder einer Modellklasse die Instanz zu über- geben, ohne dass man sich zusätzlich um das korrekte Serialisieren kümmern muss. Möchte man aber z.B. in einer Modellklasse eigenständig auf SpringBeans zugreifen, da- mit z.B. der Datenbankzugriff dann in dieser Modellklasse gekapselt ist, reicht die Annota- tion aus den anfangs geschilderten Gründen nicht aus. Man muss in diesem Fall den Pro- zess, der automatisch für alle Komponenten (die von der Klasse Component abgeleitet wurden) ausgeführt wird, für jede Instanz von Hand anstoßen. Listing 11.2 ExtendedSpringIntegrationPage.java package de.wicketpraxis.web.thema.spring; ... public class ExtendedSpringIntegrationPage extends WebPage { public ExtendedSpringIntegrationPage() { add(new Label("message",new SimpleNonWicketClass().getEmail())); } public static class SimpleNonWicketClass { @SpringBean UserDao _userDao; public SimpleNonWicketClass() { InjectorHolder.getInjector().inject(this); } public String getEmail() { User user = _userDao.getByEMail("test@wicket-praxis.de"); if (user!=null) return user.getEMail(); return null; } } }236
  • 11.2 Navigation Dazu ermitteln wir über InjectorHolder.getInjector() den durch die Applikation definierten Injector. Durch den Aufruf der Methode inject() werden die annotierten Felder initialisiert. Der Aufruf der inject()-Funktion ist an jeder Stelle und in jeder Klasse möglich. Auf diese Weise kann z.B. der Datenzugriff in einer Modellklasse gekap- selt werden.11.2 Navigation Die meisten Webanwendungen verhalten sich wie Webanwendungen. Das bedeutet, sie bieten z.B. dem Nutzer verschiedene Seiten, in denen er navigieren kann. Im folgenden Beispiel ermöglichen wir die Navigation zwischen verschiedenen Seiten und zu Unter- punkten der ausgewählten Seite. Wichtig sind an diesem Beispiel vor allem zwei Aspekte: die Struktur der Seitenklassen sowie der Komponenten und die Abstraktion der Naviga- tionsinformation. Diese Lösung ist nicht ohne Weiteres auf die eigene Problemstellung übertragbar, sondern soll nur als Anregung dienen. Als Erstes erstellen wir eine abstrakte Informationsschnittstelle, die Auskunft über den Navigationspunkt gibt. Listing 11.3 NavCallbackInterface.java package de.wicketpraxis.web.thema.howto.basepage.nav; ... public interface NavCallbackInterface { public String getName(); public List<NavCallbackInterface> getChilds(Page page); public boolean isActive(Page page); public void onClick(Page page); } Wenn der Navigationspunkt aktiv ist, dann wird die Methode getChilds() aufgerufen, die als Ergebnis eine Liste von Unterpunkten liefern kann. Die Methode onClick() wird aufgerufen, wenn der Nutzer den Navigationspunkt aktiviert. Die AbstractNavCallback- Klasse reduziert nur etwas den Schreibaufwand. Listing 11.4 AbstractNavCallback.java package de.wicketpraxis.web.thema.howto.basepage.nav; ... public abstract class AbstractNavCallback implements NavCallbackInterface { String _name; public AbstractNavCallback(String name) { _name=name; } public String getName() { return _name; } 237
  • 11 Wicket in der Praxis public List<NavCallbackInterface> getChilds(Page page) { return null; } public boolean isActive(Page page) { return false; } } Jetzt benötigen wir eine Komponente, welche die Navigationsstruktur anzeigen kann. Als Parameter wird ein Navigationspunkt übergeben. Listing 11.5 NavPanel.java package de.wicketpraxis.web.thema.howto.basepage.nav; ... public class NavPanel extends Panel { int _level=0; public NavPanel(String id, IModel<NavCallbackInterface> callbackModel) { super(id); initPanel(callbackModel); } public NavPanel(String id, IModel<NavCallbackInterface> callbackModel, int level) { super(id); _level=level; initPanel(callbackModel); } private void initPanel(IModel<NavCallbackInterface> callbackModel) { final IModel<String> nameModel= new PropertyModel<String>(callbackModel,"Name"); CascadingLoadableDetachableModel<String, NavCallbackInterface> classModel=new CascadingLoadableDetachableModel<String, NavCallbackInterface>(callbackModel) { @Override protected String load(NavCallbackInterface p) { return p.isActive(getPage())? "active" : null; } }; Link<NavCallbackInterface> link = new Link<NavCallbackInterface>("link", callbackModel) { @Override public void onClick() { getModelObject().onClick(getPage()); } @Override protected void onBeforeRender() { super.onBeforeRender(); if (nameModel.getObject()==null) setVisible(false); else setVisible(true); } };238
  • 11.2 Navigation link.add(new Label("name", nameModel)); link.add(new AttributeAppender("class",true,classModel," ")); link.add(new AttributeAppender("class",true, Model.of("level"+_level)," ")); add(link); CascadingLoadableDetachableModel<List<NavCallbackInterface>, NavCallbackInterface> childModel = new CascadingLoadableDetachableModel<List<NavCallbackInterface>, NavCallbackInterface>(callbackModel) { @Override protected List<NavCallbackInterface> load(NavCallbackInterface p) { return p.getChilds(getPage()); } }; add(new ListView<NavCallbackInterface>("childs", childModel) { @Override protected void populateItem(ListItem<NavCallbackInterface> item) { item.add(new NavPanel("child",item.getModel(),_level+1)); } }); } }Wir erstellen einen Link, der nur angezeigt wird, wenn der Navigationspunkt einen Namenhat. Ansonsten wird der Link ausgeblendet. Diese Funktion wird benötigt, wenn wir nach-her einen Navigationseinstiegspunkt übergeben, der nur Kindelemente zurückliefert, aberselbst nicht angezeigt werden darf.Im zweiten Schritt erstellen wir für die Liste aller Kindelemente jeweils ein NavPanel mitdem Unterschied, dass wir den Parameter für die Hierarchietiefe um eins erhöhen. Wir be-nutzen das Panel also rekursiv.Listing 11.6 NavPanel.html <wicket:panel> <a wicket:id="link"><span wicket:id="name"></span></a> <ul> <li wicket:id="childs" class="menu_line"> <wicket:container wicket:id="child"></wicket:container> </li> </ul> </wicket:panel>Für alle Seiten der Anwendung erstellen wir eine abstrakte Basisklasse, welche die not-wendigen CSS-Dateien einbindet, die oberste Instanz der NavPanel-Klasse erzeugt undden Rahmen für den Hauptbereich der Anwendung definiert.Listing 11.7 AbstractBasePage.java package de.wicketpraxis.web.thema.howto.basepage; ... public abstract class AbstractBasePage extends WebPage { public AbstractBasePage() { add(CSSPackageResource.getHeaderContribution(AbstractBasePage.class, "styles/base.css","all")); 239
  • 11 Wicket in der Praxis LoadableDetachableModel<NavCallbackInterface> navModel = new LoadableDetachableModel<NavCallbackInterface>() { @Override protected NavCallbackInterface load() { return NavInfo.getMainNavigation(); } }; add(new NavPanel("nav",navModel)); WebMarkupContainer webMarkupContainer = new WebMarkupContainer("main") { @Override public boolean isTransparentResolver() { return true; } }; add(webMarkupContainer); } public abstract List<NavCallbackInterface> getNavigations(); } Der Rahmen für den Hauptbereich („main“) überschreibt das Attribut für transpa- rentResolver mit „true“. Das bedeutet, dass Elemente, die auf derselben Hierarchiestufe wie der MarkupContainer hinzugefügt werden, alternativ auch im Markup innerhalb des Containers gesucht werden. Das ist notwendig, weil neue Komponenten in abgeleiteten Klassen auf derselben Hierarchiestufe hinzugefügt werden. Listing 11.8 AbstractBasePage.html <html> <head> </head> <body> <div class="content"> <div class="nav"> <wicket:container wicket:id="nav"></wicket:container> </div> <div wicket:id="main" class="main"> <wicket:child></wicket:child> </div> <div class="clear"></div> </div> </body> </html> Listing 11.9 base.css .content { width:1024px; background-color:#e0e0e0; margin:auto; } .nav { width:200px; background-color:#d0d0d0; float:left; } .main { width:800px; height:600px; margin-left:200px; padding-left:20px; background-color:#f0f0f0; } .clear {240
  • 11.2 Navigation clear:both; } .nav ul { margin:0px; padding:0px; } .nav ul li { margin:0px; padding:0px; padding-bottom:4px; padding-top:4px; list-style: none; } li a.level1 { padding-left:10px; } li a.level2 { padding-left:20px; } .active { font-weight:bold; }Die NavInfo-Klasse dient als Hilfsklasse, um an zentraler Stelle die Hauptnavigation ver-walten zu können. Dazu tragen wir alle Seiten, die in der Hauptnavigation angezeigt wer-den sollen, in die Liste ein. Die Klasse PagNavCallback ist eine Hilfsklasse, welche dieNavigationsinformation anhand der Seiteninformation erstellt.Listing 11.10 NavInfo.java package de.wicketpraxis.web.thema.howto.basepage; ... public class NavInfo { public static List<NavCallbackInterface> getPages() { List<NavCallbackInterface> ret=new ArrayList<NavCallbackInterface>(); ret.add(new PageNavCallback(StartPage.class,"Start")); ret.add(new PageNavCallback(SecondPage.class,"Zweite Seite")); return ret; } public static NavCallbackInterface getMainNavigation() { return new AbstractNavCallback(null) { public List<NavCallbackInterface> getChilds(Page page) { return getPages(); } public void onClick(Page page) {} }; }; }Wenn wir eine Seite hinzufügen, die von AbstractBasePage abgeleitet wird, dann kannbei aktivierter Seite die erweiterte Navigationsinformation durch den Aufruf von get-Navigations() ermittelt werden.Listing 11.11 PageNavCallback.java package de.wicketpraxis.web.thema.howto.basepage.nav; ... 241
  • 11 Wicket in der Praxis public class PageNavCallback extends AbstractNavCallback { Class<? extends Page> _pageClass; public PageNavCallback(Class<? extends Page> pageClass,String name) { super(name); _pageClass=pageClass; } public List<NavCallbackInterface> getChilds(Page page) { if (page.getClass()==_pageClass) { if (page instanceof AbstractBasePage) { return ((AbstractBasePage) page).getNavigations(); } } return null; } public boolean isActive(Page page) { return page.getClass()==_pageClass; } public void onClick(Page page) { page.setResponsePage(_pageClass); } } Jetzt gestaltet sich das Anlegen neuer Seiten recht einfach. Die Startseite bekommt zwei Navigationspunkte, die jeweils einen eigenen Wert in einem Modell ablegen, das durch das Label „clicked“ angezeigt wird. Listing 11.12 Start.java package de.wicketpraxis.web.thema.howto.basepage; ... public class StartPage extends AbstractBasePage { IModel<Integer> _clicked=new Model<Integer>(); public StartPage() { add(new Label("clicked",_clicked) { @Override protected void onBeforeRender() { super.onBeforeRender(); setVisible(getDefaultModelObject()!=null ? true : false); } @Override protected boolean callOnBeforeRenderIfNotVisible() { return true; } }); } @Override public List<NavCallbackInterface> getNavigations() { List<NavCallbackInterface> ret=new ArrayList<NavCallbackInterface>();242
  • 11.2 Navigation ret.add(new AbstractNavCallback("Klick 1") { public void onClick(Page page) { _clicked.setObject(1); } @Override public boolean isActive(Page page) { Integer lastClick = _clicked.getObject(); if (lastClick==null) return false; return lastClick.equals(1); } }); ret.add(new AbstractNavCallback("Klick 2") { public void onClick(Page page) { _clicked.setObject(2); } @Override public boolean isActive(Page page) { Integer lastClick = _clicked.getObject(); if (lastClick==null) return false; return lastClick.equals(2); } }); return ret; } }Wenn also die Startseite ausgewählt wurde, kann der Nutzer zusätzlich auf zwei Menü-punkte klicken. Das Markup der Seite reduziert sich auf den Hauptbereich der Anwen-dung.Listing 11.13 StartPage.html <wicket:head> <title>Startseite</title> </wicket:head> <wicket:extend> <h1>Startseite</h1> <wicket:enclosure> <p>Das letzte Mal wurde mit Link <span wicket:id="clicked"></span> geklickt.</p> </wicket:enclosure> </wicket:extend>Das wicket:enclosure-Tag sorgt dafür, dass alles innerhalb dieses Tags ausgeblendetwird, wenn die erste oder die durch das Attribut „child“ referenzierte Komponente un-sichtbar ist. Auf diese Weise wird der Text ausgeblendet, wenn noch kein Untermenüpunktangeklickt wurde.Die zweite Seite besitzt keine eigene Navigation.Listing 11.14 SecondPage.java package de.wicketpraxis.web.thema.howto.basepage; ... public class SecondPage extends AbstractBasePage { public SecondPage(){} 243
  • 11 Wicket in der Praxis @Override public List<NavCallbackInterface> getNavigations() { return null; } } Listing 11.15 SecondPage.html <wicket:head> <title>zweite Seite</title> </wicket:head> <wicket:extend> <h1>zweite Seite</h1> </wicket:extend> Das Gerüst steht, und wir haben jetzt eine Anwendung mit interessanten Navigationsmög- lichkeiten. Wie das Ganze in Aktion aussieht, kann man Abbildung 11.1 und Abbildung 11.2 entnehmen. Abbildung 11.1 Startseite Abbildung 11.2 Zweite Seite11.3 CSS einbinden Im letzten und in den vorangegangenen Beispielen haben wir bereits CSS-Dateien einge- bunden. Dabei konnten wir die CSS-Datei immer an unsere Anwendung anpassen. Das ist aber nicht immer der Fall. Es kommt nicht selten vor, das eine Agentur neben den CSS- Dateien auch meist ein HTML-Grundgerüst liefert, das auf die Angaben in der CSS-Datei abgestimmt ist. Das bedeutet, dass der erzeugte HTML-Quelltext den Anforderungen der Agentur entsprechen sollte. Um zu demonstrieren, wie man in so einem Fall vorgehen könnte, benutzen wir für das folgende Beispiel die Daten des CSS-Frameworks „960 Grid System“ (http://960.gs) als Grundlage für unsere Anwendung. Dieses CSS-Framework sorgt dafür, dass man das Lay- out der Seite aus Blöcken von div-Tags erzeugt, in dem der Wert im Attribut „class“ die Position und Größe definiert. Damit das System funktioniert, müssen wir die Struktur der Anwendung auf die gewünschte HTML-Struktur des Grid-Frameworks anpassen.244
  • 11.3 CSS einbindenWir erstellen eine Klasse, in der wir alle CSS-Referenzen konfigurieren. Auch wenn es inunserem Fall nicht notwendig wäre, möchte ich dennoch demonstrieren, wie CSS-Dateien,die nur vom Internet Explorer eingebunden werden sollen, über einen sogenannten „CSS-Hack“ eingebunden werden können.Listing 11.16 Style.java package de.wicketpraxis.web.thema.howto.css.styles; ... public class Style { public static List<IHeaderContributor> getCss() { List<IHeaderContributor> ret=new ArrayList<IHeaderContributor>(); ret.add(CSSPackageResource.getHeaderContribution(Style.class, "grid960/reset.css","all")); ret.add(CSSPackageResource.getHeaderContribution(Style.class, "grid960/text.css","all")); ret.add(CSSPackageResource.getHeaderContribution(Style.class, "grid960/960.css","all")); ret.add(CSSPackageResource.getHeaderContribution(Style.class, "base.css","all")); ret.add(IEConditionalHeader.START); ret.add(CSSPackageResource.getHeaderContribution(Style.class, "ieOnly.css","all")); ret.add(IEConditionalHeader.END); return ret; } }Die ersten drei CSS-Dateien stammen vom CSS-Framework. In der Datei „base.css“ legenwir unsere eigenen Definitionen ab, die Datei „ieOnly.css“ wird nur geladen, wenn mandie Seite mit dem Internet Explorer betrachtet.Da die Reihenfolge nicht verändert wird, in der die verschiedenen Referenzen dann imKopfbereich der Seite dargestellt werden, reicht es, wenn wir die für den „CSS-Hack“notwendigen Ausgaben vor und nach der gewünschten Referenz einbinden.Listing 11.17 IEConditionalHeader.java package de.wicketpraxis.web.thema.howto.css.styles.header; ... public class IEConditionalHeader { public static final IHeaderContributor START=new IHeaderContributor() { public void renderHead(IHeaderResponse response) { response.renderString("<!--[if IE]>"); } }; public static final IHeaderContributor END=new IHeaderContributor() { public void renderHead(IHeaderResponse response) { response.renderString("<![endif]-->"); } }; } 245
  • 11 Wicket in der Praxis Die CSS-Dateien müssen in das richtige Verzeichnis entpackt werden. Außerdem erstellen wir noch zwei eigene CSS-Dateien, in der wir ein paar Farbanpassungen vornehmen. Die zweite CSS-Datei, die nur vom Internet Explorer benutzt wird, ändert die Textfarbe auf Rot. Listing 11.18 base.css Body { background-color:#a5a5a5; } .page { background-color:#f0f0f0; } .head { background-color:#e0e0e0; } .box { border:1px dashed red; margin:4px; } Listing 11.19 ieOnly.css Body { color: red; } Damit sind alle Vorkehrungen getroffen, damit die CSS-Dateien korrekt eingebunden wer- den können. Im nächsten Schritt müssen wir eine passende HTML-Struktur erzeugen. Dazu ein paar Bemerkungen zum verwendeten Grid-CSS-Framework: Die meisten Grid- Frameworks teilen eine Seite in eine feste Anzahl von Spalten auf. Dann kann man durch die Angabe einer CSS-Klasse ein Element auf diese Spalten verteilen. Beim verwendeten Framework geschieht das z.B. durch die Angabe der Klasse „grid_1“ für eine Breite von einer Spalte oder „grid_5“ für eine Breite von fünf Spalten. Wenn man vor der Spalte etwas Platz lassen möchte, dann benutzt man den Wert „prefix_2“ für zwei Spalten und „suffix_4“, wenn man danach vier Spalten Abstand definieren möchte. Ich glaube, soweit sollte sich das Grundprinzip erschlossen haben. Wir erstellen uns eine Komponente, die diese Regeln und die notwendigen Anpassungen im Quelltext für uns zugänglich machen. Listing 11.20 Grid.java package de.wicketpraxis.web.thema.howto.css.layout; ... public class Grid extends WebMarkupContainer { int _columns; public Grid(String id,int columns) { super(id); _columns=columns; IModel<String> columnModel=new LoadableDetachableModel<String>()246
  • 11.3 CSS einbinden { @Override protected String load() { return "grid_"+_columns; } }; add(new AttributeAppender("class",true,columnModel," ")); } public int getColumns() { return _columns; } public Grid setColumns(int columns) { _columns = columns; return this; } }Die Komponente Grid besitzt kein eigenes Markup. Die Angabe der Spaltenzahl ist dafürverantwortlich, welcher Wert im class-Attribut des HTML-Tags gesetzt wird.Wie bereits beschrieben, ist es möglich, Abstände vor und hinter einem Element zu defi-nieren. Dazu leiten wir eine Klasse von Grid ab, die das Setzen der passenden Werte ver-anlasst.Listing 11.21 GridWithSpace.java package de.wicketpraxis.web.thema.howto.css.layout; ... public class GridWithSpace extends Grid { int _spaceBefore; int _spaceAfter; public GridWithSpace(String id, int columns, int spaceBefore, int spaceAfter) { super(id, columns); _spaceBefore=spaceBefore; _spaceAfter=spaceAfter; IModel<String> prefixModel=new LoadableDetachableModel<String>() { @Override protected String load() { return _spaceBefore!=0 ? "prefix_"+_spaceBefore : null; } }; add(new AttributeAppender("class",true,prefixModel," ")); IModel<String> postfixModel=new LoadableDetachableModel<String>() { @Override protected String load() { return _spaceAfter!=0 ? "suffix_"+_spaceAfter : null; } }; 247
  • 11 Wicket in der Praxis add(new AttributeAppender("class",true,postfixModel," ")); } } Um ein Element am Anfang einer neuen Zeile darstellen zu können, kann es notwendig sein, den Zeilenumbruch zu erzwingen. Dazu muss vor diesem Block ein div-Tag mit dem Attribut class="clear" eingefügt werden. Wir erzeugen eine Komponente, die uns diese Arbeit abnimmt. Wir benutzen dafür eine Border-Komponente, sodass wir alle Elemente, die in dieser Zeile angezeigt werden sollen, als Kindelemente hinzufügen. Listing 11.22 Line.java package de.wicketpraxis.web.thema.howto.css.layout; import org.apache.wicket.markup.html.border.Border; public class Line extends Border { public Line(String id) { super(id); setRenderBodyOnly(true); } } Durch das Setzen von setRenderBodyOnly() wird das HTML-Tag ausgeblendet, an das die Komponente gebunden ist. Das ist in diesem Fall notwendig, weil es sonst durch das überflüssige HTML-Tag zu Darstellungsproblemen kommt. Listing 11.23 Line.html <wicket:border><wicket:body/><div class="clear">&nbsp;</div></wicket:border> Jetzt haben wir passende Komponenten erstellt, mit denen wir das Layout der Anwendung beeinflussen können. Wir binden die notwendigen CSS-Referenzen in die Seite ein und erzeugen eine Liste von Komponenten, die alle mit einer anderen Breitenangabe versehen werden (Abbildung 11.3). Listing 11.24 BasePageWithCSS.java package de.wicketpraxis.web.thema.howto.css; ... public class BasePageWithCSS extends WebPage { public BasePageWithCSS() { List<IHeaderContributor> cssList = Style.getCss(); for (IHeaderContributor css : cssList) { add(new HeaderContributor(css)); } add(new ListView<Integer>("list",Arrays.asList(1,2,3,4)) { @Override protected void populateItem(ListItem<Integer> item) { Integer index = item.getModelObject(); Line line = new Line("start");248
  • 11.3 CSS einbinden line.add(new Grid("left",4+index)); line.add(new GridWithSpace("right",4+index,8-(index*2),0)); item.add(line); } }); } }Listing 11.25 BasePageWithCSS.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head></head> <body> <div class="container_16 page"> <div class="grid_16 head"> <h1>Seite mit <a href="http://960.gs">960.gs</a></h1> </div> <div class="clear">&nbsp;</div> <wicket:container wicket:id="list"> <div wicket:id="start"> <div wicket:id="left"> <div class="box"> Links </div> </div> <div wicket:id="right"> <div class="box"> Rechts </div> </div> </div> </wicket:container> </div> </body> </html>Abbildung 11.3 Die Layoutkomponenten in AktionZusammenfassungDas Einbinden fremder Daten ist immer mit Aufwand verbunden. Doch durch das Erstel-len eigener Komponenten können wir eine Abstraktionsschicht einfügen, die z.B. auch fürInvestitionssicherheit sorgt, da Anpassungen an der HTML-Struktur oder den verwendetenAttributen meist durch Anpassungen innerhalb der Abstraktionsschicht aufgefangen wer-den können.Trotzdem sollte man darauf drängen, dass sich eine Agentur an den Möglichkeiten der ein-gesetzten Technologie orientiert, sodass man die Aufwände klein halten, aber vor allemdas Potential von Wicket so gut wie möglich ausschöpfen kann. 249
  • 11 Wicket in der Praxis11.4 Eigene Basiskomponenten Das Aussehen der Anwendung ist definiert, und man stellt fest, dass man sehr häufig im- mer wieder dieselben Basiskomponenten benutzt. Es lohnt sich dann (auch in Anbetracht einer zukünftigen Erweiterungsfähigkeit), in diesen Fällen jeweils eigene Komponenten abzuleiten. In diesem Beispiel sollen alle Links wie Buttons aussehen. Außerdem soll der Link (also der Button) über sein Aussehen die Gefährlichkeit seiner Aktion darstellen können. Dazu bindet die ButtonLink-Klasse eine passende CSS-Datei ein und setzt das class-Attribut auf den entsprechenden Wert. Listing 11.26 ButtonLink.java package de.wicketpraxis.web.thema.howto.modify2; ... public abstract class ButtonLink<T> extends Link<T> { public enum Type { OK, INFO, CANCEL }; public ButtonLink(String id, IModel<T> model, Type type) { super(id, model); add(CSSPackageResource.getHeaderContribution( ButtonLink.class,"button.css")); add(new AttributeModifier("class",true,Model.of(type.name()))); } } Im CSS wird für jede Klasse ein passendes Aussehen definiert. Dabei ist auffällig, dass der Umfang der CSS-Datei den Umfang der Klasse bei Weitem überschreitet. Die Bilddateien, die durch das Attribut background-image referenziert werden, werden von Wicket relativ zur Position der CSS-Datei gesucht. Listing 11.27 button.css a.OK, a.INFO, a.CANCEL { text-decoration: none; display:inline-table; background-repeat:no-repeat; background-position:2px 4px; padding:3px; padding-left:20px; padding-right:4px; } a.OK { background-color:#529214; border:1px solid #497024; color:#fff; background-image:url("OK.png"); } a.OK:hover { background-color:#E6EFC2; border:1px solid #C6D880; color:#529214; } a.INFO { border:1px solid #d92; background-color: #ff2; color:#a60; padding-left:14px; background-image:url("INFO.png"); } a.INFO:hover {250
  • 11.4 Eigene Basiskomponenten border:1px solid #fd0; background-color: #ff8; color:#c70; } a.CANCEL { background-color:#d12f19; border:1px solid #842013; color:white; background-image:url("CANCEL.png"); } a.CANCEL:hover { background-color:#fbe3e4; border:1px solid #fbc2c4; color:#d12f19; }Bevor wir diese Komponenten verwenden, passen wir noch die Feedback-Komponente anunsere Bedürfnisse an. Dabei liegt der Unterschied darin, dass in diesem Fall auch eineigenes Markup definiert wird, ohne dass wir die Funktionalität der Komponente verän-dern. Das birgt die Gefahr, dass sich die Basiskomponente ändert und unser eigenes Mark-up nicht mehr passt, weil wir das Markup vollständig überschrieben und nicht abgeleitethaben.Listing 11.28 CustomFeedbackPanel.java package de.wicketpraxis.web.thema.howto.modify2; ... public class CustomFeedbackPanel extends FeedbackPanel { public CustomFeedbackPanel(String id) { super(id); add(CSSPackageResource.getHeaderContribution( CustomFeedbackPanel.class,"feedback.css")); } public CustomFeedbackPanel(String id, IFeedbackMessageFilter filter) { super(id, filter); add(CSSPackageResource.getHeaderContribution( CustomFeedbackPanel.class,"feedback.css")); } @Override protected String getCSSClass(FeedbackMessage message) { return message.getLevelAsString().toLowerCase(); } }Wir binden auch für diese Komponente eine eigene CSS-Definition ein und ändern außer-dem die Fehlerklassen für die Anzeige (getCSSClass()). In beiden Fällen wird die CSS-Datei nur einmal im Kopf eingebunden, egal wie oft die Komponente auf der Seite vor-kommt.Listing 11.29 CustomFeedbackPanel.html <wicket:panel> <div class="feedback" wicket:id="feedbackul"> <div class="replaced" wicket:id="messages"> <span class="replaced" wicket:id="message"></span> </div> </div> </wicket:panel> 251
  • 11 Wicket in der Praxis Listing 11.30 feedback.css .feedback div { border: 1px solid black; padding:4px; margin-bottom:2px; } .feedback div.info { background-color:#E6EFC2; border:1px solid #529214; color:#325202; } .feedback div.warning { border:1px solid #c70; background-color: #ff8; color:#830; } .feedback div.error { background-color:#fbe3e4; border:1px solid #e57677; color:#d12f19; font-weight: bold; } .feedback div.debug { background-color:#C2EFE6; border:1px solid #149252; color:#025232; } Auf der folgenden Seite verwenden wir beide Komponenten, sodass wir das Ergebnis un- serer Bemühungen betrachten können. Listing 11.31 OwnComponentsPage.java package de.wicketpraxis.web.thema.howto.modify2; ... public class OwnComponentsPage extends WebPage { public OwnComponentsPage() { add(new CustomFeedbackPanel("feedback")); add(new ButtonLink<String>("ok",Model.of(":)"),Type.OK) { @Override public void onClick() { info("ok"); } }); add(new ButtonLink<String>("info",Model.of("!"),Type.INFO) { @Override public void onClick() { warn("aufgepasst"); } }); add(new ButtonLink<String>("cancel",Model.of(":("),Type.CANCEL) { @Override public void onClick() { error("cancel"); } }); } } Auch wenn wir nur die drei Meldungstypen info, warn und error benutzen, gibt es trotz- dem noch die Möglichkeit, mit debug dem Benutzer die passende Rückmeldung zu geben.252
  • 11.5 Komponententausch Listing 11.32 OwnComponentsPage.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Own Components Page</title> </head> <body> <div wicket:id="feedback"></div> <a wicket:id="ok">Speichern</a> <a wicket:id="info">Merken</a> <a wicket:id="cancel">Abbrechen</a> </body> </html> Wenn man mit der Maus über der Schaltfläche ist, verändert diese ihr Aussehen. Klickt man dann auf diesen Link, erscheint ein passender Hinweis, der farblich zur Schaltfläche passt (Abbildung 11.4). Abbildung 11.4 Eigene Button- und Feedback-Komponen- ten Zusammenfassung Eigene Komponenten ersparen Schreibarbeit und sorgen für eine konsistente optische Er- scheinung. Nutzt man die eigenen Komponenten konsequent, kommen alle Anpassungen an diesen Komponenten automatisch allen Anwendungsteilen zugute. Die so kontinuierlich verbesserten Komponenten bieten eine stabile Basis für alle weiteren Projekte, sodass man Anwendungen mit immer geringerem Aufwand entwickeln kann.11.5 Komponententausch Jede Komponente hat eine eigene ID. Wicket kann daher jede Komponente eindeutig ad- ressieren. Und da alle Komponenten die gleiche Basisklasse verwenden, können alle Komponenten an jeder Stelle verwendet werden. Es ist daher naheliegend, wenn man zur Laufzeit Komponenten austauschen kann. Wicket bietet die Möglichkeit, eine Komponen- te durch eine andere Komponente mit derselben ID zu ersetzen. Im Folgenden zeigen wir zwei interessante Ansätze, die von dieser Möglichkeit Gebrauch machen. 253
  • 11 Wicket in der Praxis 11.5.1 AjaxFallbackConfirmLink Wenn der Nutzer eine Aktion durchführt, die z.B. Daten unwiderruflich löschen kann, kann eine Sicherheitsabfrage sinnvoll sein. Normalerweise wird in diesem Fall eine Dia- logbox geöffnet, und der Nutzer wird ein weiteres Mal gefragt. Das kann unter Umständen die Nutzbarkeit einer Anwendung stark beeinträchtigen. Abbildung 11.5 AjaxFallbackConfirmLink In diesem Beispiel verzichten wir auf die Anzeige eines Dialogs, sondern stellen einen einfachen eingebetteten Dialog dar. Wenn der Nutzer dort die Aktion abbricht, wird die Komponente zurückgetauscht (Abbildung 11.5). Auch wenn der Nutzer die Aktion bestä- tigt, wird die Komponente zurückgetauscht, aber die eigentliche Aktion wird im Anschluss durchgeführt. Listing 11.33 AjaxFallbackConfirmLink.java package de.wicketpraxis.web.thema.howto.replace; ... public abstract class AjaxFallbackConfirmLink<T> extends AjaxFallbackLink<T> { IModel<String> _message; public AjaxFallbackConfirmLink(String id,IModel<String> message) { super(id); _message=message; setOutputMarkupId(true); } public AjaxFallbackConfirmLink(String id,IModel<T> model, IModel<String> message) { super(id,model); _message=message; setOutputMarkupId(true); } @Override public void onClick(AjaxRequestTarget target) { ConfirmPanel confirmLink = new ConfirmPanel(this); if (target!=null) target.addComponent(confirmLink); } class ConfirmPanel extends Panel { AjaxFallbackLink<T> _parent; public ConfirmPanel(AjaxFallbackLink<T> parent) { super(parent.getId());254
  • 11.5 Komponententausch _parent=parent; setOutputMarkupId(true); add(new Label("message",_message)); add(new AjaxFallbackLink<T>("yes",parent.getModel()) { @Override public void onClick(AjaxRequestTarget target) { ConfirmPanel.this.replaceWith(_parent); if (target!=null) target.addComponent(_parent); onConfirm(target); } }); add(new AjaxFallbackLink<T>("no",parent.getModel()) { @Override public void onClick(AjaxRequestTarget target) { ConfirmPanel.this.replaceWith(_parent); if (target!=null) target.addComponent(_parent); onCancel(target); } }); parent.replaceWith(this); } @Override protected void onComponentTag(ComponentTag tag) { super.onComponentTag(tag); tag.setName("span"); } } public abstract void onConfirm(AjaxRequestTarget target); public void onCancel(AjaxRequestTarget target) { } }In der überschriebenen onClick()-Methode wird eine ConfirmPanel-Komponente er-zeugt. Diese erstellt den eingebetteten Dialog mithilfe zweier Link und einer Label-Komponente. Danach tauscht sich diese neue Komponente gegen die Link-Komponente.Zum Zurücktauschen muss sich die Komponente die vertauschte Komponente merken.Außerdem muss das HTML-Tag noch ersetzt werden, da verschachtelte Links nicht zuläs-sig sind.Listing 11.34 AjaxConfirmLink$ConfirmPanel.html <wicket:panel> <span wicket:id="message"></span> <a wicket:id="yes">Ja</a> <a wicket:id="no">Nein</a> </wicket:panel>Dieses Klasse kann dann ähnlich einfach benutzt werden wie eine normale Link-Klasse,nur dass die zu überschreibende Methode jetzt onConfirm() heißt und nur aufgerufenwird, wenn der Nutzer zweimal an der richtigen Stelle klickt. 255
  • 11 Wicket in der Praxis Listing 11.35 ComponentReplacePage.java package de.wicketpraxis.web.thema.howto.replace; ... public class ComponentReplacePage extends WebPage { public ComponentReplacePage() { final FeedbackPanel feedbackPanel = new FeedbackPanel("feedback"); feedbackPanel.setOutputMarkupId(true); add(feedbackPanel); add(new AjaxFallbackConfirmLink<String>("link",Model.of("löschen?")) { @Override public void onConfirm(AjaxRequestTarget target) { if (target!=null) target.addComponent(feedbackPanel); info("Ok. Dann mach ichs"); } @Override public void onCancel(AjaxRequestTarget target) { if (target!=null) target.addComponent(feedbackPanel); error("Ok. Besser nicht."); } }); } } Listing 11.36 ComponentReplacePage.html ... <body> <div wicket:id="feedback"></div> <a wicket:id="link">löschen</a> </body> ... Auch im Markup können wir die Komponente wie einen ganz normalen Link benutzen. Der Link-Tausch wird transparent für die Anwendung durchgeführt und ist ein gutes Bei- spiel dafür, wie man komplexe Nutzerinteraktionen in einer Komponente verstecken und sich auf das Ergebnis der Interaktion konzentrieren kann. 11.5.2 Wizard Was im letzten Beispiel nicht von Bedeutung war, ist für das folgende entscheidend: Eine Komponente ist einfach nur eine Klasse. Das bedeutet, dass der Zustand des Objekts erhal- ten bleibt. Wenn man die Komponente zurücktauscht, können wieder dieselben Informa- tionen wie vor dem Tausch angezeigt werden. Das kann man sich zunutze machen, indem man auf diese Weise eine Wizard-Komponente erstellt. Listing 11.37 WizardPage.java package de.wicketpraxis.web.thema.howto.replace; ... public class WizardPage extends WebPage { public WizardPage()256
  • 11.5 Komponententausch { add(new FeedbackPanel("feedback")); final String wid="panel"; final List<WizardPanel> list = Arrays.asList(new WizardPanel(wid,1), new WizardPanel(wid,2),new WizardPanel(wid,3)); add(list.get(0)); add(new Link("next") { @Override public void onClick() { Component curPage = WizardPage.this.get(wid); int idx=list.indexOf(curPage); idx++; if (idx>=list.size()) idx=0; curPage.replaceWith(list.get(idx)); } }); } class WizardPanel extends Panel { public WizardPanel(String id,int start) { super(id); Model<Integer> model = Model.of(start); Form<Integer> form = new Form<Integer>("form"); form.add(new TextField<Integer>("zahl",model).setRequired(true)); add(form); add(new Label("zahl",model)); } } }Wir erzeugen eine Liste von drei Instanzen der WizardPanel-Klasse. Die Instanzen be-kommen alle dieselbe ID, die erste Instanz wird als Kindkomponente hinzugefügt. DerLink tauscht die aktuelle Komponente durch die nächste in der Liste aus. Am Ende wirddie Liste wieder von vorn abgearbeitet. Die WizardPanel-Komponente beinhaltet einFormular, in dem eine Zahl eingegeben werden kann. Außerdem wird der letzte Wertdurch das Label angezeigt.Listing 11.38 WizardPage.html ... <body> <div wicket:id="feedback"></div> <div wicket:id="panel" style="width:300px; border:1px solid #e0e0e0;"> </div> <a wicket:id="next">weiter</a> </body> ...Listing 11.39 WizardPage$WizardPanel.html <wicket:panel> <form wicket:id="form"> Zahl: <input wicket:id="zahl"> <button>Setzen</button> </form> Die letzte Zahl war die <span wicket:id="zahl"></span>. </wicket:panel> 257
  • 11 Wicket in der Praxis Man kann nun bei jedem Schritt etwas anderes eingeben. Da der Zustand innerhalb der Komponente gespeichert wurde, wird der letzte Zustand beim Wechsel durch den Klick auf den Link angezeigt.11.6 Suchmaschinenoptimierung Wenn der Suchroboter einer Suchmaschine auf der eigenen Seite landet, dann wird Seite für Seite auf weiterführende Links untersucht. Der Pfad der Seite fließt dabei oft in die Bewertung der Seite ein, sodass ein geschickter, aussagekräftiger Name für die Positionie- rung in den Suchergebnissen entscheidend sein kann. Außerdem muss man dem Umstand Rechnung tragen, dass sich der Suchroboter einer Suchmaschine stark von einem normalen Browser unterscheidet. 11.6.1 Pfad für BookmarkablePages Wicket kann jeder Seite einen ansprechenden Pfad geben, sodass die Seite auch über die- sen Pfad aufgerufen werden kann. Im folgenden Beispiel verwalten wir die Pfade, unter denen wir verschiedene Seiten anbinden, über eine Annotation, sodass die Information, unter welchem Pfad die Seite angebunden wird auch innerhalb der Klassendefinition zu finden ist. Listing 11.40 MountPath.java package de.wicketpraxis.web.thema.howto.mount; import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MountPath { String Path(); Class<?> Parent() default Object.class; } Dabei kann in dieser Annotation auch eine übergeordnete Klasse angegeben werden, so- dass der Pfad aus der Liste aller übergeordneten Pfade und dem Pfad der Klasse erzeugt wird. Listing 11.41 PageMount.java package de.wicketpraxis.web.thema.howto.mount; ... public class PageMount { public static void mountPages(WebApplication app) { mountQueryString(app, MountedPage.class); mountHybrid(app, SubMountedPage.class); }258
  • 11.6 Suchmaschinenoptimierung private static void mountQueryString(WebApplication app, Class<? extends Page> page) { app.mount(new QueryStringUrlCodingStrategy(getPath(page),page)); } private static void mountHybrid(WebApplication app, Class<? extends Page> page) { app.mount(new HybridUrlCodingStrategy(getPath(page),page,true)); } private static String getPath(Class<? extends Page> page) { StringBuilder sb=new StringBuilder(); getPath(sb, page); return sb.toString(); } private static void getPath(StringBuilder sb, Class<?> page) { MountPath mountAnnotation = page.getAnnotation(MountPath.class); if (mountAnnotation!=null) { getPath(sb,mountAnnotation.Parent()); sb.append(mountAnnotation.Path()); } } }Die Hilfsklasse dient zum Erzeugen des Pfades anhand der Annotationen der Klassen.Außerdem werden zwei unterschiedliche Strategien zum Anbinden unterstützt. Man könn-te an dieser Stelle auch eigene Strategien implementieren. Außerdem gibt es noch andereStrategien, die aber an dieser Stelle nicht behandelt werden.Die zwei (noch zu erstellenden) Seitenklassen werden jeweils mit einer der beiden Strate-gien angebunden. Dazu muss die Methode in der WebApplication-Klasse aufgerufenwerden.Listing 11.42 WicketPraxisApplication.java ... protected void init() { ... PageMount.mountPages(this); ... } ...Jetzt legen wir die zwei Seiten an. Um einen Basispfad zu definieren, erstellen wir eineKlasse, die wir nur mit einer Annotation versehen.Listing 11.43 BaseClass.java package de.wicketpraxis.web.thema.howto.mount; @MountPath(Path="/mountExample") public class BaseClass { } 259
  • 11 Wicket in der Praxis Die nächste Seite wird von WebPage abgeleitet, die Annotation referenziert allerdings die BaseClass-Klasse, sodass sich der Pfad aus den beiden Annotationen zusammensetzt. Listing 11.44 MountedPage.java package de.wicketpraxis.web.thema.howto.mount; ... @MountPath(Path="/mountedPage",Parent=BaseClass.class) public class MountedPage extends WebPage { public MountedPage() { add(new BookmarkablePageLink<SubMountedPage>("current", MountedPage.class,new PageParameters("A=1,B=2"))); add(new Link("currentBookmark") { @Override public void onClick() { setResponsePage(MountedPage.class,new PageParameters("A=1,B=2")); } }); add(new Link("currentDirect") { @Override public void onClick() { setResponsePage(new MountedPage()); } }); add(new BookmarkablePageLink<SubMountedPage>("sub", SubMountedPage.class,new PageParameters("A=1,B=2"))); add(new Link("directBookmark") { @Override public void onClick() { setResponsePage(SubMountedPage.class, new PageParameters("A=1,B=2")); } }); add(new Link("direct") { @Override public void onClick() { setResponsePage(new SubMountedPage()); } }); } } Auf der Seite fügen wir drei Links auf dieselbe Seite und drei Links auf eine Unterseite ein. Die drei Links dienen der Unterscheidung der verschiedenen Verlinkungsarten und der Konsequenz für die jeweils resultierende URL. Die Links auf die zwei Seiten dienen der Unterscheidung der beiden Strategien.260
  • 11.6 SuchmaschinenoptimierungListing 11.45 MountedPage.html ... <body> <a wicket:id="current">Hier</a> <a wicket:id="currentBookmark">Hier(Bookmark)</a> <a wicket:id="currentDirect">Hier(Link)</a> <a wicket:id="sub">Sub</a> <a wicket:id="directBookmark">Sub(Link - BookmarkAble)</a> <a wicket:id="direct">Sub(Instanz)</a> </body> ...Die zweite Seite ist einfacher, da sich auf dieser Seite nur ein Link auf eine neue Instanzder Seite befindet.Listing 11.46 SubMountedPage.java package de.wicketpraxis.web.thema.howto.mount; ... @MountPath(Path="/sub",Parent=MountedPage.class) public class SubMountedPage extends WebPage { public SubMountedPage() { add(new Link("link") { @Override public void onClick() { setResponsePage(new SubMountedPage()); } }); } }Die Annotation hat eine Referenz auf die MountedPage-Klasse, sodass sich auch hier derPfad entsprechend zusammensetzt.Listing 11.47 SubMountedPage.html ... <body> <a wicket:id="link">Sub</a> </body> ...Wenn wir jetzt die Seite aufrufen, dann ergeben sich folgende URLs in der Ergebnisseite:Listing 11.48 Ergebnis.html ... <body> <a wicket:id="current" href="../mountExample/mountedPage?A=1&amp;B=2"> Hier </a> <a wicket:id="currentBookmark" href="../?wicket:interface=:0:currentBookmark::ILinkListener::"> Hier(Bookmark) </a> <a wicket:id="currentDirect" href="../?wicket:interface=:0:currentDirect::ILinkListener::"> Hier(Link) </a> 261
  • 11 Wicket in der Praxis <a wicket:id="sub" href="../mountExample/mountedPage/sub/A/1/B/2"> Sub </a> <a wicket:id="directBookmark" href="../?wicket:interface=:0:directBookmark::ILinkListener::"> Sub(Link - BookmarkAble) </a> <a wicket:id="direct" href="../?wicket:interface=:0:direct::ILinkListener::"> Sub(Instanz) </a> </body> ... Die ersten Links auf dieselbe Seite und der erste Link auf die Unterseite fallen wie erwartet aus. Der Unterschied zwischen den beiden Strategien besteht an dieser Stelle darin, wie die Parameter kodiert sind, die beim Aufruf mit übergeben wurden. Die anderen Links sind nur für die aktuelle Session gültig, sodass Wicket z.B. beim Aufruf durch einen Suchmaschinenroboter den Link nicht zuordnen kann und den Roboter auf eine Fehlerseite weiterleiten wird. Das bedeutet, dass nur die ersten Links für eine Such- maschine verwendbar sind. Jeweils der zweite Link, wo als Zielseite eine Klasse mit Parametern angegeben wurde, leitet den Nutzer auf eine Seite weiter, die unter dieser URL jederzeit aufrufbar ist. Das bedeutet, dass sich der Nutzer auf diese Seite ebenfalls ein Lesezeichen setzen kann, der Link ist nicht relativ zu Informationen aus der Session. Der Unterschied zwischen der QueryString- und HybridUrlCodingStrategy liegt in der URL der Ergebnisseite. Bei der HybridUrlCodingStrategy wird immer „.<Versions- nummer>“ (außer für den sub-Link) an die URL anhängt. Das bedeutet, der Nutzer wird auch beim „direct“-Link auf eine Seite weiterleitet, deren URL wie von einem Book- markable-Link aussieht. Legt der Nutzer sich einen Bookmark auf die Seite an, kann er später zur Seite springen. Allerdings stimmt dann die Versionsnummer nicht überein, so- dass Wicket erkennt, dass der Nutzer mit einer neuen Session die Seite aufruft. Daher legt Wicket einfach eine neue Instanz der Seite an. Das ist dann interessant, wenn der Zustand der Seite nicht in Parametern abgebildet werden kann oder soll, der Nutzer diese Seite aber als Einsprung benutzen kann. Daher kann ich für die meisten Anwendungsfälle die Hyb- rid-Variante empfehlen. 11.6.2 SessionTimeoutPage Da Wicket viele für die Navigation relevante Informationen in der Session ablegt, kann es vorkommen, dass der Nutzer auch auf die Seite zugreift, obwohl die Session, die mit dem geöffneten Browser-Fenster assoziiert ist, bereits abgelaufen ist. Dann wird der Nutzer auf eine Fehlerseite weitergeleitet. Man sollte daher auf jeden Fall eine eigene Fehlerseite erstellen, auf der sich ein Link auf die Startseite der Anwendung befindet. Mit dem Aufrufen der Fehlerseite bekommt der Nutzer eine neue Session zugewiesen. Man kann aber durch einen einfachen Trick den Nutzer auf die Seite verweisen, auf der er den letzten (zum Fehler führenden) Link angeklickt hat. Dazu muss man nur das Attribut262
  • 11.6 Suchmaschinenoptimierung„Referer“ aus dem HttpHeader des Requests auslesen und prüfen, ob man diese URLüberhaupt verlinken sollte.Listing 11.49 SessionTimeoutPage.java package de.wicketpraxis.web.thema.howto.mount; ... public class SessionTimeoutPage extends WebPage { public SessionTimeoutPage() { WebRequest wicketRequest = (WebRequest) getRequest(); HttpServletRequest request = wicketRequest.getHttpServletRequest(); String referer = request.getHeader("Referer"); boolean cookies = true; ClientInfo clientInfo = Session.get().getClientInfo(); if (clientInfo instanceof WebClientInfo) { WebClientInfo wc = (WebClientInfo) clientInfo; cookies = wc.getProperties().isCookiesEnabled(); } add(new WebMarkupContainer("cookie-not-set").setVisible(!cookies)); ExternalLink link = new ExternalLink("link", referer); link.add(new Label("url", referer)); WebMarkupContainer text = new WebMarkupContainer("text"); text.add(link); BookmarkablePageLink<? extends WebPage> start = new BookmarkablePageLink<WebPage>("start", Application.get().getHomePage()); add(start); if ((referer != null) && (referer.indexOf("wicket:") == -1)) { start.setVisible(false); } else text.setVisible(false); add(text); } }Wir zeigen außerdem noch einen Hinweistext an, wenn der Nutzer das Setzen von Cookiesunterbunden hat. Der Trick in der Unterscheidung, ob man zu dieser Seite zurückspringensollte, liegt darin, die URL nach dem Auftreten von „wicket:“ zu untersuchen. Wicket-Links (z.B. zu Seiten die nicht unter einem anderen Pfad angebunden wurden) beinhaltenimmer diesen Text. Alle anderen URLs sollten keine Probleme verursachen. Es ist undbleibt natürlich ein Trick. Aber einer, der erstaunlich gut funktioniert!Listing 11.50 SessionTimeoutPage.html ... <body> <h1>Ihre Session ist abgelaufen.</h1> <p wicket:id="cookie-not-set"> Vermutlich haben Sie das Setzen von Cookies deaktiviert. Einige Funktionen auf unseren Seiten stehen dann nicht zur Verfügung. </p> <p wicket:id="text"> Rufen Sie die letzte Seite noch einmal unter folgendem Link auf: <br><a wicket:id="link"><span wicket:id="url">Referer</span></a> </p> 263
  • 11 Wicket in der Praxis <p> <a wicket:id="start">Zurück zur Startseite</a> </p> </body> </html> Wenn man die HybridUrlCodingStrategy einsetzt, kann man auf eine eigene Session- TimeoutPage verzichten, da Wicket im Zweifelsfall eine neue Instanz der Seite erzeugt. Jetzt muss diese Seite in der WebApplication-Klasse noch als Fehlerseite gesetzt werden. Listing 11.51 WicketPraxisApplication.java ... protected void init() { ... getApplicationSettings().setPageExpiredErrorPage( SessionTimeoutPage.class); ... } ... 11.6.3 SEO-Links Links, die für Suchmaschinen geeignet sind, sind nicht automatisch auch für den Nutzer die beste Art und Weise, sich innerhalb der Anwendung zu bewegen. Damit die Seiten, die in den Suchergebnissen der Suchmaschine erscheinen sollen, auch durch den Suchroboter gefunden werden können, kann man zusätzlich zur Nutzernavigation eine Suchmaschinen- navigation in die Anwendung einbauen. Der Einsatz der BookmarkablePageLink-Klasse erscheint nicht zu Unrecht als aufwendig. Durch eine kleine Erweiterung ist es allerdings recht einfach, wichtige Informationen für den Seitenaufruf in den Parametern der Links abzulegen. Listing 11.52 PublicProperty.java package de.wicketpraxis.web.thema.howto.seolinks; import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface PublicProperty { } Wie bei Formularen bietet sich es sich an, die relevanten Parameter einer Seite in einer JavaBean abzulegen. Zusätzlich markieren wir die Attribute, die für die Verlinkung heran- gezogen werden sollen, mit einer passenden Annotation. Listing 11.53 ConfigBean.java package de.wicketpraxis.web.thema.howto.seolinks; import java.io.Serializable; public class ConfigBean implements Serializable {264
  • 11.6 Suchmaschinenoptimierung Integer _seite; String _kategorie; @PublicProperty public Integer getSeite() { return _seite; } public void setSeite(Integer seite) { _seite = seite; } @PublicProperty public String getKategorie() { return _kategorie; } public void setKategorie(String kategorie) { _kategorie = kategorie; } public ConfigBean getClone() { ConfigBean ret=new ConfigBean(); ret._kategorie=_kategorie; ret._seite=_seite; return ret; } }Die getClone()-Methode liefert eine Kopie des Objekts zurück, sodass wir die ge-wünschten Attribute einfach modifizieren können. Jetzt müssen die Informationen aus denParametern beim Seitenaufruf nur noch im Objekt abgelegt werden. Die Informationen ausdem Objekt müssen dann in den zu erzeugenden Link einfließen. So schließt sich derKreis.Listing 11.54 BeanPagePropertyUtil.java package de.wicketpraxis.web.thema.howto.seolinks; ... public class BeanPagePropertyUtil { public static <B> PageParameters getBeanPageParameters(B bean) { return new PageParameters(getParameter(bean)); } protected static <B> List<String> getPublicProperties(B bean) { List<String> ret=new ArrayList<String>(); Method[] methods = bean.getClass().getMethods(); for (Method m : methods) { PublicProperty annotation = m.getAnnotation(PublicProperty.class); if (annotation!=null) { String name = m.getName(); if (name.startsWith("get")) ret.add(name.substring(3)); else { if (name.startsWith("is")) ret.add(name.substring(2)); 265
  • 11 Wicket in der Praxis } } } return ret; } public static <B> Map<String,Object> getParameter(B bean) { Map<String,Object> ret=new HashMap<String, Object>(); Locale locale = Session.get().getLocale(); IConverterLocator converterLocator = Application.get(). getConverterLocator(); for (String s : getPublicProperties(bean)) { PropertyModel<?> propertyModel = new PropertyModel(bean,s); IConverter converter = converterLocator.getConverter(propertyModel.getObjectClass()); Object value = propertyModel.getObject(); if (value!=null) { ret.put(s, converter.convertToString(value, locale)); } } return ret; } public static <B> void setParameter(B bean, PageParameters pageParameters) { Locale locale = Session.get().getLocale(); IConverterLocator converterLocator = Application.get(). getConverterLocator(); for (String s : getPublicProperties(bean)) { PropertyModel<Object> propertyModel = new PropertyModel<Object>(bean,s); IConverter converter = converterLocator.getConverter(propertyModel.getObjectClass()); String svalue = pageParameters.getString(s); if (svalue!=null) { propertyModel.setObject( converter.convertToObject(svalue, locale)); } } } } Entscheidend sind die zwei Methoden getParameter() und setParameter(). Das Kon- vertieren der Parameter wird hierbei mit Bordmitteln durchgeführt. Dabei muss die Anno- tation auf der getXXX- oder isXXX-Methode gesetzt sein. Listing 11.55 BeanBookmarkablePageLink.java package de.wicketpraxis.web.thema.howto.seolinks; ... public class BeanBookmarkablePageLink<T extends Page,B> extends BookmarkablePageLink<T> { public BeanBookmarkablePageLink(String id, Class<T> pageClass, B bean) { super(id, pageClass, BeanPagePropertyUtil.getBeanPageParameters(bean)); } }266
  • 11.6 SuchmaschinenoptimierungIn der SeoLinksPage-Klasse werden vier Links hinzugefügt. Der erste Link benutzt dieWerte, die aus den Seitenparametern in das Objekt übertragen wurden. Der Aufruf deszweiten Links setzt den Wert im Attribut Seite, der des dritten das Attribut Kategorie.Der Aufruf des letzten Links löschte beide Attribute.Listing 11.56 SeoLinksPage.java package de.wicketpraxis.web.thema.howto.seolinks; ... public class SeoLinksPage extends WebPage { public SeoLinksPage(PageParameters pageParameters) { ConfigBean config=new ConfigBean(); BeanPagePropertyUtil.setParameter(config, pageParameters); add(new Label("Seite",new PropertyModel<Integer>(config,"Seite"))); add(new Label("Kategorie", new PropertyModel<Integer>(config,"Kategorie"))); ConfigBean configSeite = config.getClone(); configSeite.setSeite(1); add(new BeanBookmarkablePageLink<SeoLinksPage, ConfigBean>( "linkSeite", SeoLinksPage.class,configSeite)); ConfigBean configKategorie = config.getClone(); configKategorie.setKategorie("Angebot"); add(new BeanBookmarkablePageLink<SeoLinksPage, ConfigBean>( "linkKategorie", SeoLinksPage.class,configKategorie)); ConfigBean configClearSeite = config.getClone(); configClearSeite.setSeite(null); add(new BeanBookmarkablePageLink<SeoLinksPage, ConfigBean>( "linkClearSeite", SeoLinksPage.class,configClearSeite)); ConfigBean configClearKategorie = config.getClone(); configClearKategorie.setKategorie(null); add(new BeanBookmarkablePageLink<SeoLinksPage, ConfigBean>(‚ "linkClearKategorie", SeoLinksPage.class,configClearKategorie)); } }Auf diese Weise kann ein Suchmaschinenroboter jede Kategorie und jede Seite in dieserKategorie auslesen und so in die Suchergebnisse einfließen lassen.Listing 11.57 SeoLinksPage.html <html> <head> <title>Seo Links Page</title> </head> <body> <h1>Willkommen</h1> <p>Sie befinden sich auf Seite <strong> <span wicket:id="Seite">1</span></strong> in der Kategorie <strong><span wicket:id="Kategorie"></span></strong> </p> <p> <a wicket:id="linkSeite">Seite wechseln</a><br> <a wicket:id="linkKategorie">Kategorie wechseln</a><br> <a wicket:id="linkClearSeite">Seite löschen</a><br> <a wicket:id="linkClearKategorie">Kategorie löschen</a><br> </p> </body> </html> 267
  • 11 Wicket in der Praxis Den Zustand einer Anwendung in URL-Parametern zu kodieren, ist nur eine Alternative, um Informationen, die der Nutzer über eine Suchmaschine finden sollte, zur Verfügung zu stellen. Es zeigt allerdings auch eindrucksvoll, wie man durch die geschickte Kombination von bestehenden Wicket-Funktionalitäten ausgefallenere Probleme lösen kann. 11.6.4 Servlet-Filter In der Grundeinstellung setzt der Servlet-Container Cookies, um den Nutzer für die Sessi- on zu identifizieren. Wenn der Nutzer aber ohne eine aktive Session direkt eine Seite auf- ruft, dann wird an die URLs für die Links ein jsession=xxx angehängt. Das ist auch dann der Fall, wenn der Nutzer Cookies deaktiviert hat. Da diese Gruppe mit einem Anteil von rund einem Prozent relativ klein ist, könnte man diese Nutzer durchaus ignorieren. Interes- santer wird dieses Thema im Zusammenhang mit Suchmaschinen. Einer Suchmaschine ist der Parameter nicht egal, sondern Bestandteil des Seitenaufrufs. Wenn der Suchmaschi- nenroboter die Startseite abruft, wird allen weiteren Links die Session als Parameter ange- hängt. Wenn der Abruf der Folgeseite nicht zeitnah erfolgt, wird der Roboter auf eine Feh- lerseite weitergeleitet, da die Session bereits abgelaufen ist. Das Problem liegt allerdings auch noch an anderer Stelle: Wenn der Nutzer die Seite im Suchergebnis angezeigt bekommt, ist dort eine Session-ID als Parameter kodiert. Dann kann es passieren, dass auch dieser Nutzer ebenfalls auf einer Fehlerseite landet. Außer- dem geht die Suchmaschine davon aus, dass Seiten, die unterschiedliche Parameter benut- zen, auch unterschiedliche Seiten sind. Es könnte daher passieren, dass ein und dieselbe Seite mehrfach im Suchergebnis auftaucht. Die Alternative besteht darin, das Anhängen der Session als Parameter durch den Servlet- Container abzuschalten. Dass kann man erreichen, indem man einen eigenen Filter imple- mentiert. Listing 11.58 DisableJSessionIDinUrlFilter.java package de.wicketpraxis.web.thema.howto.servletfilter; ... import javax.servlet.*; import javax.servlet.http.*; public class DisabledJSessionIDinUrlFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!(request instanceof HttpServletRequest)) { chain.doFilter(request, response); return; } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // JSessionID URL encoding ausschalten HttpServletResponse wrappedResponse = wrapResponse(httpRequest,httpResponse); chain.doFilter(request, wrappedResponse); }268
  • 11.6 Suchmaschinenoptimierung protected HttpServletResponse wrapResponse( HttpServletRequest httpRequest, HttpServletResponse httpResponse) { HttpServletResponseWrapper wrappedResponse = new HttpServletResponseWrapper(httpResponse) { @Override public String encodeRedirectUrl(final String url) { return url; } @Override public String encodeRedirectURL(final String url) { return url; } @Override public String encodeUrl(final String url) { return url; } @Override public String encodeURL(final String url) { return url; } }; return wrappedResponse; } public void init(FilterConfig filterConfig) throws ServletException { } public void destroy() { } }Dieser Filter schaltet das Setzen von JSessionID-Parametern vollständig ab. Wenn wir Än-derung nur für Suchmaschinen aktivieren wollen, leiten wir eine Klasse ab, die prüft, obder Nutzer ein Suchmaschinenroboter ist.Listing 11.59 RobotJSessionIDUrlFilter.java package de.wicketpraxis.web.thema.howto.servletfilter; ... import javax.servlet.http.*; public class RobotJSessionIDUrlFilter extends DisabledJSessionIDinUrlFilter { private static final String USER_AGENT_HEADER_NAME = "User-Agent"; private static final String[] _botAgents = { "googlebot", "slurp", }; private static final String[] _noBotAgents = { "firefox","msie","opera","netscape","safari", }; private static final Set<String> _botAgentsSet=new HashSet<String>(); private static final Set<String> _noBotAgentsSet=new HashSet<String>(); 269
  • 11 Wicket in der Praxis static { for (String s : _botAgents) { _botAgentsSet.add(s); } for (String s : _noBotAgents) { _noBotAgentsSet.add(s); } } @Override protected HttpServletResponse wrapResponse( HttpServletRequest httpRequest, HttpServletResponse httpResponse) { if (isRobot(httpRequest)) { return super.wrapResponse(httpRequest, httpResponse); } return httpResponse; } private boolean isRobot(HttpServletRequest httpRequest) { String userAgent=httpRequest.getHeader(USER_AGENT_HEADER_NAME); return isRobot(userAgent); } public static boolean isRobot(String userAgent) { if (userAgent!=null) { String userAgentLowerCase=userAgent.toLowerCase(); for (String s : _noBotAgentsSet) { if (userAgentLowerCase.indexOf(s)>-1) { return false; } } for (String s : _botAgentsSet) { if (userAgentLowerCase.indexOf(s)>-1) { return true; } } } return false; } } Zu beachten ist, dass dieser Filter vor dem Filter für die Wicket-Anwendung eingebunden werden muss. Wenn man das Setzen des Parameters immer abschalten möchte, ersetzt man die Filterklasse einfach durch die Basisklasse. Listing 11.60 web.xml ... <filter> <filter-name>de.wicketpraxis.webapp.robots</filter-name> <filter-class> de.wicketpraxis.web.thema.howto.servletfilter. RobotJSessionIDUrlFilter </filter-class> </filter> ...270
  • 11.6 Suchmaschinenoptimierung <filter-mapping> <filter-name>de.wicketpraxis.webapp.robots</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>de.wicketpraxis.webapp</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ...11.6.5 Tracking mit Google AnalyticsEs gibt verschiedene Angebote, um das Nutzerverhalten auf den eigenen Seiten analysie-ren zu können. Im folgenden Beispiel beziehe ich mich auf Google Analytics 1. Es ist kos-tenlos und dient hier als Beispiel für den Fall, dass man anderen Tracking-Code in denAnwendungsseiten integrieren muss.Listing 11.61 AbstractGoogleAnalyticsPanel.java package de.wicketpraxis.web.thema.howto.trackingcode; ... public abstract class AbstractGoogleAnalyticsPanel extends Panel { public AbstractGoogleAnalyticsPanel(String id) { super(id); add(new WebMarkupContainer("javascript") { @Override protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) { MarkupElement element=markupStream.get(); String streamAsString=element.toString(); HashMap<String, String> parameter = new HashMap<String, String>(); parameter.put("Code", getAnalyticsCode()); String virtualPath = getVirtualPath(); if (virtualPath==null) virtualPath=""; else { virtualPath="""+virtualPath+"""; } parameter.put("Path", virtualPath); MapVariableInterpolator newContent= new MapVariableInterpolator(streamAsString, parameter); replaceComponentTagBody(markupStream, openTag, newContent.toString()); } }); } @Override protected void onBeforeRender() { super.onBeforeRender(); getPage().visitChildren(ExternalLink.class, 1 http://www.google.com/analytics/ 271
  • 11 Wicket in der Praxis new IVisitor<ExternalLink>() { public Object component(ExternalLink link) { String url = link.getDefaultModelObjectAsString(); if (url.startsWith("http://")) { url=url.substring("http://".length()); link.add(new AttributeModifier("onclick",true, new Model<String>( "javascript:urchinTracker(/outbound/"+url+");"))); } return IVisitor.CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER; } }); } /** * Returns Analytics Code * @return UA-xxxx-x Code */ public abstract String getAnalyticsCode(); /** * Returns VirtualPath if any * @return */ public abstract String getVirtualPath(); } Der WebmarkupContainer sorgt dafür, dass der JavaScript-Code mit den notwendigen Parametern gefüllt und der korrekte JavaScript-Quelltext erzeugt wird. Außerdem wird in der überschriebenen onBeforeRender()-Methode jeder Link auf eine externe Seite mit einem onClick()-Aufruf versehen. Listing 11.62 AbstractGoogleAnalyticsPanel.html <wicket:panel> <script type="text/javascript"> var gaJsHost =(("https:" == document.location.protocol) ? "https://ssl." : "http://www."); document.write(unescape("%3Cscript src=" + gaJsHost + "google-analytics.com/ga.js type=text/javascript%3E%3C/script%3E")); </script> <script type="text/javascript" wicket:id="javascript"> var pageTracker = _gat._getTracker("${Code}"); pageTracker._initData(); pageTracker._trackPageview(${Path}); </script> </wicket:panel> Im Markup für die Komponente sind zwei unfreiwillige Zeilenumbrüche mit „“ markiert und gehören nicht mit zum JavaScript-Code. Innerhalb der „javascript“-Komponente werden ${Code} und ${Path} ersetzt. Listing 11.63 GoogleAnalyticsCodePage.java package de.wicketpraxis.web.thema.howto.trackingcode; ... public class GoogleAnalyticsCodePage extends WebPage { public GoogleAnalyticsCodePage() { add(new ExternalLink("hanser","http://www.hanser.de")); add(new ExternalLink("wicketpraxis", 272
  • 11.6 Suchmaschinenoptimierung "http://www.wicket-praxis.de/blog")); add(new GoogleAnalyticsPanel("google")); } static class GoogleAnalyticsPanel extends AbstractGoogleAnalyticsPanel { public GoogleAnalyticsPanel(String id) { super(id); } @Override public String getAnalyticsCode() { return "UA-1234567"; } @Override public String getVirtualPath() { return getPage().getClass().getName().replace(., /); } } }Die GoogleAnalyticsPanel-Komponente sollte innerhalb einer Basisklasse, von der alleSeiten abgeleitet werden, eingebunden werden.Listing 11.64 GoogleAnalyticsCodePage.html ... <body> <a wicket:id="hanser">Hanser Verlag</a><br> <a wicket:id="wicketpraxis">Wicket Praxis</a><br> <wicket:container wicket:id="google"></wicket:container> </body> ...Das Ergebnis unserer Mühen ist nicht weiter überraschend. Allerdings können wir jetztsehen, wie sich die Nutzer auf den Seiten bewegen.Listing 11.65 Ergebnis ... <body> <a wicket:id="hanser" href="http://www.hanser.de" onclick="javascript:urchinTracker(/outbound/www.hanser.de);"> Hanser Verlag</a><br> <a wicket:id="wicketpraxis" href="http://www.wicket-praxis.de/blog" onclick="javascript:urchinTracker(/outbound/www.wicket- praxis.de/blog);"> Wicket Praxis</a><br> <wicket:container wicket:id="google"><wicket:panel> ... <script type="text/javascript" wicket:id="javascript"> var pageTracker = _gat._getTracker("UA-1234567"); pageTracker._initData(); pageTracker._trackPageview( "de/wicketpraxis/web/thema/howto/trackingcode/GoogleAnalyticsCodePage"); </script> </wicket:panel></wicket:container> </body> ... 273
  • 11 Wicket in der Praxis11.7 Ressourcen Bisher haben wir nur einfache Grafiken und CSS-Dateien in unsere Seiten eingebunden. Doch Ressourcen können beliebige Daten darstellen. Die folgende Klasse findet nur in unseren Beispielen Verwendung, um Daten, die normalerweise dynamisch erzeugt würden, aus vorhandenen Dateien auslesen zu können. Listing 11.66 ResourceIOUtil.java package de.wicketpraxis.web.thema.howto.res; ... public class ResourceIOUtil { public static byte[] getByteArrayFrom(PackageResource resource) { try { return IOUtils.toByteArray(resource.getResourceStream(). getInputStream()); } catch (IOException e) { throw new WicketRuntimeException(e); } catch (ResourceStreamNotFoundException e) { throw new WicketRuntimeException(e); } } } 11.7.1 Dynamisch erzeugte Grafiken Mit Wicket ist es sehr einfach, Grafiken anzuzeigen, die man dynamisch erzeugt hat. Je nachdem, wie man die Grafik erzeugt, gibt es zwei Wege, wie man die Grafiken in die Sei- te integriert (Abbildung 11.6). Die Klasse BufferedImages dient uns als einfache Grafik- bibliothek, die ein Rechteck mit rotem Rand erzeugt. Listing 11.67 BufferedImages.java package de.wicketpraxis.web.thema.howto.res.images; ... public class BufferedImages { public static BufferedImage getRedBorderImage(int width,int height) { BufferedImage ret=new BufferedImage(width,height, BufferedImage.TYPE_INT_RGB); Graphics2D graphics = (Graphics2D) ret.getGraphics(); graphics.setBackground(new Color(255,255,255)); graphics.setColor(new Color(255,0,0)); graphics.clearRect(0, 0, width-1, height-1); graphics.drawRect(0, 0, width-1, height-1); return ret; } }274
  • 11.7 RessourcenDas Ergebnis des Aufrufs der Grafikbibliothek können wir über die passende Resource-Klasse einbinden. Dabei wird die Grafik in eine JPEG-Datei umgewandelt, weil wir alsParameter auch diesen Dateityp ausgewählt haben. Die zweite Grafik wird innerhalb vonrender() mit Inhalt gefüllt und als PNG-Datei ausgeliefert (Standardeinstellung).Listing 11.68 DynamicImagePage.java package de.wicketpraxis.web.thema.howto.res.images; ... public class DynamicImagePage extends WebPage { public DynamicImagePage() { BufferedDynamicImageResource imageResource= new BufferedDynamicImageResource("jpeg"); imageResource.setImage(BufferedImages.getRedBorderImage(100, 100)); RenderedDynamicImageResource renderedResource= new RenderedDynamicImageResource(200,50) { @Override protected boolean render(Graphics2D graphics) { graphics.setBackground(new Color(200,200,200)); graphics.setColor(new Color(255,255,255)); graphics.clearRect(0, 0, getWidth(), getHeight()); graphics.drawString(new Date().toString(), 2, 20); return true; } }; add(new Image("image",imageResource)); add(new Image("image2",renderedResource)); add(new ResourceLink<Resource>("imageLink",imageResource)); } }Diese Grafiken kann ich wie jede andere Ressource der Image-Komponente übergeben.Außerdem kann ich die Grafik über einen ResourceLink herunterladen. Dabei ist wichtigzu erwähnen, dass die Grafiken spätestens während der Render-Phase, also vor dem Aus-liefern der Seite, erzeugt werden. Wenn dann in einem anderen Request auf die Grafik zu-gegriffen wird, ist sie bereits erzeugt und wird ausgeliefert. Sie wird nicht erst in diesemMoment erzeugt. Dieses Vorgehen führt dazu, dass die Seite erst ausgeliefert wird, wenndie letzte Grafik erzeugt wurde. Damit die Seite ausgeliefert werden kann, obwohl die Gra-fiken noch nicht erzeugt wurden, können wir auf eine andere Alternative zurückgreifen,die wir in Abschnitt 11.7.4 behandeln werden.Listing 11.69 DynamicImagePage.html ... <body> <img wicket:id="image"/><br> <a wicket:id="imageLink">als Link</a><br><br> <img wicket:id="image2"/><br> </body> ... 275
  • 11 Wicket in der Praxis Abbildung 11.6 Dynamisch erzeugte Bilddaten 11.7.2 Automatisch generierte Thumbnails Um aus einer Grafik, die in der Datenbank abgelegt wurde, schnell ein kleineres Vorschau- bild zu erzeugen, kann man auf die Klasse ThumbnailImageResource zurückgreifen (Ab- bildung 11.7). Der zweite Parameter gibt die maximale Größe an. Listing 11.70 ImageFromDatabasePage.java package de.wicketpraxis.web.thema.howto.res.images; ... public class ImageFromDatabasePage extends WebPage { public ImageFromDatabasePage() { WebResource res=new DatabaseImageResource(); add(new Image("image",res)); add(new Image("thumbnail128",new ThumbnailImageResource(res,128))); add(new Image("thumbnail64",new ThumbnailImageResource(res,64))); } static class DatabaseImageResource extends DynamicWebResource { @Override protected ResourceState getResourceState() { return new ResourceState() { @Override public String getContentType() { return "image/gif"; } @Override public byte[] getData() { PackageResource res=PackageResource.get( ImageFromDatabasePage.class,"test.gif"); return ResourceIOUtil.getByteArrayFrom(res); } }; } } }276
  • 11.7 RessourcenDie Klasse DatabaseImageResource ist nur Stellvertreter für eine Resource-Klasse, diedie Daten aus der Datenbank liest und über getContentType() und getData() bereit-stellt.Listing 11.71 ImageFromDatabasePage.html ... <body> <img wicket:id="image"/> <img wicket:id="thumbnail128"/> <img wicket:id="thumbnail64"/> </body> ... Abbildung 11.7 Automatisch generierte Thumbnails11.7.3 Download durch FormularMan kann jede Ressource über einen ResourceLink zum Download bereitstellen.Manchmal ist es notwendig, dass man nur bei korrekter Eingabe bestimmter Daten ein Do-kument herunterladen darf. Wie man ein Dokument als Ergebnis durch Absenden einesFormulars bereitstellt, zeigt das folgende Beispiel.Listing 11.72 ResourceFromFormPage.java package de.wicketpraxis.web.thema.howto.res.forms; ... public class ResourceFromFormPage extends WebPage { public ResourceFromFormPage() { Form form = new Form("form") { @Override protected void onSubmit() { StringResourceStream output= new StringResourceStream("1,2,3","text/plain"); ResourceStreamRequestTarget target= new ResourceStreamRequestTarget(output,"data.csv"); RequestCycle.get().setRequestTarget(target); } }; add(form); } } 277
  • 11 Wicket in der Praxis In diesem Beispiel wird ein String als einfache CSV-Datei bereitgestellt (Abbildung 11.8). Auf diese Weise kann man natürlich jede beliebige Ressource ausgeben. Listing 11.73 ResourceFromFormPage.html ... <body> <form wicket:id="form"> <button>CSV erstellen</button> </form> </body> ... Abbildung 11.8 Dialog zum Speichern der CSV- Daten 11.7.4 Shared Resources Bisher haben wir alle Ressourcen direkt eingebunden. Möchten wir die Ressource an ver- schiedenen Stellen der Anwendung benutzen, können wir wie bisher die Ressource einfach direkt einbinden. Dabei wird allerdings jeweils eine eigene Instanz der Ressource erzeugt. Möchten wir aber dieselbe Instanz der Ressource verwenden, müssen wir die Ressource in unserer WebApplication-Klasse einbinden. Auf die Ressource wird dann über eine ID zugegriffen. Die so eingebundene Ressource kann dann über einen konfigurierbaren Pfad direkt angesprochen und so auch in andere Anwendungen eingebunden werden. Da der Zustand der Ressource nicht mehr an die Session des Anwenders gebunden ist, müssen alle Anpassungen über Parameter an die Ressource weitergereicht werden. Dass der Name der folgenden Klasse DynamicSharedResource lautet, unterstreicht nur den Einsatzzweck, hat aber sonst keinerlei Bewandtnis. Listing 11.74 DynamicSharedResource.java package de.wicketpraxis.web.thema.howto.res.shared; ... public class DynamicSharedResource extends DynamicWebResource {278
  • 11.7 Ressourcen @Override protected ResourceState getResourceState() { ValueMap parameters = getParameters(); final int nr = parameters.getInt("Nr",-1); return new ResourceState() { @Override public String getContentType() { return "image/gif"; } @Override public byte[] getData() { return getImage(nr); } }; } public byte[] getImage(int nr) { String image = "testUnknown.gif"; switch (nr) { case 1: image="test1.gif";break; case 2: image="test2.gif";break; } PackageResource res = PackageResource.get( DynamicSharedResource.class, image); return ResourceIOUtil.getByteArrayFrom(res); } }Die Klasse unterscheidet sich von den anderen Beispielen nur im Aufruf von getParame-ters() und greift so auf die Parameter zu, die beim Aufruf übergeben werden können.Damit müssen wir die Ressource in der WebApplication-Klasse bekannt machen.Listing 11.75 WicketPraxisApplication.java ... protected void init() { ... SharedResources sharedResources = getSharedResources(); sharedResources.add("dynamicSharedRes", new DynamicSharedResource()); mountSharedResource("dynamicSharedResPath", new ResourceReference("dynamicSharedRes").getSharedResourceKey()); ... } ...Dazu wird die Instanz unter der ID „dynamicSharedRes“ eingebunden. Die Ressourcekann dann über die ID und die Klasse Application referenziert werden. Der Aufruf vonResourceReference ohne Angabe einer Basisklasse benutzt die Application-Klasse.Um den Pfad einer so eingebundenen Ressource zu ändern, darf man allerdings nicht dieID, sondern muss den Rückgabewert von getSharedResourceKey() benutzen.Die Ressource kann jetzt über die ID referenziert und das Verhalten durch die Angabe ei-ner Parameterliste verändert werden. 279
  • 11 Wicket in der Praxis Listing 11.76 SharedResourcePage.java package de.wicketpraxis.web.thema.howto.res.shared; ... public class SharedResourcesPage extends WebPage { public SharedResourcesPage() { add(new Image("image",new ResourceReference("dynamicSharedRes"))); add(new Image("image1",new ResourceReference("dynamicSharedRes"), new ValueMap("Nr=1"))); add(new Image("image2",new ResourceReference("dynamicSharedRes"), new ValueMap("Nr=2"))); } } Listing 11.77 SharedResourcePage.html ... <body> <img wicket:id="image"><br> <img wicket:id="image1"><br> <img wicket:id="image2"><br> </body> ... Im Quelltext der Ergebnisseite kann man erkennen, dass der von uns definierte Pfad und die übergebenen Parameter für den Aufruf benutzt werden. Listing 11.78 Ergebnis.html ... <body> <img wicket:id="image" src="dynamicSharedResPath"><br> <img wicket:id="image1" src="dynamicSharedResPath/Nr/1"><br> <img wicket:id="image2" src="dynamicSharedResPath/Nr/2"><br> </body> ... 11.7.5 RSS-Feed Wir wissen jetzt, was man alles mit Ressourcen machen kann. Außerdem können wir die Ressourcen unter einer selbst definierten URL zur Verfügung stellen. Im folgenden Bei- spiel sehen wir uns an, wie man einen RSS-Feed bereitstellen würde. Auf das Erzeugen der notwendigen Daten verzichten wir in diesem Beispiel. Wir liefern daher einfach nur eine passende XML-Datei aus. Listing 11.79 RssFeedResource.java package de.wicketpraxis.web.thema.howto.res.shared; ... public class RssFeedResource extends DynamicWebResource { @Override protected ResourceState getResourceState() { return new ResourceState() { @Override public String getContentType() {280
  • 11.7 Ressourcen return "text/xml"; } @Override public byte[] getData() { PackageResource res = PackageResource.get( DynamicSharedResource.class, "feed.xml"); return ResourceIOUtil.getByteArrayFrom(res); } }; } }Listing 11.80 feed.xml <?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:wicket="http://wicket.apache.org"> <author> <name>Michael Mosmann</name> </author> <title>Titel des Weblogs</title> <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> <updated>2003-12-14T10:20:09Z</updated> <entry> <title>Titel des Weblog-Eintrags</title> <link href="http://example.org/2003/12/13/atom-beispiel"/> <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> <updated>2003-12-13T18:30:02Z</updated> <summary type="html">Zusammenfassung des Weblog-Eintrags</summary> <content type="html">Volltext des Weblog-Eintrags</content> </entry> </feed>Die XML-Datei ist ein Ausschnitt aus einem RSS-Feed, der für dieses Beispiel bereinigtwurde.Listing 11.81 RssFeedPage.java package de.wicketpraxis.web.thema.howto.res.shared; ... public class RssFeedPage extends WebPage { public RssFeedPage() { add(new ResourceLink<Resource>("rss", new ResourceReference("rssFeed"))); } }Wie man sieht, benutzen wir einfach einen ResourceLink, den wir in anderen Beispielenbenutzt haben, um eine Ressource zum Download anbieten zu können. Allerdings kann dieKomponente an verschiedene Tags gebunden werden. Damit die Ressource im Kopfbe-reich der Seite landet, kann man wie in diesem Beispiel die Komponente einfach an dieserStelle positionieren oder, wenn man den Link in einer Komponente hinzufügen möchte,die Komponente innerhalb von <wicket:head></wicket:head> referenzieren.Listing 11.82 RssFeedPage.html <html> <head> <title>SharedResource Page</title> 281
  • 11 Wicket in der Praxis <link rel="alternate" type="application/rss+xml" title="RSS Feed der Seite" wicket:id="rss" /> </head> <body> <p>der RSS-Link ist im Header</p> </body> </html> Jetzt müssen wir die Ressource noch unter der ID, die wir in der Seitenklasse benutzt ha- ben, zur Verfügung stellen. Dabei binden wir die Ressource an den Pfad „rss“. Listing 11.83 WicketPraxisApplication.java ... protected void init() { ... SharedResources sharedResources = getSharedResources(); sharedResources.add("rssFeed", new RssFeedResource()); mountSharedResource("rss", new ResourceReference( "rssFeed").getSharedResourceKey()); ... } ... Jetzt hat die Seite einen RSS-Feed, der z.B. mit Firefox abonniert werden kann (Abbildung 11.9). Abbildung 11.9 Firefox-Dialog zum Abbonieren eines RSS-Feeds11.8 Links auf Seiten und Ressourcen Bisher haben wir davon profitiert, dass alle URLs von Wicket verwaltet werden. Wenn man die URL einer Seite oder Ressource aber z.B. in einer E-Mail verschicken möchte, sollte man davon absehen, die URL einfach fest einzubinden. Mit Wicket ist es möglich, die URL einer Seite (nicht nur der gerade aufgerufenen) oder einer Ressource zur Laufzeit zu ermitteln. Im folgenden Beispiel ermitteln wir die URL zu einer Seite und einer Res- source und zeigen sie an. Listing 11.84 LinksPage.java package de.wicketpraxis.web.thema.howto.links; ... public class LinksPage extends WebPage282
  • 11.8 Links auf Seiten und Ressourcen { public LinksPage() { String resourceUrl = urlFor(new ResourceReference( LinksPage.class,"resource.txt")).toString(); String pageUrl = urlFor(LinksPage.class, new PageParameters("P=1")).toString(); add(new Label("resourceUrl",resourceUrl)); add(new Label("pageUrl",pageUrl)); add(new Label("absResourceUrl", RequestUtils.toAbsolutePath(resourceUrl))); add(new Label("absPageUrl",RequestUtils.toAbsolutePath(pageUrl))); } }Listing 11.85 LinksPage.html <html> <head> <title>Links Page</title> </head> <body> <span wicket:id="resourceUrl"></span><br> <span wicket:id="pageUrl"></span><br> <span wicket:id="absResourceUrl"></span><br> <span wicket:id="absPageUrl"></span><br> </body> </html>Die URL auf eine Seite, die man außerhalb einer Wicket-Anwendung benutzt, sollte manimmer an einen selbst definierten Pfad binden, sodass eine Anpassung der Seite nicht dazuführt, dass Seitenaufrufe auf eine veraltete URL ins Leere laufen.Listing 11.86 WicketPraxisApplication.java ... protected void init() { ... mountSharedResource("linksPageResource", new ResourceReference( LinksPage.class,"resource.txt").getSharedResourceKey()); mountBookmarkablePage("linksPage", LinksPage.class); ... } ...Auf der Ergebnisseite werden jetzt die Links in relativer und absoluter Schreibweise ange-zeigt. Dabei ist zu beachten, dass der Servername sich aus dem Seitenaufruf ergibt. Wirddie Anwendung auf einem Server mit dem Namen „wicket-praxis.de“ aufgerufen, wird„localhost:8080“ entsprechend ersetzt.Listing 11.87 Ergebnis linksPageResource linksPage/P/1 http://localhost:8080/de.wicketpraxis--webapp/linksPageResource http://localhost:8080/de.wicketpraxis--webapp/linksPage/P/1 283
  • 11 Wicket in der Praxis11.9 Optimierungen Wicket bietet sehr viele Einstellmöglichkeiten, wobei ich die wichtigsten erwähnen möch- te. Die Funktion ergibt sich meistens aus dem Namen des Funktionsaufrufes. 11.9.1 Applikation Wicket hat drei verschiedene Fehlerseiten. Die interessanteste ist die Seite, die angezeigt wird, wenn die Sitzung des Nutzers abgelaufen ist. Außerdem kann man die maximale Dateigröße für Datei-Uploads setzen. Die Anpassung kann in jedem Formular überschrie- ben werden. Listing 11.88 Application.java ... protected void init() { ... IApplicationSettings applicationSettings = getApplicationSettings(); applicationSettings.setAccessDeniedPage(Page.class); applicationSettings.setInternalErrorPage(Page.class); applicationSettings.setPageExpiredErrorPage(Page.class); applicationSettings.setDefaultMaximumUploadSize(Bytes.megabytes(12)); String configurationType = getConfigurationType(); if (configurationType.equals(DEVELOPMENT)) { } if (configurationType.equals(DEPLOYMENT)) { } ... } ... Um zwischen Test- und Produktivbetrieb (DEVELOPMENT, DEPLOYMENT) unterscheiden zu können, kann man den Rückgabewert von getConfigurationType() prüfen. Wenn nichts anderes konfiguriert wurde, läuft Wicket im Development-Modus. 11.9.2 Konverter Wenn man applikationsweit eigene Converter definieren möchte, sollte man die Methode getConverterLocator() überschreiben und einen eigenen ConverterLocator imple- mentieren. Für alle nicht neu zu setzenden Converter greift man einfach auf den Wicket- eigenen ConverterLocator zurück. 11.9.3 Debug Die Fehlersuche gestaltet sich mit Wicket bereits besonders angenehm. Doch auch in die- sem Bereich glänzt Wicket durch innovative Möglichkeiten.284
  • 11.9 OptimierungenListing 11.89 Application.java ... protected void init() { ... IDebugSettings debugSettings = getDebugSettings(); debugSettings.setAjaxDebugModeEnabled(true); debugSettings.setComponentUseCheck(true); debugSettings.setDevelopmentUtilitiesEnabled(true); debugSettings.setLinePreciseReportingOnAddComponentEnabled(false); debugSettings.setLinePreciseReportingOnNewComponentEnabled(false); debugSettings.setOutputComponentPath(false); debugSettings.setOutputMarkupContainerClassName(false); ... IRequestLoggerSettings requestLoggerSettings = getRequestLoggerSettings(); requestLoggerSettings.setRequestLoggerEnabled(false); ... } ...Die Aufrufe entsprechen den Standardeinstellungen im Development-Modus. Man solltekeine der Einstellungen im Produktivbetrieb aktivieren. Die zeilengenaue Fehlermeldungsollte man nur in Notfällen aktivieren, da diese Einstellung erheblichen Einfluss auf dieGeschwindigkeit der Anwendung hat. Auf diese Funktion musste bisher ich noch nicht zu-rückgreifen, sodass ich vorerst von der Verwendung abraten würde.Wesentlich hilfreicher sind da die beiden Attribute OutputComponentPath und Output-MarkupContainerClassName. Das Aktivieren des ersten Attributs führt dazu, dass derPfad der Wicket-Komponente im Attribut wicket:path ausgegeben wird. Das Setzen vonOutputMarkupContainerClassName veranlasst Wicket, im Quelltext der Seite den Klas-sennamen der Komponente vor und nach der Komponente als Kommentar einzubetten.Auf diese Weise kann man schnell erkennen, in welcher Komponente das fehlerhafte Ele-ment eingebunden ist.Das Aktivieren des RequestLoggers führt zu einer detaillierteren Ausgabe des aktuellenAufrufs, was unter Umständen für die Fehlersuche nützlich sein kann. Auch von dieserMöglichkeit musste ich bisher keinen Gebrauch machen.Listing 11.90 RequestLogger-Ausgabe 21.05.2009 08:44:21 org.apache.wicket.protocol.http.RequestLogger log INFO: time=162, event=BookmarkablePage[de.wicketpraxis.web.thema.howto.KapHowto()], response=BookmarkablePage[de.wicketpraxis.web.thema.howto.KapHowto()], sessionid=null,sessionsize=1794,activerequests=0,maxmem=265M,total=25M,used=18M11.9.4 RessourceJe nach eingesetzter Infrastruktur kann es notwendig sein, Anpassungen an den Einstellun-gen rund um das Thema Ressource vorzunehmen. 285
  • 11 Wicket in der Praxis Listing 11.91 Application.java ... protected void init() { ... IResourceSettings resourceSettings = getResourceSettings(); resourceSettings.setAddLastModifiedTimeToResourceReferenceUrl(false); resourceSettings.setDisableGZipCompression(false); // kann evtl. zu Problemen führen, wenn die js-lib nicht sauber ist.. resourceSettings.setJavascriptCompressor( new DefaultJavascriptCompressor()); ... } ... Der Internet Explorer ist bekannt dafür, dass er Ressourcen aggressiv puffert. Das bedeu- tet, dass veränderte CSS-Dateien, Bilddaten und andere Ressourcen nicht korrekt aktuali- siert werden. Durch das Setzen von AddLastModifiedTimeToResourceReferenceUrl veranlasst man Wicket, als Parameter das Modifikationsdatum der Datei an die URL der Ressource anzuhängen. Damit ändert sich bei jeder Anpassung der Ressource auch die URL. Wicket liefert Ressourcen komprimiert aus. Wenn man auch die Webseiten komprimiert ausliefern möchte, muss man auf einen Kompressionsfilter zurückgreifen. Damit die Res- sourcen dann nicht doppelt komprimiert werden (weil es neben dem zweifelhaften Nutzen auch zwangsläufig zu Fehlern kommt), muss man die Wicket-eigene Komprimierung de- aktivieren. Wicket komprimiert außerdem JavaScript-Quelltext. Das kann unter Umständen zu Feh- lern führen, wenn der JavaScript-Code nicht korrekt von Leerzeichen und anderen über- flüssigen Inhalten befreit wurde. Wenn man solche Fehler feststellt, kann eine Änderung des Kompressors Abhilfe schaffen. Zusammenfassung In diesem Kapitel haben wir Seiten mit Navigation versehen, fremde CSS-Dateien einge- bunden und die Seitenstruktur entsprechend angepasst. Außerdem haben wir die Anwen- dung suchmaschinentauglich gemacht und JavaScript-Code eingebunden, der eine Nutzer- nachverfolgung zur Seitenoptimierung ermöglicht. Das Erzeugen von Grafiken, Dokumen- ten oder RSS-Feeds sollte nach diesem Kapitel ebenfalls keine Herausforderung darstellen, sodass wir spätestens jetzt in der Lage sind, unsere eigene Webanwendung zu entwickeln.286
  • 12 12 Fehlersuche Normalerweise gestaltet sich die Fehlersuche in Wicket-Anwendungen relativ einfach. Wenn der Code kompiliert werden konnte, ist man meistens schon fast am Ziel. Wie man die letzten Klippen umschifft und bei Fehlern schnell auf deren Ursache kommt, ist Ziel des folgenden Kapitels.12.1 Häufige Fehlerquellen Wicket bietet wenig Spielraum für schwer zu lokalisierende Fehler. Das bedeutet aber nicht, dass man nicht auch mit Wicket Fehler machen kann. Die folgenden Fehlerquellen sollte man Hinterkopf haben, damit die Suche kurz bleibt. 12.1.1 Komponenten fehlen Die häufigsten Fehlerquellen sind Abweichungen zwischen dem Komponentenbaum und den Referenzen im Markup. Das erklärt sich recht einfach: Wenn der Code kompiliert wer- den konnte, konnte man eine große Fehlerquelle erfolgreich ausschließen. Jetzt kann es aber vorkommen, dass man vergessen hat, eine Komponente im Markup zu referenzieren. Oder man hat eine Komponente im Markup referenziert, die es an der Stelle nicht gibt. Für beide Fälle gibt Wicket passende Fehlermeldungen aus, die man sich genau durchlesen sollte. Wenn Wicket behauptet, dass man eine Komponente referenziert, die im Kompo- nentenbaum nicht vorkommt, ist es auch nicht unwahrscheinlich, dass man die Komponen- te zwar erstellt, aber nicht mit add() hinzugefügt hat. 12.1.2 Komponente ist bereits vorhanden Es gibt verschiedene Situationen, die dazu führen können, dass Wicket der Meinung ist, dass es eine Komponente mit dieser ID schon gibt. Dazu muss man Folgendes beachten: Während man mit add() Komponenten hinzufügt, wird schon geprüft, ob es in der aktuel- 287
  • 12 Fehlersuche len Hierarchieebene bereits eine Komponente mit der gleichen ID gibt. Es ist zu diesem Zeitpunkt irrelevant, ob diese Komponente im Markup überhaupt referenziert wird. Im einfachsten Fall hat man wirklich eine Komponente mit der ID bereits hinzugefügt. Spannender wird es, wenn man eine Komponente hinzufügt und sich sicher ist, dass es diese ID noch nicht gibt. Das kann besonders bei Border-Komponenten auftreten, da sich die internen Komponenten auf derselben Hierarchieebene befinden. Der komplizierteste Fall ist gleichzeitig der häufigste. Wenn man mal das Muster erkannt hat, dann findet man auch diesen Fehler recht schnell. In einer ListView fügt man die Komponente fälschlicherweise nicht zum ListItem-, sondern zur ListView-Komponente hinzu (statt item.add() schreibt man nur add()). Das bedeutet, dass bei einer Listenlänge von mindestens zwei (was häufig der Fall ist) Wicket die Fehlermeldung ausgibt, dass es eine Komponente mit dieser ID bereits gibt. Man sollte der Versuchung widerstehen und prüfen, ob man eine ID doppelt vergeben hat, sondern dazu übergehen, in der entsprechen- den Methode zu prüfen, ob man die Komponenten an das ListItem und nicht an die ListView gehängt hat. 12.1.3 Ajax funktioniert nicht Dass Ajax nicht funktioniert, liegt am häufigsten daran, dass es kein geeignetes HTML- Tag gibt, an das die per Ajax zu aktualisierende Komponente gebunden ist. Die Verwen- dung von wicket:container anstelle eines div-Tags kann dazu führen, dass der Java- Script-Aufruf zum Tauschen des Inhalts kein passendes HTML-Tag finden kann. In den meisten anderen Fällen gibt Wicket entsprechende Fehlermeldungen aus.12.2 Unit-Tests Wenn man auf so einem bequemen Ruhekissen ruht, fällt es schwer, sich in dieser Ruhe stören zu lassen. Im Laufe der Zeit sinkt die Fehlerquote schnell ab, man konzentriert sich auf die Programmierung und startet die Anwendung nur, um der Anwendung optisch den letzten Schliff zu verpassen. Je nachdem, welche Anforderungen an eine Anwendung gestellt werden, kann es aber un- ter Umständen sinnvoll sein, für jede Komponente und für jede Seite Unit-Tests zu schrei- ben. Wie man in den folgenden Beispielen sehen kann, ist das Testen von Komponenten und Seiten so einfach, dass man oft schneller ist, wenn man für die Fehlersuche eine Kom- ponente einem Unit-Test unterzieht. Für unser Beispiel erstellen wir eine Komponente mit einem AjaxFallbackLink. Listing 12.1 CustomPanel.java package de.wicketpraxis.web.thema.debug.unittests; ... public class CustomPanel extends Panel {288
  • 12.2 Unit-Tests public CustomPanel(String id) { super(id); final FeedbackPanel feedbackPanel = new FeedbackPanel("feedback"); feedbackPanel.setOutputMarkupId(true); add(feedbackPanel); add(new AjaxFallbackLink("link") { @Override public void onClick(AjaxRequestTarget target) { if (target!=null) { info("Link per Ajax"); target.addComponent(feedbackPanel); } else { info("Link ohne Ajax"); } } }); } }Listing 12.2 CustomPanel.html <wicket:panel> <div wicket:id="feedback"></div> <a wicket:id="link">Klick mich</a> </wicket:panel>Diese Komponente binden wir auf einer neuen Seite ein.Listing 12.3 UnitTestPage.java package de.wicketpraxis.web.thema.debug.unittests; import org.apache.wicket.markup.html.WebPage; public class UnitTestPage extends WebPage { public UnitTestPage() { add(new CustomPanel("mypanel")); } }Listing 12.4 UnitTestPage.html ... <body> <wicket:container wicket:id="mypanel"></wicket:container> </body> ...Jetzt testen wir die Komponente auf der Seite und die Komponente unabhängig von derSeite. Dazu müssen wir die folgenden Testklassen unter src/test/java ablegen.Listing 12.5 TestCustomPanel.java package de.wicketpraxis.web.thema.debug.unittests; ... 289
  • 12 Fehlersuche public class TestCustomPanel { public void testPage() { WicketTester tester=new WicketTester(); tester.startPage(UnitTestPage.class); tester.clickLink("mypanel:link",false); tester.assertInfoMessages(new String[]{"Link ohne Ajax"}); } public void testPanel() { WicketTester tester=new WicketTester(); tester.startPanel(new TestPanelSource() { public Panel getTestPanel(String panelId) { return new CustomPanel(panelId); } }); tester.clickLink("panel:link"); tester.assertComponentOnAjaxResponse("panel:feedback"); tester.assertInfoMessages(new String[]{"Link per Ajax"}); } } Der erste Test testet die Komponente auf der Seite. Dazu wird der WicketTester mit der Seite gestartet und dann der Link mit dem Pfad „mypanel:link“ angeklickt. Dabei ist die ID „mypanel“ die ID der Komponente und „link“ die ID des Links. Dann kann man prü- fen, ob z.B. eine passende Message erzeugt wurde. Der zweite Test bettet die Komponente in einer Testkomponente ein, die ID unserer Kom- ponente wird von Wicket erzeugt und lautet „panel“. Wir aktivieren wieder den Link, allerdings dieses Mal per Ajax, sodass der Nachrichtentext ein anderer sein sollte. Man sieht, dass Unit-Tests mit Wicket sehr einfach zu realisieren sind. Welche Möglich- keiten die WicketTester-Klasse bietet, kann an dieser Stelle leider nicht behandelt wer- den, sollte sich aber durch die sprechenden Methodennamen schnell erschließen lassen. Markup Wie in Abschnitt 11.9.3 beschrieben, kann Wicket die Klasse der Komponente, die den entsprechenden Teil der Seite darstellt, im Kommentar der Seite ausgeben lassen. Auf die- se Weise findet man gerade in komplizierten Anwendungen schnell die Komponente, die geändert werden muss. Listing 12.6 DebugMarkupPage.java package de.wicketpraxis.web.thema.debug.markup; ... public class DebugMarkupPage extends WebPage { public DebugMarkupPage() { add(new EmptyPanel("empty")); add(new CustomPanel("custom")); } }290
  • 12.2 Unit-TestsListing 12.7 DebugMarkupPage.html ... <body> <div wicket:id="empty"></div> <div wicket:id="custom"></div> </body> ...In der Ergebnisseite sehen wir die Klassen aller Komponenten.Listing 12.8 Ergebnis.html <html> ... <body> <div wicket:id="empty"><wicket:panel><!-- MARKUP FOR org.apache.wicket.markup.html.panel.EmptyPanel BEGIN --> <!-- MARKUP FOR org.apache.wicket.markup.html.panel.EmptyPanel END -- ></wicket:panel></div> <div wicket:id="custom"><wicket:panel><!-- MARKUP FOR de.wicketpraxis.web.thema.debug.unittests.CustomPanel BEGIN --> <div wicket:id="feedback" id="feedback1"><wicket:panel><!-- MARKUP FOR org.apache.wicket.markup.html.panel.FeedbackPanel BEGIN --> <!-- MARKUP FOR org.apache.wicket.markup.html.panel.FeedbackPanel END -- ></wicket:panel></div> <a wicket:id="link" id="link2" ... >Klick mich</a> <!-- MARKUP FOR de.wicketpraxis.web.thema.debug.unittests.CustomPanel END -- ></wicket:panel></div> </body> </html><!-- Page Class de.wicketpraxis.web.thema.debug.markup.DebugMarkupPage -->ZusammenfassungDie Fehlersuche gestaltet sich mit dieser Vielzahl von Hilfen besonders einfach. Dabeiwird man feststellen, dass man sich auf einige wenige Möglichkeiten konzentriert, die zumpersönlichen Arbeitsstil passen. Man sollte dabei trotzdem nicht vergessen, dass manKomponenten auch unabhängig von einer konkreten Anwendung entwickeln kann. Aufdiese Weise kann man die möglichen Fehlerquellen um Größenordnungen reduzieren. 291
  • 1313 Anfang oder Ende?Wenn Sie es bis hierher geschafft haben, haben Sie in den zurückliegenden Kapiteln allewichtigen Aspekte, die bei der Entwicklung einer Webanwendung mit Wicket eine Rollespielen, kennen gelernt. Wenn Sie dann Ihre erste eigene Anwendung umsetzen, werdenSie feststellen, dass das Buch nur ein kleiner erster Schritt gewesen ist. Dann werden Sievielleicht feststellen, dass das Buch nur der Anfang einer spannenden Entwicklung war.Quellen, die mir während meiner Arbeit mit Wicket geholfen haben, finden Sie am bestenüber meine Einträge zu Wicket unter Delicious: http://delicious.com/michael.mosmann/wicket.Damit Sie eine Vorstellung davon bekommen, welche praktischen Erfahrungen ich mitWicket sammeln konnte, möchte ich an dieser Stelle zwei Anwendungen hervorheben, anderen Entwicklung ich beteiligt war. Das erste Beispiel (http://dynamisch.vergleich.de/ver-gleich/termingeld/vergleich) ist ein Teil einer Anwendung, die mit Wicket realisiert wurde.Dabei bestand die besondere Herausforderung darin, die Anwendung in ein CMS (in die-sem Fall Typo3) zu integrieren. Das zweite Beispiel (http://www.green-radish.de) ist einOnline-Shop, der vollständig mit Wicket realisiert wurde.Ich möchte mich an dieser Stelle für die Aufmerksamkeit bedanken. Ich wünsche Ihnenviel Freude mit Wicket und würde mich über jede Rückmeldung zum Buch oder andereninteressanten Themen freuen. Schreiben Sie mir einfach eine E-Mail an michael@mosmann.de. 293
  • RegisterA EAjax 102 Eclipse AjaxRequestTarget 102, 140 Installation 7 Event 103 Projektdateien 27 setOutputMarkupId() 103 setOutputMarkupPlaceholderTag() 106 FAnwendungslogik 8, 10 Feedback 85, 170, 214Anwendungsschicht 8, 41 ComponentFeedbackMessageFilter 215Application 84 siehe auch WebApplication ContainerFeedbackMessageFilter 214Applikationsschicht 24 CustomFeedbackPanel 251 FeedbackMessage 85B FeedbackPanel 170Behavior 144 FormComponentCssFeedbackBorder 218 Attribute anpassen 145 FormComponentFeedbackBorder 216 onComponentTag() 144 FormComponentFeedbackIndicator 217Border 120 MessageFilter 214 Formular 169C Ajax 205Component 51, 81 siehe auch Komponente AjaxButton 175ComponentBorder 125 AjaxComponentUpdatingBehavior 207D AjaxFallbackButton 174Datenbank AjaxFormSubmitBehavior 205 Konfiguration 20, 30, 37 AjaxFormValidatingBehavior 207 Produktivdatenbank 20, 30 AjaxSubmitLink 175 Testdatenbank 22 AutoCompleteTextField 210Datenhaltung 8 siehe auch Persistenz Button 173Dependency Injection 8 CheckBox 182 siehe auch Spring Framework CheckBoxMultipleChoice 183 CheckGroup 183 CheckGroupSelector 184 295
  • Register ChoiceRenderer 190 K DropDownChoice 188 Komponente 51, 53 FileUpload 192 AjaxEditableLabel 212 FormComponentLabel 182 AjaxFallbackConfirmLink 254 GET 175 ButtonLink 250 Label 181 callOnBeforeRenderIfNotVisible() 83, 101, 124 ListChoice 189 detach() 83 ListMultipleChoice 190, 191 getPage() 84 MultiFileUpload 194 getParent() 81 OnChangeBehavior 209 getPath() 84 onSubmit() 172 ID 81 Palette 191 isVisible() 99 POST 175 IVisitor 82 RadioButton 185 Komponentenbaum 81 RadioChoice 185 Modell 84 RadioGroup 185 onBeforeRender() 52, 83, 101, 124 Select 186 onDetach() 52, 83 SubmitLink 173 Sichtbarkeit 99 TextField 176 Vererbung 85 siehe auch Vererbung UploadProgressBar 193 Wizard 256 UploadWebRequest 193 Konventionen 1 Validatoren 195 siehe auch Validator Konverter 55, 129, 284 Fragment 119 IConverter-Interface 56 IConverterLocator 56 G Google Analytics 271 L Label 127 H MultiLineLabel 127 Hibernate 9 Link 137 HQL 10 AbstractLink 137 Schemagenerierung 32 Ajax 138 AjaxFallbackLink 139 I AjaxLink 140 InjectorHolder siehe auch Spring Framework AjaxSubmitLink 144 SpringBean 237 BookmarkablePageLink 137, 260 Installation 7 ExternalLink 141 J IndicatingAjaxFallbackLink 139 Java, Installation 7 IndicatingAjaxLink 140 JavaBean 2, 181 onClick() 138 Jetty 24 Popup 141 Maven-Plugin 26 ResourceLink 143 starten 46 SubmitLink 144 Tricks 140296
  • RegisterListe 149 Modus ColumnListView 154 Deployment 47 DataGridView 159 Development 47 DataProvider 156, 164 MVC 55 siehe auch Model-View-Controller DataTable 161 P DataView 156 Page 50, 84 DefaultDataTable 162 als XML 130 GridView 158 BookmarkablePage 258 ListView 152, 184 CSS 116 PropertyListView 153 JavaScript 116 RefreshingView 150 PageParameter 110 RepeatingView 149 RedirectPage 112Locale 94 URL 115 WebPage 109M PageMap 50Markup 3, 5, 53 PageStore 51, 61 getVariation() 94 Panel 117 Variationen 94 ParentPom 16, 26Maven Persistenz 9, 22, 33 Abhängigkeiten 15 @Transactional 35 artifactId 15 AbstractDao 34, 36 groupId 15 AbstractDaoList 163 Installation 7 DaoInterface 33 POM 11, 17 DoInterface 33 Projektverwaltung 11 Persistenz-Tests 13, 38 version 15, 16 Präsentation 10Modell 53, 55 Präsentationsschicht 8, 41 CompoundPropertyModel 74 Property DaoModel 67 Datei 98 detach() 55, 61 XML-Format 99 IModel-Interface 55 Zeichenkodierung 99 Kaskadierung 62, 65 LoadableDetachableModel 62 Q Model.of() 60 Quickstart 4, 12 modelChanged() 58 R modelChanging() 58 Request PropertyModel 72 Behandlung 51 ResourceModel 76 RequestCycle 51 Serialisierung 61 Resource 98, 274, 285 StringResourceModel 78 BufferedDynamicImageResource 275 verändern 58 Download 277Model-View-Controller 10 RenderedDynamicImageResource 275 ResourceLink 277 297
  • Register Shared Resources 278 V URL 282 Validator Verzeichnis 95 AbstractValidator 200 WebResource 276 EmailAddressValidator 197 ResourceReference 116, 134 EqualPasswordInputValidator 202 RSS-Feed 280 FormValidator 201 getDependentFormComponents() 204 S MaximumValidator 197 Schema-Update 21, 31, 40 MinimumValidator 197 Security 225 siehe auch Sicherheit Palindrom-Validator 199 SEO 264 siehe auch Suchmaschinenoptimierung RangeValidator 197 Session 50, 84 StringValidator 196 DisableJSessionIDinUrlFilter 268 UrlValidator 199 eigene Session-Klasse 225 Validatable 200 get() 84 validate() 204 RobotJSessionIDUrlFilter 269 Variation 94 setReuseItems() siehe auch Liste VCS 8 siehe auch Versionskontrolle ListView 184 Vererbung Sicherheit 5 einfach 85 Anmeldeseite 229 für Fortgeschrittene 91 geschützte Komponente 231 Markup 87 geschützte Seite 229 mit eigenem Markup 87 IAuthorizationStrategy 231 ohne eigenes Markup 86 ProtectedPage 229 Versionskontrolle 7 SimplePageAuthorizationStrategy 226 Spring Framework W Konfiguration 46 war 24 siehe auch Webarchiv SpringBean 235 WebApp 24, 42 Spring 26, 29 siehe auch Spring Framework WebApplication 50 Spring-Framework 10 Webarchiv 26 Konfiguration 29 WebMarkupContainer 126 Style 94 wicket:border 124 Subversion 7 wicket:child 86, 88 Installation 8 wicket:container 89 Suchmaschinenoptimierung 258 wicket:enclosure 100 BeanBookmarkablePageLink 266 wicket:extend 88 wicket:head 86 T wicket:message 131 Tomcat 24 wicket:panel 86 U Unit-Tests 12, 288 UserDao siehe auch Persistenz AbstractDao 36298
  • Im Kopf fängt alles an. Hunt Pragmatisches Denken und Lernen 265 Seiten. ISBN 978-3-446-41643-7 Andy Hunt führt Sie in diesem Buch durch Lern- und Verhaltenstheorie und durch Erkenntnisse der kognitiven und Neurowissenschaften. Sie werden überraschende Aspekte darüber kennen lernen, wie Ihr Gehirn arbeitet und wie Sie Vorteile daraus ziehen können, indem Sie Ihre eigenen Denk- und Lernfähigkeiten stärken. Sie werden sehen, wie Sie Ihre Wetware refaktorie- ren können – also Ihr Gehirn umgestalten und neu verdrahten –, damit Sie Ihre Arbeit effektiver und kreativer angehen können. Software entsteht in unseren Köpfen, nicht in einem Editor, einer IDE oder einem Design-Tool. Deshalb wird es Zeit, pragmatischer an das Denken und Lernen heranzugehen. Egal ob Programmierer, Manager, Wissensarbeiter, Technikfreak oder analytischer Denker, dieses Buch wird Ihnen dabei helfen.Mehr Informationen zu diesem Buch und zu unserem Programmunter www.hanser.de/computer
  • Konzentrieren Sie sich auf das Wesentliche! Hunt/Thomas Der Pragmatische Programmierer 331 Seiten. ISBN 978-3-446-22309-7 Der Pragmatische Programmierer veranschaulicht zahlreiche Best Practices der Softwareentwicklung mit Hilfe von Anekdoten, Beispielen und interessanten Analogien. Wer dieses Buch liest, lernt, · die Anwender zu begeistern, · die echten Anforderungen zu finden, · gegen Redundanz anzugehen, · dynamischen und anpassbaren Quelltext zu schreiben, · effektiv zu testen, · Teams von Pragmatischen Programmierern zu bilden und · durch Automatisierung sorgfältiger zu entwickeln.Mehr Informationen zu diesem Buch und zu unserem Programmunter www.hanser.de/computer
  • Mehr Transparenz = mehr Erfolg Aden Google Analytics Implementieren. Interpretieren. Profitieren. 357 Seiten ISBN 978-3-446-41905-6 Sicher haben Sie von Google Analytics schon gehört oder nutzen es bereits. Aber kennen Sie auch alle Feinheiten, Tipps und Tricks? Haben Sie das Tool wirklich perfekt implementiert und Ihren individuellen Bedürfnissen angepasst? Nutzen Sie es vollständig aus und leiten kon- krete Aktionen aus den vorhandenen Zahlen ab? Timo Aden, ehemaliger Google-Mitarbeiter und Web-Analyse-Experte, stellt in diesem Praxisbuch die vielfältigen Funktionen dieses Tools umfassend vor. Von nützlichen Hinweisen und technischen Kniffen bei der Implementierung und dem Tracking sämtlicher Online-Marketing- Aktivitäten, über die effektive Anwendung der Benutzeroberfläche und Berichte bis hin zur Ableitung von konkreten Aktionen – dieser Praxis- leitfaden deckt sämtliche Bereiche von Google Analytics ab. Ein täglicher Begleiter für alle Aktivitäten im Online-Marketing!Mehr Informationen zu diesem Buch und zu unserem Programmunter www.hanser.de/computer
  • Android auf dem Vormarsch Mosemann/Kose Android Anwendungen für das Handy-Betriebs- system erfolgreich programmieren 384 Seiten ISBN 978-3-446-41728-1 Android, der neue Star unter den mobilen Plattformen, wälzt unter der Führung von Google den Markt der mobilen Applikationen um. Dieses Praxisbuch zeigt Ihnen, wie Sie schnell qualitativ hochwertige Android- Applikationen entwickeln und sie erfolgreich vermarkten. Viele praxis- nahe Beispiele und Tipps helfen Ihnen, das Gelernte sofort auszuprobieren. Auf der Webseite zum Buch können Sie mit Hilfe von Lernvideos und weiteren Beispielen Ihr Wissen vertiefen.Mehr Informationen zu diesem Buch und zu unserem Programmunter www.hanser.de/computer
  • Auf die Plätze, fertig, Scrum! Gloger Scrum Produkte zuverlässig und schnell entwickeln 2., aktualisierte Auflage 334 Seiten ISBN 978-3-446-41913-1 In diesem Buch erfahren Sie, wie Sie Scrum erfolgreich einsetzen. Sie lernen die Regeln, Strukturen und Rollen von Scrum kennen. Boris Gloger – er war der erste zertifizierte ScrumTrainer in Europa – beschreibt, wie Teams durch weitgehende Selbstorganisation und durch kontinuierliche Planung Produkte erfolgreich liefern. Er zeigt auch, wie Scrum in großen Projekten mit mehreren Teams, die über viele Standorte verteilt sind, funktioniert. Zudem ist dieses Praxisbuch eine hervorragende Unterstützung für die Zertifizierung zum ScrumMaster. Egal ob Sie als Kunde, Führungskraft, ScrumMaster, Product Owner oder Teammitglied an einem Scrum-Projekt beteiligt sind oder aber erst wis- sen wollen, was Scrum eigentlich ist: Hier erhalten Sie einen umfassen- den Überblick und wertvolle Tipps, wie Sie Scrum einführen und leben können.Mehr Informationen zu diesem Buch und zu unserem Programmunter www.hanser.de/computer
  • Frischer Wind für Java. Walls Spring im Einsatz 676 Seiten. ISBN 978-3-446-41240-8 Spring ist ein frischer Wind in der Java-Landschaft. Dieses Framework für Java EE verbindet die Macht von Enterprise Applikationen mit der Einfachheit von einfachen Java-Objekten (Plain Old Java Objects, POJOs) - und macht so dem Java-Entwickler das Leben leicht. Diese zweite Auflage des Bestsellers Spring in Action deckt die Version 2.0 und alle ihre neuen Features ab. Das Buch beginnt mit den grundlegenden Konzepten von Spring und führt den Leser rasch dazu, dieses Framework aktiv kennen zu lernen. Kleine Code-Bei- spiele und eine schrittweise ausgebaute eigene Anwendung zeigen, wie einfa- che und effiziente JEE-Applikationen mit Spring entwickelt werden. Der Leser erfährt, wie Persistenz-Probleme gelöst werden, wie mit asynchronen Nach- richten umgegangen wird und wie man Remote Services erstellt und nutzt.Mehr Informationen zu diesem Buch und zu unserem Programmunter www.hanser.de/computer