4Max/shutterstock.com

12. März 2013 / von Jan Mischlich

Annotationsgesteuertes

Data Binding

Bei der Entwicklung einer grafischen Oberfläche steht man immer vor der Frage, wie man das Datemodell auf die Oberfläche abbildet. Die meisten GUI-Frameworks bieten zu diesem Zweck Funktionen basierend auf dem Observer-Pattern.

Für RCP-Anwendung lohnt sich hier die Verwendung von JFace Data Binding. Für Swing-Oberflächen bieten verschiedene Frameworks diese Funktionalität, als Bespiel wäre hier JGoodies Data Binding zu nennen oder QuasarClient DataManager.

Die genannten Frameworks erleichtern die Entwicklung der Oberfläche erheblich. Allerdings ist auch einiges an Code notwendig, um eine Verbindung zwischen dem GUI-Element und einer Value-Klasse herzustellen. In den Projekten an denen ich bisher beteiligt war, entstanden erhebliche Aufwände durch die Implementierung des Data Bindings. Deshalb möchte ich hier einen Ansatz vorstellen, mit dem wir den Aufwand auf ein Minimum reduzieren konnten.

Bespiel JFace Data Binding

Bevor ich mit der Beschreibung des Konzepts beginne, möchte ich kurz am Bespiel von JFace Data Binding zeigen, wie viel Code notwendig ist, um ein Binding zwischen zwei Objekten herzustellen.

Listing 1
DataBindingContext ctx = new DataBindingContext();
// Define the
IObservables IObservableValue target = WidgetProperties.text(SWT.Modify). observe(firstnameText);
IObservableValue model= BeanProperties. value(Person.class,"firstname").observe(person);
// Connect them
ctx.bindValue(target, model);

Listing 1 zeigt den notwendigen Code um das Attribut firstName von Person an das Text-Objekt firstNameText zu binden. Wie man sieht, ist schon bei dieser einfachen String auf Text Abbildung einiges an Code notwendig.

Für die Abbildung unterschiedlicher Datentypen und falls eine Validierung gewünscht ist kommen noch einige Zeilen hinzu, wie in Listing 2 ersichtlich. Hier muss für jedes Observable eine UpdateValueStrategy definiert werden, die über den übergebenen Converter die saubere Konvertierung der Datentypen sicherstellt.

Listing 2
DataBindingContext ctx = new DataBindingContext();

IObservableValue widgetValue = WidgetProperties.text(SWT.Modify).observe(ageTxt);
IObservableValue modelValue = BeanProperties.value(Person.class, "age").observe(person);

UpdateValueStrategy widgetStrategy = new UpdateValueStrategy();
widgetStrategy.setBeforeSetValidator(new AgeValidator());
widgetStrategy.setConverter(new StringToIntConverter());

UpdateValueStrategy modelStrategy = new UpdateValueStrategy();
modelStrategy.setConverter(new IntToStringConverter());
Binding bindValue = ctx.bindValue(widgetValue, modelValue, widgetStrategy, modelStrategy);

Weitere Informationen über jFaces Data Binding sind unter [1] zu finden.

Idee

Wie am Beispiel ersichtlich ist einiges an Code notwendig um ein Binding richtig zu erstellen. Allerdings gibt es in den meisten Projekten auch nur eine begrenzte Anzahl von notwendigen Abbildungen, die sich aus der Kombination der eingesetzten Daten-Objekten und Widgets ergibt. Die nachfolgende Matrix zeigt ein paar klassische Kombinationen:

 

Daten-TypSWT Widget-Typ
StringText
IntegerText
DoubleText
BooleanButton(SWT.CHECK)
usw. 

 

Der notwendige Code für diese Abbildungen lässt sich gut kapseln, da für die jeweilige Kombination aus Daten- und Widget-Typ meist nur eine Abbildung notwendig ist. Trotzdem muss noch für jedes Felder das richtige Binding ausgewählt werden, auch wenn die Typen eigentlich bekannt sind.

Über Reflection lässt sich der Typ einer Klasse ohne weiteres ermitteln, warum also die Anwendung nicht selbst entscheiden lassen welche Abbildung im konkreten Fall passend ist?

Diese Frage führte uns zu dem nachfolgend beschriebenen Ansatz.

Ansatz

Auch wenn man die Anwendung selbst die passende Abbildung auswählen lässt, muss das Binding irgendwie definiert werden. Wir haben uns hierfür eine Annotation in der GUI-Klasse entschieden.

Durch die Position am Widget-Objekt lässt sich dieses eindeutig identifizieren. Die Auswahl des Daten-Objekts ist über den Namen des Attributs der Entität möglich. Wie im Beispiel ersichtlich benötigt JFace Data Binding diesen ohnehin (wird in den meisten Data Binding Frameworks benötigt).

Aus meiner Erfahrung heraus, ist dies ein sehr fehleranfälliges Vorgehen. Zum einen wegen Tippfehlern, zum anderen aber auch, da Änderungen am Datenmodell leicht zu vergessenen Bindings führen, die erst zur Laufzeit festgestellt werden können.

