Skip to content


Wicket Model Transformation

Eine Liste von Ergebnissen aus einer Tabelle in der Datenbank zu laden und über ein Model zur Verfügung zu stellen, ist eine recht einfache Angelegenheit. Wicket sorgt dafür, dass die Liste kein zweites mal geladen wird, nur weil das Model durch eine zweite Komponente dargestellt wird. Wenn man z.B. so triviale Dinge wie die Summe aller angezeigten Einträge ermitteln möchte, empfiehlt es sich, auf die bereits geladenen Daten aus dem Model zurückzugreifen. Für diesen Zweck kann man auf eine spezialisierte Model-Klasse zurückgreifen, die sich darum kümmert, das die detach()-Methode auch für alle referenzierten Modelle aufgerufen wird. Der Ansatz ist recht einfach, aber es geht vielleicht noch ein wenig eleganter.

Die Transformation

Als erstes definieren wir Klassen, die für die Transformation sorgen. Da in unserem Beispiel nicht nur ein Model, sondern auch zwei oder drei Model-Referenzen benutzt werden und damit bis zu drei Parametern übergeben werden können, müssen wir auch drei unterschiedliche Schnittstellen definieren:

package de.wicketpraxis.wicket.model.transformation;

import java.io.Serializable;

public interface Function1<T,M1> extends Serializable
{
  public T apply(M1 value);
}

package de.wicketpraxis.wicket.model.transformation;

import java.io.Serializable;

public interface Function2<T,M1,M2> extends Serializable
{
  public T apply(M1 value,M2 value2);
}

package de.wicketpraxis.wicket.model.transformation;

import java.io.Serializable;

public interface Function3<T,M1,M2,M3> extends Serializable
{
  public T apply(M1 value,M2 value2, M3 value3);
}

Da die verschiedenen Modelle auch verschiedene Daten bereitstellen können, kann jeder Parameter einen eigenen Typ annehmen. Jetzt benötigen wir ein Model, dass die Daten der referenzierten Modelle lädt und der Funktion übergibt und sich das Ergebnis bis zum Aufruf von detach() merkt.

package de.wicketpraxis.wicket.model.transformation;

import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;

public abstract class Transformator<T> extends LoadableDetachableModel<T>
{
  IModel<?>[] _subModels;
  
  protected Transformator(IModel<?> ... subModels)
  {
    _subModels=subModels;
  }
  
  @Override
  protected void onDetach()
  {
    for (IModel<?> m : _subModels)
    {
      m.detach();
    }
  }
  
  public static class Model1<T,M1> extends Transformator<T>
  {
    IModel<M1> _m1;
    Function1<T, M1> _function;
    
    public Model1(IModel<M1> m1, Function1<T, M1> function)
    {
      super(m1);
      
      _m1=m1;
      _function=function;
    }
    
    @Override
    protected T load()
    {
      return _function.apply(_m1.getObject());
    }
  }
  
  public static class Model2<T,M1,M2> extends Transformator<T>
  {
    IModel<M1> _m1;
    IModel<M2> _m2;
    Function2<T, M1, M2> _function;
    
    public Model2(IModel<M1> m1, IModel<M2> m2, Function2<T, M1, M2> function)
    {
      super(m1,m2);
      
      _m1=m1;
      _m2=m2;
      _function=function;
    }
    
    @Override
    protected T load()
    {
      return _function.apply(_m1.getObject(),_m2.getObject());
    }
  }

  public static class Model3<T,M1,M2,M3> extends Transformator<T>
  {
    IModel<M1> _m1;
    IModel<M2> _m2;
    IModel<M3> _m3;
    Function3<T, M1, M2, M3> _function;
    
    public Model3(IModel<M1> m1, IModel<M2> m2, IModel<M3> m3, Function3<T, M1, M2, M3> function)
    {
      super(m1,m2,m3);
      
      _m1=m1;
      _m2=m2;
      _m3=m3;
      _function=function;
    }
    
    @Override
    protected T load()
    {
      return _function.apply(_m1.getObject(),_m2.getObject(),_m3.getObject());
    }
  }
}

