Wicket 7 – Component Queuing

Bisher ist Wicket-7 von den neuen Features recht überschaubar. Der Migration-Guide bietet eine ganz gute Übersicht, was sich tatsächlich geändert hat.

Component Queuing

Das wahrscheinlich interessanteste Feature, auf das viele Entwickler gewartet haben ist Component Queuing. Component Queuing versucht das leidige Problem zu lösen, dass wir in Wicket die Komponentenhierarchien doppelt zu pflegen haben.

<body>
 <div wicket:id="container">
     <div wicket:id="label"></div>
 </div>
</body>

Genau diesselbe Hierarchie findet sich auch im Code.

WebMarkupContainer container = new WebMarkupContainer("container");
Label label = new Label("label", new Model("Show me"));

container.add(label);
add(container);

Prinzipiell reicht es aus, die Hierarchie an einer Stelle zu definieren (beispielsweise im Markup). Der Code kann die Hierarchie der Komponenten dann anhand der Wicket-IDs automatsich herleiten. In der Theorie ganz einfach – genau das ist die Idee von Component Queuing. Ich muss ehrlich zugeben, mich hat die doppelte Hierarchie-Pflege nie wirklich gestört. Wicket zwingt uns Entwickler hier einfach sauber zu arbeiten.

Ändern wir die Component-Hierarchie ab und fügen alle Komponenten auf der gleichen Ebenen hinzu.

WebMarkupContainer container = new WebMarkupContainer("container");
Label label = new Label("label", new Model("Show me"));
//falsche Hierachie, label müsste im Container liegen
add(label);
add(container);

Es ergibt sich folgendes Bild, das alle Wicket-Entwickler mit Sicherheit schon dutzende Male gesehen haben.

Last cause: Unable to find component with id 'label' in [WebMarkupContainer [Component id = container]]
	Expected: 'container:label'.
	Found with similar names: 'label'

Die Hierarchie auf der Java-Seite stimmt nicht mit der Hierarchie im DOM überein.

Verwenden wir aber statt der so lieb gewonnenen add(..) die neue queue(…) Methode, sieht es schon wieder anders aus.

public HomePage(final PageParameters parameters)
  {
    super(parameters);
    WebMarkupContainer container = new WebMarkupContainer("container");
    Label label = new Label("label", new Model("Show me"));

    queue(label);
    queue(container);
  }

Obwohl die Hierarchie nicht übereinstimmt schafft es Wicket, aus der Hierarchie im DOM die Komponenten richtig zu schachteln.

Was aber genau passiert beim Component-Queuing?

Im uns allen bekannten MarkupContainer hält eine Liste an Komponenten, die der Hierarchie mit queue(…) hinzugefügt wurden. Sobald der RequestCycle beginnt, versucht jeder MarkupContainer, seine Queue-Komponenten aus der Queue zu entnehmen und korrekt in die Hierarchie zu setzen.

Dies geschieht in zwei Schritten

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

Zunächst wird versucht, alle Auto-Components richtig zu platzieren. Hiermit fällt viel Komplexität aus dem Code heraus, denn was dequeuAutoComponents() nur macht ist zu prüfen, ob eine AutoComponent im Container verwendet wird (z.B. <wicket:enclosure/>) und queued diese dann automatisch. Das bedeutet, nach der onInitialize sind bereits alle AutoComponents korrekt in der Component-Queue hinterlegt. Schick..

ComponentTag.IAutoComponentFactory autoComponentFactory = tag.getAutoComponentFactory();
if (autoComponentFactory != null)
{
     queue(autoComponentFactory.newComponent(this, tag));
}      

Im nächsten Schritt wird das Dequeuing durchlaufen.

public void dequeue() {
        if(this instanceof IQueueRegion) {
            DequeueContext queueRegion = this.newDequeueContext();
            this.dequeuePreamble(queueRegion);
        } else {
            MarkupContainer queueRegion1 = (MarkupContainer)this.findParent(IQueueRegion.class);
            if(!queueRegion1.getRequestFlag((short)128)) {
                queueRegion1.dequeue();
            }
        }
}

Zunächst wird unterschieden, ob die aktuelle Komponente das Interface IQueueRegion implementiert. Eine IQueueRegion ist grundsätzlich soweit ich das verstehe alles, was ein eigenes Markup mitbringt, also Fragments, Panels und Pages.

