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 - AbstractEvent Update

Wie ich bereits beschrieben habe, kann man mit Wicket relativ leicht eine lose Koppelung von Komponenten über einen Event-Mechanismus erreichen. Dabei wurden bisher nur Komponenten berücksichtigt. Manchmal ist es aber sinnvoll, die Behandlung dieser Events an einer zentralen Stelle durchzuführen. Daher habe ich die Basisklasse leicht erweitert. Dabei ist der Visitior in eine eigene Klasse gewandert. Außerdem ist es wesentlich geschickter, auch bei nicht Ajax-Requests mit Events zu arbeiten. Die Modifikation des EventListener-Interface trägt dem Rechnung.

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

public interface EventListenerInterface
{
  public void notifyEvent(AbstractEvent event);
}

Die Methode lautet nun notifyEvent statt notifyAjaxEvent .

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

import org.apache.wicket.Component;
import org.apache.wicket.Component.IVisitor;

public class NotifyVisitor implements IVisitor<Component> 
{
  private final AbstractEvent _event;
   
  public NotifyVisitor(final AbstractEvent event)
  {
    _event = event;
  }
   
  public Object component(final Component component)
  {
    if (component instanceof EventListenerInterface) 
    {
      ((EventListenerInterface) component).notifyEvent(_event);
    }
    return IVisitor.CONTINUE_TRAVERSAL;
  }
}

Der Event sucht jetzt nicht nur in allen Komponenten nach dem EventListenerInterface, sondern auch in der Application, der Session und der Page.

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

import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.Page;
import org.apache.wicket.Session;
import org.apache.wicket.Component.IVisitor;
import org.apache.wicket.ajax.AjaxRequestTarget;

public class AbstractEvent
{
  private final Component _source;
  protected final AjaxRequestTarget _target;
  
  public AbstractEvent(final Component source, AjaxRequestTarget target)
  {
    _source = source;
    _target = target;
  }
  
  public AbstractEvent(final Component source)
  {
    this(source,null);
  }
  
  public Component getSource()
  {
    return _source;
  }
  
  /**
   * notify Application, Session, Page and Components
   */
  public final void fire()
  {
    Page page=getSource().getPage();
    
    Application application=page.getApplication();
    if (application instanceof EventListenerInterface)
    {
      ((EventListenerInterface) application).notifyEvent(this);
    }
    Session session=page.getSession();
    if (session instanceof EventListenerInterface)
    {
      ((EventListenerInterface) session).notifyEvent(this);
    }
    if (page instanceof EventListenerInterface)
    {
      ((EventListenerInterface) page).notifyEvent(this);
    }
    page.visitChildren(EventListenerInterface.class, getVisitor());
    onAfterFire();
  }

  protected  void onAfterFire()
  {
    
  }

  protected IVisitor<Component> getVisitor()
  {
    return new NotifyVisitor(this);
  }

  public AjaxRequestTarget getTarget()
  {
    return _target;
  }
  
  public boolean isAjax()
  {
    return _target!=null;
  }

  public final void update(Component component)
  {
    if (_target!=null)
    {
      _target.addComponent(component);
    }
  }

}

Hinzugekommen ist außerdem die Funktion onAfterFire() die aufgerufen wird, sobald alle möglichen Listener benachrichtig wurden. An dieser Stelle könnte man dann Prüfungen einbauen, ob z.B. der Event überhaupt ein Ziel erreicht hat.

Ich glaube, dass diese Lösung zu offensichtlich war, so dass man sie leicht übersehen konnte.

Tags:

Veröffentlicht in Wicket, .

Wicket Heatmap - Ajax mit Parametern

Wer wissen möchte, wohin die Nutzer in der eigenen Anwendung so klicken (z.B. auf Dinge, von denen man selbst nicht annehmen würde, das Nutzer darauf klicken), der muss jeden Mausklick des Nutzers aufzeichnen. Aus diesen Daten kann man dann ermitteln, wohin die Nutzer ihren Mauszeiger so wandern lassen. Dafür gibt es bereits Opensourcelösungen, die meist auf PHP basieren. Unter dem Suchbergriff Heatmap wird man auch bei Google fündig. Da sich alles in diesem Blog um Java und dann noch um Wicket dreht, lag es natürlich nahe, zu prüfen, ob und wie man diese Anforderung mit Wicket realisieren kann. Dieser Beitrag wäre viel kürzer, wenn man die Frage nach dem "ob" mit nein beantworten müsste. Kommen wir also zum "wie?".

Für die Umsetzung habe ich mir anfänglich einiges aus dem Beispielen aus folgendem Blogbeitrag entlehnt. Im Laufe der Zeit ist zwar davon nicht mehr viel zu sehen, das Grundprinzip ist aber das Gleiche geblieben.

Folgende Fragestellung stand am Anfang dieses Versuchs: Wie kann man mit Wicket Werte in einem AjaxRequest übergeben (z.B. die Mausposition). Die erste Idee bestand darin, ein unsichtbares Formular zu erstellen, in das man die Werte per Javascript einfügt und dieses Formular per Ajax abschickt. Das hat auch funktioniert, war aber irgendwie auch ein wenig zu aufwendig. Nach einer Reihe missglückter Versuche, den richtigen Ansatz zu finden, stellte ich diese Frage in der Wicket-Mailingliste und bekam den entscheidenen Tipp von Ernesto Reinaldo Barreiro. Es gab einen Vortrag der London Wicket User Group, der dieses Problem löste. Mit dieser Vorarbeit begann ich das Thema umzusetzen. Dabei hat sich die eigene Implementierung vom Original entfernt. Hier nun Schritt für Schritt der vollständige Code:

AbstractParameterizedDefaultAjaxBehavior

package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;

import java.util.HashMap;
import java.util.Map;

import org.apache.wicket.Request;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.util.time.Duration;

public abstract class AbstractParameterizedDefaultAjaxBehavior extends AbstractDefaultAjaxBehavior
{
  static int sec=0;
  
  private Duration _throttleDelay;

  @Override
  public void renderHead(IHeaderResponse response)
  {
    super.renderHead(response);
    response.renderJavascriptReference(new ResourceReference(AbstractParameterizedDefaultAjaxBehavior.class,"AbstractParameterizedDefaultAjaxBehavior.js"));
  }
  
  @Override
  protected void respond(AjaxRequestTarget target)
  {
    Request request = RequestCycle.get().getRequest();
    
    Map<String,Object> map=new HashMap<String, Object>();
    Parameter<?>[] parameter = getParameter();
    for (Parameter<?> p : parameter)
    {
      String svalue = request.getParameter(p.getName());
      if (svalue!=null)
      {
        Object value=getComponent().getConverter(p.getType()).convertToObject(svalue, getComponent().getLocale());
        map.put(p.getName(), value);
      }
    }
    
    respond(target, new ParameterMap(map));
  }
  
  @Override
  public CharSequence getCallbackUrl(boolean onlyTargetActivePage)
  {
    StringBuilder sb=new StringBuilder();
    sb.append(super.getCallbackUrl(onlyTargetActivePage));
    
    Parameter<?>[] parameter = getParameter();
    for (Parameter<?> p : parameter)
    {
      sb.append("&").append(p.getName()).append("='+").append(p.getJavascript()).append("+'");
    }
    
    return sb.toString();
  }
  
  @Override
  protected final CharSequence getCallbackScript()
  {
    if (_throttleDelay!=null)
    {
      return throttleScript(super.getCallbackScript(),"thw"+(sec++),_throttleDelay);
    }
    return super.getCallbackScript();
  }
  
  protected static class Parameter<T>
  {
    String _name;
    String _javascript;
    Class<T> _type;
    
    protected Parameter(String name,Class<T> type,String javascript)
    {
      _name=name;
      _type=type;
      _javascript=javascript;
    }
    
    protected String getName()
    {
      return _name;
    }
    protected String getJavascript()
    {
      return _javascript;
    }
    protected Class<T> getType()
    {
      return _type;
    }
  }
  
  protected static <T> Parameter<T> of(String name,Class<T> type,String javascript)
  {
    return new Parameter<T>(name, type, javascript);
  }
  
  protected static class ParameterMap
  {
    Map<String, Object> _map;
    
    protected ParameterMap(Map<String, Object> map)
    {
      _map=map;
    }
    
    public <T> T getValue(Parameter<T> parameter)
    {
      return (T) _map.get(parameter.getName());
    }
  }
  
  public final AbstractParameterizedDefaultAjaxBehavior setThrottleDelay(Duration throttleDelay)
  {
    _throttleDelay=throttleDelay;
    return this;
  }
  
  protected abstract void respond(AjaxRequestTarget target,ParameterMap parameterMap);
  
  protected abstract Parameter<?>[] getParameter();

}

Die drei wichtigsten Bestandteile dieser Klasse sind folgende: Die Klasse Parameter definiert den Namen, den Typ, und das Javascript, das für das Ermitteln des Wertes im Browser aufgerufen muss. Die Methode respond(AjaxRequestTarget) liest die Werte aus dem Request aus, konvertiert diese in den gewünschen Typ und ruf damit eine zu überschreibende Methode auf. Die Methode getCallbackUrl() liefert das Javascript-Fragment für die Url, die dann per Ajax aufgerufen wird.

Um die Position des Mauszeigers ermitteln zu können, muss man sich mit einer eigenen Funktion für so einen Event registrieren. Damit mehr als eine Funktion auf so einen Event reagieren kann, sollte man die Funktion, die davor registriert war, ebenfalls aufrufen. Um für diese Problematik, die vermutlich in allen abgeleiteten Klassen vorhanden ist, besser lösen zu können, binden wir automatisch eine hilfreiche Javascript-Klasse ein.

Callback = {
  create: function(oldCallback,newCallback)
  {
    return function(a,b,c,d,e,f)
    {
      if (oldCallback) 
      {
        oldCallback(a,b,c,d,e,f);
      }
      newCallback(a,b,c,d,e,f);
    }
  },
};

Der Code sieht etwas merkwürdig aus. Das liegt an folgenden Gründen: zum einen stehe ich mit Javascript immer noch auf Kriegsfuß (mit Wicket kann man den Javascript-Teil sehr schön verstecken) und habe daher keine geeignete Lösung gefunden, mit der es mir möglich war, alle Argumente des Funktionsaufrufs an die zwei Funktionen weiterzureichen. Da es aber Javascript egal ist, mit wie vielen Parametern man eine Funktion aufruft, werden auf diese Weise bis zu 6 Übergabeparameter weitergereicht. Für Hinweise an dieser Stelle bin ich extrem dankbar.

