Ajax mit Wicket – Wie funktionierts?

Mit Wicket 1.5 ist es so einfach wie nie, Ajax basierte Webapplikationen zu erstellen.

Wie einfach das geht und wieviel Spaß das machen kann will dieser Artikel zeigen.

Zunächst erzeugen wir uns eine sehr einfache Wicket-Applikation:


mvn archetype generate

mit der Nummer 217.

Anschließend passen wir die Version in der pom noch an (im Moment wird über den Maven Archetype die Version 1.5.0 noch nicht angeboten.

Hier ist das Gradle-Build File (es gibt aber auch ein Maven Pom):


apply plugin: 'java'
apply plugin: 'eclipse'

defaultTasks "build"

version = "1.0-SNAPSHOT"

springVersion = '3.0.5.RELEASE'
wicketVersion = '1.5.0'
jettyVersion = '7.3.0.v20110203'

sourceCompatibility = 1.6
targetCompatibility = 1.6

dependencies {
compile  "org.apache.wicket:wicket:$wicketVersion"
compile  "org.apache.wicket:wicket-extensions:$wicketVersion"
compile  "org.apache.wicket:wicket-spring:$wicketVersion"
compile  "org.apache.wicket:wicket-datetime:$wicketVersion"
compile  'org.slf4j:slf4j-log4j12:1.5.8'
compile  "org.springframework:spring-core:$springVersion"
compile  "org.springframework:spring-beans:$springVersion"
compile  "org.springframework:spring-jdbc:$springVersion"
compile  "org.springframework:spring-web:$springVersion"
compile  "org.springframework:spring-orm:$springVersion"
compile  "org.springframework:spring-test:$springVersion"
testCompile     "junit:junit:4.8.1"
testCompile     "org.mockito:mockito-core:1.8.5"
testCompile "org.eclipse.jetty.aggregate:jetty-all-server:$jettyVersion"

}

sourceSets {
main {
resources {
srcDir 'src/main/resources'
srcDir 'src/main/java'
}
}
test {
resources {
srcDir 'src/test/resources'
srcDir 'src/test/java'
}
}
}

repositories {
mavenCentral()
}

Zunächst räumen wir die Applikation noch ein wenig auf, damit wir wirklich “auf der grünen Wiese” anfangen können.
Die Page sollte so aussehen:

public class HomePage extends WebPage {
private static final long serialVersionUID = 1L;

public HomePage(final PageParameters parameters) {
}
}

Anschließend löschen wir noch die Klasse “TestHomePage” unter src/test/java, Testen kann hier nicht das Thema.

Zunächst mal schauen wir uns grundsätzlich an, wie Wicket im Zusammenspiel mit Ajax überhaupt funktioniert.

Im nächsten Schritt werden wir einige Funktionale Elemente einbauen (TextFelder etc,)

Anschließend werden wir uns die Integration mit der meiner Ansicht nach coolsten Ajax-Engine die aktuell verfügbar ist anschauen.

Anschließend werden wir einen Blick auf den neuen Wicket Event Mechanismus werfen.

Der Source-Code ist übrigens verfügbar, und zwar hier. Alle wichtigen Schritte sind jeweils getaggt. Ich werde im Artikel darauf verweisen.

Im Package “src/test/java” befindet sich übrigens die Klasse Start, diese hat eine main()-Methode und startet einen Embedded-Jetty, d.h. die Applikation ist direkt aus Eclipse heraus startbar.

Schritt 1 – die Wicket Ajax Engine

Wicket ist ein etabliertes Webframework und kommt mit einer schlanken aber brauchbaren Ajax-Engine daher.

Was bietet die Wicket Ajax Engine?

Abstraktion von Browser-Inkompatibilitäten
Abstraktion der Ajax-Requests
Parallele / Synchrone Verarbeitung von Ajax-Requests

Betrachten wir doch mal, wie die AjaxEngine grundsätzlich aufgebaut ist. Hierfür fügen wir in die Form einen einfachen AjaxLink ein.

Wicket bietet von Haus aus schon eine ganze Menge an Komponenten, die Ajax-Funktionalität Out-of-the-Box mitbringen. Der Ajax-Link ist eine davon – Ein Link der einen Ajax-Request an den Server sendet.

Folgendes Fragment bauen wir in das Html der Seite ein.


Im Java Code das Pendant dazu.


add(new AjaxLink("ajaxLink"){
@Override
public void onClick(AjaxRequestTarget target) {
target.appendJavaScript("alert('hello world!');");
}
});

Die Implementierung spielt momentan keine Rolle, dazu kommen wir später noch. Zunächst mal wollen wir uns anschauen, was Wicket uns generiert hat.
Hierzu betrachten wir den Quellcode der generierten Webseite.

<a id="ajaxLink1" onclick="var wcall=wicketAjaxGet('wicket/page?0-1.IBehaviorListener.0-ajaxLink',function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$('ajaxLink1') != null;}.bind(this));return !wcall;" href="<a href=">Click me</a>

Besonders interessant ist natürlich der onclick-Handler des Links:

var wcall=wicketAjaxGet('wicket/page?0-1.IBehaviorListener.0-ajaxLink',function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$('ajaxLink1') != null;}.bind(this));return !wcall;"