Das war jetzt ein größerer Block, der aber eigentlich nur aus 3 fast gleichen Klassendefinitionen bestand, die leider nicht viel besser zusammenzuführen sind. Wichtig ist dabei die Übergabe aller Model-Referenzen an die Basisklasse, die sich in der onDetach()-Methode (die von der detach()-Methode nur aufgerufen wird, wenn für das Model noch nicht detach() aufgerufen wurde) um das Aufrufen der detach()-Methode dieser Modelle kümmert.

Noch ist dieses ganze Konstrukt nicht viel besser handhabbar als die bisher favorisierte Lösung. Wir benötigen daher noch ein paar Hilfsklassen.

package de.wicketpraxis.wicket.model.transformation;

import org.apache.wicket.model.IModel;

public abstract class ModelSet
{
  public static class Set1<M1>
  {
    IModel<M1> _m1;
    
    public Set1(IModel<M1> m1)
    {
      _m1=m1;
    }
    
    public <T> IModel<T> apply(Function1<T, M1> function)
    {
      return new Transformator.Model1<T, M1>(_m1,function);
    }
  }
  
  public static class Set2<M1,M2>
  {
    IModel<M1> _m1;
    IModel<M2> _m2;
    
    public Set2(IModel<M1> m1,IModel<M2> m2)
    {
      _m1=m1;
      _m2=m2;
    }
    
    public <T> IModel<T> apply(Function2<T, M1, M2> function)
    {
      return new Transformator.Model2<T, M1, M2>(_m1,_m2,function);
    }
  }

  public static class Set3<M1,M2,M3>
  {
    IModel<M1> _m1;
    IModel<M2> _m2;
    IModel<M3> _m3;
    
    public Set3(IModel<M1> m1,IModel<M2> m2,IModel<M3> m3)
    {
      _m1=m1;
      _m2=m2;
      _m3=m3;
    }
    
    public <T> IModel<T> apply(Function3<T, M1, M2, M3> function)
    {
      return new Transformator.Model3<T, M1, M2, M3>(_m1,_m2,_m3, function);
    }
  }
}

Wir man erkennen kann, bestehen diese Klassen auch wieder aus drei Kopien, die mehr oder weniger die selbe Funktion, nur andere Parameter haben. Bis jetzt muss das Bild noch verschwommen sein, mit dem letzten Baustein sollte es sich klären.

package de.wicketpraxis.wicket.model;

import org.apache.wicket.model.IModel;

import de.wicketpraxis.wicket.model.transformation.ModelSet;

public abstract class Models
{
  public static <T,M1> ModelSet.Set1<M1> on(IModel<M1> model)
  {
    return new ModelSet.Set1<M1>(model);
  }
  public static <T,M1,M2> ModelSet.Set2<M1,M2> on(IModel<M1> model,IModel<M2> model2)
  {
    return new ModelSet.Set2<M1,M2>(model,model2);
  }
  public static <T,M1,M2,M3> ModelSet.Set3<M1,M2,M3> on(IModel<M1> model,IModel<M2> model2,IModel<M3> model3)
  {
    return new ModelSet.Set3<M1,M2,M3>(model,model2,model3);
  }
}

Fertig. Nun, vielleicht noch nicht ganz. Wir haben eine ganze Reihe von Klassen, eine ganze Menge Code, um die selben Anforderungen zu Erfüllen, wie mit dem im ersten Moment sehr viel einfacher gehaltenen Model-Ansatz aus dem anderen Artikel? Sehen wir uns daher das ganze in der Anwendung an.

Beispiel

In dem folgenden Beispiel benötigen wir ein Model, das eine Zufallszahl bereitstellt:

package de.wicketpraxis.web.blog.pages.migration.model.transformation;

import org.apache.wicket.model.LoadableDetachableModel;

public class RandomNumberModel extends LoadableDetachableModel<Double>
{
  @Override
  protected Double load()
  {
    return Math.random();
  }
}

Diese Modell benutzen wir auf einer Seite als Datenquelle:

package de.wicketpraxis.web.blog.pages.migration.model.transformation;

import org.apache.wicket.ajax.AjaxSelfUpdatingTimerBehavior;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.IModel;
import org.apache.wicket.util.time.Duration;

import de.wicketpraxis.wicket.model.Models;
import de.wicketpraxis.wicket.model.transformation.Function1;
import de.wicketpraxis.wicket.model.transformation.Function2;