WicketWindowJavascript

Für vieles bringt Wicket bereits fertige Implementierungen mit. Allerdings verstecken sich diese manchmal an unauffälligen Stellen. Für das Ermitteln des sichtbaren Bereichs fand ich die nötigen Javascript-Funktionen bei der ModelWindow-Klasse. Da ich nicht das vollständige ModalWindow-Javascript einbinden wollte, habe ich diese allgemeinen Funktionen in eine eigene Resource verpackt.

package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;

import org.apache.wicket.ResourceReference;

public class WicketWindowJavascript
{
  private WicketWindowJavascript()
  {
  }
  
  public static final ResourceReference RESOURCE=new ResourceReference(WicketWindowJavascript.class,"WicketWindowJavascript.js");
}
// aus der Datei modal.js zur Klasse ModalWindow

if (typeof(Wicket.Window) == "undefined") {
  Wicket.Window = { };
}

/**
 * Returns the height of visible area.
 */
Wicket.Window.getViewportHeight = function() {
  if (window.innerHeight != window.undefined) 
    return window.innerHeight;
  
  if (document.compatMode == 'CSS1Compat') 
    return document.documentElement.clientHeight;
    
  if (document.body) 
    return document.body.clientHeight;
     
  return window.undefined; 
}

/**
 * Returns the width of visible area.
 */
Wicket.Window.getViewportWidth =  function() {
  if (window.innerWidth != window.undefined) 
    return window.innerWidth;
     
  if (document.compatMode == 'CSS1Compat') 
    return document.documentElement.clientWidth; 
    
  if (document.body) 
    return document.body.clientWidth;
     
  return window.undefined;
}

/**
 * Returns the horizontal scroll offset
 */
Wicket.Window.getScrollX = function() {
  var iebody = (document.compatMode && document.compatMode != "BackCompat") ? document.documentElement : document.body  
  return document.all? iebody.scrollLeft : pageXOffset
}

/**
 * Returns the vertical scroll offset
 */
Wicket.Window.getScrollY = function() {
  var iebody = (document.compatMode && document.compatMode != "BackCompat") ? document.documentElement : document.body  
  return document.all? iebody.scrollTop : pageYOffset
}


/**
 * Returns element offset
 */
Wicket.Window.getXYOffset = function(obj)
{
  var curleft = 0;
  var curtop = 0;
  if (obj.offsetParent)
  {
    while (obj.offsetParent)
    {
      curleft += obj.offsetLeft;
      curtop += obj.offsetTop;
      obj = obj.offsetParent;
    }
  }
  else
  {
    if (obj.x)
    {
      curleft += obj.x;
    }
    if (obj.y)
    {
      curtop += obj.y;
    }
  }
  
  if (Wicket.Browser.isIE())
  {
    bodyElement=document.getElementsByTagName('body')[0];
    // In IE there's a default margin in the page body. If margin's not defined,
    // use defaults
    var marginLeftExplorer  = parseInt(bodyElement.style.marginLeft);
    var marginTopExplorer   = parseInt(bodyElement.style.marginTop);
    /* assume default 10px/15px margin in explorer */
    if (isNaN(marginLeftExplorer)) {marginLeftExplorer=10;}
    if (isNaN(marginTopExplorer)) {marginTopExplorer=15;}
    curleft=curleft+marginLeftExplorer;
    curtop=curtop+marginTopExplorer;
  }

  return [curleft,curtop];
}

Hinzugefügt habe ich nur die Methode Wicket.Window.getXYOffset(), um den Offset für ein bestimmtes oder das erste Kindelement der Seite ermitteln zu können. Eine Klasse, die diese Methoden benötigt, muss die Resource entsprechend einbinden.

WindowResizeBehavior

Um die Heatmap in der richtigen Größe zeichnen zu können, benötigen wir Informationen über die Dimensionen des sichtbaren Bereichs. Für window.onresize wird ein neuer Callback registriert. Die Werte können über die ensprechend Wicket.Window-Funktionen ermittelt werden. Daher wird nicht nur der Callback registiert, sondern die Funktion nach dem Laden der Seite direkt aufgerufen. Auf diese Weise bekommen wir die Informationen über den sichtbaren Bereich nicht erst, wenn der Nutzer die Fenstergröße verändert.

package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;

import org.apache.wicket.ResourceReference;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.util.time.Duration;

public abstract class WindowResizeBehavior extends AbstractParameterizedDefaultAjaxBehavior
{
  static final Parameter<Integer> WIDTH=of("width", Integer.class, "Wicket.Window.getViewportWidth()");
  static final Parameter<Integer> HEIGHT=of("height", Integer.class, "Wicket.Window.getViewportHeight()");

  @Override
  public void renderHead(IHeaderResponse response)
  {
    super.renderHead(response);
    response.renderJavascriptReference(WicketWindowJavascript.RESOURCE);
    response.renderOnDomReadyJavascript(getJavascript());
    response.renderOnDomReadyJavascript(getCallbackScript().toString());
  }
  
  protected final String getJavascript()
  {
    return "window.onresize = Callback.create(window.onresize,function () {"+getCallbackScript()+"});";
  }

  @Override
  protected final Parameter<?>[] getParameter()
  {
    return new Parameter<?>[]{ WIDTH,HEIGHT};
  }
  