Um zu verstehen, was das bedeutet betrachten wir uns gleichzeitig die Implementierung der entsprechenden Stelle in den Wicket-Sources.
Die Javascript Implementierung befindet sich im Modul wicket-core in der Datei wicket-ajax.js.

function wicketAjaxGet(url, successHandler, failureHandler, precondition, channel) {

var call = new Wicket.Ajax.Call(url, successHandler, failureHandler, channel);

if (typeof(precondition) != "undefined" && precondition != null) {
call.request.precondition = precondition;
}

return call.call();
}

Die wicketAjaxGet-Routine hat also folgende Parameter:

url, successHandler, failureHandler, channel

Die Parameter sind folgendermaßen belegt:

url : wicket/page?0-1.IBehaviorListener.0-ajaxLink
successHandler : function() { }.bind(this)
failureHandler :function() { }.bind(this)
channel :function() {return Wicket.$('ajaxLink1') != null;}.bind(this));

Ok, schauen wir uns ein wenig genauer an, wie der Wicket-Ajax-Mechanismus überhaupt funktioniert.
Die Url wicket/page?0-1.IBehaviorListener.0-ajaxLink adressiert ein AjaxBehavior.

Wicket Behaviors bieten die einfache Möglichkeit, zusätzliches Verhalten für Wicket-Komponenten zu definieren.
Im folgenden Code-Schnipsel wird das grob veranschaulicht.
Zunächst definieren wir im Html ein einfaches div-Element zur Anzeige eines Textes.


und zusätzlich folgenden Java-Code:

Label label = new Label("ajaxText","Ajax Clickable Text");
add(label);
label.add(new AjaxEventBehavior("onclick"){

@Override
protected void onEvent(AjaxRequestTarget target) {
target.appendJavaScript("alert('clicked')");
}});

Auf diese Art und Weise kann praktisch jedes Element ajaxifiziert werden.
Wie aber kommt man jetzt vom Javascript auf der Clientseite zum Serverseitigen Code?

Ganz einfach, über die URL.

wicket/page?0-1.IBehaviorListener.0-ajaxLink

Hiermit wird die PageMap 0 adressiert, in dieser PageMap die Page mit der Version 1. Adressiert wird ein IBehaviorListener, und zwar
mit der Version 0 das auf der Komponente mit der ID “ajaxLink” liegt.
Wenn euch genauer interessiert, wie diese URL geparsed wird, könnt ihr beispielsweise die Klasse PageInstanceMapper genauer unter die Lupe
nehmen.

Das Schema einer URL ist in der Klasse URL definiert (natürlch die in den wicket-packages…)

Hier wird beispielsweise auch aus dem String IBehaviorListener die entsprechende Klasse erzeugt, das Mapping hierzu findet ihr in der KLasse
RequestListenerInterface.

Anschließend wird in der Klasse ListenerInterfaceRequestHandler die eigentliche Requestverarbeitung angestossen.

Ok, aber das könnt ihr alles selber sehen, wenn ihr einfach mal den Debugger anschmeißt und schaut, was passiert. Ich kann das nur empfehlen, ist wirklich hochinteressant.

Ok, lange Rede kurzer Sinn, was Wicket macht, wenn man diese URL aufruft (die man übrigens auch problemlos einfach im Browser aufrufen könnte), es mappt den Request in ein AjaxRequestTarget.

Überhaupt lohnt sich ein etwas genauerer Blick in die Klasse RequestListenerInterface und in die Klasse RequestCycle, da hier viel passiert und man sich relativ schnell ein Verständnis aufbauen kann, wie die Requestverarbeitung in Wicket funktioniert.

Nochmal zur Erinnerung die RequestParameter:

url : wicket/page?0-1.IBehaviorListener.0-ajaxLink
successHandler : function() { }.bind(this)
failureHandler :function() { }.bind(this)
channel :function() {return Wicket.$('ajaxLink1') != null;}.bind(this));

Wicket Ajax Success – bzw. FailureHandler

SuccessHandler ist leer, d.h. es gibt keine bestimmte AKtion, wenn der Ajax-Requets erfolgreich ist.

Das Gleiche gilt für den Failure-Handler, sollte der Ajax Request fehlschlagen, dann schlägt der AjaxRequest eben fehl.
Ich hatte das Problem schon mehrfach in diversen Projekten, dass die Infrastruktur uns einen Strich durch die Rechnung macht -
beispielsweise Firewalls, Loadbalancer etc., in diesen Fällen wird der FailureHandler plötzlich sehr interessant.

Wie aber kann ich als Entwickler jetzt diese Success- bzw. FailureHandler setzen.

Hierzu schauen wir uns kurz mal die Klasse AbstractDefaultAjaxRequestTarget an.

/**
* @return javascript that will run when the ajax call finishes with an error status
*/
protected CharSequence getFailureScript()
{
return null;
}

/**
* @return javascript that will run when the ajax call finishes successfully
*/
protected CharSequence getSuccessScript()
{
return null;
}

Das sollte uns jetzt schon bekannt vorkommen.
Wo wird das aber nun aufgerufen?

Hierzu schaut ihr euch am besten in AbstractDefaultAjaxBehavior mal die Methode

protected CharSequence generateCallbackScript(final CharSequence partialCall)

an.

Am besten wirds sein, wir implementieren einfach mal den Success- bzw. auch den FailureHandler.
Wir haben noch das AjaxEventBehavior von vorhin.
Hier implementieren wir noch die zuvor erwähnten Methoden.

