Skip to content


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, .

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, .

Wicket Komponentenübersicht

Ich habe in den letzten Tagen damit angefangen, die verschiedenen Componenten, die Wicket bereits in dem Basisbibliotheken bietet, zu visualisieren. Den ersten Versuch habe ich noch von Hand durchgeführt, um schnell festzustellen, dass man auf diese Weise nicht besonders schnell vorwärts kommt. Der zweite und damit aktuelle Ansatz ist da schon sehr viel versprechend. Es werden die Wicket-Quellen geparst und alle Klassendefinitionen eingelesen, die Abhängigkeiten untereinander ermittelt, verschiedene Filter angewendet um die Darstellung auf ein Thema einzugrenzen und das ganze dann mit dem dot-Programm aus dem Graphviz-Paket in ein Diagramm umgewandelt.

Die folgenden Grafiken sind das vorläufige Zwischenergebnis. Es ist nicht auszuschließen, dass es Fehler gibt. Für jeden gemeldeten Fehler bin ich daher sehr dankbar, aber auch Anregungen sind natürlich herzlich willkommen.

Wicket Kompontenten - Basis

Wicket Komponenten inklusive Extensions

Wicket Modelle

Wicket Behavior

Wicket Listener

Wicket RequestTarget

Tags:

Veröffentlicht in Technologie, Wicket, .