Training „git“
07.10.2010 - 08.10.2010, Essen
Training „Eclipse RCP“
28.03.2011 - 01.04.2011, Dortmund
Eclipse RCP

JFace Data Binding

JFace Data Binding stellt eine allgemeinen Mechanismus bereit, um beliebige Werte aneinander zu binden. Diesen Mechanismus kann man in RCP-Anwendungen verwenden, um Modellobjekte mit den entsprechenden GUI-Elementen zu verknüpfen. Hat man eine Eigenschaft eines Modellobjektes z.B. an ein Textfeld gebunden, synchronisiert JFace Data Binding diese beiden Werte automatisch. Verändert man den Text im Textfeld, wird auch die Eigenschaft des Modellobjektes gesetzt und umgekehrt. Dabei anfallende Aufgaben wie Konvertierung, Validierung und Beachtung von Thread-Zugriffsregeln (SWT-Steuerelemente dürfen bspw. nur aus dem UI-Thread verändert werden) können dabei mit erledigt werden.

Observables

Ein Observable ist eine Abstraktion für Objekte, die sich beobachten lassen, d.h. bei Veränderungen ein Ereignis auslösen. Solche Observables werden vor allem mittels bereitgestellter Factory-Klassen erzeugt. Bei der Beobachtung von Java-Objekten hat man die Wahl zwischen BeansObservables und PojoObservables (Plain Old Java Object). Ein Bean meint hier ein Objekt, welches PropertyChangeSupport gemäß der Java Bean Spezifikation unterstützt und von außen beobachtet werden kann, z.B.:

public class Person {

    private PropertyChangeSupport changes = new PropertyChangeSupport(this);

    private String name;
    private String vorname;

    public String getName() { return name; }
    public String getVorname() { return name; }

    public void setName(String name) {
        changes.firePropertyChange("name", this.name, this.name = name);
    }

    public void setVorname(String vorname) {
        changes.firePropertyChange("vorname", this.vorname, this.vorname = vorname);
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        changes.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        changes.removePropertyChangeListener(listener);
    }

}

Für eine Eigenschaft eines solchen Objektes kann mittels BeansObservables ein Observable erzeugt werden:

IObservableValue nameObservable = BeansObservables.observeValue(person, "name");

Wird PropertyChangeSupport nicht implementiert, kann ein PojoObservable verwendet werden. Bedingung dabei ist jedoch, dass das Objekt ausschließlich durch JFace Data Binding manipuliert wird:

IObservableValue nameObservable = PojoObservables.observeValue(person, "name");

Eigenschaften von SWT-Steuerelementen können mit SWTObservables beobachtet werden:

Text name = new Text(parent, SWT.BORDER);
IObservableValue nameTextObservable = SWTObservables.observeText(nameText, SWT.Modify);

Binding

Mit einem Binding werden zwei Observables miteinander verbunden und sind fortan automatisch synchronisiert:

JFace Data Binding: Binding and Observable

Für die Erzeugung und Verwaltung von Bindings ist der DataBindingContext verantwortlich:

JFace Data Binding: DataBindingContext

Die Observables aus dem obigen Beispiel könnten folgendermaßen gebunden werden:

DataBindingContext context = new DataBindingContext();
context.bindValue(nameText, nameTextObservable);
context.bindValue(vornameText, vornameTextObservable);

Dabei ist die Reihenfolge der Observable-Parameter zu beachten. Das erste Observable ist das Target, das zweite das Model. Initial synchronisiert JFace Data Binding den Wert vom Model zum Target, der initiale Zustand des Targets wird also verworfen.

Update-Strategien

Über eine UpdateValueStrategy können die beiden Richtungen eines Bindings beeinflusst werden. Diese können bei der Erzeugung des Bindings angegeben werden:

public final Binding bindValue(IObservableValue targetObservableValue,
    IObservableValue modelObservableValue);

public final Binding bindValue(IObservableValue targetObservableValue,
    IObservableValue modelObservableValue,
    UpdateValueStrategy targetToModel, UpdateValueStrategy modelToTarget);

Dabei kann eine updatePolicy spezifiziert werden, die den Zeitpunkt der Aktualisierung des Zielobjektes bestimmt:

Beispiel: Um die Bearbeitung ein Feldes für die Bearbeitung im GUI zu sperren, würde man den Weg von Zielobjekt (Textfeld) zum Modellobjekt mit UpdateValueStrategy.POLICY_NEVER versehen:

context.bindValue(dateText, dateProperty,
    new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER), null);

Konvertierung

Die Werte der gebundenen Observables können auf dem Weg von Model zu Target und Target zu Model konvertiert werden. Dazu ist der UpdateValueStrategy mit setConverter ein IConverter-Objekt zu setzen. Dies ist insbesondere notwendig, wenn die gebundenen Werte nicht den gleichen Typ haben und daher eine manuelle Typkonvertierung erfolgen muss. Für simple Konvertierungen wie String <-> int verwendet JFace Data Binding automatisch einen passenden Converter.

Die Konvertierung eines Date mit einem SimpleDateFormat kann folgendermaßen realisiert werden:

UpdateValueStrategy targetToModel = new UpdateValueStrategy();
modelToTarget.setConverter(new IConverter() {
    
    public Object getFromType() {
        return String.class;
    }

    public Object getToType() {
        return Date.class;
    }
                
    public Object convert(Object fromObject) {
        return SimpleDateFormat.getDateInstance().parse(String.valueOf(fromObject));
    }
});

context.bindValue(dateText, dateProperty,
    targetToModel, new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER));

Validierung

Über die UpdateValueStrategy kann auch ein Validator für die Wege zwischen Modell und Zielobjekt festgelegt werden. Die Validierung kann dabei zu drei verschiedenen Zeitpunkten erfolgen:

Die Rückmeldung der Validator-Klasse erfolgt über Status-Objekte, die mit ValidationStatus erzeugt werden können:

updateValueStrategy.setAfterGetValidator(new IValidator() {

    public IStatus validate(Object value) {
        if (String.valueOf(value).length() > 10)
            return ValidationStatus.error("Maximal 10 Zeichen!");
        else
            return ValidationStatus.ok();
    }
    
});

Eine Validierungsregel kann sich auch über mehrere Felder erstrecken. Dazu verwendet man einen MultiValidator, der bei der Validierung mehrere Observables berücksichtigt und den DataBindingContext um das Validierungsergebnis ergänzt:

MultiValidator datesInOrder = new MultiValidator() {

    @Override
    protected IStatus validate() {
        Date startDate = (Date) startDateObservable.getValue();
        Date endDate = (Date) endDateObservable.getValue();
        return (endDate.after(startDate)) ? ValidationStatus.ok() : ValidationStatus
                .error("End date needs to be after start date.");
    }
};

context.addValidationStatusProvider(datesInOrder);

Wichtig ist dabei, dass die Abhängigkeiten des Validators (d.h. welche Änderungen eine Neu-Validierung notwendig machen) automatisch durch die Beobachtung der Zugriffe auf die Observables in der validate-Methode ermittelt werden. Daher sollte das Holen dieser Observables nicht von Bedingungen abhängig sein, die sich zur Lebenszeit des Validators ändern können.

Umfangreichere Beispiele zur feldübergreifenden Validierung finden sich in Snippet021MultiFieldValidation.

Realms

Unter Umständen muss der Zugriff auf die beobachteten Objekte oder die Logik zur Validierung in einem bestimmten Thread erfolgen. JFace Data Binding bildet diese Notwendigkeit über Realms ab. Ein Realm definiert einen Kontext, aus dem heraus ein Zugriff auf Objekte erfolgen soll.

Sie können z.B. beim Erzeugen von Observables einen solchen Realm angeben:

BeansObservables.observeValue(realm, bean, propertyName)

Über die angegebene Realm-Implementierung könnten Sie den Zugriff auf das Bean in einem bestimmten Kontext ausführen lassen. Wird kein Realm angegeben, wird der Default-Realm verwendet. Der Default-Realm kann explizit spezifiert werden, indem das Binding in einem Realm.runWithDefault()-Block durchgeführt wird:

Realm.runWithDefault(defaultRealm, new Runnable() {

    public void run() {
        DataBindingContext context = new DataBindingContext();
        // binding logic
    }
}

Eine RCP-Anwendung wird immer mit dem SWT-Realm als Default-Realm ausgeführt, alle Data Binding Zugriffe erfolgen daher standardmäßig auf dem UI-Thread.

Viewer binden

Mit der Klasse ViewerSupport können JFace Viewer an Listen gebunden werden. Dabei wird sowohl die Liste selbst beobachtet (das Hinzufügen oder Entfernen von Elementen führt zur Aktualisierung der Tabellenansicht) als auch die Objekt-Attribute selbst:

IObservableList list = new WritableList(someList, SomeObject.class);
ViewerSupport.bind(someTableViewer, list,
        BeanProperties.values(new String[] { "name", "street", "country.name" }));

Um die Selektion eines Viewers zu beobachten, stehen in der Klasse ViewersObservables Methoden zur Verfügung, mit dem entsprechende Observables erzeugt werden können:

IObservableValue selection = ViewersObservables.observeSingleSelection(someViewer);

Mehr Beispiele zum Binden von Viewern finden Sie hier:

JFace Data Binding Snippets

In dem Projekt org.eclipse.jface.examples.databinding finden sich viele Beispiel-Snippets zur korrekten Verwendung von JFace Data Binding. Sie können das Projekt mit folgenden Schritten aus dem Eclipse CVS-Repository auschecken:

Holger Spiering, 03. März, 13:03 Uhr

Der Absatz zum MultiValidator ist irreführend. Wann die validate() Methode aufgerufen wird hängt davon ab, an welche Observables der Validator gebunden wird. Dies geschieht aber nicht automatisch durch die Observable-Aufrufe aus der validate() Methode heraus, sondern dazu werden bereits beim Binding, über den MultiValidator via observeValidatedValue() Methode, explizit Wrapper für die eigentlichen Observables erzeugt.
Aus dem Code-Beispiel Snippet021MultiFieldValidation sollte das eigentlich deutlich hervorgehen.

Christian Trutz, 30. April, 14:10 Uhr

Danke für diese Einführung in JFace Data Binding. Sehr gut lesbar und verständlich :-)

Christian

Michael Müller, 02. Juni, 08:06 Uhr

Sehr gut geschrieben und super erklärt. Gibt es eine Möglichkeit bei der Databinding eine Undo/Redo Funktion miteinzubauen, damit es der EclipseRCP Undo/Redo History hinzugefügt wird?

Gruß Michael

Ralf Ebert, 03. Juni, 10:59 Uhr

Undo/Redo wird meistens mit Data Binding zu einem EMF-Modell realisiert, welches automatisch eine Historie der Änderungen anlegen kann, siehe
http://help.eclipse.org/galileo/index.jsp?topic=/org.eclipse.emf.workspace.doc/references/overview/undoredo.html

Michael Müller, 05. Juni, 13:13 Uhr

Vielen Dank für die schnelle Antwort. Das heißt konkret ohne ein EMF Model werden die Undo/Redo Funktionen für Databinding nicht unterstützt?

Über Ihre Kommentare und Hinweise freue ich mich sehr:
Ralf Ebert | Eclipse RCP Buch | JFace Data Binding