This tutorial explains the usage of the Eclipse JFace Data Binding framework which 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 to synchronize changes in properties of objects. Validation and conversion during the synchronization process is supported.
For example, you could bind the String property called firstName
of a Person
object to a text
property of the 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 to changes in SWT widgets and JFace UI elements.
Other Java object must implement this support.
For example you can implement PropertyChangeSupport
according to the Java Bean specification in these Java elements.
Or you can use the WritableValue
interface from JFace inside the model.
The following plug-ins implement JFace 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 Properties API
provides factories to create IObservableValue
objects.
The following table gives an overview of available classes to create instances of IObservableValue
.
Factory | Description |
---|---|
|
Used to create |
|
Used for Java Beans. A Java Bean is a Java object which follows the Java Bean
specification. This specification
requires that the class
implements
|
|
Used for properties of SWT widgets. |
|
Used for properties of JFace Viewer. |
|
Used for properties of non specialized types, like Objects, Collections or Maps. |
|
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", String.class).observe(person);
// prefer using beans if your data model provides property change support
var beamsModel = BeanProperties.value("firstName").observe(person);
var beamsModel2 = 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 firstNameText
.
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
// more code
var observe = WidgetProperties.text(SWT.Modify).observe(text);
1.2. Example for a Java bean implementation
A Java class which 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 could extend this class. The following example demonstrates that.
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 JFaces WritableValue class to track changes
If you are able to use JFace API in your domain model, you can use WritableValue
to provide the option to track values.
import org.eclipse.core.databinding.observable.value.WritableValue;
public class Person {
private WritableValue<String> firstName = new WritableValue<>();
private WritableValue<String> lastName = new WritableValue<>();
/**
* @return the person's first name
* @TrackedGetter
*/
public String getFirstName() {
return firstName.getValue();
}
/**
* @param firstName
* The summary to set.
*/
public void setFirstName(String firstName) {
this.firstName.setValue(firstName);
}
/**
* @return Returns the description.
* @TrackedGetter
*/
public String getLastName() {
return lastName.getValue();
}
/**
* @param lastName
* The last name to set.
*/
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 these properties cannot be observed, you
you
can
still
use data binding for a
uni-directional binding.
For example, you can connect the text property of a SWT Text
field to the summary field of a Todo
object, even if Todo cannot be observed.
In this case, updates in the summary of the
Todo
object will not update the
Text
widget. But relevant change in the
Text
widget
user interface will update the
Todo
property.
2. Create bindings
2.1. Using the ISideEffect API
Eclipse 4.6 introduced the ISideEffect
databinding API.
It allows you to run code whenever one or more observables changes.
An ISideEffect
is a lot like a listener except that it doesn’t need to be attached to anything.
Instead, it reacts automatically 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(
() -> {return "Your username is: " + username.getValue();},
yourUsername::setText);
ISideEffects can react to changes in multiple observables, and will avoid performing 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 life cycle. For example, all ISideEffect instances that are created by the ISideEffectFactory obtained from the WidgetSideEffects#createFactory(Widget disposableWidget) method are automatically disposed once the corresponding widget is disposed.
ISWTObservableValue personFirstNameTextObservable = WidgetProperties.text(SWT.Modify)
.observe(personFirstNameText);
ISWTObservableValue 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.
Via the DataBindingContext.bindValue()
method two IObservableValue
objects are connected.
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.
The initial copying from model to target is useful for the
initial
synchronization.
For example, if you have an attribute of a Person
p object and the text attribute of a
Text
txtName widget, you typically want to copy the value from p to
txtName at the beginning.
// create new Context
DataBindingContext ctx = new DataBindingContext();
// define the IObservables
IObservableValue target = WidgetProperties.text(SWT.Modify).
observe(firstName);
IObservableValue model= BeanProperties.
value(Person.class,"firstName").observe(person);
// connect them
ctx.bindValue(target, model);
You can also observe nested model properties, e.g.,
attributes of classes which are contained in another class.
The
following code
demonstrates how to access the
country
property in the
address
field of the object
person
.
IObservable model = PojoProperties.value(Person.class, "address.country").observe(person);
2.3. Observing map properties
You can also observe entries of a map via an
IObservableMap
with 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.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 IObservableMap attributesMap = new WritableMap();
private DataBindingContext dbc;
private Todo todo;
@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 txtFirstAttributeObservable =
WidgetProperties.text(SWT.Modify).observe(txtFirstAttribute);
ISWTObservableValue txtSecondAttributeObservable = WidgetProperties.text(SWT.Modify)
.observe(txtSecondAttribute);
// create observables for the Map entries
IObservableValue firstAttributeObservable =
Observables.observeMapEntry(attributesMap, FIRST_ATTRIBUTE);
IObservableValue secondAttributeObservable =
Observables.observeMapEntry(attributesMap, SECOND_ATTRIBUTE);
dbc.bindValue(txtFirstAttributeObservable, firstAttributeObservable);
dbc.bindValue(txtSecondAttributeObservable, secondAttributeObservable);
}
@Inject
@Optional
public void setModel(@Named(IServiceConstants.ACTIVE_SELECTION) Todo todo) {
if (todo != null) {
this.todo = todo;
// set new values for the map entries from a model object
attributesMap.put(FIRST_ATTRIBUTE, todo.getSummary());
attributesMap.put(SECOND_ATTRIBUTE, todo.getDescription());
}
}
@Persist
public void save() {
if (todo != null) {
// only store the actual values on save and not directly
todo.setSummary((String) attributesMap.get(FIRST_ATTRIBUTE));
todo.setDescription((String) attributesMap.get(SECOND_ATTRIBUTE));
}
}
}
3. Update strategy, converters and validators
3.1. UpdateValueStrategy
The bindValue()
method from 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 values are permitted:
Value | Description |
---|---|
|
Policy constant denoting that the source observable’s state should not be tracked and that the destination observable’s value should never be updated. |
|
Policy constant denoting that the source observable’s state should not be
tracked, but that validation,
conversion and updating the destination observable’s value should be performed when
explicitly requested.
You can call |
|
Policy constant denoting that the source observable’s state should be tracked, including Validate changes except for validateBeforeSet(Object), but that the destination observable’s value should only be updated on request. |
|
Policy constant denoting that the source observable’s state should be tracked, and that validation, conversion and updating the destination observable’s value should be performed automatically on every change of the source observable value. |
If no UpdateValueStrategy
is specified, the UpdateValueStrategy.POLICY_UPDATE
is used by default.
You can register converters and validators in the UpdateValueStrategy
object.
3.2. Converter
Converters allow to convert the values between the observed properties.
Converters are defined based on the IConverter
interface.
UpdateValueStrategy.setConverter()
allows to set the converter.
The IConverter.create(Object, Object, Function)
allows to create a converter using a lambda expression.
The static UpdateValueStrategy.create(IConverter)
method allows to create an update strategy with a converter.
For example, you can convert a String to another object and vice versa.
For example, assume you have a Person
object with a programmingSkills
property.
package com.vogella.rcp.databinding.parts;
import com.vogella.rcp.databinding.parts.ModelObject;
public class Person 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 text separated by comma.
Therefore a conversion is necessary.
In order to apply these converters an UpdateValueStrategy
for the binding needs to be defined and applied.
package com.vogella.rcp.databinding.parts;
import jakarta.annotation.PostConstruct;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.beans.BeanProperties;
import org.eclipse.core.databinding.conversion.IConverter;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.jface.databinding.swt.WidgetProperties;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
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.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
public class SamplePart {
@PostConstruct
@SuppressWarnings("unchecked")
public void createPartControl(Composite parent) {
// create the Person model with programming skills
Person person = new Person();
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 convertToStringArray =
IConverter.create(String.class, String[].class, (o1) -> ((String) o1).split(","));
IConverter convertToString =
IConverter.create(String[].class, String.class, (o1) -> convert((String[])o1));;
// create the observables, which should be bound
IObservableValue<Text> programmingSkillsTarget = WidgetProperties.text(SWT.Modify).observe(programmingSkillsText);
IObservableValue<Person> programmingSkillsModel = BeanProperties.value("programmingSkills").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(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
for (String string : person.getProgrammingSkills()) {
System.out.println(string);
}
}
});
}
private static String convert(String[] fromObject) {
String[] stringArray = fromObject;
StringBuilder sb = new StringBuilder();
int length = stringArray.length;
for (int i = 0; i < length; i++) {
String string = stringArray[i];
sb.append(string);
if (i + 1 < length) {
sb.append(",");
}
}
return sb.toString();
}
}
3.3. Validator
A validator allows you to implement validation of the data before it is propagated to the other connected property.
A class which wants to provide this functionality must implement the org.eclipse.core.databinding.validation.IValidator
interface.
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.beans.BeanProperties;
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 convertToStringArray = IConverter.create(String.class, String[].class,
(o1) -> ((String) o1).split(","));
// ensure perl is not accepted as programming language
UpdateValueStrategy updateStrategy = UpdateValueStrategy.create(convertToStringArray);
updateStrategy.setAfterGetValidator((o1) -> {
String s = (String) o1;
if (!s.contains("Perl")) {
return ValidationStatus.ok();
}
return ValidationStatus.error("Perl is not a programming language");
});
Binding bindValue =
ctx.bindValue(widgetValue, modelValue, updateStrategy, null);
The |
4. More on bindings
4.1. ControlDecorators
JFace Data Binding allows you to use icon decorators in
the user
interface which reflect the status of the field
validation. This
allows you to provide immediate feedback to the user. For the
creation of the control decoration you use the return object from the
bindvalue()
method of
DataBindingContext
object.
// The following code assumes that a Validator is already defined
Binding bindValue =
ctx.bindValue(widgetValue, modelValue, strategy, null);
// add some decorations to the control
ControlDecorationSupport.create(bindValue, SWT.TOP | SWT.LEFT);
The result might look like the following screenshot.
4.2. Binding values of a radio button group
When using radio buttons each button usually represents a certain
value. In order to
bind the value according to the selected state in a
radio button group, the
SelectObservableValue
class is the right choice.
The following example shows how to bind the currently selected radio
value to a
Label
.
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 = new String[] { "Matthew Hall", "Dirk Fauth", "Lars Vogel",
"Simon Scholz" };
SelectObservableValue selectedRadioButtonObservable = new SelectObservableValue();
for (String name : names) {
Button button = new Button(group, SWT.RADIO);
button.setText(name);
// Add name as option value in case the appropriate button is selected
selectedRadioButtonObservable.addOption("Selected: " + name, WidgetProperties.selection().observe(button));
}
Label label = new Label(shell, SWT.NONE);
GridDataFactory.fillDefaults().applyTo(label);
ISWTObservableValue 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 this:
4.3. Listening to all changes in the binding
You can register a listener to all bindings of the
DataBindingContext
class. Your listener will be called when something has changed.
For example this can be used to determine the status of a part which behaves like an editor. If its data model changes, this editor marks itself as dirty.
// define your change listener
// dirty holds the state for the changed status of the editor
IChangeListener listener = new IChangeListener() {
@Override
public void handleChange(ChangeEvent event) {
// Ensure dirty is not null
if (part!=null){
part.setDirty(true);
}
};
private void updateUserInterface(Todo todo) {
// check that the user interface is available
if (txtSummary != null && !txtSummary.isDisposed()) {
// Deregister change listener to the old binding
IObservableList providers = ctx.getValidationStatusProviders();
for (Object o : providers) {
Binding b = (Binding) o;
b.getTarget().removeChangeListener(listener);
}
// dispose the binding
ctx.dispose();
// NOTE
// HERE WOULD BE THE DATABINDING CODE
// INTENTIALLY LEFT OUT FOR BREVITY
// get the validation status provides
IObservableList bindings =
ctx.getValidationStatusProviders();
// mot all validation status providers
// are bindings, e.g. MultiValidator
// otherwise you could use
// context.getBindings()
// register the listener to all bindings
for (Object o : bindings) {
Binding b = (Binding) o;
b.getTarget().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.
You can exchange this reference in
WritableValue
and the databinding will use the new (reference) object for its
binding.
This way you can create the binding once and still exchange
the object
which is bound by databinding.
To bind to a
WritableValue
you use the
observeDetail()
method,
to inform the framework that you would like to observe
the
contained
object.
WritableValue value = new WritableValue();
// create the binding
DataBindingContext ctx = new DataBindingContext();
IObservableValue target = WidgetProperties.
text(SWT.Modify).observe(text);
IObservableValue model = BeanProperties.value("firstName").
observeDetail(value);
ctx.bindValue(target, model);
// create a Person object called 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 is able to compute a
value from observable trackers,
which are referenced in it’s
calculate
method.
public class FormattedName extends ComputedValue {
private IObservableValue<String> firstName;
private IObservableValue<String> lastName;
FormattedName(IObservableValue<String> firstName, IObservableValue<String> lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
protected Object calculate() {
// since lastName and firstName are tracked getter (see JavaDoc of
// getValue() mehtod) the value of this ComputedValue gets recomputed once
// lastName or firstName are changed
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;
}
}
Since Eclipse Neon a new
create
method has been introduced, which accepts a Supplier that contains
the actual implementation of the calculate method.
So there’s no need any more to derive from
ComputedValue
.
IObservableValue<String> lastName = // ...
IObservableValue<String> firstName = // ...
IObservableValue<String> formattedName = ComputedValue.create(() -> {
// since lastName and firstName are tracked getter (see JavaDoc of
// getValue() mehtod) the value of this ComputedValue gets recomputed once
// lastName or firstName are changed
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;
});
So basically ComputedValue can easily combine different observables and is recomputed every time one of the tracked getter are touched.
A really popular example of a
ComputedValue
implementation is the
AggregateValidationStatus
, which keeps track of all validation statuses, which have been
passed to it and aggregates them to one MultiStatus or just returns
the one with maximum severity.
It is also a great alternative to converter implementations. For
instance the previously described
AggregateValidationStatus
returns an
IStatus
as value, but if this should be bound to the enable state of a
Finish button, having a boolean value would be necessary.
// Track the validation status from all binding 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 buttonEnabledObservable = WidgetProperties.enabled().observe(finishButton);
// bind the enablement of the finish button to the validation
dbc.bindValue(buttonEnabledObservable, isValidationOk);
Applying an IConverter to convert from IStatus to boolean would be far more overhead.
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 distinguish between changes in the collection and changes in the individual object.
In the case that 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
which 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());
// create sample data
List<Person> persons = createExampleData();
// wrap the input into a writable list
IObservableList 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 also use the
ObservableMapLabelProvider
class
to observe changes of the list elements.
ObservableListContentProvider contentProvider =
new ObservableListContentProvider();
// create the label provider which includes monitoring
// of changes to update the labels
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);
}
};
5.3. ViewerSupport
ViewerSupport
simplifies the setup for JFace viewers in cases where selected columns
should be
displayed. It
registers changes
listeners on the collection as
well as
on
the
individual
elements.
ViewerSupport
creates via the
bind()
method
the
LabelProvider
and
ContentProvider
for a viewer
automatically.
// the MyModel.getPersons() method call returns a List<Person> object
// the WritableList object wraps this object in an IObservableList
input = new WritableList(MyModel.getPersons(), Person.class);
// The following creates and binds the data
// for the Table based on the provided input
// no additional label provider /
// content provider / setInput required
ViewerSupport.bind(viewer, input,
BeanProperties.
values(new String[] { "firstName", "lastName", "married" }));
5.4. Master Detail binding
The
ViewerProperties
class allows you to create
IObservableValue
for properties of the viewer. For example you can track the current
selection, e.g., 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 you can use the
PojoProperties
or the
BeanProperties
class. Both provide the
value().observeDetail()
method chain,
which allows you to observe a detailed value of an
IObservableValue
object.
For example the following will map the
summary
property of the
Todo
domain object to a
Label
based on the selection of a
ComboViewer
.
// assume we have Todo domain objects
// todos is a of type: List<Todo>
final ComboViewer viewer = new ComboViewer(parent, SWT.DROP_DOWN);
viewer.setContentProvider(ArrayContentProvider.getInstance());
viewer.setLabelProvider(new LabelProvider() {
public String getText(Object element) {
Todo todo = (Todo) element;
return todo.getSummary();
};
});
viewer.setInput(todos);
// 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 target = WidgetProperties.text().observe(label);
// observe the selection
IViewerObservableValue selectedTodo = ViewerProperties
.singleSelection().observe(viewer);
// observe the summary attribute of the selection
IObservableValue detailValue =
PojoProperties
.value("summary", String.class)
.observeDetail(selectedTodo)
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 viewerSelectionSummaryObservable =
ViewerProperties.singleSelection()
.value(BeanProperties.value("summary", String.class))
.observe(viewer);
IListProperty siblingNames = BeanProperties.
value("parent").list("children").values("name");
IObservableList siblingNamesObservable =
siblingNames.observe(node);
6. Extending data binding with custom observables
6.1. Developing custom observables
You can also implement observables for your own classes.
This implementation can extend the AbstractObservable
class or one of it’s subclasses, like the AbstractObservableValue
class.
For example, assume you have the following custom widget which draws a text on a canvas
public class CustomWidget extends Canvas {
private String text;
private Point textExtent;
public CustomWidget(Composite parent) {
super(parent, SWT.NONE);
addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
GC gc = e.gc;
// draw the text, which can be set
String textValue = getText() != null ? getText() : "Good Default";
gc.drawText(textValue, 5, 5);
textExtent = gc.textExtent(textValue);
}
});
}
@Override
public Point computeSize(int wHint, int hHint, boolean changed) {
checkWidget();
return textExtent != null ? textExtent : new Point(30, 12);
}
public String getText() {
checkWidget();
return text;
}
public void setText(String text) {
checkWidget();
this.text = text;
redraw();
}
}
Since there is no predefined
IObservable
for custom widgets, you have to
implement a custom one.
While is is possible to use
PojoProperties
class
to create an
IObservableValue
by using
PojoProperties.value("text").observe(customWidget)
this would not result in an
ISWTObservableValue .
Only by using an
ISWTObservable
classes like
ControlDecorationSupport
would work, because only those offer a
public Widget getWidget();
method, so that the widget may be
decorated.
|
6.2. Directly implement IObservable
The
CustomWidgetObservableValue
extends
AbstractObservableValue
and also implements the
ISWTObservableValue
interface.
public class CustomWidgetObservableValue extends AbstractObservableValue
implements ISWTObservableValue {
private 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 Object doGetValue() {
return customWidget.getText();
}
@Override
protected void doSetValue(Object value) {
customWidget.setText(value.toString());
}
@Override
public Widget getWidget() {
// implement the ISWTObservableValue interface to enable ControlDecorationSupport
return customWidget;
}
}
This observable can then be used like this:
@PostConstruct
public void createComposite(Composite parent) {
parent.setLayout(new GridLayout(1, false));
DataBindingContext dbc = new DataBindingContext();
CustomWidget widget = new CustomWidget(parent);
CustomWidgetObservableValue customWidgetObservableValue = new CustomWidgetObservableValue(widget);
Task task = //...
IObservableValue todoSummaryObservable = PojoProperties.value("summary").observe(task);
dbc.bindValue(customWidgetObservableValue, todoSummaryObservable);
}
6.3. Implement an IProperty rather than IObservable directly
A better approach is to implement the
IProperty
interface, like
IValueProperty
,
IWidgetValueProperty
and others.
So let’s implement the solution of the previous section with an
IProperty
implementation.
public class CustomWidgetProperty extends WidgetValueProperty {
@Override
public Object getValueType() {
return String.class;
}
@Override
protected Object doGetValue(Object source) {
if(source instanceof CustomWidget) {
return ((CustomWidget) source).getText();
}
return "";
}
@Override
protected void doSetValue(Object source, Object value) {
if(source instanceof CustomWidget && value instanceof String) {
((CustomWidget) source).setText((String) value);
}
}
}
This
WidgetValueProperty
can be used like this:
@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 afterwards
CustomWidgetProperty customWidgetProperty = new CustomWidgetProperty();
ISWTObservableValue customWidgetObservableValue = customWidgetProperty.observe(widget);
Task task = //...
IObservableValue todoSummaryObservable = PojoProperties.value("summary").observe(task);
dbc.bindValue(customWidgetObservableValue, todoSummaryObservable);
}
6.4. Delegates for common properties of different objects
Delegating properties act a bit like a factory for
IProperty
objects,
where the
IProperty
is not created directly, but at the moment, when the
observe
method is called. For instance a
DelegatingValueProperty
creates a
IValueProperty
according
to the object, which should be observed.
So you can decision, which
IValueProperty
is used, is made at the moment, when the
observe
method is actually called.
Imagine, we want such a factory for the
IValueProperty
classes we created
in the former sections. Therefore we can derive from
DelegatingValueProperty
.
public class VogellaDelegatingValueProperty extends DelegatingValueProperty {
public VogellaDelegatingValueProperty() {
this(null);
}
public VogellaDelegatingValueProperty(Object valueType) {
super(valueType);
}
@Override
protected IValueProperty doGetDelegate(Object source) {
// return appropriate IValueProperty according to the given source,
// which is passed, when the observe method is called
if(source instanceof CustomWidget) {
return new CustomWidgetProperty();
}else if (source instanceof DateTime) {
return new DateTimeSelectionProperty();
}
return null;
}
}
In order to have a factory like the
WidgetProperties
class, we could
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() {
// return a vogella property, which can handle different vogella
// properties
return new VogellaDelegatingValueProperty();
}
}
You might want to have a look at
the
WidgetProperties
, which contains several methods with more sense for
those delegates.
Here is the code how to use the factory for "vogella" properties and some samples, which fit to the intention of the delegates.
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 dateTimeJava8Observable = VogellaProperties.vogellaProperty().observe(dateTime);
// ### Real common properties for different kinds of objects ###
Button button = new Button(parent, SWT.CHECK);
CCombo combo = new CCombo(parent, SWT.READ_ONLY);
// see WidgetSelectionProperty for more details
ISWTObservableValue oldDateTimeObservable = WidgetProperties.selection().observe(dateTime);
ISWTObservableValue buttonSelectionObservable = WidgetProperties.selection().observe(button);
ISWTObservableValue comboSelectionObservable = WidgetProperties.selection().observe(combo);
// and much more
}
}
7. Example: Custom observable for the DateTime Widget and the Java DateTime
Java provides a Data and Time API. While JFace databinding provides a standard way to bind to it, we use this as an example how to write your own custom binding. This should demonstrate how to create your own binding, even though for this particular example you should use the offical binding.
Implement the following class.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.Calendar;
import java.util.Date;
import org.eclipse.jface.databinding.swt.WidgetValueProperty;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.DateTime;
public class DateTimeSelectionProperty extends WidgetValueProperty {
// this constant is used to correct the differences concerning the month counting
private static final int MONTH_MAPPING_VALUE = 1;
public DateTimeSelectionProperty() {
super(SWT.Selection);
}
@Override
public Object getValueType() {
// use TemporalAccessor here, because it is the common interface for
// LocalDate, LocalTime and LocalDateTime.
return TemporalAccessor.class;
}
@Override
protected Object doGetValue(Object source) {
DateTime dateTime = (DateTime) source;
// create LocalTime instance, if SWT.TIME is used,...
if ((dateTime.getStyle() & SWT.TIME) != 0) {
return LocalTime.of(dateTime.getHours(), dateTime.getMinutes(),
dateTime.getSeconds());
}
// ... otherwise LocalDate
return LocalDate.of(dateTime.getYear(), dateTime.getMonth()
+ MONTH_MAPPING_VALUE, dateTime.getDay());
}
@Override
protected void doSetValue(Object source, Object value) {
DateTime dateTime = (DateTime) source;
if (value == null)
throw new IllegalArgumentException(
"Cannot set null selection on DateTime");
TemporalAccessor temporalAccessor = getTemporalAccessor(value);
if (temporalAccessor == null)
throw new IllegalArgumentException(
"Cannot find TemporalAccessor for the given value");
// set only hours, minutes and seconds in case the SWT.TIME flag is
// set,...
if ((dateTime.getStyle() & SWT.TIME) != 0) {
dateTime.setTime(temporalAccessor.get(ChronoField.HOUR_OF_DAY),
temporalAccessor.get(ChronoField.MINUTE_OF_HOUR),
temporalAccessor.get(ChronoField.SECOND_OF_MINUTE));
} else {
// ... otherwise set year, month and day.
dateTime.setDate(temporalAccessor.get(ChronoField.YEAR),
temporalAccessor.get(ChronoField.MONTH_OF_YEAR)
- MONTH_MAPPING_VALUE,
temporalAccessor.get(ChronoField.DAY_OF_MONTH));
}
}
// get TemporalAccessor from a Date, Calendar or TemporalAccessor object
private TemporalAccessor getTemporalAccessor(Object value) {
TemporalAccessor temporalAccessor = null;
if (value instanceof Date) {
temporalAccessor = LocalDateTime.from(((Date) value).toInstant());
} else if (value instanceof TemporalAccessor) {
temporalAccessor = (TemporalAccessor) value;
} else if (value instanceof Calendar) {
temporalAccessor = LocalDateTime.from(((Calendar) value)
.toInstant());
}
return temporalAccessor;
}
}
Here we implement a binding between the DateTime
SWT widget and a TemporalAccessor
, which for instance can be a java.time.LocalDate
or java.time.LocalTime
.
The usage of the DateTimeSelectionProperty
is demonstrated by the following listing.
@PostConstruct
public void createComposite(Composite parent) {
parent.setLayout(new GridLayout(1, false));
DataBindingContext dbc = new DataBindingContext();
DateTime dateTime = new DateTime(parent, SWT.DROP_DOWN);
// use the previously created DateTimeSelectionProperty
DateTimeSelectionProperty dateTimeSelectionProperty = new DateTimeSelectionProperty();
dateTimeObservableValue = dateTimeSelectionProperty.observe(dateTime);
// create and initialize a placeholder observable
writableDateTimeModel = new WritableValue();
writableDateTimeModel.setValue(LocalDate.now());
// bind DateTime widget to the writable value using our obserables
dbc.bindValue(dateTimeObservableValue, writableDateTimeModel);
Label label = new Label(parent, SWT.BORDER);
GridDataFactory.fillDefaults().grab(true, false).applyTo(label);
ISWTObservableValue dateTimeLabelObservable = WidgetProperties.text().observe(label);
// bind a Label to the DateTime Widget, in order to see the changes,
// which are made in the DateTime widget
dbc.bindValue(dateTimeLabelObservable, dateTimeObservableValue);
tableViewer = new TableViewer(parent);
tableViewer.getTable().setLayoutData(new GridData(GridData.FILL_BOTH));
tableViewer.add("27.04.1986 00:30:20");
tableViewer.add("23.06.1976 03:15:30");
tableViewer.add("27.06.1980 12:05:40");
tableViewer.add("26.03.2015 22:25:50");
tableViewer.add("06.12.2015 20:00:10");
final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
// databinding could be used here similar to the following code:
// dbc.bindValue(ViewerProperties.singleSelection().
// observe(tableViewer),
// writableDateTimeModel,
// stringToLocalDateTimeUpdateStrategy,
// LocalDateTimeToStringUpdateStrategy)
// the following used an ISelectionChangedListener instead of databinding as this
// results in shorter code
tableViewer.addSelectionChangedListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection = event.getSelection();
if (selection instanceof IStructuredSelection) {
String string = (String) ((IStructuredSelection) selection).getFirstElement();
// parse string from table with the DateTimeFormatter from above
LocalDateTime localDateTime = LocalDateTime.parse(string, dateTimeFormatter);
// set the selected localDateTime to the WritableValue,
// which is bound to the DateTime widget
writableDateTimeModel.setValue(localDateTime);
}
}
});
}
In this sample we bind the DateTime
widget to a WritableValue
, which contains a TemporalAccessor
instance.
This WritableValue
is changed by selecting a date in the TableViewer.
The `dateTimeObservableValue
is also bound to a label, in order to see the changes by manually manipulating to the DateTime
widget.
The result looks similar to the following screenshot.
If you select a date in the TableViewer
, this updates the WritableValue
.
Therefore the DateTime
widget and the Label
are updated, because all these are bound together.
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 Plugin 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 "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 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.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.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.selection().observe(marriedButton);
modelValue = BeanProperties.value(Person.class, "married").observe(person);
ctx.bindValue(widgetValue, modelValue);
widgetValue = WidgetProperties.selection().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 also update. If you try to input something else than a number in the age field you will get an error symbol in the UI and 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.
Create the following
StringLongerThenTwo
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 StringLongerThenTwo 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 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.BeanProperties;
import org.eclipse.core.databinding.beans.BeansObservables;
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.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.StringLongerThenTwo;
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 StringLongerThenTwo());
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.selection().observe(marriedButton);
modelValue = BeanProperties.value(Person.class, "married").observe(
person);
ctx.bindValue(widgetValue, modelValue);
widgetValue = WidgetProperties.selection().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.BeanProperties;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.jface.databinding.swt.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 target = WidgetProperties.text(SWT.Modify).observe(
text);
IObservableValue 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.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 listens 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.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
include::../10_Include/sourcejava.adoc[] If you need more assistance we offer https://learn.vogella.com/[Online Training] and https://www.vogella.com/training/[Onsite training] as well as https://www.vogella.com/consulting/[consulting]