This tutorial explains the usage of the Eclipse JFace Data Binding framework that can be used to synchronize data between different objects.

1. Data binding with JFace

1.1. Using JFace Data Binding

The JFace data binding framework allows you to synchronize changes in object properties. Validation and conversion during the synchronization process are supported.

For example, you can bind the firstName String property of a Person object to the text property of an SWT Text widget. If you change the text in the user interface, the corresponding property in the Person object is updated.

To observe changes in an attribute of a Java object, the data binding framework registers itself as a listener to this attribute. JFace data binding provides API to register for changes in SWT widgets and JFace UI elements. Other Java objects must implement this support. For example, they can use PropertyChangeSupport according to the Java Bean specification. Alternatively, you can use the WritableValue class from JFace inside the model.

The following plug-ins implement data binding.

  • org.eclipse.core.databinding

  • org.eclipse.core.databinding.beans

  • org.eclipse.core.databinding.property

  • org.eclipse.jface.databinding

The IObservableValue interface is the basis for observing properties in JFace. The data binding framework provides factory classes to create IObservableValue objects. The following table gives an overview of available classes to create instances of IObservableValue.

Table 1. Factories for the creation of an IObservableValue object
Factory Description

PojoProperties

Used to create IObservableValue for Java objects. The term POJO (Plain Old Java Object) refers to a Java object that does not implement a specific framework API.

BeanProperties

Used for Java Beans. A Java Bean is a Java object that follows the Java Bean specification. This specification requires that the class implements getter and setter methods for all its attributes. It must also implement property change support via the PropertyChangeSupport class and propagate changes to registered listeners.

WidgetProperties

Used for properties of SWT widgets.

ViewerProperties

Used for properties of JFace viewers.

Properties

Used for properties of non-specialized types, like Objects, Collections or Maps.

Observables

Used for properties of special objects, collections, maps and entries of an IObservableMap.

The following code demonstrates how to create an IObservableValue object for the firstName property of a Java object called person.

import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.beans.typed.PojoProperties;
// more code

// if person is a POJO
var pojoModel = PojoProperties.value(Person.class, "firstName", String.class).observe(person);
var pojoModel2 = PojoProperties.value(Person.class, "firstName").observe(person);

// prefer beans if your data model provides property change support
var beansModel = BeanProperties.value("firstName").observe(person);
var beansModel2 = BeanProperties.value(Person.class, "firstName", String.class).observe(person);

The next example demonstrates how to create an IObservableValue for the text property of an SWT Text widget called text.

import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
// more code

var observe = WidgetProperties.text(SWT.Modify).observe(text);

1.2. Example of a Java bean implementation

A Java class that provides PropertyChangeSupport looks like the following example.

package com.vogella.databinding.example;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class ModelObject {
    private final PropertyChangeSupport changeSupport = 
            new PropertyChangeSupport(this);

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

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

    protected void firePropertyChange(String propertyName, Object oldValue,
            Object newValue) {
        changeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }
}

Other domain classes can extend this class, as demonstrated in the following example.

package com.vogella.databinding.example;

public class Person extends ModelObject {
    private String name;

    public String getName() {
        return name;
    }

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

1.3. Using the WritableValue class to track changes

If your domain model supports the JFace API, you can use WritableValue to provide the option to track values.

import org.eclipse.core.databinding.observable.value.WritableValue;

public class Person {

    private final WritableValue<String> firstName = new WritableValue<>();

    private final WritableValue<String> lastName = new WritableValue<>();

    /**
     * @TrackedGetter
     */
    public String getFirstName() {
        return firstName.getValue();
    }

    public void setFirstName(String firstName) {
        this.firstName.setValue(firstName);
    }

    /**
     * @TrackedGetter
     */
    public String getLastName() {
        return lastName.getValue();
    }

    public void setLastName(String lastName) {
        this.lastName.setValue(lastName);
    }
}

1.4. Data binding and Java objects without change notification

If you connect two properties and one of them cannot be observed, you can still use data binding for a unidirectional binding. For example, you can connect the text property of an SWT Text field to the summary field of a Task object, even if a task cannot be observed. In this case, updates in the summary of the Task object do not update the Text widget. A change in the Text widget updates the property in the task object.

2. Create bindings

2.1. Using the ISideEffect API

The ISideEffect data binding API allows you to run code whenever an observable changes. An ISideEffect is similar to a listener, but it does not need to be attached to any component. Instead, it automatically reacts to changes in the tracked properties.

For example, the following code updates the label with the value of the user name every time the user name changes.

IObservableValue<String> username = ...
Label yourUsername = ...

ISideEffect sideEffect = ISideEffect.create(
        () -> "Your username is: " + username.getValue(),
        yourUsername::setText);

An ISideEffect can react to changes in multiple observables and avoids excessive updates if the observables fire many change events in a short period of time.

You can use the ISideEffectFactory to create several ISideEffect instances that share the same lifecycle. For example, all ISideEffect instances created by the ISideEffectFactory from the WidgetSideEffects.createFactory(Widget) method are automatically disposed once the corresponding widget is disposed.

ISWTObservableValue<String> personFirstNameTextObservable = WidgetProperties.text(SWT.Modify)
    .observe(personFirstNameText);
ISWTObservableValue<String> personLastNameTextObservable = WidgetProperties.text(SWT.Modify)
    .observe(personLastNameText);

ISideEffectFactory sideEffectFactory = WidgetSideEffects.createFactory(personFirstNameText);

sideEffectFactory.create(person::getFirstName, personFirstNameText::setText);
sideEffectFactory.create(personFirstNameTextObservable::getValue, person::setFirstName);
sideEffectFactory.create(person::getLastName, personLastNameText::setText);
sideEffectFactory.create(personLastNameTextObservable::getValue, person::setLastName);

2.2. Connecting properties with the DataBindingContext

The DataBindingContext class provides the functionality to connect IObservableValue objects.

The DataBindingContext.bindValue() method connects two IObservableValue objects. The first parameter is the target and the second is the model. During the initial binding, the value from the model is copied to the target. For example, if you bind an attribute of a Person object p to the text attribute of a Text widget txtName, the value from p is initially copied to txtName.

// create a new context
DataBindingContext ctx = new DataBindingContext();

// define the observables
IObservableValue<String> target = WidgetProperties.text(SWT.Modify).observe(firstName);
IObservableValue<String> model = BeanProperties.value(Person.class, "firstName", String.class)
        .observe(person);

// connect them
ctx.bindValue(target, model);

You can observe nested model properties, for example, attributes of classes that are contained in another class. The following code demonstrates how to access the country property in the address field of the object person.

var model = PojoProperties.value(Person.class, "address.country").observe(person);

2.3. Observing map properties

You can observe entries of an IObservableMap via the Observables.observeMapEntry() method. This allows you to control the point in time when you want to update or use the values from the map.

import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.observable.Observables;
import org.eclipse.core.databinding.observable.map.IObservableMap;
import org.eclipse.core.databinding.observable.map.WritableMap;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.ui.di.Persist;
import org.eclipse.e4.ui.services.IServiceConstants;
import org.eclipse.jface.databinding.swt.ISWTObservableValue;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Text;

import com.vogella.tasks.model.Task;

public class ObservableMapEntry {

    private static final String SECOND_ATTRIBUTE = "secondAttribute";
    private static final String FIRST_ATTRIBUTE = "firstAttribute";

    private final IObservableMap<String, String> attributesMap = new WritableMap<>();
    private DataBindingContext dbc;
    private Task task;

