Skip to content


Wicket AjaxBookmarkablePageLink - Ajax und SEO

Nutzer haben eine hohe Erwartung an die Geschwindigkeit, mit der auch Webanwendungen auf Nutzereingaben reagieren sollen. Nicht erst seit gestern wird deshalb auf verschiedene Arten versucht, die Antwortzeit auf ein Minimum zu drücken. Häufig werden dann Seiteninhalte per Ajax ausgetauscht. Das ist angenehm für den Nutzer, Suchmaschinen sind aber (zur Zeit) nicht in der Lage, über solche Interaktionshürden zu springen und damit diese Inhalte in den Suchindex aufzunehmen.

Bisher hat man sich dann oft für das eine (kein Ajax) oder das andere (Ajax, versteckte SEO-Links) entschieden. Mit Wicket ist es nicht so schwer, einen sehr mächtigen Ansatz zu wählen, der beides kombiniert und das Leben des Entwicklers vereinfachen kann.

Wir zäumen das Pferd etwas von hinten aus, aber am Ende wird das alles einen Sinn ergeben. Versprochen. Zuerst brauchen wir etwas, was eine Seitenklasse und PageParameter in einen Wert vereint:

public class BookmarkablePageDestination<T extends WebPage> {

  private final Class<? extends T> _pageClass;
  private final PageParameters _pageParameters;

  public BookmarkablePageDestination(Class<? extends T> pageClass, PageParameters pageParameters) {
    _pageClass = pageClass;
    _pageParameters = new PageParameters(pageParameters);
  }

  public Class<? extends T> getPageClass() {
    return _pageClass;
  }

  public PageParameters getPageParameters() {
    // not immutable, so we have to copy
    return new PageParameters(_pageParameters);
  }

  public String asUrl() {
    return RequestCycle.get().urlFor(_pageClass, _pageParameters).toString();
  }

  public static <T extends WebPage> BookmarkablePageDestination<T> with(Class<T> pageClass) {
    return new BookmarkablePageDestination<T>(pageClass, new PageParameters());
  }

  public static <T extends WebPage> BookmarkablePageDestination<T> with(Class<T> pageClass,
      PageParameters pageParameters) {
    return new BookmarkablePageDestination<T>(pageClass, pageParameters);
  }
}

Da die Klasse PageParameters nicht unveränderlich ist, müssen wir eine Kopie anfertigen, um unerwünschte Nebeneffekte zu vermeiden. Als nächstes erweitern wir die AjaxLink-Klasse um eine Funktion, die sie aus Suchmaschinensicht zu einem normalen Link macht.

public abstract class AjaxBookmarkablePageLink<T, P extends WebPage> extends AjaxLink<T> {

  public AjaxBookmarkablePageLink(String id, IModel<T> model) {
    super(id, model);
  }

  @Override
  protected void onInitialize() {
    super.onInitialize();

    add(new AttributeModifier("href", new LoadableDetachableModel<String>() {

      @Override
      protected String load() {
        return getDestination(getModelObject()).asUrl();
      }
    }));
  }

  protected abstract BookmarkablePageDestination<P> getDestination(T modelValue);
}

Da sich das Ziel jederzeit ändern kann, muss das href-Attribut immer wieder erneuert werden. Dabei machen wir das Ergebnis abhängig vom Wert aus dem Model. Wichtiger Hinweis an dieser Stelle: normalerweise müsste man innerhalb der LoadableDetachableModel-Klasse für das verwendete Model die detach()-Methode aufrufen. Da das Model bereits durch die Link-Komponente in den Wicket-Renderzyklus eingebunden ist, ist das hier nicht notwendig. Man sollte das aber immer im Hinterkopf behalten, wenn man die Funktionsweise ändert.

Wir benötigen noch etwas, wass den Zustand der Seite abbildet und das man in Seitenparameter konvertieren und aus Seitenparametern auslesen kann. Wir nehmen ein einfaches Beispiel und verändern einen Zähler.

public interface IState extends Serializable {
  IState oneUp();
  IState oneDown();
  int getCounter();
}

public class StateConverter {

  private StateConverter() {
    // no instance
  }

  enum StateParameter {
    Count,
  };

  public static IModel<IState> asModel(PageParameters pageParameters) {
    return new Model<IState>(asState(pageParameters));
  }

  private static State asState(PageParameters pageParameters) {
    int count = pageParameters.get(StateParameter.Count.name()).toInt(0);
    return new State(count);
  }

  public static PageParameters asPageParameters(IState state) {
    return new PageParameters().add(StateParameter.Count.name(), state.getCounter());
  }

  static class State implements IState, Serializable {

    final int _count;

    public State(int count) {
      _count = count;
    }
    
    @Override
    public IState oneUp() {
      return new State(_count + 1);
    }
    
    @Override
    public IState oneDown() {
      return new State(_count - 1);
    }

