Skip to content


Wicket - lose Koppelung von Komponenten

Auch bei Webanwendungen entstehen schnell komplexe Oberflächen. Es ist nur eine Frage der Zeit, bis man Komponenten, die miteinander interagieren sollen, gegenseitig bekannt macht. Diese Vorgehensweise ist limitiert und außerdem sehr aufwendig. Wie ich bereits im Buch beschrieben habe, kann man die Koppelung von Komponenten aufweichen, die per Ajax neu gezeichnet werden müssen. Dabei ist es zu kurz gedacht, dass man die Entkoppelung von Komponenten nur aus diesem Grund forciert.

Der Unterschied zwischen einem normalen Request und einem Ajax-Request liegt darin, dass bei einem Ajax-Request nur Teile der Seite neu gerendert werden. Dazu müssen die Komponenten markiert werden, die in der Zielseite ersetzt werden sollen. Wenn es sich um einen normalen Request handelt, ist das nicht notwendig, da die ganze Seite und damit alle Komponenten neu gerendert werden.

Solange sich alle Aktionen auf die Änderungen von Daten beziehen, die dann entweder per Ajax neu dargestellt werden oder beim Darstellen der ganzen Seite automatisch angezeigt werden, besteht eigentlich kein Grund, mehr Aufwand in das Entkoppeln von Komponenten zu stecken. Doch oft ist das Ändern der Daten nicht trivial, der Code verteilt sich unwillkürlich auf verschiedene Komponenten.

Um dieser Entwicklung entgegen zu wirken, erweitert man das Event-Konzept einfach auch auf normale Requests. Im folgenden Code werden daher alle notwendigen Klassen aufgeführt. Die im Buch verwendeten Klassen können durch diese ersetzt werden.

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

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

Das EventListenerInterface muss jede Komponente implementieren, dass auf Events reagieren muss.

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

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

public class AbstractEvent
{
  Component _source;
  AjaxRequestTarget _requestTarget;
  
  protected AbstractEvent(Component source,AjaxRequestTarget requestTarget)
  {
    _source=source;
    _requestTarget=requestTarget;
  }
  
  public Component getSource()
  {
    return _source;
  }
  
  public void fire()
  {
    Page page = _source.getPage();
    if (page instanceof EventListenerInterface)
    {
      ((EventListenerInterface) page).notifyAjaxEvent(this);
    }
    page.visitChildren(EventListenerInterface.class, new AjaxEventVisitor(this));
  }
  
  public void update(Component component)
  {
    if (_requestTarget!=null) _requestTarget.addComponent(component);
  }
  
  protected static class AjaxEventVisitor implements IVisitor<Component>
  {
    AbstractEvent _event;
    
    protected AjaxEventVisitor(AbstractEvent event)
    {
      _event=event;
    }
    
    public Object component(Component component)
    {
      ((EventListenerInterface) component).notifyAjaxEvent(_event);
      return IVisitor.CONTINUE_TRAVERSAL;
    }
  }
}

Die Eventklasse wurde dahingehend erweitert, dass jetzt auch für die Seite geprüft wird, ob das EventListernerInterface implementiert wurde. Ob Ajax benutzt wurde oder nicht, wird in der update()-Methode überprüft, so dass man diese Prüfung nicht mehr selbst vornehmen muss.

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

import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;

public class ChangeEvent extends AbstractEvent
{
  int _change;
  
  protected ChangeEvent(Component source, AjaxRequestTarget requestTarget, int change)
  {
    super(source, requestTarget);
    
    _change=change;
  }

  public int getChange()
  {
    return _change;
  }
}

Der ChangeEvent wurde abgeleitet und um eine Information erweitert. In der folgenden Komponente wird diese Information ausgewertet:

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

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

public class CounterPanel extends Panel implements EventListenerInterface
{
  IModel<Integer> _counter;
  
  public CounterPanel(String id,IModel<Integer> counter)
  {
    super(id);
    
    setOutputMarkupId(true);
  
    _counter=counter;
    
    add(new Label("counter",_counter));
  }