public class TransformationPage extends WebPage
{
  public TransformationPage()
  {
    IModel<Double> randModel1=new RandomNumberModel();
    IModel<Double> randModel2=new RandomNumberModel();
    
    IModel<Double> diffResult = Models.on(randModel1, randModel2)
      .apply(new Function2<Double, Double, Double>()
    {
      public Double apply(Double value, Double value2)
      {
        return value-value2;
      }
    });
    
    IModel<Integer> scaleResult = Models.on(randModel1)
      .apply(new Function1<Integer, Double>()
    {
      public Integer apply(Double value)
      {
        return (int)(value*100);
      }
    });
    
    WebMarkupContainer ajaxUpdate=new WebMarkupContainer("ajaxUpdate");
    ajaxUpdate.setOutputMarkupId(true);
    ajaxUpdate.add(new AjaxSelfUpdatingTimerBehavior(Duration.seconds(1)));
    
    ajaxUpdate.add(new Label("rand1",randModel1));
    ajaxUpdate.add(new Label("rand2",randModel2));
    ajaxUpdate.add(new Label("diffResult",diffResult));
    ajaxUpdate.add(new Label("scaleResult",scaleResult));
    
    add(ajaxUpdate);
  }
}

In diesem Beispiel erstellen wir zwei unabhängige Modelle für jeweils zwei Zufallszahlen. Diese werden über zwei Funktionsinstanzen manipuliert und als Model bereitgestellt. Dabei ist zu beachten, dass nicht das Ergebnis als Model bereitgestellt wird, sondern dass ein Model erzeugt wird, bei dem beim ersten Aufruf von getObject() die load()-Methode des LoadableDetachedModel aufgerufen wird, dass dann dort die Daten aus den referenzierten Modellen ermittelt und durch die Funktion transformiert. Das Ergebnis wird bis zum Aufruf von detach() (durch die angebundene Komponente) gepuffert.

Damit die Zufallszahlen immer wieder erzeugt werden, lassen wir die Anzeige sich per Ajax selbsttätig aktualisieren.

<html>
  <head>
    <title>Transformation Page</title>
  </head>
  <body>
    <h1>Zufallszahlen</h1>
    <div wicket:id="ajaxUpdate">
      <table>
        <tr>
          <td>Nummer 1</td><td><span wicket:id="rand1"></span></td>
        </tr>
        <tr>
          <td>Nummer 2</td><td><span wicket:id="rand2"></span></td>
        </tr>
        <tr>
          <td>Number 1*100</td><td><span wicket:id="scaleResult"></span></td>
        </tr>
        <tr>
          <td>Number 1-Nummer 2</td><td><span wicket:id="diffResult"></span></td>
        </tr>
      </table>
    </div>
  </body>
</html>

Fazit

Wo liegt jetzt der Vorteil? Zum einen sind die Implementierungen einfacher, da laut Schnittstellendefinition nur eine Methode implementiert werden muss. Zum anderen muss man nicht die benutzten Model-Klassen kennen, die diese Transformationen durchführen. Die Lesbarkeit ist wesentlich besser, weil aus dem Code abgeleitet werden kann, was da in etwa passiert.

Ist das der Weisheit letzter Schluss? Vermutlich nicht. Anregungen sind daher willkommen.

Tags:

Veröffentlicht in Refactoring, Wicket, .

Migration zu Wicket: Model

Wicket ist ein Webframework nach dem MVC-Prinzip. Dabei orientiert sich Wicket in vielen Dingen eher an Grundprinzipien der Anwendungsentwicklung von Desktop-Anwendungen. Eine wesentliche Rolle spielen dabei die Modelle, die in Komponenten benutzt werden und die die Daten liefern, die durch verschiedenen Komponenten zur Anzeige gebracht werden. Dabei gibt es aber gerade in diesem Bereich sehr viele Irritationen. Problematisch erweist sich dabei vielleicht der Umstand, dass man auch ohne Modelle zu benutzen, mit Wicket bereits Daten zur Anzeige bringen kann. Man versteht dann erst einmal nicht, wie sich dass von einer Anwendung z.B. in PHP unterscheiden soll. Betrachtet man die Label-Komponente (die häufig für die Darstellung herangezogen wird) näher, stellt man fest, dass auch der Aufruf ohne Model intern dazu führt, dass der Übergabeparameter in ein Model gelegt wird.