  @Override
  protected void respond(AjaxRequestTarget target, ParameterMap parameterMap)
  {
    onResize(target, parameterMap.getValue(WIDTH), parameterMap.getValue(HEIGHT));    
  }
  
  protected abstract void onResize(AjaxRequestTarget target, int width, int height);
}

ElementOffsetBehavior

Die Bestimmung der richtigen Mauskoordinaten gestaltet sich insofern schwierig, als das nicht jede Webseite bei jeder Fenstergröße gleich aussieht. Damit die Koordinaten bei der Auswertung verwendbar bleiben, berechnen wir die Position in Relation zu einem Element, dass den Rahmen der Seite darstellen sollte (z.B. das erste div-Tag innerhalb des body-Tags, dass auf eine feste Breite gesetzt wurde und im sichtbaren Bereich zentriert dargestellt wird).

package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;

import org.apache.wicket.ResourceReference;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.IHeaderResponse;

import de.wicketpraxis.web.blog.pages.questions.ajax.parameter.AbstractParameterizedDefaultAjaxBehavior.Parameter;

public abstract class ElementOffsetBehavior extends AbstractParameterizedDefaultAjaxBehavior
{
  static final Parameter<Integer> X_OFFSET=of("xOffset", Integer.class, "xOffset");
  static final Parameter<Integer> Y_OFFSET=of("yOffset", Integer.class, "yOffset");
  
  private String _contentId;

  @Override
  public void renderHead(IHeaderResponse response)
  {
    super.renderHead(response);
    response.renderJavascriptReference(WicketWindowJavascript.RESOURCE);
    response.renderJavascriptReference(new ResourceReference(ElementOffsetBehavior.class,"ElementOffsetBehavior.js"));
    response.renderOnDomReadyJavascript(getJavascript());
  }
  
  public ElementOffsetBehavior()
  {
    
  }
  
  public ElementOffsetBehavior(String contentId)
  {
    _contentId=contentId;
  }
  
  protected final String getJavascript()
  {
    if (_contentId!=null) return "ElementOffsetBehavoir.init(function (xOffset,yOffset) {"+getCallbackScript()+"},'"+_contentId+"');";
    return "ElementOffsetBehavoir.init(function (xOffset,yOffset) {"+getCallbackScript()+"});";
  }

  @Override
  protected final Parameter<?>[] getParameter()
  {
    return new Parameter<?>[]{ X_OFFSET,Y_OFFSET};
  }
  
  @Override
  protected void respond(AjaxRequestTarget target, ParameterMap parameterMap)
  {
    onOffset(target, parameterMap.getValue(X_OFFSET), parameterMap.getValue(Y_OFFSET));   
  }
  
  protected abstract void onOffset(AjaxRequestTarget target, int xOffset, int yOffset);
}

Im Gegensatz zum letzten Behavior sind wir in diesem Fall auf etwas mehr Javascript angewiesen.

ElementOffsetBehavoir =
{
  init : function(callback, contentId)
  {
    function Listener(callback, contentId)
    {
      this.xOffset = 0;
      this.yOffset = 0;
      this.firstElement = null;

      // function(xOffset,yOffset)
      this.onOffsetChanged = callback;

      this.updateOffsets = function()
      {
        var offsets = Wicket.Window.getXYOffset(this.firstElement);
        this.xOffset = offsets[0];
        this.yOffset = offsets[1];
        this.onOffsetChanged(this.xOffset, this.yOffset);
      };

      var bodyElement = document.getElementsByTagName('body')[0];
      this.firstElement = bodyElement.childNodes[1];
      if (contentId != null)
      {
        this.firstElement = document.getElementById(contentId);
      }
    }

    var listener = new Listener(callback, contentId);
    listener.updateOffsets();
    window.onresize = Callback.create(window.onresize, function()
    {
      listener.updateOffsets();
    });
  }
}

Der Listener wird über die init-Methode initialisiert und wird ebenfalls aufgerufen, wenn die Fenstergröße verändert wurde. Außerdem wird die updateOffsets()-Methode bereits bei der Initialisierung aufgerufen. Somit ist auch in diesem Fall der Offset eines Elements bekannt.

PageMouseClickBehavior

Das letzte Behavior ist gleichzeitig das aufwendigste. In diesem Fall muss nicht nur die Position des Klicks ermittelt werden, sondern gleichzeitig in Relation zu einem Element gebracht werden. Auf diese Weise kommen die Koordinaten schon passend an und können z.B. auch negative Werte annehmen.

package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;

import org.apache.wicket.Request;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.IHeaderResponse;

public abstract class PageMouseClickBehavior extends AbstractParameterizedDefaultAjaxBehavior
{
  static final Parameter<Integer> MOUSE_X=of("x", Integer.class, "x");
  static final Parameter<Integer> MOUSE_Y=of("y", Integer.class, "y");
  
  String _contentId;
  
  public PageMouseClickBehavior()
  {
  }
  
  public PageMouseClickBehavior(String contentId)
  {
    _contentId=contentId;
  }
  
  @Override
  public void renderHead(IHeaderResponse response)
  {
    super.renderHead(response);
    
    response.renderJavascriptReference(WicketWindowJavascript.RESOURCE);
    response.renderJavascriptReference(new ResourceReference(PageMouseClickBehavior.class,"PageMouseClickBehavior.js"));
    response.renderOnDomReadyJavascript(getJavascript());
  }
  