  public void notifyAjaxEvent(AbstractEvent event)
  {
    if (event instanceof ChangeEvent)
    {
      int change = ((ChangeEvent) event).getChange();
      Integer cur = _counter.getObject();
      
      info("Aktuell: "+cur+" Change: "+change);
      
      _counter.setObject(cur+change);
      event.update(this);
    }
  }
}

Man beachte, dass für die Komponente setOutputMarkupId() aufgerufen wird, da diese Komponente evtl. per Ajax aktualisiert werden kann.

<wicket:panel>
  <span wicket:id="counter"></span>
</wicket:panel>

Jetzt benötigen wir noch eine Komponente, die diesen Event auslöst:

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

import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;

public class ActionPanel extends Panel
{
  public ActionPanel(String id,IModel<Integer> change)
  {
    super(id);
    
    AjaxFallbackLink<Integer> link = new AjaxFallbackLink<Integer>("link",change)
    {
      @Override
      public void onClick(AjaxRequestTarget target)
      {
        new ChangeEvent(ActionPanel.this,target,getModelObject()).fire();
      }
    };
    link.add(new Label("change",change));
    add(link);
  }
}
<wicket:panel>
  <a wicket:id="link"><span wicket:id="change"></span></a>
</wicket:panel>

Nachdem wir alle Komponten erstell habe, benutzen wir sie in einer Anwendung:

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

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

public class EventPage extends WebPage
{
  public EventPage()
  {
    add(new EventFeedbackPanel("feedback"));
    
    add(new ActionPanel("changeAdd1",Model.of(1)));
    add(new ActionPanel("changeSub1",Model.of(-1)));
    
    add(new CounterPanel("counter",Model.of(5)));
  }
  
  static class EventFeedbackPanel extends FeedbackPanel implements EventListenerInterface
  {
    public EventFeedbackPanel(String id)
    {
      super(id);
      
      setOutputMarkupId(true);
    }
    
    public void notifyAjaxEvent(AbstractEvent event)
    {
      event.update(this);
    }
  }
}

Wie man sieht, habe ich auch ein FeedbackPanel erstellt, dass auf Events (und in diesem Fall jedes) reagiert.

<html>
  <head>
    <title>EventPage</title>
  </head>
  <body>
    <div wicket:id="feedback"></div>
    
    <wicket:container wicket:id="changeAdd1"></wicket:container>
    <wicket:container wicket:id="changeSub1"></wicket:container>
    
    <div wicket:id="counter"></div>
  </body>
</html>

Wenn Komponenten per Ajax aktualisiert werden müssen, funktioniert das nur, wenn es auch ein Html-Tag mit der passenden ID gibt, dass per Javascript gefunden und ersetzt werden kann. Daher muss man die Komponente dann an ein Html-Tag binden, die Einbettung über wicket:container funktioniert nicht.

Betätigt man nun einen der Links in der AktionPanel-Komponente, dann wird der Zähler in der CounterPanel-Komponenten angepasst und eine entsprechende Meldung im FeedbackPanel angezeigt. Wenn man nun das CounterPanel einfach ausbaut, passiert nichts. Das zeigt zum einen, dass die Komponenten wirklich unabhängig voneinander sind, zum anderen aber auch, welches Risiko man eingeht: es kann vorkommen, dass Events nicht verarbeitet werden.

Dieses Beispiel veranschaulicht, wie einfach man Komponenten entkoppeln kann. Doch dieses Beispiel ist nur eine einfache Anwendung. Wie weit man diese Vorgehensweise in der Praxis treiben kann, möchte ich in ein paar Sichtpunkten erwähnen:

  • Anwendung der Möglichkeiten von Vererbung auf Events
  • Prüfung, ob ein Event einen Empfänger erreicht hat
  • Kaskadierung von Events (wenn Event A eintrifft, wird Event B ausgelöst)
  • Mehr als ein Sender und mehr als einen Empfänger für Events
  • Ein Event kann mit und ohne Ajax ausgelöst werden, ohne dass der Code auf Empfängerseite angepasst werden muss.

Ich hoffe, dass dient als Anregung oder Vorlage. Komplexen Oberflächen mit Wicket steht nun nichts mehr im Weg.

Tags:

Veröffentlicht in Allgemein, Refactoring, Wicket, .