    @PostConstruct
    public void createUI(Composite parent) {
        dbc = new DataBindingContext();

        Text txtFirstAttribute = new Text(parent, SWT.BORDER);
        Text txtSecondAttribute = new Text(parent, SWT.BORDER);

        // create observables for the Text controls
        ISWTObservableValue<String> txtFirstAttributeObservable = WidgetProperties.text(SWT.Modify)
                .observe(txtFirstAttribute);
        ISWTObservableValue<String> txtSecondAttributeObservable = WidgetProperties.text(SWT.Modify)
                .observe(txtSecondAttribute);

        // create observables for the map entries
        IObservableValue<String> firstAttributeObservable = Observables.observeMapEntry(attributesMap,
                FIRST_ATTRIBUTE);
        IObservableValue<String> secondAttributeObservable = Observables.observeMapEntry(attributesMap,
                SECOND_ATTRIBUTE);

        dbc.bindValue(txtFirstAttributeObservable, firstAttributeObservable);
        dbc.bindValue(txtSecondAttributeObservable, secondAttributeObservable);
    }

    @Inject
    @Optional
    public void setModel(@Named(IServiceConstants.ACTIVE_SELECTION) Task task) {
        if (task != null) {
            this.task = task;
            // set new values for the map entries from a model object
            attributesMap.put(FIRST_ATTRIBUTE, task.getSummary());
            attributesMap.put(SECOND_ATTRIBUTE, task.getDescription());
        }
    }

    @Persist
    public void save() {
        if (task != null) {
            // only store the values in the model on save
            task.setSummary(attributesMap.get(FIRST_ATTRIBUTE));
            task.setDescription(attributesMap.get(SECOND_ATTRIBUTE));
        }
    }
}

3. Update strategy, converters and validators

3.1. UpdateValueStrategy

The bindValue() method of DataBindingContext allows you to specify UpdateValueStrategy objects as third and fourth parameters. These objects allow you to control the update of the values. The following update policies are available:

Table 2. UpdateValueStrategy
Value Description

UpdateValueStrategy.POLICY_NEVER

The state of the source observable is not tracked and the value of the destination observable is never updated.

UpdateValueStrategy.POLICY_ON_REQUEST

The state of the source observable is not tracked. Validation, conversion and the update of the destination observable are performed only when explicitly requested.

Use DataBindingContext.updateModels() or DataBindingContext.updateTargets() to update all bindings at once.

Use Binding.updateTargetToModel() or Binding.updateModelToTarget() to update a single binding.

UpdateValueStrategy.POLICY_CONVERT

The state of the source observable is tracked, including validation of changes (except for validateBeforeSet(Object)), but the value of the destination observable is updated only on request.

UpdateValueStrategy.POLICY_UPDATE

The state of the source observable is tracked. Validation, conversion and updating the value of the destination observable are performed automatically on each change of the source observable.

If no UpdateValueStrategy is specified, UpdateValueStrategy.POLICY_UPDATE is used by default. You can register converters and validators in the UpdateValueStrategy object.

3.2. Converter

Converters convert the values between the observed properties. Converters are defined based on the IConverter interface.

UpdateValueStrategy.setConverter() sets the converter. The static IConverter.create(Function) and IConverter.create(Object, Object, Function) methods create a converter from a lambda expression. The static UpdateValueStrategy.create(IConverter) method creates an update strategy with a converter.

For example, you can convert a String to another object and vice versa. Assume you have a Developer object with a programmingSkills property.

package com.vogella.databinding.example;

public class Developer extends ModelObject {
    private String name;
    private String[] programmingSkills;

    public String getName() {
        return name;
    }

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

    public String[] getProgrammingSkills() {
        return programmingSkills;
    }

    public void setProgrammingSkills(String[] programmingSkills) {
        firePropertyChange("programmingSkills", this.programmingSkills,
                this.programmingSkills = programmingSkills);
    }
}

The entries of programmingSkills should be shown in the Text widget as comma-separated text. Therefore, a conversion is necessary in both directions, defined via an UpdateValueStrategy for each direction of the binding.

package com.vogella.databinding.example;

import jakarta.annotation.PostConstruct;

import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.conversion.IConverter;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;

public class ConverterPart {

    @PostConstruct
    public void createPartControl(Composite parent) {
        // create the Developer model with programming skills
        Developer person = new Developer();
        person.setName("John");
        person.setProgrammingSkills(new String[] { "Java", "JavaScript", "Groovy" });
        GridLayoutFactory.swtDefaults().numColumns(2).applyTo(parent);
        Label programmingSkillsLabel = new Label(parent, SWT.NONE);
        programmingSkillsLabel.setText("Programming Skills");
        GridDataFactory.swtDefaults().applyTo(programmingSkillsLabel);
        Text programmingSkillsText = new Text(parent, SWT.BORDER);
        GridDataFactory.fillDefaults().grab(true, false).applyTo(programmingSkillsText);
        // do the actual binding and conversion
        DataBindingContext dbc = new DataBindingContext();

        // define converters
        IConverter<String, String[]> convertToStringArray = IConverter.create(String.class, String[].class,
                s -> s.split(","));
        IConverter<String[], String> convertToString = IConverter.create(String[].class, String.class,
                skills -> String.join(",", skills));

        // create the observables, which should be bound
        var programmingSkillsTarget = WidgetProperties.text(SWT.Modify).observe(programmingSkillsText);
        var programmingSkillsModel = BeanProperties.value(Developer.class, "programmingSkills", String[].class)
                .observe(person);

        // bind observables together with the appropriate UpdateValueStrategies
        dbc.bindValue(programmingSkillsTarget, programmingSkillsModel,
                UpdateValueStrategy.create(convertToStringArray),
                UpdateValueStrategy.create(convertToString));

        // button to check the data model
        Button button = new Button(parent, SWT.PUSH);
        button.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
        button.setText("Show data model");
        button.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
            for (String skill : person.getProgrammingSkills()) {
                System.out.println(skill);
            }
        }));
    }
}

3.3. Validator

A validator allows you to implement validation of the data before it is propagated to the other connected property. A class that provides this functionality must implement the org.eclipse.core.databinding.validation.IValidator interface.

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.conversion.IConverter;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.validation.ValidationStatus;

// converter as before
IConverter<String, String[]> convertToStringArray = IConverter.create(String.class, String[].class,
        s -> s.split(","));

// reject Perl as programming language
UpdateValueStrategy<String, String[]> updateStrategy = UpdateValueStrategy.create(convertToStringArray);
updateStrategy.setAfterGetValidator(s -> s.contains("Perl")
        ? ValidationStatus.error("Perl is not a programming language")
        : ValidationStatus.ok());

Binding binding = dbc.bindValue(widgetValue, modelValue, updateStrategy, null);

The WizardPageSupport class connects the validation result of the data binding context to a wizard page. This updates the completion state and the error message of the wizard page.

4. More on bindings

4.1. ControlDecorators

Data binding allows you to use icon decorators in the user interface that reflect the status of the field validation. This allows you to provide immediate feedback to the user. To create the control decoration, use the return object from the bindValue() method of the DataBindingContext object.

// assumes that the update strategy has a validator
Binding binding = ctx.bindValue(widgetValue, modelValue, strategy, null);

// show validation results as decorations on the control
ControlDecorationSupport.create(binding, SWT.TOP | SWT.LEFT);

The result might look like the following screenshot.

ControlDecoration with databinding

4.2. Binding values of a radio button group

In a radio button group, each button usually represents a certain value. To bind the value of the selected radio button, use the SelectObservableValue class.

The following example shows how to bind the currently selected radio value to a Label.

import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.observable.value.SelectObservableValue;
import org.eclipse.jface.databinding.swt.ISWTObservableValue;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;

// more code

Group group = new Group(shell, SWT.NONE);
group.setText("Radio Group with Names");
GridLayoutFactory.fillDefaults().applyTo(group);
GridDataFactory.fillDefaults().grab(true, true).applyTo(group);