    @Override
    public int getCounter() {
      return _count;
    }
  }
}

Jetzt brauchen wir eine Linkklasse, die den Zähler verändert.

public abstract class CountLink extends AjaxBookmarkablePageLink<IState, SeoPage> {

  private final WebMarkupContainer _ajaxBorder;

  public CountLink(String id, IModel<IState> model, WebMarkupContainer ajaxBorder) {
    super(id, model);
    _ajaxBorder = ajaxBorder;
  }

  @Override
  protected BookmarkablePageDestination<SeoPage> getDestination(IState state) {
    return BookmarkablePageDestination.with(SeoPage.class, StateConverter.asPageParameters(nextState(state)));
  }

  @Override
  public void onClick(AjaxRequestTarget target) {
    setModelObject(nextState(getModelObject()));
    target.add(_ajaxBorder);
  }

  protected abstract IState nextState(IState state);
}

Bei einem Klick auf den Link wird der Wert im Model verändert, der Wert aus getDestination() zeigt auf das potentielle Ziel, der Einfachheit halber wird außerdem eine Komponente übergeben, die per Ajax aktualisiert werden soll. Jetzt fügen wir alles zusammen:

public class SeoPage extends WebPage {

  public SeoPage(PageParameters pageParameters) {
    final IModel<IState> stateModel = StateConverter.asModel(pageParameters);

    final WebMarkupContainer ajaxUpdate = new WebMarkupContainer("ajaxUpdate");
    ajaxUpdate.setOutputMarkupId(true);

    ajaxUpdate.add(new Label("count", new PropertyModel<Integer>(stateModel, "counter")));

    ajaxUpdate.add(new CountLink("up", stateModel, ajaxUpdate) {

      @Override
      protected IState nextState(IState state) {
        return state.oneUp();
      }
    });

    ajaxUpdate.add(new CountLink("down", stateModel, ajaxUpdate) {

      @Override
      protected IState nextState(IState state) {
        return state.oneDown();
      }
    });

    add(ajaxUpdate);
  }
}

Zuerst wird aus den PageParametern der aktuelle Zustand der Seite ermittelt und in ein Model gepackt. Das Label zeigt den aktuellen Wert an, die zwei Links verändern den Wert. Das passende Markup dazu:

<html>
<head>
  <title>Seo Ajax Bookmarkable Page Links</title>
</head>
<body>
  <div wicket:id="ajaxUpdate">
    <span wicket:id="count"></span><br> 
    <a wicket:id="up">Up</a>
     | 
    <a wicket:id="down">Down</a>
  </div>
</body>
</html>

Und dann erhält man folgendes Ergebnis:

<body>
  <div wicket:id="ajaxUpdate" id="ajaxUpdate1">
    <span wicket:id="count">0</span><br> 
    <a wicket:id="up" id="up2" href="./seoPage?Count=1" onclick="var wcall=wicketAjaxGet(&#039;./seoPage?0-1.IBehaviorListener.1-ajaxUpdate-up&#039;,function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$(&#039;up2&#039;) != null;}.bind(this));return !wcall;">Up</a>
     | 
    <a wicket:id="down" id="down3" href="./seoPage?Count=-1" onclick="var wcall=wicketAjaxGet(&#039;./seoPage?0-1.IBehaviorListener.1-ajaxUpdate-down&#039;,function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$(&#039;down3&#039;) != null;}.bind(this));return !wcall;">Down</a>
  </div>
</body>

Wie man sieht, kann man beide Anforderungen mit etwas Aufwand vereinen. Je nach Anforderung kann man das Konzept in verschiedenste Richtungen erweitern. Ich hoffe, ich konnte ein wenig die Richtung zeigen:)

Tags:

Veröffentlicht in Wicket, .

Wicket Page Serializer und Performanceoptimierung - Teil 1

Wicket bildet den Zustand einer Anwendung, einer Webseite nicht wie andere Frameworks über Parameter in der Url ab. Wicket-Komponenten können viel mehr ihren eigenen Zustand über normale Java-Sprachmittel abbilden. Eine Komponente kann auf diese Weise die komplexesten Informationen halten, die möglicherweise sehr schwer über Url-Parameter abgebildet werden könnten. Das hat Vorteile, aber eben auch Nachteile. Der vermutlich größte Nachteil ist der Speicherbedarf, denn die Komponenten müssen für jeden Nutzer getrennt erzeugt und verwaltet werden. Anwendungen, die den Zustand einer Nutzerinteraktion immer über Url-Parameter abbilden, können nach dem ausliefern der Seite alle temporär erzeugten Daten verwerfen.