  protected String getJavascript()
  {
    if (_contentId!=null) return "PageMouseClickBehavoir.init(function (x,y,xOffset,yOffset) {"+getCallbackScript()+"},'"+_contentId+"');";
    return "PageMouseClickBehavoir.init(function (x,y,xOffset,yOffset) {"+getCallbackScript()+"});";
  }

  @Override
  protected Parameter<?>[] getParameter()
  {
    return new Parameter<?>[] { MOUSE_X, MOUSE_Y };
  }
  
  @Override
  protected void respond(AjaxRequestTarget target, ParameterMap map)
  {
    onClick(target, map.getValue(MOUSE_X), map.getValue(MOUSE_Y));    
  }

  protected abstract void onClick(AjaxRequestTarget target, int x, int y);
}

Das dazugehörige Javascript kümmert sich um die Verarbeitung:

PageMouseClickBehavoir = 
{
  init: function(callback,contentId)
  {
    function Listener(callback,contentId)
    {
      this.xOffset=0;
      this.yOffset=0;
      this.firstElement=null;
  
      // function(x,y)
      this.onMouseEvent=callback;
      
      this.mouseEvent= function(e)
      {
        tempX=0;
        tempY=0;
        
        if (Wicket.Browser.isIE() || Wicket.Browser.isGecko())
        {
          tempX = e.clientX + Wicket.Window.getScrollX();
          tempY = e.clientY + Wicket.Window.getScrollY();
        }
        else
        {
          tempX = e.pageX
          tempY = e.pageY
        }
        tempX-=this.xOffset;
        tempY-=this.yOffset;
        this.onMouseEvent(tempX,tempY);
        return true;
      };
      
      this.updateOffsets=function()
      {
        var offsets=Wicket.Window.getXYOffset(this.firstElement);
        this.xOffset=offsets[0];
        this.yOffset=offsets[1];
      };
      
      var bodyElement=document.getElementsByTagName('body')[0];
      this.firstElement=bodyElement.childNodes[1];
      if (contentId!=null)
      {
        this.firstElement=document.getElementById(contentId);
      }
      
      this.updateOffsets();
    }
  
  
    var listener=new Listener(callback,contentId);
    
    document.onmousedown=Callback.create(document.onmousedown,function(e)
    {
      listener.mouseEvent(e)
    });
    window.onresize=Callback.create(window.onresize,function()
    {
      listener.updateOffsets();
    });
  },
}

Sobald der Nutzer klickt (auch wenn es eigentlich nichts klickbares gibt), wird eine Request an die Anwendung gesendet. Diese Behavior können wir nun in die Anwendung einbauen, um die Mausklicks der Nutzer aufzuzeichnen.

Die Anwendung

In unserer Beispielanwendung werfen wir alles in einen Topf. Wir zeichnen alle Mausklicks auf und aktualisieren dann die Heatmap.

package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.image.Image;
import org.apache.wicket.markup.html.image.NonCachingImage;
import org.apache.wicket.markup.html.image.resource.DynamicImageResource;
import org.apache.wicket.markup.html.image.resource.RenderedDynamicImageResource;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.protocol.http.WebResponse;
import org.apache.wicket.util.time.Duration;

public class HeatMapPage extends WebPage
{
  List<Pos> _points=new ArrayList<Pos>();
  int _xOffset=0;
  int _yOffset=0;
  
  public HeatMapPage()
  {
    final FeedbackPanel feedbackPanel = new FeedbackPanel("feedback");
    feedbackPanel.setOutputMarkupId(true);
    add(feedbackPanel);

    final WebMarkupContainer box=new WebMarkupContainer("box");
    final ClickMap imageResource = new ClickMap(100, 100);
    final Image image = new NonCachingImage("map",imageResource);
    box.setOutputMarkupId(true);
    box.add(image);
    add(box);
    
    add(new ElementOffsetBehavior("#content")
    {
      @Override
      protected void onOffset(AjaxRequestTarget target, int xOffset, int yOffset)
      {
        info("Offset: "+xOffset+","+yOffset);
        _xOffset=xOffset;
        _yOffset=yOffset;
        imageResource.invalidate();
        target.addComponent(feedbackPanel);
        target.addComponent(box);
      }
    }.setThrottleDelay(Duration.milliseconds(250)));
    
    add(new PageMouseClickBehavior("#content")
    {
      @Override
      protected void onClick(AjaxRequestTarget target, int x, int y)
      {
        info("Clicked: "+x+","+y);
        _points.add(new Pos(x,y));
        imageResource.invalidate();
        target.addComponent(box);
        target.addComponent(feedbackPanel);
      }
    }.setThrottleDelay(Duration.milliseconds(50)));
    
    add(new WindowResizeBehavior()
    {
      @Override
      protected void onResize(AjaxRequestTarget target, int width, int height)
      {
        info("Size changed: "+width+","+height);
        imageResource.setWidth(width);
        imageResource.setHeight(height);
        imageResource.invalidate();
        
        target.addComponent(feedbackPanel);
        target.addComponent(box);
      }
    }.setThrottleDelay(Duration.milliseconds(250)));
    
  }
  
  class ClickMap extends RenderedDynamicImageResource
  {
    public ClickMap(int width, int height)
    {
      super(width, height,"jpg");
      setCacheable(false);
    }
    
