Introduce the new Properties API
In 3.5 Eclipse-Databinding introduces a new API which should be used in favor of the 3.4 EMFObservables-Factory because it provides many features who couldn’t be provided by the old API (e.g. the new API provides you with the possibility to observe nested attributes in Table/TreeViewers, allows you do observe different object types in the same viewer which is very handy in case of a TreeViewer).
Like in previous releases the Core-Eclipse-Databinding bundles define an API and provide an abstract base implementations and downstream modules need to provide the concrete implementation for the domain model implementation and its notification system.
Let’s take a look at the bundles and their dependencies:
As you notice EMF-Databinding is split into 2 bundles:
- org.eclipse.emf.databinding: The core implementation of the Eclipse-Databinding-API which makes changes using EObject#eSet(), EObject#eAdd() and EObject#eRemove() methods
- org.eclipse.emf.databinding.edit: Implementation which builds upon the EMF-Databinding-Core implementation but uses features provided by the EMF-Edit. Changes to the model instance are not made directly on the model-instance but are done by executing EMF-Commands on so called EditingDomain. Going through this additional framework provides us new features like Undo/Redo-Support. Naturally whenever you get new features you have to pay a price – going through this additional API causes a bit overhead.
An example might make the difference between modifying a domain instance in pure EMF vs EMF-Edit clearer. In pure EMF setting the lastname attribute of a Person is done like this:
public void example(Person p) { p.eSet( ProjectPackage.Literals.PERSON__LASTNAME, "Schindl" ); }
And when using EMF-Edit:
public void example(EditingDomain eDomain, Person p) { Command cmd = SetCommand.create( eDomain, p, ProjectPackage.Literals.PERSON__LASTNAME, "Schindl"); if( cmd.canExecute() ) { eDomain.getCommandStack().execute(cmd); } }
Now that we saw the highlevel overview it’s time to take a closer look at the new Eclipse-Databinding-API. It is called Properties-API because of the name of the root interface org.eclipse.core.databinding.property.IProperty.
The nature of nested properties
A nested property is a property which is not the direct member of an observed class but one of its member instances. In our example application we have many different nested properties for example the lastname of a committer:
If we have a CommitterShip instance we are not able to directly access the last name but have to go through the person-property.
public void example(CommitterShip cship) { // CommitterShip.person Person p = cship.eGet( ProjectPackage.Literals.COMMITTER_SHIP__PERSON ); // Person.lastname String lastname = p.eGet( ProjectPackage.Literals.PERSON__LASTNAME ); }
What we are doing here is traversing the object-tree to get access to the property we are interested in and this traversal is often referred to as following a path through the model.
Observing nested properties is quite a common use case but the 3.4 Databinding-API didn’t provided such support out of the box though it was available in some situations.
This changed with the new Properties-API which has built in support for nested properties and so the EMF-Databinding honors this with a set of factory methods (see below for more informations on this) and one special class describing the path to a nested property.
So specifing the path shown above we’d write Java code like this:
// /person/lastname FeaturePath path = FeaturePath.fromList( ProjectPackage.Literals.COMMITTER_SHIP__PERSON, ProjectPackage.Literals.PERSON__LASTNAME );
The Factories
The EMF-Databinding implementations hide the implementation behind 2 factories:
This class provides factory methods to create instances of IEMFValueProperty, IEMFListProperty and IEMFMapProperty by passing the EStructuralFeature or the path to a nested EStructuralFeature the property should represent.
// Direct attribute IEMFProperty pSurname = EMFProperties.value( ProjectPackage.Literals.PERSON__LASTNAME ); // Nested attribute IEMFProperty committerSurname = EMFProperties.value( FeaturePath.fromList( ProjectPackage.Literals.COMMITTER_SHIP__PERSON, ProjectPackage.Literals.PERSON__LASTNAME ) );
EMFEditProperties
This class provides factory methods to create instances of IEMFEditValueProperty, IEMFEditListProperty and IEMFEditMapProperty by passing the EStructuralFeature or the path to a nested EStructuralFeature the property should represent and the EditingDomain through which the modifications are made.
The usage of the API is very similar to the one of EMFProperties
// Direct attribute IEMFEditProperty pSurname = EMFEditProperties.value( eDomain, ProjectPackage.Literals.PERSON__LASTNAME ); // Nested attribute IEMFEditProperty committerSurname = EMFEditProperties.value( eDomain, FeaturePath.fromList( ProjectPackage.Literals.COMMITTER_SHIP__PERSON, ProjectPackage.Literals.PERSON__LASTNAME ) );
Observing single value attributes/properties
The IValueProperty implementations IEMFValueProperty and IEMFEditValueProperty provide the possibility to create observables of single value attribute/properties of an object like the lastname of Person.
Let’s take a look how we can use this new API in our Java-Code. Code below only uses the Pure-EMF-API and not the EMF-Edit one because the only difference would be the creation of the IValueProperty (The EditingDomain has to be passed there).
// 1. Use case - Create an observable public IObservableValue uc1(Person p) { IEMFValueProperty prop = EMFProperties.value( ProjectPackage.Literals.PERSON__LASTNAME ); return prop.observe(p); } // 2. Use case - Create an observable for a nested attribute public IObservableValue uc2(CommitterShip c) { IEMFValueProperty prop = EMFProperties.value( FeaturePath.fromList( ProjectPackage.Literals.COMMITTER_SHIP__PERSON, ProjectPackage.Literals.PERSON__LASTNAME ) ); return prop.observe(c); } // 3. Use case - Create multiple observables public IObservableValue[] uc3(Person p) { IEMFValueProperty[] props = EMFProperties.values( ProjectPackage.Literals.PERSON__LASTNAME, ProjectPackage.Literals.PERSON__FIRSTNAME, ); return new IObservableValue[] { props[0].observe(p), props[1].observe(p) }; } // 4. Use case - Create multiple observables // for a nested attributes public IObservableValue[] uc4(CommitterShip c) { IEMFValueProperty[] props = EMFProperties.values( FeaturePath.fromList( ProjectPackage.Literals.COMMITTER_SHIP__PERSON, ProjectPackage.Literals.PERSON__LASTNAME ), FeaturePath.fromList( ProjectPackage.Literals.COMMITTER_SHIP__PERSON, ProjectPackage.Literals.PERSON__FIRSTNAME ) ); return new IObservableValue[] { props[0].observe(c), props[1].observe(c) }; } // 5. Use case - Create a detail observable public IObservableValue uc5(IObservableValue master) { IEMFValueProperty prop = EMFProperties.value( ProjectPackage.Literals.PERSON__LASTNAME ); return prop.observeDetail(master); } // 6. Use case - Observe the lastname // attribute of list of persons public IObservableMap uc6(IObservableSet persons) { IEMFValueProperty prop = EMFProperties.value( ProjectPackage.Literals.PERSON__LASTNAME ); return prop.observeDetail(persons); }
IEMFListProperty/IEMFEditListProperty
The IListProperty implementations IEMFListProperty and IEMFEditListProperty provide the possibility to create observables of multi valued attribute/properties like e.g. subprojects of a project.
Let’s take a look at some lines of Java how we are using this API:
// 1. Use case - observe the subprojects public IObservableList uc1(Project p) { IEMFListProperty prop = EMFProperties.list( ProjectPackage.Literals.PROJECT__SUBPROJECTS ); return prop.observe(p); } // 2. Use case - observe the nested list // The list of all subprojects of the projects parent public IObservableList uc2(Project p) { IEMFListProperty prop = EMFProperties.list( FeaturePath.formList( ProjectPackage.Literals.PROJECT__PROJECT, ProjectPackage.Literals.PROJECT__SUBPROJECTS ) ); return prop.observe(p); } // 3. Use case - Observe a detail list public IObservableList uc3(IObservableValue master) { IEMFListProperty prop = EMFProperties.list( ProjectPackage.Literals.PROJECT__SUBPROJECTS ); return prop.observeDetail(master); } // 4. Use case - Combine to lists into one public IObservableList uc4(Project p) { IEMFListProperty prop = EMFProperties.multiList( ProjectPackage.Literals.PROJECT__SUBPROJECTS, ProjectPackage.Literals.PROJECT__COMMITTERS ); return prop.observe(p); }
IEMFMapProperty/IEMFEditMapProperty
We are not useing maps in the example code so I’m not going to show the API in detail here but the idea behind it should be quite obvious. With the type of properties you can observe an java.util.Map-Type of multi-valued property.
NO IEMFSetProperty/IEMFEditSetProperty
You might ask why those to files are grayed out a bit in the screenshot. The reason is that support for this didn’t made it into EMF 2.5 but we are looking into it for 2.6.
Hidden secrets of the properties API
The IProperty API is quite an impressive piece of API – extremly powerful but still easy to use and in the next parts of this blog series we’ll see most the API in action. One of it’s big advantages is that it decouples the creation of the observable from specifying which attribute/property from which domain model one likes to observe which is really powerful when it comes to writing API code which should be domain model agnostic.
Suppose we write a library on top of Eclipse-Databinding which allows us to create text-input-forms in a more condensed way but the API should be useable with any domain model technology.
If we’d used the 3.4 Observables-Factories like (BeanObservables, EMFObservables, EMFEditObservables) we would have to write an specialized implementation for every widget technology because the specification of the attribute and how the binding is created is different for each of them.
Using the new Properties-API which defines a common description of a property a observable is going to be created later would lead to an API like this.
import java.util.ArrayList; import java.util.List; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.property.value.IValueProperty; import org.eclipse.jface.databinding.swt.IWidgetValueProperty; import org.eclipse.jface.databinding.swt.WidgetProperties; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; import org.eclipse.emf.databinding.EMFUpdateValueStrategy; /** * Helper class which builds a two column form * with labels and text-fields * @param the value property type */ public class FormBuilder<P extends IValueProperty> { private class Entry { private String label; private P property; private String nullMessage; private Entry(String label, P property, String nullMessage) { this.label = label; this.property = property; this.nullMessage = nullMessage; } } private List<Entry> entries = new ArrayList<Entry>(); /** * Add a text entry * @param label the label to display * @param property the property to bind * @param nullMessage the message shown * when the property gets set to null */ public void addTextEntry(String label, P property, String nullMessage) { entries.add( new Entry(label, property, nullMessage) ); } /** * Build a two column form with the elements added * * @param dbc the databinding context * @param parent the parent the form is created on * @param object the object to bind * @return the form container */ public Composite build(DataBindingContext dbc, Composite parent, Object object) { Composite container = new Composite(parent, SWT.NONE); container.setLayout(new GridLayout(2, false)); IWidgetValueProperty textProp = WidgetProperties.text( SWT.Modify ); for (Entry e : entries) { Label l = new Label(container, SWT.NONE); l.setText(e.label); Text t = new Text(container, SWT.BORDER); t.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); IObservableValue uiObs = textProp.observeDelayed( 400, t ); IObservableValue mObs; if (object instanceof IObservableValue) { mObs = e.property.observeDetail( (IObservableValue)object ); } else { mObs = e.property.observe(object); } dbc.bindValue( uiObs, mObs, new EMFUpdateValueStrategy().setBeforeSetValidator( new EmptyStringValidator(e.nullMessage) ), null ); } return container; } }
The UFaceKit-project has a similar abstraction to provide domain model agnostic API so something like this was doable also before the introduction of the new API. Still having this concept at the core of the implementation is highly appreciated.
Now that we got introduced into the most important parts of the new Properties-API we are ready to start working on our programm in the next blog series.
Pingback: Galileo: Improved EMF-Databinding-Support « Tomsondev Blog
Thank you for doing this series. I’ve been interested in using the new Properties API with my EMF-Databinding project.
Looking forward to the rest of the installments!
Hello, in one of you use case I wonder if there is a mistake.
// 2. Use case – observe the nested list
// The list of all subprojects of the projects parent
public IObservableList uc2(Project p) {
IEMFListProperty prop = EMFProperties.list(
FeaturePath.formList(
ProjectPackage.Literals.PROJECT__PROJECT,
ProjectPackage.Literals.PROJECT__SUBPROJECTS
)
);
return prop.observe(p);
}
you are talking of obsevring subproject or PARENT project so the feature path should be
FeaturePath.formList(
ProjectPackage.Literals.PROJECT__PARENT, ///and not PROJECT
ProjectPackage.Literals.PROJECT__SUBPROJECTS
)
I think.
Anyway this is great post.
Thanks.
SeB.
Pingback: 2010 in review « Tomsondev Blog
Hi thanks for this topic 🙂 !
I guess there’s a small mistake right here :
// 2. Use case – Create an observable for a nested attribute
public IObservableValue uc2(CommitterShip c) {
IEMFValueProperty prop = EMFProperties.value(
FeaturePath.fromList(
ProjectPackage.Literals.COMMITTER_SHIP__PERSON,
ProjectPackage.Literals.PERSON__LASTNAME
)
);
return prop.observe(p);
}
the return should be : return prop.observe(c); instead of return prop.observe(p);
Thanks
Correct – it is fixed – thanks!
Hi Tom, and thanks for your tuts – they’re always useful!
A question about the nested properties : I like the “FeaturePath” concept very much, but it does not support multi valued features on the way (actually, it does accept them only at the last position).
* I think that constraint shouldn’t be checked in the FeaturePath constructor but in the methods that use a featurePath and that do not support multi-valued features – that would allow me to reuse it as-is in my proprietary “multi-valued-friendly” implementation…
* Is there any implementation (except my own!!) somewhere that does support multi-valued feature paths (by merging resulting values…)?
Thanks in advance!
Yes you can traverse a multi-valued feature using the IEMFListProperty-API which is much more low-level.