// options for the radio buttons
String[] names = { "Matthew Hall", "Dirk Fauth", "Lars Vogel", "Simon Scholz" };

SelectObservableValue<String> selectedRadioButtonObservable = new SelectObservableValue<>();
for (String name : names) {
    Button button = new Button(group, SWT.RADIO);
    button.setText(name);
    // the option value is used if the corresponding button is selected
    selectedRadioButtonObservable.addOption("Selected: " + name, WidgetProperties.buttonSelection().observe(button));
}

Label label = new Label(shell, SWT.NONE);
GridDataFactory.fillDefaults().applyTo(label);
ISWTObservableValue<String> labelTextObservable = WidgetProperties.text().observe(label);

DataBindingContext dbc = new DataBindingContext();
// bind label text to currently selected option
dbc.bindValue(selectedRadioButtonObservable, labelTextObservable);

The result of this snippet should look like the following.

Radio group with names where Simon Scholz is selected

4.3. Listening to all changes in the binding

You can register a listener to all bindings of the DataBindingContext class. Your listener is called whenever something changes.

For example, a part that behaves like an editor can use this to mark itself as dirty if its data model changes.

// part is the injected MPart, ctx the DataBindingContext field of the editor
IChangeListener listener = event -> {
    // part is null if this class is not used as a part
    if (part != null) {
        part.setDirty(true);
    }
};

private void updateUserInterface(Task task) {
    // check that the user interface is available
    if (txtSummary != null && !txtSummary.isDisposed()) {

        // deregister from all validation status providers, a MultiValidator is not a binding
        ctx.getValidationStatusProviders()
                .forEach(p -> p.getTargets().forEach(t -> t.removeChangeListener(listener)));

        // dispose the bindings, a disposed context cannot be reused
        ctx.dispose();
        ctx = new DataBindingContext();

        // the data binding code is left out for brevity

        // register the listener with the targets of all validation status providers
        ctx.getValidationStatusProviders()
                .forEach(p -> p.getTargets().forEach(t -> t.addChangeListener(listener)));
    }
}

4.4. Placeholder binding with WritableValue

You can create bindings to a WritableValue object. A WritableValue object can hold a reference to another object.

If you exchange this reference, data binding uses the new object for its binding. This way, you create the binding once and still exchange the bound object.

To bind to the object contained in a WritableValue, use the observeDetail() method.

WritableValue<Person> value = new WritableValue<>();

// create the binding
DataBindingContext ctx = new DataBindingContext();
IObservableValue<String> target = WidgetProperties.text(SWT.Modify).observe(text);
IObservableValue<String> model = BeanProperties.value(Person.class, "firstName", String.class)
        .observeDetail(value);

ctx.bindValue(target, model);

// create a Person object named p
Person p = new Person();

// make the binding valid for this new object
value.setValue(p);

4.5. ComputedValue and observable trackers

A ComputedValue provides an abstract calculate method, which computes a value from the observables that it reads.

public class FormattedName extends ComputedValue<String> {
    private final IObservableValue<String> firstName;
    private final IObservableValue<String> lastName;

    FormattedName(IObservableValue<String> firstName, IObservableValue<String> lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    protected String calculate() {
        // getValue() is a tracked getter, so this ComputedValue is recomputed
        // whenever lastName or firstName changes
        String lastName = this.lastName.getValue();
        String firstName = this.firstName.getValue();
        lastName = (lastName == null || lastName.isEmpty()) ? "[Last Name]" : lastName;
        firstName = (firstName == null || firstName.isEmpty()) ? "[First Name]" : firstName;

        return lastName + ", " + firstName;
    }
}

The static create method accepts a Supplier which implements the calculation.

IObservableValue<String> lastName = // ...
IObservableValue<String> firstName = // ...

IObservableValue<String> formattedName = ComputedValue.create(() -> {
    // getValue() is a tracked getter, so this value is recomputed
    // whenever lastName or firstName changes
    String lastNameValue = lastName.getValue();
    String firstNameValue = firstName.getValue();
    lastNameValue = (lastNameValue == null || lastNameValue.isEmpty()) ? "[Last Name]" : lastNameValue;
    firstNameValue = (firstNameValue == null || firstNameValue.isEmpty()) ? "[First Name]" : firstNameValue;

    return lastNameValue + ", " + firstNameValue;
});

A ComputedValue can combine different observables and is recomputed every time one of the observables it read changes.

An example of a ComputedValue implementation is the AggregateValidationStatus. It keeps track of all validation statuses that have been passed to it and aggregates them to one MultiStatus or returns the one with maximum severity.

A ComputedValue is a good alternative to converter implementations. For instance, the AggregateValidationStatus returns an IStatus as value. To bind it to the enabled state of a Finish button, you need a boolean value.

// track the validation status of all bindings in the DataBindingContext
AggregateValidationStatus aggregateValidationStatus = new AggregateValidationStatus(dbc.getBindings(),
        AggregateValidationStatus.MAX_SEVERITY);

// create a boolean observable, which depends on the OK status
IObservableValue<Boolean> isValidationOk = ComputedValue
        .create(() -> aggregateValidationStatus.getValue().isOK());

ISWTObservableValue<Boolean> buttonEnabledObservable = WidgetProperties.enabled().observe(finishButton);

// bind the enablement of the finish button to the validation
dbc.bindValue(buttonEnabledObservable, isValidationOk);

An IConverter from IStatus to boolean would require considerably more code.

5. Data binding for JFace viewers

5.1. Binding viewers

JFace data binding provides functionality to bind the data of JFace viewers. Data binding for these viewers distinguishes between changes in the collection and changes in the individual object.

If data binding observes a collection, it requires a content provider which notifies the viewer once the data in the collection changes.

The ObservableListContentProvider class is a content provider that requires a list implementing the IObservableList interface. The Properties class allows you to wrap another list with its selfList() method into an IObservableList.

The following snippet demonstrates the usage.

// use ObservableListContentProvider
viewer.setContentProvider(new ObservableListContentProvider<Person>());

// create sample data
List<Person> persons = createExampleData();

// wrap the input into an observable list
IObservableList<Person> input = Properties.selfList(Person.class).observe(persons);

// set the IObservableList as input for the viewer
viewer.setInput(input);

5.2. Observing list details

You can use the ObservableMapLabelProvider class to observe changes of the list elements.

ObservableListContentProvider<Person> contentProvider = new ObservableListContentProvider<>();

// the label provider monitors the known elements and updates the labels on changes
IObservableSet<Person> knownElements = contentProvider.getKnownElements();

IObservableMap<Person, String> firstNames = BeanProperties
    .value(Person.class, "firstName", String.class).observeDetail(knownElements);
IObservableMap<Person, String> lastNames = BeanProperties
    .value(Person.class, "lastName", String.class).observeDetail(knownElements);

IObservableMap<?, ?>[] labelMaps = { firstNames, lastNames };

ILabelProvider labelProvider = new ObservableMapLabelProvider(labelMaps) {
    @Override
    public String getText(Object element) {
        return firstNames.get(element) + " " + lastNames.get(element);
    }
};

5.3. ViewerSupport

ViewerSupport simplifies the setup for JFace viewers in cases where selected columns should be displayed. It registers change listeners on the collection as well as on the individual elements. The ViewerSupport.bind() method creates the label provider and the content provider for a viewer automatically.

// MyModel.getPersons() returns a List<Person>,
// WritableList wraps it in an IObservableList
WritableList<Person> input = new WritableList<>(MyModel.getPersons(), Person.class);

// creates the content and label provider and sets the input,
// the table columns show the given properties
ViewerSupport.bind(viewer, input,
        BeanProperties.values(Person.class, "firstName", "lastName", "married"));

5.4. Master-detail binding

The ViewerProperties class allows you to create an IObservableValue for properties of the viewer. For example, you can track which data object is currently selected. This binding is called master-detail binding, as you track the selection of a master.

To access fields in the selection, use the PojoProperties or the BeanProperties class. Both provide the value().observeDetail() method chain that allows you to observe a detailed value of an IObservableValue object.

For example, the following code synchronizes the summary property of the Task domain object to a Label based on the selection of a ComboViewer.

import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.typed.PojoProperties;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.jface.databinding.viewers.IViewerObservableValue;
import org.eclipse.jface.databinding.viewers.typed.ViewerProperties;

// assume we have Task domain objects
// tasks is of type: List<Task>
ComboViewer viewer = new ComboViewer(parent, SWT.DROP_DOWN);
viewer.setContentProvider(ArrayContentProvider.getInstance());
viewer.setLabelProvider(LabelProvider.createTextProvider(element -> ((Task) element).getSummary()));
viewer.setInput(tasks);

// create a Label to map to
Label label = new Label(parent, SWT.BORDER);
// parent has a GridLayout assigned
label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

DataBindingContext dbc = new DataBindingContext();

// for binding to the label
IObservableValue<String> target = WidgetProperties.text().observe(label);

// observe the selection
IViewerObservableValue<Task> selectedTask = ViewerProperties.singleSelection(Task.class).observe(viewer);
// observe the summary attribute of the selection
IObservableValue<String> detailValue = PojoProperties.value(Task.class, "summary", String.class)
        .observeDetail(selectedTask);

dbc.bindValue(target, detailValue);

5.5. Chaining properties

You can chain properties together to simplify observing nested properties. The following examples demonstrate this usage.

IObservableValue<String> viewerSelectionSummaryObservable = ViewerProperties
    .singleSelection(Task.class)
    .value(BeanProperties.value(Task.class, "summary", String.class))
    .observe(viewer);
IListProperty<Node, String> siblingNames = BeanProperties.value(Node.class, "parent")
    .list("children")
    .values("name", String.class);
IObservableList<String> siblingNamesObservable = siblingNames.observe(node);

6. Extending data binding with custom observables

6.1. Developing custom observables

You can implement observables for your own classes. This implementation can extend the AbstractObservable class or one of its subclasses, like the AbstractObservableValue class.

For example, assume you have the following custom widget that draws a text on a canvas.

public class CustomWidget extends Canvas {