    @Override
    protected boolean render(Graphics2D graphics)
    {
      graphics.setBackground(new Color(255,255,255));
      graphics.setColor(new Color(0,0,0,50));
      graphics.clearRect(0, 0, getWidth(), getHeight());
 
      graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      
      for (Pos p : _points)
      {
        graphics.fillArc(_xOffset+p.getX()-5, _yOffset+p.getY()-5, 9, 9, 0, 360);
      }
      return true;
    }
  }
  
  static class Pos implements Serializable
  {
    int _x;
    int _y;
    
    public Pos(int x, int y)
    {
      super();
      _x = x;
      _y = y;
    }
    
    public int getX()
    {
      return _x;
    }
    
    public int getY()
    {
      return _y;
    }
  }
}

Im Markup müssen wir dann nur das Image hinter der Seite platzieren und schon stimmen Mausklick und Heatmap überein.

<html>
  <head>
    <title>Heatmap Page</title>
  </head>
  <body>
    <div id="#content" style="width:800px; margin:auto; border:1px solid #888;">
      <div wicket:id="feedback"></div>
    </div>
    <div wicket:id="box" style="z-index: -1; position: absolute; top: 0px;left: 0px;">
      <img wicket:id="map">
    </div>
  </body>
</html>

In diesem Beispiel haben ich den Rahmen auf eine Breite von 800 Pixeln gesetzt und zentriert. Trotzdem werden die Mausklicks immer korrekt angezeigt.

wicket-heatmap-ajax

Wie man sehen kann, ist die Interaktion von Wicket mit Ereignissen, die durch Javascript ausgelöst werden, ohne weiteres möglich. Dabei können diese Komponenten ohne weiteres in beliebigen Anwendungen benutzt werden, ohne das sich ein anderer Entwickler mit den Implementierungsdetails beschäftigen muss. Allerdings hätte ich mir gewünscht, dass die Parameterisierung von Ajax-Aufrufen bereits in den Kern von Wicket integriert wäre.

Viel Spass mit dem ausprobieren:)

Tags:

Veröffentlicht in Wicket, .

Wicket Extensions - Anpassungen am ModalWindow

Wicket liefert im Bereich Extensions Komponenten mit, die zwar nicht direkt zum Kern von Wicket gehören, die aber fast so häufig wie die Standardwicketkomponenten zum Zuge kommen. Im folgenden Beitrag zeige ich, wie man die ModalWindow-Komponente benutzen kann und wie man die Darstellung an eigene Wünsche anpasst. Als erstes benötigen wir eine Komponente, die durch das ModalWindow angezeigt werden soll.

package de.wicketpraxis.web.blog.pages.questions.ajax.modal;

import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
import org.apache.wicket.markup.html.panel.Panel;

public abstract class AbstractModalClosePanel extends Panel
{
  public AbstractModalClosePanel(ModalWindow modalWindow)
  {
    super(modalWindow.getContentId());
    
    add(new AjaxLink<Void>("link")
    {
      @Override
      public void onClick(AjaxRequestTarget target)
      {
        AbstractModalClosePanel.this.onClick(target);
      }
    });
  }
  
  public abstract void onClick(AjaxRequestTarget target);
}

Die ModalWindow-Komponente kann nur eine andere Komponente anzeigen. Daher ist die Komponenten-ID fest definiert. Daher macht es Sinn, statt einer eigenen ID einfach das ModalWindow, in dem die Komponente angezeigt werden soll als Parameter zu übergeben. Damit diese Komponente auch eine Funktion bereitstellt, fügen wir einen Link hinzu. Wichtig: Wenn man Formulare benutzen möchte, muss man diese bei Anzeige durch ein ModalWindow per Ajax absenden.

Das Markup der Komponente ist wenig überraschend:

<wicket:panel>
  <div>
    Nothing to hide:)
    <a wicket:id="link">Link</a>
  </div>
</wicket:panel>

Die ModalWindow-Komponente unterscheidet sich von anderen Komponenten in einem wesentlichen Punkt. Die Darstellung wird nicht durch ein Markup realisiert, dass man überschreiben könnte. Der Rahmen für das Fenster wird per Javascript erzeugt. Um das Aussehen der Komponente anzupassen müssen wir also etwas tiefer in die Trickkiste greifen.

In diesem Beispiel nehmen wir Veränderungen an verschiedenen Stellen vor. Fangen wir mit den Grafiken für den Rahmen an. Der Einfachheit halber verändere ich nur die Farbwirkung aber nicht die Dimensionen, so dass ich die vorhandenen Grafiken (die man sich aus dem wicket-extension.jar extrahieren kann) nur leicht verändere:

frame-custom-1-alphaframe-custom-1-ieframe-custom-2-alphaframe-custom-2-ie

Es gibt zwei Grafiken, wobei es für jede noch eine Version für den InternetExplorer gibt, die auf transparente Schatten verzichtet (der InternetExplorer hat da Schwierigkeiten, ähm.. nicht nur da). Wie man sieht befinden sich die Grafikinformationen für die Ränder, Ecken und Buttons in einer Grafik. Das Stylesheet sorgt dafür, dass an der richtigen Stelle die richtige Grafik eingeblendet wird. Daher müssen wir eine eigene Stylesheet-Datei einbinden, die unsere Anpassungen wiederspiegelt. Auch an dieser Stelle ist es sinnvoll, sich den Inhalt der ModalWindow-eigenen Stylesheet-Datei anzuschauen.