label.add(new AjaxEventBehavior("onclick"){

@Override
protected void onEvent(AjaxRequestTarget target) {
target.appendJavaScript("alert('clicked')");
}

@Override
protected CharSequence getFailureScript() {
return "alert('there was a failure!!')";
}

@Overridearauf
protected CharSequence getSuccessScript() {
return "alert('Ajax call was successful')";
}
});

Das Gleiche versuchen wir jetzt mal mit einem AjaxFehler. Nehmen wir beispielsweise den HTTP-Errorcode 503 (Gateway bzw. Server antwortet nicht).
Darauf müssen wir in einer Applikation normalerweise reagieren.
Das zu simulieren ist natürlich nicht ganz so trivial wie der Succes-Fall, aber weit weniger kompliziert, wie erwartet.

Beispielsweise könnten wir in der onEvent-Methode des AjaxBehaviors einfach einen HTTP-Fehlercode zurückgeben.

@Overrides
protected void onEvent(AjaxRequestTarget target) {
throw new AbortWithHttpErrorCodeException(503, "I need to check that");
]

Mit einer normalen Exception funktion das übrigens nicht, denn das ist kein HTTP-Fehler und wird Serverseitig behandelt und zeigt normalerweise eine ErrorPage oder ähnliches an.

Den letzten Parameter finde ich besonders interessant.

channel :function() {return Wicket.$('ajaxLink1') != null;}.bind(this));

Was sind jetzt AjaxChannels?

Stellen wir uns das Szenario vor, wir haben eine Wicket-Applikation die sehr viel mit Ajax arbeitet.
Stellen wir uns weiterhin vor, diese Applikation hat viele sogenannte Akkordeon-Module (das sind Module die via Javascript auf- bzw. zugeklappt
werden können). Das Auf und zuklappen ist jeweils ein eigener Ajax-Call gegen das Backend, weil ja ggf. BackendServices getriggert werden müssen, um
die Daten in den aufgeklappten Panels anzuzeigen. Ein einfaches Beispiel hierzu wäre die Anzeige von Adressdaten, die mit Hilfe einer Customer-ID vom Backend geladen werden müssen.
Da die Daten erst geladen werden müssen kann es passieren, dass ein ProgressIndicator etwas länger angezeigt wird.
Ungeduldige Kunden fangen dann an, und klicken wild auf der Applikation hin und her, öffnen und Schließen Panels und triggern so natürlich unbewusst BackendCalls, die die Applikation noch langsamer machen als sie sowieso schon ist.

Denn keine Ajax-Request gehem im Default-Modus verloren, jeder Backend-Call wird abgearbeitet, auch wenn das vielleicht gar nicht mehr notwendig wäre(
im schlimmsten Fall klickt der Kunde für jedes Akkordion den Aufklappen-Button, der Backend-Call wird abgesetzt, die Daten geladen und das Akkordion blitzt nur kurz auf, weil der Kunde schon 3 Akkordeons weiter ist, das Laden der Daten war also völlig umsonst.)s

Ajax Channels

Wicket arbeitet per Default im synchronen Ajax-Modus, d.h. alle Ajax-Requests werden gequeued.
Wie das gemacht wird sieht man am besten wieder, wenn man wieder einen Blick in die Datei wicket-ajax.js wirft.

Hier hab ich für euch die relevanten Sourcen kopiert.

/**
* Channel management
*
* Wicket Ajax requests are organized in channels. A channel maintain the order of
* requests and determines, what should happen when a request is fired while another
* one is being processed. The default behavior (stack) puts the all subsequent requests
* in a queue, while the drop behavior limits queue size to one, so only the most
* recent of subsequent requests is executed.
* The name of channel determines the policy. E.g. channel with name foochannel|s is
* a stack channel, while barchannel|d is a drop channel.
*
* The Channel class is supposed to be used through the ChannelManager.
*/
Wicket.Channel = Wicket.Class.create();
Wicket.Channel.prototype = {
initialize: function(name) {
var res = name.match(/^([^|]+)\|(d|s)$/)
if (res == null)
this.type ='s'; // default to stack
else
this.type = res[2];
this.callbacks = new Array();
this.busy = false;
},

schedule: function(callback) {
if (this.busy == false) {
this.busy = true;
try {
return callback();
} catch (exception) {
this.busy = false;
Wicket.Log.error("An error occurred while executing Ajax request:" + exception);
}
} else {
Wicket.Log.info("Channel busy - postponing...");
if (this.type == 's') // stack
this.callbacks.push(callback);
else /* drop */
this.callbacks[0] = callback;
return null;
}
},

done: function() {
var c = null;

if (this.callbacks.length > 0) {
c = this.callbacks.shift();
}

if (c != null && typeof(c) != "undefined") {
Wicket.Log.info("Calling postponed function...");
// we can't call the callback from this call-stack
// therefore we set it on timer event
window.setTimeout(c, 1);
} else {
this.busy = false;
}
}
};

/**
* Channel manager maintains a map of channels.
*/
Wicket.ChannelManager = Wicket.Class.create();
Wicket.ChannelManager.prototype = {
initialize: function() {
this.channels = new Array();
},

// Schedules the callback to channel with given name.
schedule: function(channel, callback) {
var c = this.channels[channel];
if (c == null) {
c = new Wicket.Channel(channel);
this.channels[channel] = c;
}
return c.schedule(callback);
},

// Tells the ChannelManager that the current callback in channel with given name
// has finished processing and another scheduled callback can be executed (if any).
done: function(channel) {
var c = this.channels[channel];
if (c != null)
c.done();
}
};