    private static final int MARGIN = 5;

    private String text;

    public CustomWidget(Composite parent) {
        super(parent, SWT.NONE);
        addPaintListener(e -> e.gc.drawText(getDisplayText(), MARGIN, MARGIN));
    }

    @Override
    public Point computeSize(int wHint, int hHint, boolean changed) {
        checkWidget();
        GC gc = new GC(this);
        try {
            Point extent = gc.textExtent(getDisplayText());
            return new Point(extent.x + 2 * MARGIN, extent.y + 2 * MARGIN);
        } finally {
            gc.dispose();
        }
    }

    public String getText() {
        checkWidget();
        return text;
    }

    public void setText(String text) {
        checkWidget();
        this.text = text;
        // the preferred size depends on the text
        requestLayout();
        redraw();
    }

    private String getDisplayText() {
        return text != null ? text : "Good Default";
    }
}
Since there is no predefined IObservable for custom widgets, you must implement your own.

While it is possible to use the PojoProperties class to create an IObservableValue (for example, PojoProperties.value("text").observe(customWidget)), this does not produce an ISWTObservableValue. As a result, classes such as ControlDecorationSupport do not work out of the box with your custom widget.

6.2. Directly implement IObservable

The CustomWidgetObservableValue extends AbstractObservableValue and implements the ISWTObservableValue interface.

public class CustomWidgetObservableValue extends AbstractObservableValue<String>
        implements ISWTObservableValue<String> {

    private final CustomWidget customWidget;

    public CustomWidgetObservableValue(CustomWidget customWidget) {
        this(customWidget, Realm.getDefault());
    }

    public CustomWidgetObservableValue(CustomWidget customWidget, Realm realm) {
        super(realm);
        this.customWidget = customWidget;
    }

    @Override
    public Object getValueType() {
        return String.class;
    }

    @Override
    protected String doGetValue() {
        return customWidget.getText();
    }

    @Override
    protected void doSetValue(String value) {
        customWidget.setText(value);
    }

    // ISWTObservableValue enables ControlDecorationSupport for this observable
    @Override
    public Widget getWidget() {
        return customWidget;
    }
}

This observable can then be used as follows.

@PostConstruct
public void createComposite(Composite parent) {
    parent.setLayout(new GridLayout(1, false));

    DataBindingContext dbc = new DataBindingContext();

    CustomWidget widget = new CustomWidget(parent);
    var customWidgetObservableValue = new CustomWidgetObservableValue(widget);

    Task task = // ...
    IObservableValue<String> taskSummaryObservable = PojoProperties
            .value(Task.class, "summary", String.class).observe(task);

    dbc.bindValue(customWidgetObservableValue, taskSummaryObservable);
}

6.3. Implement an IProperty rather than IObservable directly

A better approach is to implement the IProperty interface, like IValueProperty, IWidgetValueProperty and others.

The following code implements the solution of the previous section with an IProperty implementation.

public class CustomWidgetProperty extends WidgetValueProperty<CustomWidget, String> {

    @Override
    public Object getValueType() {
        return String.class;
    }

    @Override
    protected String doGetValue(CustomWidget source) {
        return source.getText();
    }

    @Override
    protected void doSetValue(CustomWidget source, String value) {
        source.setText(value);
    }
}

This WidgetValueProperty can be used as follows.

@PostConstruct
public void createComposite(Composite parent) {
    parent.setLayout(new GridLayout(1, false));

    DataBindingContext dbc = new DataBindingContext();

    CustomWidget widget = new CustomWidget(parent);

    // create the property and then observe the widget
    CustomWidgetProperty customWidgetProperty = new CustomWidgetProperty();
    ISWTObservableValue<String> customWidgetObservableValue = customWidgetProperty.observe(widget);

    Task task = // ...
    IObservableValue<String> taskSummaryObservable = PojoProperties
            .value(Task.class, "summary", String.class).observe(task);

    dbc.bindValue(customWidgetObservableValue, taskSummaryObservable);
}

6.4. Delegates for common properties of different objects

Delegating properties act like a factory for IProperty objects, where the IProperty is not created directly, but when the observe method is called. For instance, a DelegatingValueProperty creates an IValueProperty according to the object that should be observed.

Imagine you want such a factory that returns the CustomWidgetProperty from the previous section for a CustomWidget and the existing WidgetProperties.localDateSelection() for a DateTime. For this, derive from DelegatingValueProperty.

public class VogellaDelegatingValueProperty extends DelegatingValueProperty {

    public VogellaDelegatingValueProperty() {
        this(null);
    }

    public VogellaDelegatingValueProperty(Object valueType) {
        super(valueType);
    }

    // called by observe(source) to pick the property for the given source
    @Override
    protected IValueProperty doGetDelegate(Object source) {
        return switch (source) {
            case CustomWidget _ -> new CustomWidgetProperty();
            case DateTime _ -> WidgetProperties.localDateSelection();
            default -> null;
        };
    }
}

To have a factory like the WidgetProperties class, create a VogellaProperties class.

package com.vogella.operationhistory.databinding;

import org.eclipse.core.databinding.property.value.IValueProperty;

public class VogellaProperties {

    private VogellaProperties() {
    }