div.wicket-modal div.w_content_3
{
  border:0px;
}

div.wicket-modal div.w_caption
{
  background-color: inherit;
}

div.wicket-modal a.w_close {  
  top: 3px;
}

div.wicket-modal div.custom div.w_left,
div.wicket-modal div.custom div.w_right {
  background-image: url('frame-custom-2-alpha.png'); 
  _background-image: url('frame-custom-2-ie.png');
}


div.wicket-modal div.custom div.w_top,
div.wicket-modal div.custom div.w_bottom,
div.wicket-modal div.custom div.w_topLeft,
div.wicket-modal div.custom div.w_topRight,
div.wicket-modal div.custom div.w_bottomRight,
div.wicket-modal div.custom div.w_bottomLeft,
div.wicket-modal div.custom a.w_close {
  background-image: url('frame-custom-1-alpha.png'); 
  _background-image: url('frame-custom-1-ie.png');
}

Wie ich bereits weiter oben erwähnt habe, wird das Html für den Rahmen nicht wie üblich sondern per Javascript erzeugt. Da wir auch an dieser Stelle Anpassungen vornehmen wollen, müssen wir die entsprechende Funktion überschreiben:

Wicket.Window.getMarkup = function(idWindow, idClassElement, idCaption, idContent, idTop, idTopLeft, idTopRight, idLeft, idRight, idBottomLeft, idBottomRight, idBottom, idCaptionText, isFrame) {
  var s =
      "<div class=\"wicket-modal\" id=\""+idWindow+"\" style=\"top: 10px; left: 10px; width: 100px;\"><form style='background-color:transparent;padding:0px;margin:0px;border-width:0px;position:static'>"+
      "<div id=\""+idClassElement+"\">"+
        "<div class=\"w_caption\"  id=\""+idCaption+"\">"+
          "<a class=\"w_close\" href=\"#\"></a>"+                 
          "<span id=\""+idCaptionText+"\" class=\"w_captionText\"></span>"+
        "</div>"+
        
        "<div class=\"w_top_1\">"+

        "<div class=\"w_topLeft\" id=\""+idTopLeft+"\">"+
        "</div>"+       

        "<div class=\"w_topRight\" id=\""+idTopRight+"\">"+
        "</div>"+

        "<div class=\"w_top\" id='"+idTop+"'>"+                 
        "</div>"+

        "</div>"+
                
        "<div class=\"w_left\" id='"+idLeft+"'>"+
          "<div class=\"w_right_1\">"+
            "<div class=\"w_right\" id='"+idRight+"'>"+
              "<div class=\"w_content_1\" onmousedown=\"if (Wicket.Browser.isSafari()) { event.ignore = true; }  else { Wicket.stopEvent(event); } \">"+                                      
              
                "<div class=\"w_content_2\">"+
                "<div class=\"w_content_3\">"+
                  "<div class=\"w_content\">";
        if (isFrame) {
          if (Wicket.Browser.isIELessThan7() || !Wicket.Browser.isIE()) {                       
            s+= "<iframe src='\/\/:' frameborder=\"0\" id='"+idContent+"' allowtransparency=\"false\" style=\"height: 200px\">"+
                    "</iframe>";
          } else {
            s+= "<iframe src='about:blank' frameborder=\"0\" id='"+idContent+"' allowtransparency=\"false\" style=\"height: 200px\">"+
            "</iframe>";
          }
        } else {
          s+=
                    "<div id='"+idContent+"'></div>";
        }
          s+=             
                  "</div>"+
                "</div>"+
                "</div>"+
              "</div>"+
            "</div>"+
          "</div>"+
        "</div>"+


        "<div class=\"w_bottom_1\" id=\""+idBottom+"\">"+         
          
          "<div class=\"w_bottomRight\"  id=\""+idBottomRight+"\">"+
          "</div>"+
          
          "<div class=\"w_bottomLeft\" id=\""+idBottomLeft+"\">"+
          "</div>"+

          "<div class=\"w_bottom\" id=\""+idBottom+"\">"+       
          "</div>"+       


        "</div>"+       


      "</div>"+
    "</form></div>";
    
    return s;
}

Da die Javascript-Datei der ModalWindow-Komponente automatisch eingebunden wird (da von dieser Klasse abgeleitet wird), muss ich nur diese eine Funktion überschreiben. Ich habe mich in diesem Beispiel darauf beschränkt, den Bereich für die Überschrift und den CloseButton oberhalb des Rahmens darstellen zu lassen (Das div mit der Klasse "w_caption").

In unsere Komponentenklasse betreten wir nun wieder vertrautes Terrain. Wir leiten von ModalWindow ab und passen ein paar grundsätzliche Dinge an:

package de.wicketpraxis.web.blog.pages.questions.ajax.modal;

import org.apache.wicket.ResourceReference;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
import org.apache.wicket.markup.html.CSSPackageResource;
import org.apache.wicket.markup.html.JavascriptPackageResource;
import org.apache.wicket.markup.html.resources.CompressedResourceReference;
import org.apache.wicket.markup.html.resources.JavascriptResourceReference;