// Default channel manager instance
Wicket.channelManager = new Wicket.ChannelManager();

Interessant ist zunächst mal die Regular Expression im Initialize-Callback des Ajax-Channels.

name.match(/^([^|]+)\|(d|s)$/)

Wie interpretiert man diese Regular Expression jetzt?

Falls euch langweilig ist versucht ihr selber das herauszufinden, für alle anderen gibts direkt hier die Lösung.
Die Regular-Expression greift für alles, was folgendes Muster hat:

myAjaxChannel|s
myAjaxChannel|d
irgendEinString|s
irgendEinString|d

Was aber bedeuten diese Ausdrücke?

Hierzu muss man wissen, dass Wicket zwei Modi unterscheidet, wie Ajax-Requests verarbeitet werden.

Synchroner / Asynchroner Ajax Modus

Im synchronen (Queue- oder Stack-Modus) werden alle Ajax-Requests in einer oder mehreren Queues gepoolt.
Die verschiedenen Modi werden durch sogenannte Channels abgebildet.
Werfen wir hierzu nochmals einen genauen Blick auf die Implementierung eines Ajax-Channels und hier speziell die schedule-Routine.

schedule: function(callback) {
if (this.busy == false) {
this.busy = true;
try {
return callback();
} catch (exception) {
this.busy = false;
Wicket.Log.error("An error occurred while executing Ajax request:" + exception);
}
} else {
Wicket.Log.info("Channel busy - postponing...");
if (this.type == 's') // stack
this.callbacks.push(callback);
else /* drop */
this.callbacks[0] = callback;
return null;
}
}

Der erste Teil des if-Blocks ist erst mal uninteressant, denn jeder Ajax-Request wird sofort verarbeitet, wenn nicht gerade ein anderer AjaxRequest aktiv ist.
Viel interessanter ist der else-Zweig.

Wenn der Channel-Type ‘s’ ist (S steht für Stackable, es handelt sich also um einen Queue-Channel), wird der Ajax-Call in eine Liste gespeichert.
Wenn es sich nicht um einen Stack-Channel handelt, dann wird das oberste Element in der Liste ersetzt (in diesem Fall kann sowieso nur ein Element in der Liste sein) und ist quasi der Top-Kandidat als nächster AjaxRequest.

Jetzt sollte auch langsam klar werden, was es mit den zuvor erwähnten, etwas kryptischen Regular-Expressions auf sich hat.

Channelnames haben entweder die Form

meinAjaxChannel|s

oder die Form

meinAjaxChannel|d

Über den Character hinter dem |-Symbol unterscheidet Wicket, welche Art Channel für einen bestimmten Ajax-Call verwendet wird.

Wenn wir jetzt das Beispiel von vorhin ein wenig weiterspinnen, wären die AjaxRequests zum Öffnen der Akkordion-Panels nicht Stackable- bzw. Queue-Channels, sondern Drop-Channels, dann würde die Wicket tatsächlich immer nur den letzten Akkordion öffnen und den entsprechenden BackendCall absetzen.
Alle anderen Requests würden einfach verfallen und niemals zum Server geschickt werden.

Bevor wir noch tiefer einsteigen schauen wir und das Ganze am besten an einem Beispiel an.

Hierfür erzeugen wir uns zunächst mal in HomePage.html folgendes Schnipsel:


<a href="#">

</a>

Dies ist zum Einen ein WicketContainer, der als Container für einen ListView dient, in diesem sollen 4 AjaxLinks gerendert werden, die ein Laufrad zeigen, sobald der Button betätigt wird.

In die onClick-Methode bauen wir einen TimeOut von sagen wir 5 Sekunden ein, um langlaufende Backend-Transaktionen zu simulieren.

Der Java-Code ist hier:


List links = Arrays.asList(new String[] { "Link1", "Link2",
"Link3", "Link4" });
ListView listView = new ListView("linkList", links) {

private static final long serialVersionUID = 1L;

@Override
protected void populateItem(ListItem item) {
item.add(new IndicatingAjaxLink("indicatingAjaxLink") {

private static final long serialVersionUID = 1L;

@Override
public void onClick(AjaxRequestTarget target) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
target.appendJavaScript("alert('indicating link clicked')");
}

}.add(new Label("message", item.getModelObject())));
}
};

Um es uns möglichst einfach zu machen verwenden wir keine einfachen AjaxLinks sondern IndicatingAjaxLinks, da diese schon genau das Verhalten simulieren, das wir jetzt brauchen.
Die IndicatingAjaxLinks blenden ein Laufrad ein, sobald sie geklickt werden. Das ist einfach über ein Behavior gelöst.
Öffnet man jetzt die Seite und klickt alle Links an, ergibt sich folgendes Bild.


Jeder Link hat einen Timeout von 5 Sekunden in der onClick-Methode definiert.
Hier sieht man sehr schön die serielle Abarbeitung von AjaxRequests.
Es sieht zwar so aus, als wären alle Requests bereits abgeschickt, in Wirklichkeit aber
ist nur der erste Request zum Server geschickt, hier wird der onClick-Handler aufgerufen und
der Thread pausiert.