    public static IValueProperty vogellaProperty() {
        // handles all widget types known to VogellaDelegatingValueProperty
        return new VogellaDelegatingValueProperty();
    }
}

The WidgetProperties class contains several methods that are better examples of delegating properties.

The following code uses the factory for "vogella" properties and shows samples that fit the intention of the delegates.

import org.eclipse.jface.databinding.swt.ISWTObservableValue;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;

// more code

public class VogellaPropertiesPart {

    @PostConstruct
    public void createPartControl(Composite parent) {
        CustomWidget customWidget = new CustomWidget(parent);

        // observable for the CustomWidget by using the delegate
        IObservableValue customWidgetObservable = VogellaProperties.vogellaProperty().observe(customWidget);

        DateTime dateTime = new DateTime(parent, SWT.DROP_DOWN);
        // observable for the DateTime by using the delegate
        IObservableValue dateTimeObservable = VogellaProperties.vogellaProperty().observe(dateTime);

        // common properties for different kinds of widgets
        Button button = new Button(parent, SWT.CHECK);
        CCombo combo = new CCombo(parent, SWT.READ_ONLY);
        
        // see WidgetSelectionProperty for more details
        ISWTObservableValue oldDateTimeObservable = WidgetProperties.widgetSelection().observe(dateTime);
        ISWTObservableValue buttonSelectionObservable = WidgetProperties.widgetSelection().observe(button);
        ISWTObservableValue comboSelectionObservable = WidgetProperties.widgetSelection().observe(combo);

        // and much more
    }
}

7. Example: Binding a date range with validation

WidgetProperties.localDateSelection() binds the SWT DateTime widget to a java.time.LocalDate, and WidgetProperties.localTimeSelection() binds it to a java.time.LocalTime. The following example uses it for a booking form with a check-in and a check-out date.

Checking each date on its own is not enough, as the check-out date must be after the check-in date. A MultiValidator validates several observables together. Its observeValidatedValue() method wraps a target observable, so that the model receives a value only while the whole validator reports a valid status.

@PostConstruct
public void createComposite(Composite parent) {
    GridLayoutFactory.swtDefaults().numColumns(2).spacing(15, 8).applyTo(parent);

    DataBindingContext dbc = new DataBindingContext();

    // the model, receives only valid date ranges
    WritableValue<LocalDate> checkIn = new WritableValue<>(LocalDate.now(), LocalDate.class);
    WritableValue<LocalDate> checkOut = new WritableValue<>(LocalDate.now().plusDays(3), LocalDate.class);

    new Label(parent, SWT.NONE).setText("Check-in:");
    DateTime checkInWidget = new DateTime(parent, SWT.DATE | SWT.DROP_DOWN);
    new Label(parent, SWT.NONE).setText("Check-out:");
    DateTime checkOutWidget = new DateTime(parent, SWT.DATE | SWT.DROP_DOWN);

    IObservableValue<LocalDate> checkInTarget = WidgetProperties.localDateSelection().observe(checkInWidget);
    IObservableValue<LocalDate> checkOutTarget = WidgetProperties.localDateSelection().observe(checkOutWidget);

    // validates both widgets together
    MultiValidator dateRangeValidator = new MultiValidator() {
        @Override
        protected IStatus validate() {
            if (!checkOutTarget.getValue().isAfter(checkInTarget.getValue())) {
                return ValidationStatus.error("Check-out must be after check-in");
            }
            return ValidationStatus.ok();
        }
    };
    dbc.addValidationStatusProvider(dateRangeValidator);

    // the validated values only pass a valid date range on to the model
    dbc.bindValue(dateRangeValidator.observeValidatedValue(checkInTarget), checkIn);
    dbc.bindValue(dateRangeValidator.observeValidatedValue(checkOutTarget), checkOut);

    ControlDecorationSupport.create(dateRangeValidator, SWT.TOP | SWT.LEFT);

    // derived from the model, so it always shows the last valid booking
    DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
    IObservableValue<String> summary = ComputedValue.create(() -> {
        long nights = ChronoUnit.DAYS.between(checkIn.getValue(), checkOut.getValue());
        return formatter.format(checkIn.getValue()) + " to " + formatter.format(checkOut.getValue()) + " ("
                + nights + (nights == 1 ? " night)" : " nights)");
    });

    Label summaryLabel = new Label(parent, SWT.NONE);
    GridDataFactory.fillDefaults().span(2, 1).grab(true, false).applyTo(summaryLabel);
    dbc.bindValue(WidgetProperties.text().observe(summaryLabel), summary, UpdateValueStrategy.never(), null);

    Label statusLabel = new Label(parent, SWT.NONE);
    GridDataFactory.fillDefaults().span(2, 1).grab(true, false).applyTo(statusLabel);

    Button bookButton = new Button(parent, SWT.PUSH);
    bookButton.setText("Book");
    GridDataFactory.swtDefaults().span(2, 1).align(SWT.END, SWT.CENTER).applyTo(bookButton);

    // converters turn the validation status into a message and an enabled state
    IObservableValue<IStatus> status = dateRangeValidator.getValidationStatus();
    dbc.bindValue(WidgetProperties.text().observe(statusLabel), status, UpdateValueStrategy.never(),
            UpdateValueStrategy.create(IConverter.create(IStatus.class, String.class,
                    s -> s.isOK() ? "" : s.getMessage())));
    dbc.bindValue(WidgetProperties.enabled().observe(bookButton), status, UpdateValueStrategy.never(),
            UpdateValueStrategy.create(IConverter.create(IStatus.class, Boolean.class, IStatus::isOK)));
}

The example combines several data binding features:

  • ControlDecorationSupport shows the error of the MultiValidator on both DateTime widgets.

  • A ComputedValue derives the number of nights from the model and formats the dates with a localized DateTimeFormatter.