In einem kleinen Beispiel möchte ich veranschaulichen, wie die verschiedenen Model-Klassen zum Einsatz kommen können und warum man immer Modelle benutzen sollte.

Wir erstellen zuerst eine Model-Klasse, die eine Liste von Ergebnissen liefert. In diesem Fall werden die Einträge zwar fest kodiert, könnten aber genauso gut aus der Datenbank kommen.

package de.wicketpraxis.web.blog.pages.migration.model;

public class Result
{
  String _name;
  
  Double _betrag;
  
  public Result(String name, double betrag)
  {
    _name=name;
    _betrag=betrag;
  }
  
  public String getName()
  {
    return _name;
  }
  
  public Double getBetrag()
  {
    return _betrag;
  }
}

package de.wicketpraxis.web.blog.pages.migration.model;

import java.util.ArrayList;
import java.util.List;

import org.apache.wicket.model.LoadableDetachableModel;

public class ResultListModel extends LoadableDetachableModel<List<Result>>
{
  @Override
  protected List<Result> load()
  {
    List<Result> ret=new ArrayList<Result>();
    ret.add(new Result("Popcorn",3.15));
    ret.add(new Result("Eis",2.80));
    ret.add(new Result("Eintritt",5.50));
    return ret;
  }
}

Das LoadableDetachableModel ist immer dann die beste Wahl, wenn Daten dynamisch erzeugt werden sollen und jedes mal sicher gestellt werden soll, dass die aktuellsten Daten verwendet werden. Wir das Model während eines Request geladen, werden die Daten so lange vorgehalten, bis die detach()-Methode aufgerufen wird, und die Liste zurückgesetzt wird. Das Model liefert uns in diesem Fall 3 Datensätze der Result-Klasse.

Die Beispielliste ist eine Liste von Posten auf einer Rechnung. Wir möchten aber zum Betrag auch noch den MwSt-Anteil ausweisen. Dazu schreiben wir ein allgemeines Model, dass von einer Erweiterung des LoadableDetachableModel abgeleitet ist: CascadingLoadableDetachableModel.

package de.wicketpraxis.web.blog.pages.migration.model;

import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;

import de.wicketpraxis.web.blog.model.CascadingLoadableDetachableModel;

public class MwStAnteilModel extends CascadingLoadableDetachableModel<Double, Double>
{
  static final Double MWST=19.0;
  
  public MwStAnteilModel(IModel<? extends Double> parent)
  {
    super(parent);
  }

  @Override
  protected Double load(Double brutto)
  {
    if (brutto!=null)
    {
      return brutto*MWST/100.0;
    }
    return null;
  }
}

Das Model entnimmt dem referenzierten Model den Betrag und ermittelt den MwSt-Anteil, den es seinerseits bereitstellt. Der Wert wird auch hier bis zum Aufruf von detach() gepuffert. Als Referenzmodell kann jede Klasse dienen, die IModel implementiert.

Jetzt fügen wir die Bestandteile zusammen:

package de.wicketpraxis.web.blog.pages.migration.model;

import java.util.List;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.PropertyListView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.PropertyModel;

import de.wicketpraxis.web.blog.model.CascadingLoadableDetachableModel;

public class SomeModelsPage extends WebPage
{
  public SomeModelsPage()
  {
    ResultListModel resultList=new ResultListModel();
    
    add(new PropertyListView<Result>("list",resultList)
    {
      @Override
      protected void populateItem(ListItem<Result> item)
      {
        IModel<Result> model = item.getModel();
        item.add(new Label("name",new PropertyModel<String>(model,"name")));
        item.add(new Label("betrag"));
        item.add(new Label("mwst",new MwStAnteilModel(new PropertyModel<Double>(model,"betrag"))));
      }
    });

    IModel<Double> summeModel = new CascadingLoadableDetachableModel<Double, List<Result>>(resultList)
    {
      @Override
      protected Double load(List<Result> liste)
      {
        if (liste!=null)
        {
          double summe=0;
          for (Result r : liste)
          {
            summe=summe+r.getBetrag();
          }
          return summe;
        }
        return null;
      }
    };
    
    add(new Label("summe",summeModel));
    add(new Label("mwst",new MwStAnteilModel(summeModel))); 
  }
}