Die Ajax-Indikatoren sind per Javascript eingeblendet.
Sind die ersten 5 Sekunden vorbei, startet der zweite Request, das dauert wieder 5 Sekunden.
Es dauert also insgesamt 20 Sekunden, bis alle Ajax-Requests abgehandelt sind, da immer nur
einer zum Server geschickt wird, der Rest landet in der AjaxChannel-Queue des ChannelManagers.

Betrachten wir kurz noch das generierte Javascript der AjaxLinks.

if (function(){return Wicket.$('indicatingAjaxLink8') != null;}.bind(this)()) { Wicket.showIncrementally('indicatingAjaxLink8--ajax-indicator');}var wcall=wicketAjaxGet('wicket/page?1-2.IBehaviorListener.1-linkList-1-indicatingAjaxLink',function() { ;Wicket.hideIncrementally('indicatingAjaxLink8--ajax-indicator');}.bind(this),function() { ;Wicket.hideIncrementally('indicatingAjaxLink8--ajax-indicator');}.bind(this), function() {return Wicket.$('indicatingAjaxLink8') != null;}.bind(this));return !wcall;

Das meiste sollte uns bekannt vorkommen.
Was auffällt, man sieht hier nichts von Channels. Per Default wird kein Channel generiert und
alle Wicket-Ajax-Funktionalität läuft im Default-Channel,also bereits beim Client
serialisiert.

Jetzt betrachten wir, die die Situation verbessert werden kann.
Denn klickt der User auf alle 4 Links kann (muss aber nicht) es besser sein, nur den jeweils
letzten Request zu verarbeiten.

Um das zu simulieren überschreiben wir einfach das onClick-Ajaxbehavior des Links. Hierzu muss man wissen, jede Komponente kann nur jeweils einen Event-Handler für ein bestimmtes Event haben.