  • Converters in an UpdateValueStrategy turn the validation status into the message of a label and the enabled state of the Book button.

If you select a check-out date before the check-in date, the form shows the error and disables the button. The summary still shows the last valid booking, as the invalid date never reaches the model.

Booking form with a check-out date before the check-in date

8. Prerequisites for this tutorial

This article assumes what you have basic understanding of development for the Eclipse platform. Please see Eclipse RCP Tutorial or Eclipse plug-in Tutorial.

For the databinding with JFace Viewers you should already be familiar with the concept of JFace Viewers.

For an introduction on JFace Viewers please see JFace Overview, JFace Tables and JFace Trees.

9. Data Binding with SWT controls

9.1. First example

Create a new Eclipse RCP project named de.vogella.databinding.example using the template RCP application with a View.

Create the de.vogella.databinding.person.model package and the following model classes.

package de.vogella.databinding.example.model;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class Person implements PropertyChangeListener {
    private String firstName;
    private String lastName;
    private boolean married;
    private String gender;
    private Integer age;
    private Address address;
    private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(
            this);

    public Person() {
    }

    public void addPropertyChangeListener(String propertyName,
            PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
    }

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

    public String getFirstName() {
        return firstName;
    }

    public String getGender() {
        return gender;
    }

    public String getLastName() {
        return lastName;
    }

    public boolean isMarried() {
        return married;
    }

    public void setFirstName(String firstName) {
        propertyChangeSupport.firePropertyChange("firstName", this.firstName,
                this.firstName = firstName);
    }

    public void setGender(String gender) {
        propertyChangeSupport.firePropertyChange("gender", this.gender,
                this.gender = gender);
    }

    public void setLastName(String lastName) {
        propertyChangeSupport.firePropertyChange("lastName", this.lastName,
                this.lastName = lastName);
    }

    public void setMarried(boolean isMarried) {
        propertyChangeSupport.firePropertyChange("married", this.married,
                this.married = isMarried);
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        propertyChangeSupport.firePropertyChange("age", this.age,
                this.age = age);
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        address.addPropertyChangeListener("country", this);
        propertyChangeSupport.firePropertyChange("address", this.address,
                this.address = address);
    }

    @Override
    public String toString() {
        return firstName + " " + lastName;
    }

    @Override
    public void propertyChange(PropertyChangeEvent event) {
        propertyChangeSupport.firePropertyChange("address", null, address);
    }

}
package de.vogella.databinding.example.model;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class Address {
    
    public static final String FIELD_STREET = "street";
    public static final String FIELD_NUMBER = "number";
    public static final String FIELD_POSTALCODE = "postalCode";
    public static final String FIELD_CITY = "city";
    public static final String FIELD_COUNTRY = "country";

    private String street;
    private String number;
    private String postalCode;
    private String city;
    private String country;
    
    private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(
            this);

    public void addPropertyChangeListener(String propertyName,
            PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
    }

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

    public Address() {
    }

    public Address(String postalCode, String city, String country) {
        this.postalCode = postalCode;
        this.city = city;
        this.country = country;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        propertyChangeSupport.firePropertyChange(FIELD_STREET, this.street,
                this.street = street);
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        propertyChangeSupport.firePropertyChange(FIELD_NUMBER, this.number,
                this.number = number);
    }

    public String getPostalCode() {
        return postalCode;
    }

    public void setPostalCode(String postalCode) {
        propertyChangeSupport.firePropertyChange(FIELD_POSTALCODE, this.postalCode,
                this.postalCode = postalCode);
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        propertyChangeSupport.firePropertyChange(FIELD_CITY, this.city,
                this.city = city);
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        propertyChangeSupport.firePropertyChange(FIELD_COUNTRY, this.country,
                this.country = country);
    }

    public String toString() {
        String s = "";
        s += street != null ? street + " " : "";
        s += number != null ? number + " " : "";
        s += postalCode != null ? postalCode + " " : "";
        s += city != null ? city + " " : "";
        s += country != null ? country + " " : "";

        return s;
    }

}

Add the JFace Data Binding plug-ins as a dependency to your plug-in.

Change the View class to the following.

package de.vogella.databinding.example;

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.part.ViewPart;

import de.vogella.databinding.example.model.Address;
import de.vogella.databinding.example.model.Person;

public class View extends ViewPart {
  public static final String ID = "de.vogella.databinding.person.swt.View";
  private Person person;

  private Text firstName;
  private Text ageText;
  private Button marriedButton;
  private Combo genderCombo;
  private Text countryText;

  @Override
  public void createPartControl(Composite parent) {

    person = createPerson();
    // Lets put thing to order
    GridLayout layout = new GridLayout(2, false);
    layout.marginRight = 5;
    parent.setLayout(layout);

    Label firstLabel = new Label(parent, SWT.NONE);
    firstLabel.setText("Firstname: ");
    firstName = new Text(parent, SWT.BORDER);

    GridData gridData = new GridData();
    gridData.horizontalAlignment = SWT.FILL;
    gridData.grabExcessHorizontalSpace = true;
    firstName.setLayoutData(gridData);

    Label ageLabel = new Label(parent, SWT.NONE);
    ageLabel.setText("Age: ");
    ageText = new Text(parent, SWT.BORDER);

    gridData = new GridData();
    gridData.horizontalAlignment = SWT.FILL;
    gridData.grabExcessHorizontalSpace = true;
    ageText.setLayoutData(gridData);

    Label marriedLabel = new Label(parent, SWT.NONE);
    marriedLabel.setText("Married: ");
    marriedButton = new Button(parent, SWT.CHECK);

    Label genderLabel = new Label(parent, SWT.NONE);
    genderLabel.setText("Gender: ");
    genderCombo = new Combo(parent, SWT.NONE);
    genderCombo.add("Male");
    genderCombo.add("Female");

    Label countryLabel = new Label(parent, SWT.NONE);
    countryLabel.setText("Country");
    countryText = new Text(parent, SWT.BORDER);

    Button button1 = new Button(parent, SWT.PUSH);
    button1.setText("Write model");
    button1.addSelectionListener(new SelectionAdapter() {

      @Override
      public void widgetSelected(SelectionEvent e) {
        System.out.println("Firstname: " + person.getFirstName());
        System.out.println("Age " + person.getAge());
        System.out.println("Married: " + person.isMarried());
        System.out.println("Gender: " + person.getGender());
        System.out.println("Country: "
            + person.getAddress().getCountry());
      }
    });

    Button button2 = new Button(parent, SWT.PUSH);
    button2.setText("Change model");
    button2.addSelectionListener(new SelectionAdapter() {
      @Override
      public void widgetSelected(SelectionEvent e) {
        person.setFirstName("Lars");
        person.setAge(person.getAge() + 1);
        person.setMarried(!person.isMarried());
        if (person.getGender().equals("Male")) {
            person.setGender("Male");
        } else {
          person.setGender("Female");
        }
        if (person.getAddress().getCountry().equals("Deutschland")) {
          person.getAddress().setCountry("USA");
        } else {
          person.getAddress().setCountry("Deutschland");
        }
      }
    });

    // now lets do the binding
    bindValues();
  }

  private Person createPerson() {
    Person person = new Person();
    Address address = new Address();
    address.setCountry("Deutschland");
    person.setAddress(address);
    person.setFirstName("John");
    person.setLastName("Doo");
    person.setGender("Male");
    person.setAge(12);
    person.setMarried(true);
    return person;
  }

  @Override
  public void setFocus() {
  }

  private void bindValues() {
    // The DataBindingContext object will manage the databindings
    // Lets bind it
    DataBindingContext ctx = new DataBindingContext();
    IObservableValue widgetValue = WidgetProperties.text(SWT.Modify)
        .observe(firstName);
    IObservableValue modelValue = BeanProperties.value(Person.class,
        "firstName").observe(person);
    ctx.bindValue(widgetValue, modelValue);

    // Bind the age including a validator
    widgetValue = WidgetProperties.text(SWT.Modify).observe(ageText);
    modelValue = BeanProperties.value(Person.class, "age").observe(person);
    // add an validator so that age can only be a number
    IValidator validator = new IValidator() {
      @Override
      public IStatus validate(Object value) {
        if (value instanceof Integer) {
          String s = String.valueOf(value);
          if (s.matches("\\d*")) {
            return ValidationStatus.ok();
          }
        }
        return ValidationStatus.error("Not a number");
      }
    };

    UpdateValueStrategy strategy = new UpdateValueStrategy();
    strategy.setBeforeSetValidator(validator);

    Binding bindValue = ctx.bindValue(widgetValue, modelValue, strategy,
        null);
    // add some decorations
    ControlDecorationSupport.create(bindValue, SWT.TOP | SWT.LEFT);

    widgetValue = WidgetProperties.buttonSelection().observe(marriedButton);
    modelValue = BeanProperties.value(Person.class, "married").observe(person);
    ctx.bindValue(widgetValue, modelValue);

    widgetValue = WidgetProperties.comboSelection().observe(genderCombo);
    modelValue = BeanProperties.value("gender").observe(person);

    ctx.bindValue(widgetValue, modelValue);

    // address field is bound to the Ui
    widgetValue = WidgetProperties.text(SWT.Modify).observe(countryText);

    modelValue = BeanProperties.value(Person.class, "address.country")
        .observe(person);
    ctx.bindValue(widgetValue, modelValue);

  }
} 

Run the example and test it. Each time you change the UI element the model changes automatically. If you change the model then the UI will update. If you try to input something else than a number in the age field you will get an error symbol in the UI. If the mouse hovers over the symbol, you see the error message.

9.2. More Customer Validations and ControlDecoration

The following extends the example with the usage of Validators and Decorators.

In this example, the Validators ensures that the firstName has at least 2 characters. A new label displays the validation status via a Decorator.

validation10

Create the following StringLongerThanTwo class.

package de.vogella.databinding.example.validators;

import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

public class StringLongerThanTwo implements IValidator {