Wicket arbeitet anders. In Wicket ist eine Seite eine Sammlung verschiedenster Komponenten und Zustände in beliebigen Objekten. Wenn der Nutzer eine Aktion auf der Seite auslöst, wird die passende Komponente in diesem Komponentenbaum gesucht und die entsprechende Methode aufgerufen. Jede Interaktion erzeugt einen neuen Zustand und Wicket erzeugt eine neue Version der Seite für den Fall, dass der Nutzer z.B. mit dem Browser-Back-Buttom auf eine ältere Seite mit einem anderen Zustand zurück navigiert. Da der Arbeitsspeicher meist ein knappes Gut ist, werden die älteren Versionen einer Seite als Objektgraph mit all den Zuständen serialisiert und auf einem Datenträger abgelegt. Und hier wird es nun spannend.

Je mehr Zustand die Komponenten halten, je mehr Daten dafür notwendig sind, desto größer ist natürlich der Speicherbedarf zu Laufzeit aber auch der Platzbedarf auf dem Datenträger. Um nicht unnötig den Speicher und die Festplatte mit temporären Daten zu belasten, wird nach dem Ausliefern der Ergebnisseite für alle Komponenten die detach()-Methode aufgerufen, die sich darum kümmern muss, alles was für den Zustand der Komponente irrelevant ist, aufzuräumen.

Überraschung vorraus

Nun hat man seine Wicket-Komponenten geschrieben und vermutet, dass man alles richtig gemacht hat oder hat einfach akzeptiert, dass z.B. der Speicherbedarf so hoch sein muss wie er eben gerade ist. Wenn man genügend Speicher hat, muss man ja nichts verändern, aber das ist vermutlich selten der Fall. Zum Glück bietet die Vorgehensweise, die Wicket an den Tag legt sehr interessante Möglichkeiten, um verschiedeneste Optimierungen vorzunehmen.

Wenn die Menge der Daten, die auf Festplatte geschrieben werden sehr hoch ist und die die CPU's im Rechner eher langweilen, kann man recht günstig Plattenplatz und Rechnengeschwindigkeit eintauschen (typische Kompression 70%, Mehrbelastung der CPU vermutlich 1-5%). Dafür benutzt man statt dem Standard-Serializer einfach die DeflatedJavaSerializer-Implementierung. Vorsicht: die Daten, die mit dem Standard-Serializer auf die Festplatte geschrieben wurden, können nicht zurückgelesen werden.

Besser wäre es natürlich, gleich von Anfang an weniger Temporärdaten zu erzeugen. Doch was genau welchen Platz im Speicher und später auf der Festplatte belegt, ist meist nicht ganz so einfach zu ermitteln.

Der Anfang