item.add(new IndicatingAjaxLink("indicatingAjaxLink") {

private static final long serialVersionUID = 1L;

protected AjaxEventBehavior newAjaxEventBehavior(String event) {
return new AjaxEventBehavior("onclick"){

@Override
protected void onEvent(AjaxRequestTarget target) {
onClick(target);
}

protected String getChannelName() {
return "ajaxChannel|d";
};

};
};

Für uns von Interesse ist hier die Methode getChannelName() des AbstractDefaultAjaxBehaviors.

Diese Methode liefert genau eines der zuvor bereits definierten Pattern (channelName|s oder channelName|d oder nullwas einem |s Channel entspricht).

Da wir einen Drop-Channel verwenden möchten, verwenden wir den ChannelName “ajaxChannel|d“.

Betrachten wir sofort wieder das generierte Javascript.


if (function(){return Wicket.$('indicatingAjaxLink6') != null;}.bind(this)()) { Wicket.showIncrementally('indicatingAjaxLink6--ajax-indicator');}var wcall=wicketAjaxGet('wicket/page?0-1.IBehaviorListener.1-linkList-3-indicatingAjaxLink',function() { ;Wicket.hideIncrementally('indicatingAjaxLink6--ajax-indicator');}.bind(this),function() { ;Wicket.hideIncrementally('indicatingAjaxLink6--ajax-indicator');}.bind(this), function() {return Wicket.$('indicatingAjaxLink6') != null;}.bind(this), 'ajaxChannel|d');

Ganz am Ende im Skript sieht man, dass der richtige Ajax-Channel generiert wurde.

Wie aber verhält sich die Applikation?

Scheinbar etwas unerwartet, aber nur bis man etwas genauer darüber nachdenkt.

Das Laufrad bei Link1 verschwindet nach 5 Sekunden. Das Laufrad bei Link4 nach weiteren 5 Sekunden. Die Laufräder bei Link2 + Link3 verschwinden überhaupt nicht.

Das macht Sinn.

  1. Beim Klick auf Link1 wird der Request abgeschickt und verarbeitet.
  2. Der Klick auf Link2 setzt einen neuen Request in die Queue.
  3. Der Klick auf Link3 überschreibt den Request von Link2.
  4. Der Klick auf Link4 überschreibt den Request von Link3.
  5. Ist der Link1-Request verarbeitet, wird sofort der Link4-Request verarbeitet. Die Request von Link2 und Link3 sind verschwunden.

Das Ausblenden des Laufrads wird aber im Success- bzw. im Fehlerfall ausgeblendet, nicht jedoch wenn der Request gar nicht erst verarbeitet wird. Ein sehr schönes Beispiel.

Übrigens, mit Wicket 1.5.1 wird das Ganze noch schöner, ich habe mich sehr daran gestört, dass man hier mit Strings arbeiten muss. Deshalb hab ich für die AjaxChannels eine kleine Convenience-API bereit gestellt die ab Wicket 1.5.1 verfügbar sein wird. Wer sich das Ganze jetzt schon anschauen möchte, hier ein kleiner Vorgeschmack.

AjaxRequestTarget

Was wir jetzt schon einige Male verwendet haben, worauf ich aber noch nicht genauer eingegagen bin ist das AjaxRequestTarget.

Diesen Code kennen wir schon:


add(new AjaxLink("ajaxLink") {
@Override
public void onClick(<strong>AjaxRequestTarget</strong> target) {
target.appendJavaScript("alert('hello world!');");
}
});

Quasi in jeder Ajax-Methode bekommen wir vom Framework freundlicherweise den Schlüssel zur Wicket-Ajax-Engine übergeben, und zwar das AjaxRequestTarget.

Was kann ich mit dem AjaxRequestTarget machen?

  1. Javascript direkt zum Client schicken und zwar nach dem AjaxCall
  2. Javascript direkt zum Client schicken und zwar vor dem Call
  3. Bestimmte Komponenten neu zeichnen bzw. updaten.

Um Javascript direkt zum Client zu schicken bietet das AjaxRequestTarget die Methoden appendJavascript(String script) bzw. prependJavascript(String script).

Die Funktionsweise ist sprechend, deswegen gehe ich hier nicht näher darauf ein.

Um Komponenten neu zu zeichnen wird die add(Component component)-Methode verwendet. Wicket bietet mit dieser Methode die Möglichkeit, Komponenten in die RenderQeueu zu legen, die dann auf der Clientseite upgedatet werden.

Wichtig! Alle Komponenten die upgedatet werden sollen müssen eine Markup-ID rendern, das geht entweder, indem der Komponente bereits im Html eine id vergeben wird.


oder Codeseitig, indem folgendes aufgerufen wird.


Label label = new Label("text","Anzeigetext")
label.setOutputMarkupId(true);

AjaxLazyLoadPanel

Es gibt die Möglichkeit, Komponenten “Lazy” zu laden. D.h. beim Laden der Komponente wird ein Laufrad eingeblendet, und erst wenn die Komponente fertig geladen ist, wird die Komponente eingeblendet.

Das kann zum Einen verwendet werden, um lang ladenden Komponenten Zeit zu geben, sich fertig zu rendern, oder aber auch um auf Fehler zu reagieren, die bei eventuellen Backend-Calls auftreten können.

Betrachten wir das Ganze ein wenig genauer.
Die Klasse, die uns diese wunderbare Funktionalität bietet ist das AjaxLazyLoadPanel.

Um das AjaxLazyLoadPanel verwenden zu können müssen wir uns die Wicket-Extensions besorgen.
Der Maven-Archetype war so freundlich, und diese gleich mit zu generieren, wir müssen sie nur einkommentieren, falls noch nicht geschehen:


    org.apache.wicket
    wicket-extensions
    ${wicket.version}

für den gradle-build einfach folgende Zeile in die Dependencies mit aufnehmen:

compile  "org.apache.wicket:wicket-extensions:$wicketVersion"

Der relevante Codeteil im LazyLoadPanel ist hier zu sehen:

protected void respond(final AjaxRequestTarget target)
			{
				if (state < 2)
				{
					Component component =   getLazyLoadComponent( LAZY_LOAD_COMPONENT_ID);
					AjaxLazyLoadPanel.this.replace(component);
					setState((byte)2);
				}
				target.add(AjaxLazyLoadPanel.this);

			}

Am besten wird sein, wir betrachten das LazyLoadPanel einfach mal in Aktion:


Hier der Code dazu:

<div wicket:id="lazyLoading"/>

und den Javacode dazu:

protected AjaxLazyLoadPanel lazyLoadingPanel(){
		return new AjaxLazyLoadPanel("lazyLoading") {

			@Override
			public Component getLazyLoadComponent(String markupId) {
				try {
					Thread.sleep(10000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				return new Label(markupId, "Lazy Loading works!");
			}
		};
	}

Das LazyLoadPanel rendert zunächst den ProgressIndicator, und ändert diesen in die richtige Komponente die von getLazyLoadComponent aufgerufen wird.
Das schöne ist, hier können wir sogar noch ExceptionHandling einbauen.

Das Problem ist, normalerweise treten Exceptions nicht im Konstruktor der Komponente auf, sondern beim Rendern.
Beispielsweise treten die meisten Exceptions beim Zugriff auf BackendSysteme auf. Der Zugriff auf diese Systeme erfolgt meistens über LoadableDetachableModels, und zum Zeitpunkt der Exception kann man leider meistens nicht mehr angemessen darauf reagieren.

Das sieht anders aus bei der Verwendung von AjaxLazyLoadPanels, wir simulieren einfach mal eine Komponente, die mit einem Model arbeitet, dass zur Laufzeit eine Exception wirft, also eine echte Real-Life-Problematik.

Folgendes Model verwenden wir hierzu:

public class ExceptinThrowingModel extends LoadableDetachableModel{

	@Override
	protected Object load() {

		throw new RuntimeException("Error loading Component");
	}

}

Und bauen uns das Label im LazyLoadPanel folgendermaßen auf:

return new Label(markupId, new ExceptinThrowingModel());

Das Ganze resultiert wie zu erwarten hierin:

Das schöne ist aber, dass wir jetzt die Möglichkeit haben, angemessen darauf zu reagieren, und zwar so:

try {
					IModel model = new ExceptinThrowingModel();
					//check the Backend Call
					model.getObject();
					return new Label(markupId, model);
				} catch (Exception e) {
					return new Label(markupId, "Es ist ein Fehler aufgetreten!");
				}

Der Code selber gefällt mir nicht besonders, gerade weil wir hier das model.getObject() extra aufrufen müssen um auf eventuelle Fehler zu reagieren, das verursacht natürlich unnötigen Traffic, aber mir ist momentan keine Möglichkeit bekannt, das besser zu machen.

Für Tipps bin ich sehr dankbar.

Wicket & JQuery

Wicket bringt schon eine sehr mächtige Ajax-Engine mit, mit der man alle “normalen” Ajax-Probleme und Anforderungen umsetzen kann. Geht es aber darum, der Oberfläche noch den letzten Schliff zu verpassen, dann wirds ein wenig komplizierter. Spätestens hier lohnt es sich, sich ein ausgewachsenes Ajax-Framework mal genauer anzuschauen.

Mein Tipp hier: JQuery – eine unglaublich einfach zu verwendendes, aber gleichzeitig unglaublich mächtiges Ajax-Framework.

Dies hier ist kein Artikel über JQuery, nichtsdesto trotz betrachten wir kurz, was mit JQuery alles möglich ist.

Hierzu laden wir uns zunächst mal die JQuery-Library, einfacherweise ist dies nur ein Javascript-File und binden die vorerst mal direkt in unsere Html-Seite ein. Hierzu laden wir das File herunter und legen es in das lokale Filesystem, und zwar im webapp-Ordner.

<script type="text/javascript" src="jquery-1.6.4.min.js"></script>
<script type="text/javascript">
$(document).ready(function(){
alert("Jquery is in place!");
})
</script>

Zunächst binden wir die JQuery-API ein.
Anschließend arbeiten wir mit folgendem Skript:

$(document).ready(function(){})

Über $(document) sprechen wir das aktuelle Dokument an. Über ready() haben wir eine Möglichkeit, einen Callback zu hinterlegen, der dann ausgeführt wird, wenn der DOM vollständig geladen wurde.

In diesem Fall geben wir zunächst einfach einen alert() aus, um zu prüfen, ob die JQuery-Anbindung funktioniert.

JQuery ist also zunächst bereit.

Schauen wir uns also mal an, was man damit alles so machen kann.

Die mächtigste Funktion von JQuery sind mit Sicherheit die Selektoren, es gibt unzählige Möglichkeiten, Elemente aus dem DOM zu selektieren. Beispielsweise so:


$(document).ready(function(){
$('.headline').click(function(){
alert('on click handler in place')
})
})

Wichtig ist, das Script im onReady-Handler der Seite aufzurufen, da nur hier alle Elemente bereit sind.

Mit dem Selektor $(‘.headline’) werden alle Elemente ausgewählt, die eine bestimmte CSS-Klasse haben, in diesem Beispiel alle Elemente mit der Klasse headLine.

Um dies zu testen geben wir der Headline die soweiso schon da ist die Klasse, nun ja, ‘headLine’.


<h1 class="headline" wicket:id="hello"></h1>

Laden wir die Seite nun neu, ist der onClick-Handler installiert.

Nun ist ein alert nicht gerade Magie, was aber zum Beispiel auch geht ist folgendes:


$(document).ready(function(){
$('.headline').click(function(){
$(this).fadeOut();
})
})

Dies blendet die Überschrift langsam aus, bis sie verschwunden ist. Über $(this) kann das Element referenziert werden, auf dem der Handler installiert wurde.

Ok, das Schöne an JQuery ist, dass es sehr schnell zu erlernen ist. Idealerweise baut ihr zunächst mal direkt auf dem Beispiel auf und experimentiert ein bisschen.

Integration mit Wicket

Was wir bisher gemacht haben hätte auch wunderbar ohne Wicket funktioniert, wir hätten auch direkt JQuery in eine leere Html-Seite einbauen können. Was uns hier aber primär interessiert ist ja die Möglichkeit, JQuery mit Wicket zusammen zu verwenden. Zunächst mal sollten wir analysieren, wie wir JQuery in die Wicket Applikation einbinden. Mit Wicket 1.5 ist das sehr einfach über ein Behavior realisierbar.


public class JQueryBehavior extends Behavior {

private static final long serialVersionUID = 1L;

public static final String JQUERY = "jquery-1.6.4.min.js";

@Override
public void renderHead(Component component, IHeaderResponse response) {
super.renderHead(component, response);
response.renderJavaScriptReference(new PackageResourceReference(JQueryBehavior.class, JQUERY ));
}
}}

Über die Klasse IHeaderResponse hat man die Möglichkeit, CSS- oder Javascript-Referenzen im <Head> zu rendern.
Das Schöne an Behaviors ist, dass sie auch dafür sorgen, dass Referenzen nur einmal gerendert werden und nicht mehrmals.

Damit das Ganze funktioniert kopieren wir die JQuery-Datei in das Package, wo auch die Klasse liegt.

Das Behavior kann jetzt überall in der Applikation verwendet werden.
Wir werden das Behavior direkt in der Page verwenden, und zwar so:

public HomePage(final PageParameters parameters) {
add(new JQueryBehavior());
...
}

Was Wicket uns hier rausrendert ist das:

<script type="text/javascript" src="<a href="view-source:http://localhost:8080/wicket/resource/de.md.jquery.JQueryBehavior/jquery-1.6.4.min-ver-1316715616000.js">wicket/resource/de.md.jquery.JQueryBehavior/jquery-1.6.4.min-ver-1316715616000.js</a>"></script>

Die Skripte funktionieren weiter wie bisher, wenn wir sie vom HEAD in den Body verschieben.
Im Head ist die Reihenfolge noch wichtig, in der die Skripte importiert werden und die können wir leider nicht kontrollieren.

Hier nochmal zur Sicherheit das komplette Html dazu:

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
<meta charset="utf-8" />
<title>Apache Wicket Quickstart</title>
<link
href='http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:regular,bold'
rel='stylesheet' type='text/css' />
<link rel="stylesheet" href="style.css" type="text/css" media="screen"
title="Stylesheet" />
</head>
<body>
<script type="text/javascript">
$(document).ready(function(){
$('.headline').click(function(){
$(this).fadeOut();
})
})
</script>
<h1 wicket:id="hello"></h1>
<a href="#" wicket:id="ajaxLink">Click me</a>
<div wicket:id="ajaxText" />

<wicket:container wicket:id="linkList">
<a href="#" wicket:id="indicatingAjaxLink">
<span wicket:id="message" />
</a>
</wicket:container>

<div wicket:id="lazyLoading"/>

<div style="width:500px; height:100px; background: gray;">
<form wicket:id="form">
Anrede: <div wicket:id="salutation"/>
Vorname: <div wicket:id="firstName"/><br/>
<input type="submit"/>
</form>
</div>
</body>
</html>

Das ist zunächst mal alles was notwendig ist, um JQuery einzubinden. Normalerweise ist es so, dass man in einer Applikation eine abstrakte Page bereitstellt.
Dies ist der perfekte Ort, um dieses Behavior einzubinden.

Zunächst mal legen wir einen neuen Style direkt im <head> an:

<style type="text/css">
.blackBeauty{display:none;position:absolute; right: 0px; top: 0px;width:150px; height:150px; background:black;color: red}
</style>

Dieser Style rendert eine Schwarze Box mit roter Schrift im rechten oberen Eck der Seite.
Das Element ist initial unsichtbar.

Jetzt wollen wir einen Link rendern, der dieses Element einblendet und zwar mit ein wenig Dynamik, wozu haben wir
schließlich JQuery, oder? Gleichzeitig blendet das Skript den Link aus, so dass er nur einmalig geklickt werden kann.
Ich weiß, dies ist ein sehr einfacher Use-Case, zeigt aber wie JQuery sehr einfach verwendet werden kann, der Rest ist
reine Spielerei.

Zunächst legen wir uns ein kleines Skript-File an (fadein.js):

$(".blackBeauty").fadeIn("slow");
$("#${id}").fadeOut("slow");

Dazu schreiben wir uns wieder ein Behavior:


public class FadeInBehavior extends AjaxEventBehavior {

public FadeInBehavior(String event) {
super(event);
}

@Override
protected IAjaxCallDecorator getAjaxCallDecorator() {
return new AjaxCallDecorator() {
@Override
public CharSequence decorateScript(Component c, CharSequence script) {
PackageTextTemplate template = new PackageTextTemplate(FadeInBehavior.class, "fadein.js");
Map<String, Object> map = new HashMap<String, Object>();
map.put("id", getComponent().getMarkupId());
return template.interpolate(map).asString();
}
};
}

@Override
protected void onEvent(AjaxRequestTarget target) {
//never called
}
}

Wir benutzen hierfür eine AjaxEventBehavior, von dem wir ableiten. Dieses Behavior rendert uns automatisch einen onClick-Handler, der normalerweise
das Serverseitige Behavior aufruft, das möchten wir hier aber nicht, sondern wir möchten ein Clientseitiges JQuery-Skript aufrufen.

Hierfür überschreiben wir die Methode getAjaxCallDecorator – Mit einem AjaxCallDecorator hat man die Möglichkeit, das gerenderte Ajaxskript
zu dekorieren.
Wir benutzen den Decorator nicht, um das Skript zu dekorieren, sondern wir überschreiben es einfach.

Mit folgendem Code-Fragment laden wir das externe Javascript-File und interpolieren die Platzhalter:

PackageTextTemplate template = new PackageTextTemplate(FadeInBehavior.class, "fadein.js");
Map<String, Object> map = new HashMap<String, Object>();
map.put("id", getComponent().getMarkupId());
return template.interpolate(map).asString();

Der Platzhalter ${id} wird ersetzt durch die Markup-ID des Links und fertig.

So, dies war ein relativ langer Artikel, ich hoffe ich konnte einigermaßen klar machen, wie schön und einfach Ajax
mit Wicket funktioniert.
Ich freue mich über Kommentare , Anregungen etc.

Den Sourcecode könnt ihr euch von Github ziehen.


War dieser Blogeintrag für Sie interessant? Evtl. kann ich noch mehr für Sie tun.

Trainings & Know-How aus der Praxis zu

  • Apache Wicket 1.4.x, 1.5.x, 1.6.x
  • GIT – Best Practices, Einsatz, Methoden
  • Spring
  • Java
  • Scrum & Kanban
  • Agiles Arbeiten
Consulting & Softwareentwicklung

  • Requirements Engineering
  • Qualitätssicherung
  • Software-Entwicklung
  • Architektur
  • Scrum & Kanban

2 Gedanken zu „Ajax mit Wicket – Wie funktionierts?

  1. Pingback: Wicket 6.0 – Teil 1 : JQuery Backing Library | Softwareentwicklung und Agiles Arbeiten

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ photo

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s