    @Override
    public IStatus validate(Object value) {
        if (value instanceof String) {
            String s = (String) value;
            // We check if the string is longer than 2 signs
            if (s.length() > 2) {
                return Status.OK_STATUS;
            } else {
                return ValidationStatus
                        .error("Name must be longer than two letters");
            }
        } else {
            throw new RuntimeException(
                    "Not supposed to be called for non-strings.");
        }
    }
}

The following shows the new code for View.java.

package de.vogella.databinding.example;

import org.eclipse.core.databinding.AggregateValidationStatus;
import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.part.ViewPart;

import de.vogella.databinding.example.model.Address;
import de.vogella.databinding.example.model.Person;
import de.vogella.databinding.example.validators.StringLongerThanTwo;

public class View extends ViewPart {
    public View() {
    }

    public static final String ID = "de.vogella.databinding.person.swt.View";
    private Person person;

    private Text firstName;
    private Text ageText;
    private Button marriedButton;
    private Combo genderCombo;
    private Text countryText;
    private Label errorLabel;

    @Override
    public void createPartControl(Composite parent) {

        person = createPerson();
        GridLayout layout = new GridLayout(2, false);
        layout.marginRight = 5;
        parent.setLayout(layout);

        Label firstLabel = new Label(parent, SWT.NONE);
        firstLabel.setText("Firstname: ");
        firstName = new Text(parent, SWT.BORDER);

        GridData gridData = new GridData();
        gridData.horizontalAlignment = SWT.FILL;
        gridData.grabExcessHorizontalSpace = true;
        firstName.setLayoutData(gridData);

        Label ageLabel = new Label(parent, SWT.NONE);
        ageLabel.setText("Age: ");
        ageText = new Text(parent, SWT.BORDER);

        gridData = new GridData();
        gridData.horizontalAlignment = SWT.FILL;
        gridData.grabExcessHorizontalSpace = true;
        ageText.setLayoutData(gridData);

        Label marriedLabel = new Label(parent, SWT.NONE);
        marriedLabel.setText("Married: ");
        marriedButton = new Button(parent, SWT.CHECK);

        Label genderLabel = new Label(parent, SWT.NONE);
        genderLabel.setText("Gender: ");
        genderCombo = new Combo(parent, SWT.NONE);
        genderCombo.add("Male");
        genderCombo.add("Female");

        Label countryLabel = new Label(parent, SWT.NONE);
        countryLabel.setText("Country");
        countryText = new Text(parent, SWT.BORDER);

        Button button1 = new Button(parent, SWT.PUSH);
        button1.setText("Write model");
        button1.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                System.out.println("Firstname: " + person.getFirstName());
                System.out.println("Age " + person.getAge());
                System.out.println("Married: " + person.isMarried());
                System.out.println("Gender: " + person.getGender());
                System.out.println("Country: "
                        + person.getAddress().getCountry());
            }
        });

        Button button2 = new Button(parent, SWT.PUSH);
        button2.setText("Change model");
        button2.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                person.setFirstName("Lars");
                person.setAge(person.getAge() + 1);
                person.setMarried(!person.isMarried());
                if (person.getGender().equals("Male")) {

                } else {
                    person.setGender("Male");
                }
                if (person.getAddress().getCountry().equals("Deutschland")) {
                    person.getAddress().setCountry("USA");
                } else {
                    person.getAddress().setCountry("Deutschland");
                }
            }
        });

        // this label displays all errors of all bindings
        Label descAllLabel = new Label(parent, SWT.NONE);
        descAllLabel.setText("All Validation Problems:");
        errorLabel = new Label(parent, SWT.NONE);
        gridData = new GridData();
        gridData.horizontalAlignment = SWT.FILL;
        gridData.grabExcessHorizontalSpace = true;
        gridData.horizontalAlignment = GridData.FILL;
        gridData.horizontalSpan = 1;
        errorLabel.setLayoutData(gridData);

        // perform the binding
        bindValues();
    }

    private Person createPerson() {
        Person person = new Person();
        Address address = new Address();
        address.setCountry("Deutschland");
        person.setAddress(address);
        person.setFirstName("John");
        person.setLastName("Doo");
        person.setGender("Male");
        person.setAge(12);
        person.setMarried(true);
        return person;
    }

    @Override
    public void setFocus() {
    }

    private void bindValues() {
        // the DataBindingContext object will manage the databindings
        DataBindingContext ctx = new DataBindingContext();
        IObservableValue widgetValue = WidgetProperties.text(SWT.Modify)
                .observe(firstName);
        IObservableValue modelValue = BeanProperties.value(Person.class,
                "firstName").observe(person);
        // define the UpdateValueStrategy
        UpdateValueStrategy update = new UpdateValueStrategy();
        update.setAfterConvertValidator(new StringLongerThanTwo());
        ctx.bindValue(widgetValue, modelValue, update, null);

        // bind the age including a validator
        widgetValue = WidgetProperties.text(SWT.Modify).observe(ageText);
        modelValue = BeanProperties.value(Person.class, "age").observe(person);
        // add an validator so that age can only be a number
        IValidator validator = new IValidator() {
            @Override
            public IStatus validate(Object value) {
                if (value instanceof Integer) {
                    String s = String.valueOf(value);
                    if (s.matches("\\d*")) {
                        return ValidationStatus.ok();
                    }
                }
                return ValidationStatus.error("Not a number");
            }
        };

        UpdateValueStrategy strategy = new UpdateValueStrategy();
        strategy.setBeforeSetValidator(validator);

        Binding bindValue = ctx.bindValue(widgetValue, modelValue, strategy,
                null);
        // add some decorations
        ControlDecorationSupport.create(bindValue, SWT.TOP | SWT.LEFT);

        widgetValue = WidgetProperties.buttonSelection().observe(marriedButton);
        modelValue = BeanProperties.value(Person.class, "married").observe(
                person);
        ctx.bindValue(widgetValue, modelValue);

        widgetValue = WidgetProperties.comboSelection().observe(genderCombo);
        modelValue = BeanProperties.value("gender").observe(person);

        ctx.bindValue(widgetValue, modelValue);

        widgetValue = WidgetProperties.text(SWT.Modify).observe(countryText);

        modelValue = BeanProperties.value(Person.class, "address.country")
                .observe(person);
        ctx.bindValue(widgetValue, modelValue);

        // listen to all errors via this binding
        // we do not need to listen to any SWT event on this label as it never
        // changes independently
        final IObservableValue errorObservable = WidgetProperties.text()
                .observe(errorLabel);
        // this one listenes to all changes
        ctx.bindValue(errorObservable,
                new AggregateValidationStatus(ctx.getBindings(),
                        AggregateValidationStatus.MAX_SEVERITY), null, null);

    }
}

10. Tutorial: WritableValue

Create a new View in your "de.vogella.databinding.example" plug-in with the following class. Via the buttons you can change the details of the WritableObject.

package de.vogella.databinding.example;

import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Text;

import de.vogella.databinding.example.model.Person;

public class ViewWritableValue extends View {
    private WritableValue value;