public class CustomModalWindow extends ModalWindow
{
  private static ResourceReference CSS = new CompressedResourceReference(CustomModalWindow.class, "styles/custom-modal.css");
  private static ResourceReference JAVASCRIPT = new JavascriptResourceReference(CustomModalWindow.class,  "styles/custom-modal.js");
  
  public CustomModalWindow(String id)
  {
    super(id);
    
    setCssClassName("custom");
    
    add(JavascriptPackageResource.getHeaderContribution(JAVASCRIPT));
    add(CSSPackageResource.getHeaderContribution(CSS));
    
    setCloseButtonCallback(new CloseButtonCallback()
    {
      public boolean onCloseButtonClicked(AjaxRequestTarget target)
      {
        return onCloseClicked(target);
      }
    });
    
    setWindowClosedCallback(new WindowClosedCallback()
    {
      public void onClose(AjaxRequestTarget target)
      {
        CustomModalWindow.this.onClose(target);
      }
    });
  }
  
  protected void onClose(AjaxRequestTarget target)
  {
    
  }

  protected boolean onCloseClicked(AjaxRequestTarget target)
  {
    return true;
  }
}

Wir setzen die Css-Klasse für den Rahmen, fügen unsere CSS- und Javascript-Anpassungen hinzu (da unsere Referenzen nach den ModalWindow-Referenzen eingebunden werden, überschreiben wir sowohl die CSS-Definitionen als auch die Javascript-Funktionen). Außerdem fügen wir zwei Callbacks hinzu, die auf einen Klick auf das Schließen-Symbol reagieren. Für diese Komponenten benötigen wir keine eigene Markup-Datei.

Jetzt möchten wir diese Komponente endlich verwenden. Dazu legen wir eine Seitenklasse an:

package de.wicketpraxis.web.blog.pages.questions.ajax.modal;

import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.util.time.Duration;

public class ModalWindowPage extends WebPage
{
  public ModalWindowPage()
  {
    final CustomModalWindow modalWindow = new CustomModalWindow("modal");
    
    modalWindow.setTitle("Popup");
    modalWindow.setInitialWidth(400);
    modalWindow.setInitialHeight(400);
    modalWindow.setResizable(true);
    modalWindow.setContent(new AbstractModalClosePanel(modalWindow)
    {
      @Override
      public void onClick(AjaxRequestTarget target)
      {
        modalWindow.close(target);
      }
    });
    add(modalWindow);
    
    add(new AjaxLink<Void>("open")
    {
      @Override
      public void onClick(AjaxRequestTarget target)
      {
        modalWindow.show(target);
      }
    });
    
    add(new AbstractAjaxTimerBehavior(Duration.seconds(2))
    {
      @Override
      protected void onTimer(AjaxRequestTarget target)
      {
        modalWindow.show(target);
        stop();
      }
    });
  }
}

Wir erstellen eine Instanz unsere ModalWindow-Komponente und fügen ein Panel als Content-Element hinzu. Dabei wird beim Klick auf den Link innerhalb der Komponente das Fenster geschlossen. Da das ModalWindow per Ajax geöffnet und geschlossen werden muss, benötigen wir zum Öffnen einen AjaxLink. Wenn man dem Nutzer das Fenster bereits beim Aufruf der Seite einmalig anzeigen möchte, kann man sich damit behelfen, das man eine Instanz AbstractAjaxTimerBehavior-Klasse erstellt und die Zeit entsprechend klein wählt (weniger als die zwei Sekunden aus diesem Beispiel). In der onTimer()-Methode führt der Aufruf von stop() dazu, dass die Methode nach der abgelaufenen Zeit nicht noch einmal aufgerufen wird.

<html>
  <head>
    <title>ModalWindow Page</title>
  </head>
  <body>
    <div wicket:id="modal"></div>
    <a wicket:id="open">Open</a>
  </body>
</html>

Und so sieht das Ergebnis aus:

wicket-custom-modalwindow

Der Aufwand für eine eigene Komponente ist zwar wesentlich höher als in anderen Fällen, aber der Aufwand lohnt schon deshalb, weil dann das Aussehen der ModalWindow-Kompoente zur restlichen Anwendung passt.

Tags:

Veröffentlicht in Allgemein, Technologie, Wicket, .

Wicket Ajax Tipp - AjaxRequestTarget nachträglich ermitteln

Gestern wurde ich gefragt, ob und wie man denn vermeiden kann, dass man das AjaxRequestTarget immer als Funktionsargument weiterreichen muss, wenn man an anderer Stelle darauf zugreifen möchte. Ich muss zugeben, dass ich die einfache Antwort nicht sofort wusste (was, wie man sehen wird, erstaunlich ist). Ich wusste aber, dass mir kürzlich ein Stück Wicket-Code begegnet ist, wo ermittelt werden musste, ob ein AjaxRequest oder ein normaler Request abgearbeitet wird.

Ich machte mich also erneut auf die Suche und fand die Code-Zeile in der AjaxFallbackButton-Klasse. Ich war kurz davor ein paar Zeilen Code daraus abzuleiten und warf nur einen flüchtigen Code in die AjaxRequestTarget-Klasse, um festzustellen, das es das natürlich schon gibt.

AjaxRequestTarget target=AjaxRequestTarget.get();

So einfach geht das. So einfach, dass ich es fast übersehen hätte.

Tags:

Veröffentlicht in Allgemein, Wicket, .