Zu erst habe ich versucht, die Funktion, die bei Wicket für die aufschlußreiche Darstellung sorgt, welche Komponente nicht serialisierbare Objekte enthält entsprechend zu erweitern. Allerdings steigt man da in eine der eher nicht so gut gelüfteten Kammern von Java hinunter und kann sich sicher leicht verirren, so das eine andere Alternative gefunden werden musste. Zum Glück hatten dieses Gefühl wohl auch andere und haben das Projekt Kryo gestartet (http://code.google.com/p/kryo/). Kryo ist eine Alternative zur Serialisierung von Javaobjekten, die sehr konfigurierbar und trotz Reflection auch schneller und kompakter serialisieren soll.

Dann gab es recht schnell bei wicketstuff ein Projekt, dass Kryo in Wicket integriert hat. Allerdings bisher nur für die Version 1.x. Ich habe das dann für Kryo2 nachgeholt und entsprechend modularisiert, so dass man in den Serialisierungsprozess eingreifen kann und auf diese Weise verschiedene Informationen ermitteln kann. Doch dazu später mehr.

Setup

Zuerst muss man sein Wicket-Projekt um eine Dependency erweitern:

<dependency>
  <groupId>org.wicketstuff</groupId>
  <artifactId>wicketstuff-serializer-kryo2</artifactId>
  <version>${wicket.version}</version>
</dependency>

Dabei gibt es sowohl für Wicket 1.5 (ab Version 1.5.9) als auch für Wicket 6 (ab Version 6.2.1) eine Version in Maven Central.

Dann ergänzen wir unsere WebApplication-Klasse in der init()-Methode um folgende Codezeilen:

// output of report of type sizes, sorted tree report (by size), aggregated tree 
ISerializedObjectTreeProcessor typeAndSortedTreeAndCollapsedSortedTreeProcessors = TreeProcessors.listOf(
new TypeSizeReport(), new SortedTreeSizeReport(), new SimilarNodeTreeTransformator(
new SortedTreeSizeReport()));

// strips class object writes from tree
TreeTransformator treeProcessors = new TreeTransformator(
typeAndSortedTreeAndCollapsedSortedTreeProcessors,
TreeTransformator.strip(new TypeFilter(Class.class)));

// serialization listener notified on every written object
ISerializationListener serializationListener = SerializationListeners.listOf(
new DefaultJavaSerializationValidator(), new AnalyzingSerializationListener(
new NativeTypesAsLabel(new ComponentIdAsLabel()), treeProcessors));

// customized serializer
InspectingKryoSerializer serializer = new InspectingKryoSerializer(Bytes.megabytes(30L),
serializationListener);

// set custom serializer as default
getFrameworkSettings().setSerializer(serializer);

getStoreSettings().setAsynchronous(false);
getStoreSettings().setInmemoryCacheSize(0);
getPageSettings().setVersionPagesByDefault(true);

Ab sofort wird für jede Seite, die serialisiert wird, eine detailierte Übersicht über die verwendeten Komponenten und den Speicherbedarf ausgegeben.

Beispielausgabe

Wenn nun eine Seite von Wicket serialisiert wird, dann wird während des Schreibens des Objektgraphen die Anzahl der geschriebenen Bytes ermittelt und den passenden Objekten zugeteilt. Dabei gibt es gewisse Unschärfen. Wenn ein Objekt referenziert wird, wird nur das erste mal das Objekt selbst geschrieben. Jedes weitere mal wird nur eine Referenz auf das Objekt serialisiert, was natürlich viel weniger Platz in Anspruch nimmt. Insofern ist die Auswertung immer dann mit Vorsicht zu genießen, wenn ein Objekt an verschiedenen Stellen referenziert wird und nur einer den "Zuschlag" erhält.

Die Ausgabe und der ganze Serialisierungsprozess ist hochgradig anpassbar. In der Beispielkonfiguration entsteht für eine Beispielseite folgendes Ergebnis:

DEBUG - KryoSerializer             - Going to serialize: '[Page class = de.wicketpraxis.usecase.models.ModelsV1Page, id = 4, render count = 1]'
DEBUG - TypeSizeReport             - 
===================================================================
|Type........................................................bytes|
-------------------------------------------------------------------
|de.wicketpraxis.usecase.models.ModelsV1Page...................133|
|org.apache.wicket.markup.html.basic.Label.....................102|
|java.lang.Integer..............................................21|
|java.lang.String................................................9|
|de.wicketpraxis.usecase.models.ModelsV1Page$1...................3|
|de.wicketpraxis.usecase.models.ModelsV1Page$2...................3|
|org.apache.wicket.request.mapper.parameter.PageParameters.......3|
|[Ljava.lang.Object;.............................................2|
===================================================================

DEBUG - TreeSizeReport             - 
===========================================================================================
|  #|Type.............................................................%|.sum|.local|.child|
-------------------------------------------------------------------------------------------
| #0|de.wicketpraxis.usecase.models.ModelsV1Page(4)................100%|.276|...129|...147|
| #4|  [Ljava.lang.Object;..........................................48%|.135|.....2|...133|
|#12|    org.apache.wicket.markup.html.basic.Label(now2)............24%|..67|....51|....16|
|#15|      java.lang.Integer(=1343434906)............................1%|...5|.....5|.....0|
|#17|      java.lang.String("now2")..................................1%|...5|.....5|.....0|
|#14|      org.apache.wicket.model.LoadableDetachableModel...........1%|...4|.....3|.....1|
| #0|        de.wicketpraxis.usecase.models.ModelsV1Page(4)..........0%|...1|.....1|.....0|
|#16|      java.lang.Integer(=-1)....................................0%|...1|.....1|.....0|
| #0|      de.wicketpraxis.usecase.models.ModelsV1Page(4)............0%|...1|.....1|.....0|
| #5|    org.apache.wicket.markup.html.basic.Label(now).............23%|..66|....51|....15|
| #9|      java.lang.Integer(=1343434906)............................1%|...5|.....5|.....0|
| #8|      org.apache.wicket.model.LoadableDetachableModel...........1%|...4|.....3|.....1|
| #0|        de.wicketpraxis.usecase.models.ModelsV1Page(4)..........0%|...1|.....1|.....0|
|#11|      java.lang.String("now")...................................1%|...4|.....4|.....0|
|#10|      java.lang.Integer(=-1)....................................0%|...1|.....1|.....0|
| #0|      de.wicketpraxis.usecase.models.ModelsV1Page(4)............0%|...1|.....1|.....0|
|#18|  java.lang.Integer(=1342845082)................................1%|...5|.....5|.....0|
|#22|  org.apache.wicket.request.mapper.parameter.PageParameters.....1%|...3|.....3|.....0|
| #2|  java.lang.Integer(=1).........................................0%|...1|.....1|.....0|
|#19|  java.lang.Integer(=-1)........................................0%|...1|.....1|.....0|
|#20|  java.lang.Integer(=4).........................................0%|...1|.....1|.....0|
|#23|  java.lang.Integer(=1).........................................0%|...1|.....1|.....0|
===========================================================================================

DEBUG - TreeSizeReport             - 
============================================================================================
|   #|Type.............................................................%|.sum|.local|.child|
--------------------------------------------------------------------------------------------
|  #0|de.wicketpraxis.usecase.models.ModelsV1Page(4)................100%|.276|...129|...147|
|  #4|  [Ljava.lang.Object;..........................................48%|.135|.....2|...133|
| #12|    org.apache.wicket.markup.html.basic.Label(now2)............24%|..67|....51|....16|
|null|      java.lang.Integer(=1343434906|=-1)........................2%|...6|.....6|.....0|
| #17|      java.lang.String("now2")..................................1%|...5|.....5|.....0|
| #14|      org.apache.wicket.model.LoadableDetachableModel...........1%|...4|.....3|.....1|
|  #0|        de.wicketpraxis.usecase.models.ModelsV1Page(4)..........0%|...1|.....1|.....0|
|  #0|      de.wicketpraxis.usecase.models.ModelsV1Page(4)............0%|...1|.....1|.....0|
|  #5|    org.apache.wicket.markup.html.basic.Label(now).............23%|..66|....51|....15|
|null|      java.lang.Integer(=1343434906|=-1)........................2%|...6|.....6|.....0|
|  #8|      org.apache.wicket.model.LoadableDetachableModel...........1%|...4|.....3|.....1|
|  #0|        de.wicketpraxis.usecase.models.ModelsV1Page(4)..........0%|...1|.....1|.....0|
| #11|      java.lang.String("now")...................................1%|...4|.....4|.....0|
|  #0|      de.wicketpraxis.usecase.models.ModelsV1Page(4)............0%|...1|.....1|.....0|
|null|  java.lang.Integer(=1|=1342845082|=-1|=4)......................3%|...9|.....9|.....0|
| #22|  org.apache.wicket.request.mapper.parameter.PageParameters.....1%|...3|.....3|.....0|
============================================================================================

Die erste Tabelle enthält eine Aufschlüsselung nach Typen, die zweite Tabelle stellt den fast ungefilterten Komponentenbaum dar (die Klasse Component verwaltet Kindkomponenten und andere Dinge in einem Object-Array, weshalb im Baum [Ljava.lang.Object auftaucht). Die dritte Tabelle versucht gleiche Zweige zusammen zu fassen. Dabei werden die Größen aufadiert und die Labels (in Klammern dahinter) vereint. Die Angaben in den Klammern hinter den Klassennamen stellen ein Label für die Klasse dar. Bei Wicket-Komponenten ist das die Id, bei nativen Datentypen der Wert.

Und das nächste Mal sehen wir uns an, was für interessante Entdeckungen man damit machen kann. Doch zuvor ein Hinweis. Das sollten sie so nie in Produktivumgebungen einsetzen. Auch kann es bei bestehenden Anwendung zu sehr sehr umfangreichen Ausgaben kommen, so dass man in einer IDE die Logausgabe vielleicht besser in eine Datei umlenkt.

Viel Spass mit dem Experimentieren.

Den Code für die Beispielen findet man unter https://github.com/michaelmosmann/wicket-praxis/tree/master/quickstarts/de.wicketpraxis-quickstarts-15-kryo2 .

Tags:

Veröffentlicht in Maven, Refactoring, Wicket, .

Wicket - Back Button zuverlässig erkennen

Die Entwicklung moderner Webanwendungen orientiert sich zunehmend am Interaktionsmodel wie es Desktopanwendungen bieten. Wicket unterstütz dabei erheblich, in dem z.B. der Einsatz von Ajax keine große Hürde mehr darstellt. Eine Interaktionsmöglichkeit ist Desktopanwendungen doch bisher eher fremd: der Back Button im Browser. Der Nutzer erwartet, dass er auf einen vorherigen Zustand zurück navigieren kann. Je komplexer die Webanwendung, desto aufwendiger war es mit anderen Frameworks, die Nutzererwartung auch in ein sinnvolles Anwendungsverhalten zu übertragen. Wicket bietet mit versionierten Seiten bereits eine gute Unterstützung, die einem Entwickler ermöglicht, die kleine "Zeitreise", die der Nutzer vornimmt, auf Anwendungsseite entsprechend abbilden zu können.

Trotzdem möchte man unter Umständen wissen, dass der Nutzer den Back Button betätigt hat, weil es vielleicht auch einen Rückschluss darauf zulässt, dass der Nutzer in der Anwendung nicht die richtige Interaktionsmöglichkeit gefunden hat (z.B. in einem Bestellvorgang doch nochmal die Anschrift ändern zu wollen).

Folgendes Beispiel ist eine Möglichkeit, zu erkennen, ob der Nutzer mit dem Browser vor und zurück navigiert hat. Dabei gibt es zwei Dinge zu beachten. Der erste Seitenaufruf darf nicht in einer Url enden, die den Konstruktor der Seite aufruft. Denn dann sieht das aus Anwendungssicht so aus, als ob der Nutzer erneut auf die Seite navigiert ist. Zum zweiten muss man den Browser dazu zwingen, die Seite in jedem Fall neu vom Server abzurufen.

Daher brauchen wir zu erst eine Sprungseite:

package de.wicketpraxis.web.blog.pages.questions.backbutton;

import org.apache.wicket.markup.html.WebPage;

public class BackButtonStartPage extends WebPage {

  public BackButtonStartPage() {
    setResponsePage(new BackButtonPage());
    setRedirect(true);
  }
}

Die Sprungseite ruft die eigentliche Seite auf. Außerdem ist der Aufruf von setRedirect() notwendig, damit nicht nur die neue Seite dargestellt wird, sondern sich auch die Url entsprechend ändert.

Die Seite mit der Erkennung der Button-Nutzung sieht dann wie folgt aus:

package de.wicketpraxis.web.blog.pages.questions.backbutton;

import java.util.Date;

import org.apache.wicket.MetaDataKey;
import org.apache.wicket.Session;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.protocol.http.WebResponse;

public class BackButtonPage extends WebPage {

  static final TimeStampKey KEY = new TimeStampKey();
  private long _pageInitTimeStamp;

  public BackButtonPage() {
    //setVersioned(true);

    _pageInitTimeStamp = new Date().getTime();
    setPageTimeStamp(_pageInitTimeStamp);

    add(new FeedbackPanel("feedback"));

    add(new Link<Void>("link") {

      @Override
      public void onClick() {
        setResponsePage(new BackButtonPage());
      }
    });
  }

  private static void setPageTimeStamp(long timeStamp) {
    Session.get().setMetaData(KEY, timeStamp);
  }

  @Override
  protected void onBeforeRender() {
    super.onBeforeRender();

    Long lastPageRendered = Session.get().getMetaData(KEY);
    setPageTimeStamp(_pageInitTimeStamp);

    if (lastPageRendered > _pageInitTimeStamp) {
      info("BackButton pressed");
    }
    if (lastPageRendered < _pageInitTimeStamp) {
      info("ForwardButton pressed");
    }
  }

  @Override
  protected void configureResponse() {
    super.configureResponse();
    WebResponse response = getWebRequestCycle().getWebResponse();
    response.setHeader("Cache-Control", "no-cache, max-age=0,must-revalidate, no-store");
  }

  static class TimeStampKey extends MetaDataKey<Long> {

  }
}

Das Markup der Seite:

<html>
  <head>
    <title>BackButton Page</title>
  </head>
  <body>
    <div wicket:id="feedback"></div>
    <a wicket:id="link">Link</a>
  </body>
</html>

Das Prinzip funktioniert wie folgt: Die Seite bekommt einen Zeitstempel über den Zeitpunkt der Erstellung. Dieser Zeitpunkt wird außerdem als Metainformation in die Session gesetzt (Hinweis: um diese Funktion bei mehr als einer Seite benutzen zu können, ist natürlich etwas mehr Aufwand nötig). Das bedeutet, dass jede Version der Seite einen eigenen Zeitstempel hat. In onBeforeRender() kann man nun prüfen, ob diese Seite auch die aktuelle Seite ist, oder ob der Nutzer auf einen neuere oder ältere Version zurückgegriffen hat. Der Link dient dazu, immer neue Version der Seite zu erzeugen.

Damit der Browser die Seite aber in jedem Fall neu lädt, muss man entsprechende Header in den Http-Response einfügen. Eine Möglichkeit besteht darin, die configureResponse()-Methode entsprechend zu überschreiben.

Wenn man nun auf der Seite den Link klickt und dann mit dem Back Button oder dem Forward Button navigiert, erscheinen die entsprechenden Hinweise auf der Seite.

Tags:

Veröffentlicht in Allgemein, Wicket, .

Wicket - Mainstream, Github, MongoDB

In den letzten Wochen und Monaten habe ich recht wenig neues über Wicket gelesen. Man könnte meinen, dass es ruhig geworden ist um Wicket. Doch wenn man sich z.B. die Mailinglisten ansieht, spricht das eine deutlich andere Sprache. Jobangebote fragen zunehmend ab, ob Wicket-KnowHow vorhanden ist. Man sieht mehr und mehr Anwendungen, die mit Wicket realisiert wurden. Wicket ist im Mainstream angekommen.

Vielleicht ist das auch nur eine sehr persönliche Sichtweise, aber ich glaube, das auch Git im Mainstream angekommen ist. Git wurde bereits in bestimmten Bereichen auf einer breiten Basis eingesetzt. Aber der bedeutende Unterschied liegt darin, dass man sich bei neuen Projekten eigentlich immer die Frage stellt: Subversion oder besser gleich Git?

Ich habe alle meine Projekte von Subversion auf Git umgestellt und dabei gemerkt, das Subversion und Git nicht wirklich aufeinander abbildbar sind, aber die Vorteile die Git mitbringt sich selbst dann auszahlen, wenn man Git noch wie Subversion benutzt. Man findet ab jetzt die Quelltexte für das Buch bei Github unter http://github.com/michaelmosmann/wicket-praxis.git.

Da ich in meinen aktuellen Projekten mich mit der Frage beschäftigt habe, ob nicht eine Dokumentendatenbank wie MongoDB die bessere Alternative ist, sei an dieser Stelle auch noch auf einen Objektmapper verwiesen, denn in ich diesem Zusammenhang entwickelt habe: http://github.com/michaelmosmann/mongoom.flapdoodle.de

In Zukunft an dieser Stelle wieder mehr und dann vielleicht rund um Wicket in Zusammenarbeit mit Dokumentendatenbanken.

Tags:

Veröffentlicht in Allgemein, Wicket, .

Wicket - Flexibilität mit Factories

Komplexe Komponenten entstehen in Wicket durch das zusammenfügen von einfacheren Komponenten. Dabei werden die verwendeten Komponenten direkt adressiert. Nach außen ist nicht sichtbar, wie sich eine Komponente zusammensetzt. Um von dieser Komponente eine leicht abgewandelte Form zu erstellen, kann man auf z.B. Vererbung zurückgreifen, Komponenten ausblenden, das Markup überschreiben. Je mehr Variationen nötig sind, desto komplizierter wird der Aufbau. Der Aufwand steigt erheblich an.

Einen Ausweg aus dieser Situation könnte die Verwendung von Factories liefern. Dazu benötigen wir ein sehr einfach gehaltenes Interface:

package de.wicketpraxis.web.blog.pages.questions.factories;

import org.apache.wicket.Component;

public interface IComponentFactory<T extends Component>
{
  T newComponent(String id);
}

Eine einfach Implementierung, die immer ein Label mit einem bestimmten Text liefert, können wir wie folgt implementieren:

package de.wicketpraxis.web.blog.pages.questions.factories;

import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.IModel;

public class LabelFactory implements IComponentFactory<Label>
{
  IModel<?> _model;
  
  public LabelFactory(IModel<?> model)
  {
    _model = model;
  }
  
  public Label newComponent(String id)
  {
    return new Label(id,_model);
  }
}

Wir übergeben hierbei ein Model, das durch das Label angezeigt wird. Soll ein anderer Text angezeigt werden, muss man dafür eine neue Factory erstellen. Bis jetzt ist noch kein Vorteil dieser Lösung absehbar. Deshalb steigern wir etwas die Komplexität. Wir erstellen eine Factory, die einen Rahmen um eine Komponente ziehen kann. Dabei wird für die Darstellung das style-Attribut erweitert.

package de.wicketpraxis.web.blog.pages.questions.factories;

import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;

public class BorderPanelFactory implements IComponentFactory<Panel>
{
  private final IComponentFactory<? extends Component> _content;
  private final IModel<String> _style;

  public BorderPanelFactory(IComponentFactory<? extends Component> content, IModel<String> style)
  {
    _content = content;
    _style = style;
  }
  
  public Panel newComponent(String id)
  {
    return new BorderPanel(id, _content, _style);
  }
  
  static class BorderPanel extends Panel
  {
    public BorderPanel(String id,IComponentFactory<? extends Component> content,IModel<String> style)
    {
      super(id);
      
      WebMarkupContainer border=new WebMarkupContainer("border");
      border.add(content.newComponent("content"));
      border.add(new AttributeAppender("style", true, style,";"));
      add(border);
    }
  }
}

Wir übergeben daher eine Factory, die Komponenten erzeugt und ein Model, dass die Styleattribute beinhaltet. In dem Panel, was innerhalb der Factory erzeugt wird, wir dann eine Komponente eingebunden ("content"), die aus der übergebenen Factory kommt. Wir benötigen noch eine passende Markup-Datei (BorderPanelFactory$BorderPanel.html):

<wicket:panel>
  <div wicket:id="border" style="padding: 8px">
    <wicket:container wicket:id="content"></wicket:container>
  </div>
</wicket:panel>

Um zu demonstrieren, welche Flexibilität man mit diesen wenigen Klassen bereits erreicht hat, verwenden wir beide Factories in einem Beispiel:

package de.wicketpraxis.web.blog.pages.questions.factories;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.model.Model;

public class ComponentFactoryPage extends WebPage
{
  public ComponentFactoryPage()
  {
    Model<String> redBorderStyle = Model.of("border:1px solid red; background-color: #fff0f0;");
    Model<String> greenBorderStyle = Model.of("border:1px solid green; background-color: #f0fff0;");
    Model<String> blueBorderStyle = Model.of("border:1px solid blue; background-color: #f0f0ff;");
    
    LabelFactory haveFunLabelFactory = new LabelFactory(Model.of("Have Fun"));
    
    BorderPanelFactory redBorderHasFunFactory = new BorderPanelFactory(haveFunLabelFactory,redBorderStyle);
    BorderPanelFactory greenBorderWrapsRedFactory = new BorderPanelFactory(redBorderHasFunFactory,greenBorderStyle);
    BorderPanelFactory blueBorderWrapsAllFactory = new BorderPanelFactory(greenBorderWrapsRedFactory,blueBorderStyle);
    
    add(blueBorderWrapsAllFactory.newComponent("element"));
  }
}

Wir erstellen 3 Modelle mit unterschiedlichen Werten für das style-Attribut. Um etwas Text anzuzeigen benutzen wir die LabelFactory. Danach werden drei BorderPanelFactory-Instanzen erzeugt, die eine andere Factory "umwickelt". Zum Schluss wird ein Element erzeugt und in der Seite benutzt. Das Markup ist entsprechend einfach:

<html>
  <head>
    <title>ComponentFactory Page</title>
  </head>
  <body>
    <wicket:container wicket:id="element"></wicket:container>
  </body>
</html>

Das Ergebnis sieht dann wie folgt aus:

Um zu zeigen, wie schnell die Möglichkeiten wachsen, die man mit diesem Ansatz abdecken kann, erstellen wir eine weitere Factory. In diesem Fall möchten wir zwei Elemente nebeneinander dargestellen:

package de.wicketpraxis.web.blog.pages.questions.factories;

import org.apache.wicket.Component;
import org.apache.wicket.markup.html.panel.Panel;

public class TwoInARowFactory implements IComponentFactory<Component>
{
  private final IComponentFactory<? extends Component> _left;
  private final IComponentFactory<? extends Component> _right;

  public TwoInARowFactory(IComponentFactory<? extends Component> left, IComponentFactory<? extends Component> right)
  {
    _left = left;
    _right = right;
  }
  
  public Component newComponent(String id)
  {
    return new ContainerPanel(id, _left, _right);
  }
  
  static class ContainerPanel extends Panel
  {
    public ContainerPanel(String id,IComponentFactory<? extends Component> left, IComponentFactory<? extends Component> right)
    {
      super(id);
      
      add(left.newComponent("left"));
      add(right.newComponent("right"));
    }
  }
}

Es werden daher zwei Factories übergeben, die dann für die Erzeugung des linken und des rechten Elements zuständig sind. Das Markup benutzt der Einfachheit halber Html-Tabellen für die Anordnung:

<wicket:panel>
  <table>
    <tr>
      <td><wicket:container wicket:id="left"></wicket:container></td>
      <td><wicket:container wicket:id="right"></wicket:container></td>
    </tr>
  </table>
</wicket:panel>

Unsere Seitenklasse ergänzen wir entsprechend:

package de.wicketpraxis.web.blog.pages.questions.factories;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.model.Model;

public class ComponentFactoryPage extends WebPage
{
  public ComponentFactoryPage()
  {
    Model<String> redBorderStyle = Model.of("border:1px solid red; background-color: #fff0f0;");
    Model<String> greenBorderStyle = Model.of("border:1px solid green; background-color: #f0fff0;");
    Model<String> blueBorderStyle = Model.of("border:1px solid blue; background-color: #f0f0ff;");
    
    LabelFactory haveFunLabelFactory = new LabelFactory(Model.of("Have Fun"));
    
    BorderPanelFactory redBorderHasFunFactory = new BorderPanelFactory(haveFunLabelFactory,redBorderStyle);
    BorderPanelFactory greenBorderWrapsRedFactory = new BorderPanelFactory(redBorderHasFunFactory,greenBorderStyle);
    BorderPanelFactory blueBorderWrapsAllFactory = new BorderPanelFactory(greenBorderWrapsRedFactory,blueBorderStyle);
    
    add(blueBorderWrapsAllFactory.newComponent("element"));
    
    TwoInARowFactory twoInARowFactory = new TwoInARowFactory(redBorderHasFunFactory, greenBorderWrapsRedFactory);
    
    add(twoInARowFactory.newComponent("two"));
  }
}

Das Markup muss ebenfalls angepasst werden:

<html>
  <head>
    <title>ComponentFactory Page</title>
  </head>
  <body>
    <wicket:container wicket:id="element"></wicket:container>
    <wicket:container wicket:id="two"></wicket:container>
  </body>
</html>

Das Ergebnis kann sich sehen lassen:

Wie man an diesem Beispiel sehr gut erkennen kann, liegt in diesem Ansatz sehr viel Potential, gerade wenn die Anforderungen an die Flexibilität sehr hoch sind. In Projekten, die eine hohe Flexibilität erforderten hat sich dieses System bereits erfolgreich bewährt. Dabei kommt eine Kombinationen aus dem "klassischen" und dem Factory-Ansatz zum Einsatz, wodurch sich die meisten Anforderungen wesentlich besser abdecken lassen.

Gibt es noch ganz andere Lösungsstrategien?

Tags:

Veröffentlicht in Refactoring, Wicket, .