Inspiriert von der R-Klasse von Android haben wir einen Generator entwickelt, der die Attribute aller Daten-Klassen scannt und daraus Konstanten erzeugt. Im Projekt wurden keine Transport-Objekte verwendet, sodass sich der Generator die Relevanten Daten-Klassen anhand der JPA-Annotationen ermitteln konnte. Listing 3 zeigt die generierte Klasse für die Klasse Person:

Listing 3
public interface BindingConst {
  public static final class Person {
    public static final String AGE = "age";
    public static final String FIRSTNAME = "firstname";
    public static final String SURNAME = "surname";
    public static final String ADDRESS = "address";
    public static final String ADDRESS_PLZ = "address.plz";
  }
}

Durch die Verwendung von inneren Klassen, die die gleiche Benennung wie die Daten-Klasse vorweisen, wird die Auswahl der Konstanten vereinfacht. Die generierten Konstanten entsprechen den Namen der Attribute der Klasse. Weiterhin werden auch Konstanten für assoziierte Klassen generiert. JFace Data Binding unterstützt die dabei verwendete Notation, sodass man die Konstanten im Binding verwendet kann.

Überflüssige Bindings zwischen zwei Klassenattributen sind auf diese Weise bei Änderungen am Datenmodell sofort ersichtlich, da Eclipse die fehlenden Konstanten direkt als Fehler anzeigt. Den Generator starten wir dabei über einen Ant-Task.

Listing 4 zeigt die Verwendung der Konstanten in Kombination mit der von uns definierten Annotation.

Listing 4
@GUIBinding(key= BindingConst.Person.FIRSTNAME)
private Text firstnameText;

Um alle in einem RCP Composite gesetzten Annotationen auszuwerten, haben wir den DataBindingContext um die Methode setInput erweitert. SetInput iteriert zur Laufzeit über alle annotierten Attribute des übergebenen Composites und ermittelt ihren Datentyp sowie den Datentyp des Attributs in dem übergebenen Daten-Objekts. Mit diesen Informationen kann die jeweils passende BindingStrategy ermittelt werden.

Binding_Strategy

Die ausgewählte BindingStrategy kapselt den notwendigen Code, um das Binding zwischen Daten- und Widget-Objekt herzustellen. Weiterhin hat sie über das Field-Objekt die Möglichkeit weitere Annotation, die in der Daten-Klasse für das Attribut gesetzt sind, auszuwerten. Sie kann z.B. die JPA-Annotation @Column auswerten, um die maximale Länge eines Strings oder die Anzahl der Nachkommastellen zu ermitteln. Auch eine Kombination mit weiteren eigenen Annotationen oder aus Bean-Validator ist denkbar. Auf diese Weise ist es möglich die Validierung für das Widget-Objekt zu steuern.

Falls die Möglichkeiten zur Konfiguration nicht ausreichen, kann es auch sinnvoll sein, @GUIBinding um Parameter zu erweitern, um die Auswahl eines Validators zu ermöglichen.

Fazit

Abschließend lässt sich sagen, dass der vorgestellte Ansatz zur Kapselung der Binding-Logik einige Vorteile bietet. So konnten wir den Aufwand für das Data Binding im Projekt auf ein Minimum reduzieren und Redundanzen vermeiden, da Konfigurationen wie Längenbeschränkungen nur einmalig definiert werden müssen.

Weiterhin zeigte sich, dass die, durch die dynamische Auswahl der BindingStrategy, resultierende Entkopplung, ein sehr effizientes Vorgehen bei der Entwicklung ermöglicht und den Mehraufwand für die Implementierung der GenericDataBindingContext schnell amortisiert wurde.

Durch die Verwendung einer einfachen DefaultBindingStrategy können von Beginn der GUI-Entwicklung an „echte“ Daten angezeigt werden. Validierungen und daraus resultierende Fehlermeldungen/Tooltipps können später eingefügt werden, ohne dass eine Änderung an der GUI-Klasse notwendig ist. Eine Parallelisierung der Entwicklung von Validatoren und GUI ist ebenfalls möglich.

Die hier vorgestellt Umsetzung bezieht sich auf RCP-Anwendungen, eine Wiederverwendbarkeit ist hier gegeben. Wie in der Einleitung erwähnt, konnten wir das Konzept aber auch auf Swing Applikationen bei der Verwendung des QuasarClient-DataManagers übertragen.

Listen- und Trees-Strukturen haben wir bisher nicht betrachtet, ein ähnliches Vorgehen ist aber auch hier möglich und bietet vielleicht Inhalt für einen weiteren Blog-Eintrag 😉

 

Referenzen:

  1. JFace Data Binding – Tutorial
  2. JGoodies: Understanding Binding

Autor

Jan Mischlich Jan ist seit September 2011 als Senior Software Ingenieur bei der Accelerated Solutions GmbH und beschäftigt sich dort mit Projektleitung und Software-Entwicklung hauptsächlich in Java.
Weitere Artikel

Das könnte Sie auch interessieren