Zuerst erstellen wir eines Instanz des ResultListModel. Die Ergebnisliste wird durch eine PropertyListView dargestellt. Der Unterschied zur ListView-Komponente besteht darin, dass bei Komponenten ohne eigenes Model die Id als Property-Expression herangezogen wird, um das Model für die Anzeige der Daten zu ermitteln. Die Schreibarbeit, die man für das Label "name" aufwenden müsste, kann man auf diese Weise erheblich reduzieren. Wie man beim Label mit der Id "mwst" erkennen kann, können die Modelle beliebig geschachtelt werden.

Um die Summe aller Einträge zu ermitteln, könnten wir jetzt die Ergebnisliste durchgehen und die Summe berechnen. Doch der Konstruktor einer Komponente wird ja nur einmal aufgerufen. Ändert sich durch eine Aktion auf der Seite oder in einer Komponente etwas an den zugrunde liegenden Daten, dann wird die Summe so nicht automatisch aktualisiert. Daher erstellen wir ein Model, dass diese Berechnung immer wieder durchführt. Dann können wir die aktuelle Summe und auch hierfür den MwSt-Anteil anzeigen lassen. Sollte sich an den Daten in der Datenbank etwas ändern, dann werden automatisch alle angezeigten Werte neu berechnet.

In diesem Beispiel sind mehrere wichtige Themen versteckt:

  • Alle Daten sollten in einem Model abgelegt werden. Daten sollten nicht im Konstruktor einer Komponente aufbereitet werden.
  • Für Modelle, die nicht an eine Komponente gebunden sind, wird die detach()-Methode nicht automatisch aufgerufen. Die CascadingLoadableDetachableModel-Klasse behandelt diesen Umstand entsprechend.
  • Model-Klassen kann man wiederverwenden.
  • Das Beispiel veranschaulicht, wie man z.B. die Summe aller sichtbaren Einträge über den Model-Weg ermitteln kann.

Ich hoffe, ich konnte wieder etwas Licht in das Dunkel rund um die Model-Klassen bringen. Fragen und Anregungen gerne in dem Kommentaren oder per Email.

Tags:

Veröffentlicht in Migration, Wicket, .

Modell-Referenzen

Wie zeige ich eine Liste von Elementen und die Länge der Liste mit Wicket an, ohne dass ich die Liste zweimal erzeugen muss?

Für die Liste würde man ein LoadableDetachableModel benutzen und in load() das Ergebnis zurückliefern.

final LoadableDetachableModel<List<Something>> modelListe = new LoadableDetachableModel<List<Something>>()
{
  @Override
  protected List<Something> load()
  {
    return Something.asList();
  }
};

Für die Anzahl der Einträge würde ich jetzt auf dieses Modell zurückgreifen:

LoadableDetachableModel<Integer> modelAnzahl = new LoadableDetachableModel<Integer>()
{
  @Override
  protected Integer load()
  {
    return modelListe.getObject().size();
  }

  @Override
  public void detach()
  {
    modelListe.detach();
    super.detach();
  }
};

Wenn modelListe nicht mit einer Wicketkomponente verbunden wäre, würde detach für diese Modell nie aufgerufen und die Liste damit nicht neu erzeugt. Änderungen an der Liste würden sich nicht sofort in der Anzeige wiederspiegeln. Daher muss man in dem Modell, dass mit einer Wicketkomponente verbunden ist auch detach für alle referenzierten Modelle aufrufen. Es gibt eine einfachere Lösung:

public abstract class CascadingLoadableDetachableModel<M,P> extends LoadableDetachableModel<M>
{
  IModel<P> _parent;

  public CascadingLoadableDetachableModel(IModel<P> parent)
  {
    super();
    _parent=parent;
  }

  @Override
  public void detach()
  {
    super.detach();
    _parent.detach();
  }

  @Override
  protected M load()
  {
    return load(_parent.getObject());
  }

  protected abstract M load(P parentModelData);
}

Man übergibt die Modell-Referenz im Konstruktor, bekommt die Modell-Daten automatisch als Methodenparameter und detach wird auch automatisch aufgerufen:

CascadingLoadableDetachableModel<Integer,List<Something>> modelAnzahl = new CascadingLoadableDetachableModel<Integer,List<Something>>()
{
  @Override
  protected Integer load(List<Something> parentModelData)
  {
    return parentModelData.size();
  }
};

Schön kurz.

Tags:

Veröffentlicht in Wicket, .