    @Override
    public void createPartControl(Composite parent) {
        value = new WritableValue();
        parent.setLayout(new GridLayout(3, false));
        GridData gd = new GridData();
        gd.grabExcessHorizontalSpace = true;
        Text text = new Text(parent, SWT.BORDER);
        Button button = new Button(parent, SWT.PUSH);
        button.setText("New Person");
        button.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                Person p = new Person();
                p.setFirstName("Lars");
                value.setValue(p);
            }
        });

        button = new Button(parent, SWT.PUSH);
        button.setText("Another Person");
        button.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                Person p = new Person();
                p.setFirstName("Jack");
                value.setValue(p);
            }
        });
        DataBindingContext ctx = new DataBindingContext();
        IObservableValue<String> target = WidgetProperties.text(SWT.Modify).observe(
                text);
        IObservableValue<String> model = BeanProperties.value("firstName")
                .observeDetail(value);
        ctx.bindValue(target, model);
    }

    @Override
    public void setFocus() {
    }
}

11. Exercise: Data binding for a JFace Viewer

Create a new Eclipse RCP project "de.vogella.databinding.viewer" using the "RCP Application with a view" template. Add the databinding plug-ins as dependency to your plug-in project.

Create the de.vogella.databinding.viewer.model package and re-create the Person and Address class from the previous example in this script in this package.

Create the following MyModel class to get some example data.

package de.vogella.databinding.viewer.model;

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

public class MyModel {
    public static List<Person> getPersons() {
        List<Person> persons = new ArrayList<Person>();
        Person p = new Person();
        p.setFirstName("Joe");
        p.setLastName("Darcey");
        persons.add(p);
        p = new Person();
        p.setFirstName("Jim");
        p.setLastName("Knopf");
        persons.add(p);
        p = new Person();
        p.setFirstName("Jim");
        p.setLastName("Bean");
        persons.add(p);
        return persons;
    }
}

Create a new view called ViewTable add it to your RCP application. Change ViewTable.java to the following.

package de.vogella.databinding.viewer;

import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.observable.list.WritableList;
import org.eclipse.jface.databinding.viewers.ViewerSupport;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;

import de.vogella.databinding.viewer.model.MyModel;
import de.vogella.databinding.viewer.model.Person;

public class ViewTable extends View {
    private TableViewer viewer;
    private WritableList input;

    @Override
    public void createPartControl(Composite parent) {
        parent.setLayout(new GridLayout(1, false));
        GridData gd = new GridData();
        gd.grabExcessHorizontalSpace = true;

        // Define the viewer
        viewer = new TableViewer(parent);
        viewer.getControl().setLayoutData(
                new GridData(SWT.FILL, SWT.FILL, true, true));
        TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
        column.getColumn().setWidth(100);
        column.getColumn().setText("First Name");
        column = new TableViewerColumn(viewer, SWT.NONE);
        column.getColumn().setWidth(100);
        column.getColumn().setText("Last Name");
        column = new TableViewerColumn(viewer, SWT.NONE);
        column.getColumn().setWidth(100);
        column.getColumn().setText("Married");
        viewer.getTable().setHeaderVisible(true);

        // now lets bind the values
        // No extra label provider / content provider / setInput required
        input = new WritableList(MyModel.getPersons(), Person.class);
        ViewerSupport.bind(
                viewer,
                input,
                BeanProperties.values(new String[] { "firstName", "lastName",
                        "married" }));

        // The following buttons are there to test the binding
        Button delete = new Button(parent, SWT.PUSH);
        delete.setText("Delete");
        delete.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (!viewer.getSelection().isEmpty()) {
                    IStructuredSelection selection = viewer.getStructuredSelection();
                    Person p = (Person) selection.getFirstElement();
                    input.remove(p);
                }
            }
        });

        Button add = new Button(parent, SWT.PUSH);
        add.setText("Add");
        add.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                Person p = new Person();
                p.setFirstName("Test1");
                p.setLastName("Test2");
                input.add(p);
            }
        });
        Button change = new Button(parent, SWT.PUSH);
        change.setText("Switch First / Lastname");
        change.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (!viewer.getSelection().isEmpty()) {
                    IStructuredSelection selection = viewer.getStructuredSelection();
                    Person p = (Person) selection.getFirstElement();
                    String temp = p.getLastName();
                    p.setLastName(p.getFirstName());
                    p.setFirstName(temp);
                }
            }
        });
        
    }

    @Override
    public void setFocus() {
        viewer.getControl().setFocus();
    }
}

In this example, the user interface is updated if you delete and element or add an element to the collection. Run this example and test it.

12. Using ObservableListContentProvider and ObservableMapLabelProvider

If you use WritableList and ObservableListContentProvider you only listen to the changes in the list. You can use ObservableMapLabelProvider to listen to changes of the individual objects.

Change the View.java to the following.

package de.vogella.databinding.viewer;

import java.util.List;

import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.observable.list.WritableList;
import org.eclipse.core.databinding.observable.map.IObservableMap;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.jface.databinding.viewers.ObservableListContentProvider;
import org.eclipse.jface.databinding.viewers.ObservableMapLabelProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ListViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.part.ViewPart;

import de.vogella.databinding.viewer.model.MyModel;
import de.vogella.databinding.viewer.model.Person;

// direct usage of ObservableListContentProvider
// listens to the labels changes too via ObservableMapLabelProvider

public class View extends ViewPart {
    private ListViewer viewer;
    private WritableList input;

    @Override
    public void createPartControl(Composite parent) {
        parent.setLayout(new GridLayout(1, false));
        GridData gd = new GridData();
        gd.grabExcessHorizontalSpace = true;

        // define the viewer
        viewer = new ListViewer(parent);
        viewer.getControl().setLayoutData(
                new GridData(SWT.FILL, SWT.FILL, true, true));
        ObservableListContentProvider contentProvider = new ObservableListContentProvider();
        viewer.setContentProvider(contentProvider);

        // create the label provider including monitoring 
        // of label changes
        IObservableSet knownElements = contentProvider.getKnownElements();
        final IObservableMap firstNames = BeanProperties.value(Person.class,
                "firstName").observeDetail(knownElements);
        final IObservableMap lastNames = BeanProperties.value(Person.class,
                "lastName").observeDetail(knownElements);

        IObservableMap[] labelMaps = { firstNames, lastNames };

        ILabelProvider labelProvider = new ObservableMapLabelProvider(labelMaps) {
            public String getText(Object element) {
                return firstNames.get(element) + " " + lastNames.get(element);
            }
        };

        viewer.setLabelProvider(labelProvider);

        // create sample data
        List<Person> persons = MyModel.getPersons();
        input = new WritableList(persons, Person.class);
        // set the writeableList as input for the viewer
        viewer.setInput(input);

        Button delete = new Button(parent, SWT.PUSH);
        delete.setText("Delete");
        delete.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                deletePerson();
            }

        });

        Button add = new Button(parent, SWT.PUSH);
        add.setText("Add");
        add.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                addPerson();
            }

        });
        Button change = new Button(parent, SWT.PUSH);
        change.setText("Switch First / Lastname");
        change.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                switchFirstLastName();
            }

        });
    }

    public void switchFirstLastName() {
        if (!viewer.getSelection().isEmpty()) {
            IStructuredSelection selection = viewer.getStructuredSelection();
            Person p = (Person) selection.getFirstElement();
            String temp = p.getLastName();
            p.setLastName(p.getFirstName());
            p.setFirstName(temp);
        }
    }

    public void deletePerson() {
        if (!viewer.getSelection().isEmpty()) {
            IStructuredSelection selection = viewer.getStructuredSelection();
            Person p = (Person) selection.getFirstElement();
            input.remove(p);
        }
    }

    public void addPerson() {
        Person p = new Person();
        p.setFirstName("Test1");
        p.setLastName("Test2");
        input.add(p);
    }

    @Override
    public void setFocus() {
        viewer.getControl().setFocus();
    }

}

13. Eclipse Data Binding resources

Home Tutorials Training Consulting Books Company Contact us


Get more...