Die Unterscheidung ist aber nur marginal, denn intern wird nichts anderes gemacht, als den State der Komponente auf QUEUED GERADE zu setzen und anschließend mit dem Dequeuing-Prozess zu beginnen.

Das Dequeuing selbst ist sehr einfach und basiert auf der Struktur des DOM.

Wicket iteriert über alle Nodes im DOM.

Für alle Nodes wird folgender Algorithmus durchlaufen.

  1. Komponente mit Node-ID bereits geadded? Wenn ja, für diesen Node nichts weiter zu tun.
  2. Iteriere über alle Container inkl. der aktuellen IQueueRegion und durchsuche deren Queues nach einer Komponente mit der Wicket-ID des Nodes. (Start immer bei einer IQueueRegion, also Page oder Panel). Je höher in der HIerarchie, desto früher werden die Komponenten geadded.
  3. Wird die Komponente irgendwo in der IQueueRegion gefunden, rufe add(..) für diese Komponente auf dem aktuellen Container auf.

Boundaries

Component-Queuing arbeitet in engen Grenzen die vom Interface IQueueRegion vorgegeben werden.

public interface IQueueRegion
{
	/**
	 * Creates a new {@link DequeueContext} that will be used to dequeue children of this region.
	 * 
	 * Usually containers will create a context with their associated markup by getting it via
	 * {@link MarkupContainer#getAssociatedMarkup()}, but components that do not render markup in a
	 * standard way (such as repeaters and borders) may choose to override this method to implement
	 * custom behavior for the dequeueing process.
	 */
	public DequeueContext newDequeueContext();

	/**
	 * Starts component dequeueing on this {@link IQueueRegion}. This is the entry point into the
	 * dequeuing process, it creates the {@link DequeueContext} and delegates the operation to the
	 * {@link org.apache.wicket.MarkupContainer#dequeue(DequeueContext)} method which performs the
	 * actual dequeueing. The context's markup is retrieved using the {@link MarkupContainer#getAssociatedMarkup()}.
	 */
	public void dequeue();
}

Die IQueueRegion erlaubt das Erzeugen eines neuen DequeueContext. Der DequeueContext hält eine Liste an allen Containern sowie eine Liste
an Tags, die aktuell bearbeitet werden.

Erzeugen wir uns ein einfaches Panel mit folgendem Markup:

<wicket:panel>
    <div wicket:id="panelLabel"/>;
</wicket:panel>

Zusätzlich fügen wir das Panel der Page hinzu.

<div wicket:id="container">
    <div wicket:id="label"></div>
</div>
<strong><div wicket:id="panel"></div>
</strong>

Wie erwartet erhalten wir:

org.apache.wicket.markup.MarkupException: Unable to find component with id 'panelLabel' in [MyPanel [Component id = panel]]

Interessanterweise erhalten wir außerdem:

org.apache.wicket.WicketRuntimeException: Detach called on component with id '0' while it had a non-empty queue: ComponentQueue{queueSize=1, queue=[[Component id = panelLabel], null, null, null, null, null, null, null]}

Unser gequeutes Label ist immer noch da, da es nicht aufgelöst werden konnte.

Queues verlassen ihre Boundaries also nicht.

Fazit:

Tolles Feature. Mir persönlich gefällt vor allem das saubere Arbeiten mit AutoComponents durch diese Änderung sehr gut. Ich bin mir noch nicht sicher, ob ich grundsätzlich dazu raten würde Queueing der bisherigen Arbeitsweise vorzuziehen, das muss die Praxis zeigen.
Component-Queuing ist derzeit nur im 7.0-SNAPSHOT verfügbar.

Links


Announcement auf der Wicket Mailing List


Component Queuing Artikel (Igor Vaynberg)

Derzeit noch offene Issues mit Component Queuing

Source-Code auf GitHub

Wicket 7 – Migration Guide

Effective Trainings & Consulting - Martin Dilger



Hat Ihnen dieser Blog-Eintrag gefallen? Ich stelle in diesem Blog Informationen über Tools, Frameworks und Werkzeuge zur Verfügung, die mich produktiver machen. Vielleicht kann ich auch Ihnen helfen, produktiver zu werden.


Ich unterstütze Sie als freier Mitarbeiter bei der Entwicklung von Software-Projekten, Agiler Arbeit sowie Schulungen / Fortbildungen.


Jeden Tag ein bisschen produktiver - ab heute