Introducing e(fx)clipse

UPDATE
Please download the latest version from efxclipse.org

People who’ve read my blog in the last few weeks have seen that I invested some time in getting the Eclipse 4 Application Platform running on top of JavaFX.

One of the key technologies of JavaFX is that one uses CSS to theme the whole application but the default CSS-Editors are not of much help because the properties used are specific to JavaFX (read they are all customized extension starting with -fx).

… and because I decided already some time ago that I had to teach myself how to write my own DSL using Xtext it was the prefect chance to create a CSS-Editor for JavaFX using Xtext.

… and because I was traveling a lot in the last 2 weeks (I’ve been to 3 democamps) I had a lot of spare time to work on something I’d like to call “e(fx)clipse” a start of an Eclipse Tooling for JavaFX.

So what do I have for Release 0.0.1 of the Tooling? Well all I have is a start for a CSS-Editor built using Xtext.

Here are 2 screenshots of the current CSS-Editor running in Eclipse 4.1 (it runs as well in 3.7 naturally).

So as you see the editor has not too many features as of now:

  • Basic Syntax-Highlighting
  • Proposals for CSS-Attributes
  • Support for Attribute value proposals

but I hope to improve the initial features.

BTW one of the main features of the CSS-Editor is that it allows people to plug in their own Property-Rules so e.g. e4 could plug-in their own rules, so one can for mozilla, webkit, … -specific rules.

As of now I have 2 property-sets:

  • CSS 2.1
  • JavaFX

but they are not part of the core plugin but contributed simply as OSGi-Services :-)

So if you want to give the editor a spin. You can download the p2-repo from my github account. Please note this is a very very early release and the editor currently is only registered for .fxcss-Files.

Update:
There’s one JavaFX syntax not yet support by the editor showing an error marker but the problem is not the CSS-Editor but in reality the CSS-Definition speced by JavaFX. I’ve already filed a Jira-Ticket where I describe the problem and proposed a CSS-2.1 compatible definition:

linear from( <size> , <size> ) to( <size> , <size> ) [ stop( <number> , <color> ) ]+ [ repeat | reflect ]?
radial [ center( <size> , <size> ) , ]? <size> [ focus( <size> , <size> ) ] ? [ stop( <number> , <color> ) ]+ [ repeat | reflect ]?

/* Example */
linear from(0%,0%) to(100%,100%) stop(0.0,red) stop(1.0,black)
radial center(25%,25%) , 50% focus(20%,20%) stop(0.0,gray) stop(0.50,darkgray) stop(1.0,dimgray) reflect 

Jax 2011: EMF-Databinding talk

I had the pleasure to present EMF-Databinding to interested people on the Model Day at Jax 2011. I’m desperately sorry that people have been unable to install my sources but there has been a misunderstanding I’ll apologize for.

Everyone who was at the tutorial should already have the slides and sources but for those who don’t.

Here they are:

To make the stuff run you’d ideally use Eclipse SDK 4.1M7 or Eclipse SDK 3.7M7 (and install e4 stuff as mentioned in the slides)

Interesting new feature in EMF-Databinding

I’ve invested sometime into EMF-Databinding to add a new quite interesting feature i guess. Currently we have to problem that if your nested path crosses a multi-valued feature (=java.util.List) at some point the only solution today is to create a volatile transient feature which observes your EList and fire change events but this really clutters your Ecore-Model.

Let’s take a look at an example model

you’d like to present in an UI like this:

Until EMF 2.7M7 you’d have to modify your EMF-model like this:

So that you could e.g. write something like this to get access to the private address’ city property:

import static at.bestsolution.e4.addressbook.model.addressbook.AddressbookPackage.Literals.*;

// ...

IEMFValueProperty mProp = EMFProperties.value(FeaturePath.fromList(PERSON__PRIVATE_ADDRESS,ADDRESS__CITY));

// or in longer builder type syntax
IEMFValueProperty mProp = EMFProperties.value(PERSON__PRIVATE_ADDRESS).value(ADDRESS__CITY);

With the new feature I’ve added to IEMFListProperty#value(ListElementAccess) one now does not have to create those volatile transient features any more but can write the long form like this:

IEMFValueProperty mProp = EMFProperties.list(PERSON__ADDRESSES).value(
  new ElementAccessImpl(AddressType.PRIVATE)
);

// ...
private class ElementAccessImpl extends ListElementAccess<Address> {
 private AddressType type;

 public ElementAccessImpl(AddressType type) {
  this.type = type;
 }

 @Override
 public int getReadValueIndex(List<Address> list) {
  int i = 0;
  for (Address o : list) {
   if (o.getType() == type) {
    return i;
   }
   i++;
  }
  return -1;
 }

 @Override
 public int getWriteValueIndex(List<Address> list) {
  int i = 0;
  for (Address o : list) {
   if (o.getType() == type) {
    return i;
   }
   i++;
  }
  return -1;
 }
}

I’d like to note that this feature is an experimental one and we don’t guarantee to break it e.g. by pushing it upstream to Eclipse Databinding but you are free to give it a try and report usefulness, problems, like, dislike, … .

BTW did you notice that the screenshot from the above application was taken from a Swing-Application? How can that be? Isn’t Eclipse Databinding (and because of that EMF Databinding ) only useful within SWT driven apps (e.g. Eclipse RCP ones)?

Without saying too much: No – because many core Eclipse technologies OSGi, Databinding, EMF, Eclipse 4 Application Platform to name some can be used with any UI-Technology

Navigating/Querying EMF-Models using XPath

To make the new e4 way of building a complete model from small model pieces – named fragments – more flexible in 4.1, I’ve been developing a JXPath extension which works ontop of the reflective EMF-API.

I know that there’s already the possibility to query models using OCL but there are many more people familiar with XPath than the sometimes very cryptic OCL syntax. Some code samples might make clear why I think querying and navigating through EMF-Models using XPath is a very useful thing.

As an example model I’m using the Library-Model which is well known to most people who’ve worked with EMF.

An instance of the model would probably look like this:

The XML-Source helps probably to understand the references:

<?xml version="1.0" encoding="ASCII"?>
<extlib:Library xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:extlib="http:///org/eclipse/emf/examples/library/extlibrary.ecore/1.0.0" xsi:schemaLocation="http:///org/eclipse/emf/examples/library/extlibrary.ecore/1.0.0 ../org.eclipse.emf.examples.library/model/extlibrary.ecore">
  <stock xsi:type="extlib:Book" borrowers="//@borrowers.1" title="Mystery Book 1" author="//@writers.0"/>
  <stock xsi:type="extlib:Book" borrowers="//@borrowers.0" title="Sience Book 1" category="ScienceFiction" author="//@writers.0"/>
  <stock xsi:type="extlib:Book" borrowers="//@borrowers.1" title="Mystery Book 2" author="//@writers.1"/>
  <stock xsi:type="extlib:Book" borrowers="//@borrowers.0" title="Sience Book 2" category="ScienceFiction" author="//@writers.1"/>
  <writers address="Hometown" firstName="Tom" lastName="Schindl" books="//@stock.0 //@stock.1"/>
  <writers address="Homecity" firstName="Boris" lastName="Bokowski" books="//@stock.2 //@stock.3"/>
  <borrowers address="Hometown" firstName="Paul" lastName="Webster" borrowed="//@stock.1 //@stock.3"/>
  <borrowers address="Homecity" firstName="Remy" lastName="Suen" borrowed="//@stock.0 //@stock.2"/>
</extlib:Library>

Now let’s try to answer some questions:

  • Find all “Mystery Books”
  • Authors of “Mystery Books”
  • Find all writers and borrowers in “Hometown”
  • Find all borrowers “Mystery books”

The XPath-Code one can use with the new support is like this.

  • Load the model and setup a context for the XPath-Query
    public class Application implements IApplication {
      public Object start(IApplicationContext context) throws Exception {
        ResourceSet resourceSet = new ResourceSetImpl();
        resourceSet.getResourceFactoryRegistry()
          .getExtensionToFactoryMap()
          .put(Resource.Factory.Registry.DEFAULT_EXTENSION,new XMIResourceFactoryImpl());
    
        URI uri = URI.createPlatformPluginURI("/testxpath/Library.xmi",true);
        Resource resource = resourceSet.getResource(uri, true);
    
        Library l = (Library) resource.getContents().get(0);
        XPathContextFactory<EObject> f = EcoreXPathContextFactory.newInstance();
        XPathContext xPathContext = f.newContext(l);
        // Execute the XPaths
      }
    }
    
  • Find all “Mystery Books”
    {
      System.out.println("Mystery Books:");
      Iterator<Book> it = xPathContext.iterate("/books[category='Mystery']");
      while( it.hasNext() ) {
        System.out.println("	" + it.next().getTitle());
      }			
    }
    
  • Authors of “Mystery Books”
    {
      System.out.println("Mystery Book Authors:");
      Iterator<Writer> it = xPathContext.iterate("/books[category='Mystery']/author");
      while( it.hasNext() ) {
        Writer w = it.next();
        System.out.println("	" + w.getFirstName() + "," + w.getLastName());
      }
    }
    
  • Find all writers and borrowers in “Hometown”
    {
      System.out.println("Borrower/Writer in Hometown:");
      Iterator<Person> it = xPathContext.iterate(
        "/borrowers[address='Hometown']|/writers[address='Hometown']"
      );
      while( it.hasNext() ) {
        Person b = it.next();
        System.out.println("	" + b.getFirstName() + "," + b.getLastName());
      }	
    }
    
  • Find all borrowers “Mystery books”
    {
      System.out.println("Borrower of Mystery books:");
      Iterator<Borrower> it = xPathContext.iterate(
        "/borrowers[borrowed/category='Mystery']");
      while( it.hasNext() ) {
        Borrower b = it.next();
        System.out.println("	" + b.getFirstName() + "," + b.getLastName());
      }
    }
    

Executing the code leads to the following output:

Mystery Books:
	Mystery Book 1
	Mystery Book 2
Mystery Book Authors:
	Tom,Schindl
	Boris,Bokowski
Borrower/Writer in Hometown:
	Paul,Webster
	Tom,Schindl
Borrower of Mystery books:
	Remy,Suen

I think the above shows how easy it is to navigate/query an EMF-Model-Instance using JXPath and using this new EMF-extension.

I hope we’ll manage to integrate this support into one of the next Eclipse 4.1 I-builds until then you can access the source from the e4-cvs-repository. I’m also thinking about moving the code at some point to EMF directly because there’s no dependency on e4 and such an implementation could be of use for others as well.

ESE 09 – My Slides

Back from ESE 09 – once more a nice event and got to know even more people. I was quite busy this year taking part in 5 Sessions so a short review from my side:

EMF Tutorial

I think Ed, Eike and me did a good job though there’ve been too many people so working through examples was not possible. The next time (EclipseCon 09) we need probably skip some parts. Beside that I think I could demostrate fairly good how flexible Eclipse-Databinding can be used in RCP-Applications.

E4 Symposia

Boris and me had many people in our symposia and I think we could explain those people our vision of e4 and how the internals are structured. Boris gave a short overview about the 20 things, how DI is working using the IEclipseContext and WebUI and I talked about our EMF-driven Application model and our flexible rendering story.
I think people start to get our point that E4 is a flexible Application Framework they can build any UI-Application (the UI technology doesn’t really matter) if you start accepting that rendering is just another service like it is the selection, logging, … service.

What’s in E4

This was an overview talk given by Boris, Kai, Hallvard, Yves and me on e4. I gave a short overview about the reasoning and how the application platform is designed around our EMF-Model. I think we could at least reduce the confusion in the community.

e4 in Detail – The model workbench and it’s possibilities

In this talk I tried to explain in more detail how the e4-application platform uses EMF, DI and the rendering services to create a flexible UI-Application. The slides are available here.

UFaceKit – A uniform UI development model for different UI and Runtime platforms

This was my last talk where I explain the reasons and various cool things you can with UFaceKit which since a week before can be optionally backed up by an Ecore-Model so that Application can developed (and of needed deployed) using EMF-Technologies. The slides are here.

Galileo: EMF-Databinding – Part 5

Though the main focus of this article is going to be the usage of the properties API to setup a TableViewer there are some importanted I didn’t explain in Part 4 of this series.

Using the properties API in master-detail (continued)

Showing validation states

The first interesting thing is how to present validation errors to user. I’m personally not a friend of ControlDecorations but prefer to display the validation error in the form-header.
Detail Area Error
This means the header presents an aggregation of all binding states and because this a quite common thing Eclipse-Databinding has built in support for this kind of thing.

private void addStatusSupport(ObservablesManager mgr, 
  final DataBindingContext ctx)
{
  AggregateValidationStatus aggregateStatus = 
    new AggregateValidationStatus(
      ctx.getValidationStatusProviders(),
      AggregateValidationStatus.MAX_SEVERITY
  );

  aggregateStatus.addValueChangeListener(
    new IValueChangeListener()
    {
      public void handleValueChange(ValueChangeEvent event)
      {
        handleStateChange(
          (IStatus)event.diff.getNewValue(), 
          ctx
        );
      }
    });
  mgr.addObservable(aggregateStatus);
}

The above aggregates the validation states of all validation status providers. By the default there’s a provider for binding states attached to a databinding context but one can attach also custom ones which opens an interesting opportunity e.g. when used inconjunction with the EMF-Validation framework (probably a topic for another Blog post in the next weeks).
The final task is to display the status to the user through the form header control like this

private void handleStateChange(IStatus currentStatus, 
  DataBindingContext ctx)
{
  if (form.isDisposed() || form.getHead().isDisposed())
  {
    return;
  }

  if (currentStatus != null 
    && currentStatus.getSeverity() != IStatus.OK)
  {
    int type = convertType(currentStatus.getSeverity());

    List<IMessage> list = new ArrayList<IMessage>();
    Iterator< ? > it = ctx.getValidationStatusProviders()
     .iterator()
    ;

    while (it.hasNext())
    {
      ValidationStatusProvider validationStatusProvider = 
        (ValidationStatusProvider)it.next()
      ;
      final IStatus status = (IStatus)
        validationStatusProvider.getValidationStatus()
        .getValue()
      ;

      if (!status.isOK())
      {
        list.add(new IMessage()
        {
          public Control getControl()
          {
            return null;
          }

          public Object getData()
          {
            return null;
          }

          public Object getKey()
          {
            return null;
          }

          public String getPrefix()
          {
            return null;
          }

          public String getMessage()
          {
            return status.getMessage();
          }

          public int getMessageType()
          {
            return convertType(status.getSeverity());
          }
        });
      }
    }
    form.setMessage("Data invalid", 
      type, 
      list.toArray(new IMessage [0])
    );
  }
  else
  {
    form.setMessage(null);
  }
}

Master-Detail, Validation and null
That’s kind of a strange title but and not easy to track down but an undiscovered regression in Eclipse-Databinding since about 2 years and was discovered too late to get fixed in 3.5 timeframe.

In short the problem is that if you are producing an validation error in for a model attribute which initially had the value null the wrong value on the target (=UI-Widget) is not cleared when modifing master. Believe this sounds more complex than it is reality and bug 278301 holds an nice example to reproduce the behaviour.

Nonetheless we need a working solution for now because of the location of the problem we won’t see a fix until 3.6 ships which is 1 year from now. I’m demonstrating in the following code a possible work-around but there are also other solutions. My solution uses the AggregateValidationStatus-support and forces an target update when the master observable is changed and the current validation status is NOT OK.

public class Util
{
  public static void masterDetailFixup(
    final DataBindingContext ctx, 
    IObservableValue master)
  {
    final AggregateValidationStatus s = 
      new AggregateValidationStatus(
        ctx, 
        AggregateValidationStatus.MAX_SEVERITY);

    master.addChangeListener(new IChangeListener()
      {

        public void handleChange(ChangeEvent event)
        {
          IStatus status = (IStatus)s.getValue();
          if (status != null && !status.isOK())
          {
            ctx.updateTargets();
          }
        }
      });
  }
}

Setting up a TableViewer with EMF-Databinding

Let’s now take a look a the next topic for this post where we are going to set up a TableViewer using Eclipse-Databinding for JFace/SWT and EMF. The TableViewer is taking up the complete lower right part of the application
Table Viewer

In part 3 we saw how to setup a TreeViewer, in part 4 we saw how we are setting up a TableViewer without columns and as you’ll see setting up a TableViewer with multiple columns is very similar. Let’s take a look at the code first

private void init(IViewSite site, 
  CTabFolder folder, 
  DataBindingContext ctx, 
  EditingDomain editingDomain, 
  IObservableValue master)
{
  final TableViewer viewer = new TableViewer(
    folder, SWT.FULL_SELECTION
  );

  viewer.getTable().setHeaderVisible(true);
  ObservableListContentProvider cp = 
    new ObservableListContentProvider()
  ;

  {
    IObservableMap[] attributeMap = new IObservableMap [2];
    attributeMap[0] = EMFEditProperties.value(
      editingDomain,
      FeaturePath.fromList(
        ProjectPackage.Literals.COMMITTER_SHIP__PERSON, 
        ProjectPackage.Literals.PERSON__LASTNAME
      )
    ).observeDetail(cp.getKnownElements());

    attributeMap[1] = EMFEditProperties.value(
      editingDomain,
      FeaturePath.fromList(
        ProjectPackage.Literals.COMMITTER_SHIP__PERSON, 
        ProjectPackage.Literals.PERSON__FIRSTNAME
      )
    ).observeDetail(cp.getKnownElements());

    TableViewerColumn column = new TableViewerColumn(
      viewer, SWT.NONE
    );
    column.getColumn().setText("Name");
    column.getColumn().setWidth(150);
    column.setLabelProvider(
      new GenericMapCellLabelProvider(
        "{0}, {1}", 
        attributeMap
      )
    );
  }

  {
    IObservableMap attributeMap = EMFEditProperties.value(
      editingDomain, 
      ProjectPackage.Literals.COMMITTER_SHIP__START
    ).observeDetail(cp.getKnownElements());

    TableViewerColumn column = new TableViewerColumn(
      viewer, SWT.NONE
    );
    column.getColumn().setText("Start");
    column.getColumn().setWidth(100);
    column.setLabelProvider(
      new GenericMapCellLabelProvider(
        "{0,date,short}", 
        attributeMap
      )
    );
  }

  {
    IObservableMap attributeMap = EMFEditProperties.value(
      editingDomain, 
      ProjectPackage.Literals.COMMITTER_SHIP__END
    ).observeDetail(cp.getKnownElements());

    TableViewerColumn column = new TableViewerColumn(
      viewer, SWT.NONE
    );
    column.getColumn().setText("End");
    column.getColumn().setWidth(100);
    column.setLabelProvider(
      new GenericMapCellLabelProvider(
        "{0,date,short}", 
        attributeMap
      )
    );
  }

  IListProperty prop = EMFEditProperties.list(
    editingDomain, 
    ProjectPackage.Literals.PROJECT__COMMITTERS
  );
  viewer.setContentProvider(cp);
  viewer.setInput(prop.observeDetail(master));

  MenuManager mgr = new MenuManager();
  mgr.add(
    new Action(
      "Hide historic committers", 
      IAction.AS_CHECK_BOX
    )
  {
    @Override
    public void run()
    {
      if (isChecked())
      {
        viewer.addFilter(new ViewerFilterImpl());
      }
      else
      {
        viewer.setFilters(new ViewerFilter [0]);
      }
    }
  });

  viewer.getControl().setMenu(
    mgr.createContextMenu(viewer.getControl())
  );
  site.registerContextMenu(
    Activator.PLUGIN_ID + ".committers", mgr, viewer);
}

The only thing not already explained before is a special type of CellLabelProvider which allows one to use MessageFormat-Syntax to specify how the label is constructed. This label provider is not part of the JFace-Databinding implementation but written by me but it’s quite easy to implement.

public class GenericMapCellLabelProvider 
  extends ObservableMapCellLabelProvider
{
  private IObservableMap[] attributeMaps;
  private String messagePattern;

  /**
   * Create a new label provider
   * @param messagePattern the message pattern
   * @param attributeMaps the values to observe
   */
  public GenericMapCellLabelProvider(
    String messagePattern, 
    IObservableMap... attributeMaps)
  {
    super(attributeMaps);
    this.messagePattern = messagePattern;
    this.attributeMaps = attributeMaps;
  }

  @Override
  public void update(ViewerCell cell)
  {
    Object element = cell.getElement();
    Object[] values = new Object [attributeMaps.length];
    int i = 0;
    for (IObservableMap m : attributeMaps)
    {
      values[i++] = m.get(element);
      if (values[i - 1] == null)
      {
        cell.setText("");
        return;
      }
    }
    cell.setText(
      MessageFormat.format(messagePattern, values)
    );
  }
}

That’s all we need to have a working TableViewer though there’s even more one can do with the Databinding-API e.g. Inline-Editing is supported and there are helpers to setup viewers in even less lines of code.

Galileo: EMF-Databinding – Part 4

Using the properties API in master-detail

In the last blog entry we saw how to set up a the left part of our example application which is a JFace-TreeViewer. This blog article deals with the upper right part of the application detail_area
who allows the user to edit the data of the selected project in the TreeViewer which is a very common use case in Databinding and called Master-Detail-Scenario where the TreeViewer acts as the master and the upper right area as the detail part.

Creation of the master observable

Looking at the API for Master-Detail
Master Detail API
we recognize that we need to have a Master-ObservableValue when creating the Detail-ObservableValue so before we can proceed we need to go back to the ProjectExplorerPart who is responsible for the TreeViewer creation and make it provide us an ObservableValue.
Most of the time the creation of the master observable is quite easy because the only thing one needs to do is to observe the current selection like this:

private IObservableValue createMasterObs(Viewer viewer) {
 return ViewerProperties.singleSelection().observe(viewer);
}

but in our case it’s a bit more difficult because our tree has different object types (Project and CommitterShip) but our detail can only handle Projects which means we can’t use the selection directly but have to add a mediator in between.

The mediator we are useing is a WritableValue which we declare in the class body:

/**
 * Part responsible for rendering the project tree
 */
public class ProjectExplorerPart
{
  // ...
  private IObservableValue master = new WritableValue();
  // ...
}

afterwards we observe the TreeViewer-Selection and adjust the WritableValue manually:

private TreeViewer init(Composite parent, 
  Foundation foundation)
{
  TreeViewer viewer = new TreeViewer(parent);

  // ...

  IObservableValue treeObs = 
    ViewerProperties.singleSelection().observe(viewer);
  treeObs.addValueChangeListener(new IValueChangeListener() 
  {
    public void handleValueChange(ValueChangeEvent event) {
      if (event.diff.getNewValue() instanceof Project) {
        Project pr = (Project)event.diff.getNewValue();
        master.setValue(pr);
      } else if (event.diff.getNewValue() != null) {
        CommitterShip cm = 
          (CommitterShip)event.diff.getNewValue();
        master.setValue(cm.getProject());
      }     
    }
  });

  // ...
}

Create the detail part

Binding with default conversion

We can now go on at the upper right side and use the obervable to bind our detail controls. All the code shown below resides in ProjectFormAreaPart. For standard text controls where no conversion is needed this is straight forward:

private void createFormArea(
  IViewSite site,
  final Composite parent,
  FormToolkit toolkit,
  final IModelResource resource,
  ObservablesManager manager,
  final IObservableValue master)
{
  final EditingDomain editingDomain = 
    resource.getEditingDomain();
  ctx = new EMFDataBindingContext();

  // ...

  IWidgetValueProperty prop = 
    WidgetProperties.text(SWT.Modify);

  final IEMFValueProperty shortProp = 
    EMFEditProperties.value(
      editingDomain, 
      ProjectPackage.Literals.PROJECT__SHORTNAME
   );

  toolkit.createLabel(body, "&Short name");
  Text t = toolkit.createText(body, "");
  t.setLayoutData(
    new GridData(SWT.FILL, SWT.DEFAULT, true, false, 2, 1)
  );
  ctx.bindValue(
    prop.observeDelayed(400, t), 
    shortProp.observeDetail(master)
  );
  // ...
}

Though as mentioned above it is straight forward there are still some things to notice:

  • EMFDatabindingContext: Is needed because IEMFObservables are exposing the EStructuralFeature as their value type but the default converters (e.g. String to Integer, Integer to String) who are created by the standard DatabindingContext work on top of java.lang.Class informations. EMFDatabindingContext uses default EMF conversion strageties and applies them instead.
  • IWidgetValueProperty.observeDelayed(Widget,int): This is an important detail to improve the users Undo/Redo experience because if not used every key stroke would result in an attribute change which would result in an undo/redo command. IWidgetValueProperty.observeDelayed() instructs the created observable to wait for 400 milliseconds before informing syncing the value back to the model and if another change happens within this time to cancel the update process and wait for another 400 milliseconds resulting in far less model changes and an improved undo/redo experience.

Binding with customized conversion

Now that we learned how to bind a Text-Widget to a String-property let’s take a look at another slightly more interesting binding use case – binding a Text-Widget to a Date-property which involves data conversion from Date to String and String to Date. EMFDatabindingContext comes with a standard implementation for this but our users might not expect to see a DateTime-Value including timezone informations when editing a date. So the first thing we need to do is create an improved Date to String and String to Date converter:

/**
 * Convert a String to a date
 */
public class StringToDateConverter extends Converter
{
  private List<DateFormat> formats = 
    new ArrayList<DateFormat>();

  private String message;

  /**
   * New converter
   * @param message message when conversion fails
   */
  public StringToDateConverter(String message)
  {
    super(String.class, Date.class);
    this.message = message;
    formats.add(
      DateFormat.getDateInstance(DateFormat.SHORT)
    );
    formats.add(new SimpleDateFormat("yyyy-MM-dd"));
  }

  public Object convert(Object fromObject)
  {
    if (fromObject != null 
      && fromObject.toString().trim().length() == 0)
    {
      return null;
    }

    for (DateFormat f : formats)
    {
      try
      {
        return f.parse(fromObject.toString());
      }
      catch (ParseException e)
      {
        // Ignore
      }
    }

    throw new RuntimeException(message);
  }
}

To implement such a converter one normally inherits from a given base class named Converter and implements the convert-method. In this case the value conversion is done useing Java’s DateFormat-classes. As a special addon an empty String is converted into a null value. This converter is used as at the target to model side converting the value entered into the UI into the model type. The converter for the other side looks like this:

/**
 * Convert a date to a string
 */
public class DateToStringConverter extends Converter
{
  private DateFormat format = 
    DateFormat.getDateInstance(DateFormat.SHORT);

  /**
   * New converter
   */
  public DateToStringConverter()
  {
    super(Date.class, String.class);
  }

  public Object convert(Object fromObject)
  {
    if (fromObject == null)
    {
      return "";
    }
    return format.format(fromObject);
  }
}

To teach Eclipse-Databinding to use our custom converters instead of the default ones we simply need to set them on the UpdateValueStrategys passed to DatabindingContext.bindValue(IObservableValue,IObservableValue,UpdateValueStrategy,UpdateValueStrategy). Setting such a custom conversion strategy though leads to many lines of code you repeat in many places. What I do in my project to reduce this amout of code is to move the creation of a common UpdateValueStrategy like String-Date-String into a factory like this:

public class UpdateStrategyFactory
{
  public static EMFUpdateValueStrategy stringToDate(
    String message)
  {
    EMFUpdateValueStrategy strat = 
      new EMFUpdateValueStrategy();

    StringToDateConverter c = 
      new StringToDateConverter(message);

    strat.setConverter(c);
    return strat;
  }

  /**
   * Create an update strategy which converts a date 
   * to a string
   * @return the update strategy
   */
  public static EMFUpdateValueStrategy dateToString()
  {
    EMFUpdateValueStrategy strat = 
      new EMFUpdateValueStrategy();
    DateToStringConverter c = new DateToStringConverter();
    strat.setConverter(c);
    return strat;
  }
}

resulting into binding code like this:

IEMFValueProperty mProp = EMFEditProperties.value(
  editingDomain, 
  ProjectPackage.Literals.PROJECT__END
);

toolkit.createLabel(body, "End Date");
Text t = toolkit.createText(body, "");
t.setLayoutData(
  new GridData(SWT.FILL, SWT.DEFAULT, true, false, 2, 1)
);
ctx.bindValue(
  prop.observeDelayed(400, t),
  mProp.observeDetail(master),
  UpdateStrategyFactory.stringToDate(
    NLSMessages.ProjectAdminViewPart_EndDateNotParseable
  ),
  UpdateStrategyFactory.dateToString());

Binding with customized conversion and validation

But there’s even more you can do with the UpdateValueStrategy – You ensure for example that an a project is not allowed to have an empty == null start date by setting an IValidator on the target to model UpdateValueStrategy. Once more this method is part of the UpdateStrategyFactory introduced above.

/**
  * Create an update strategy which converts a string to 
  * a date and ensures that the date value on the 
  * destinations  is not set to null
  * @param convertMessage the message shown when 
  *   the conversion fails
  * @param validationMessage the message when the 
  *   value is set to null
  * @return the update strategy
  */
public static EMFUpdateValueStrategy stringToDateNotNull(
  String convertMessage, final String validationMessage)
{
  EMFUpdateValueStrategy strat = 
    stringToDate(convertMessage);

  strat.setBeforeSetValidator(new IValidator()
  {
    public IStatus validate(Object value)
    {
      if (value == null)
      {
        return new Status(
          IStatus.ERROR, 
          Activator.PLUGIN_ID, 
          validationMessage
        );
      }
      return Status.OK_STATUS;
    }
  });
  return strat;
}

and is used like this

IEMFValueProperty mProp = EMFEditProperties.value(
  editingDomain, 
  ProjectPackage.Literals.PROJECT__START
);
toolkit.createLabel(body, "Start Date");
Text t = toolkit.createText(body, "");
t.setLayoutData(
  new GridData(SWT.FILL, SWT.DEFAULT, true, false, 2, 1)
);
ctx.bindValue(
  prop.observeDelayed(400, t), 
  mProp.observeDetail(master), 
  // Custom conversion
  UpdateStrategyFactory.stringToDateNotNull(
    NLSMessages.ProjectAdminViewPart_StartDateNotParseable,
    "Start date must not be null"
  ), 
  UpdateStrategyFactory.dateToString()
);

ensuring that the start date is not set to null.

Binding to a list of values

The last binding presented is how to bind the value of a multi value attribute to to a Table-Widget which used as a List in this case.

IEMFListProperty mProp = EMFEditProperties.list(
  editingDomain, 
  ProjectPackage.Literals.PROJECT__PROJECTLEADS);

toolkit.createLabel(body, "Project Leads").setLayoutData(
  new GridData(SWT.TOP, SWT.DEFAULT, false, false)
);

Table c = toolkit.createTable(
  body, 
  SWT.MULTI | SWT.FULL_SELECTION 
  | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER
);

final TableViewer tv = new TableViewer(c);
tv.setLabelProvider(new ColumnLabelProvider()
{
  @Override
  public String getText(Object element)
  {
    Person p = (Person)element;
    return p.getLastname() + ", " + p.getFirstname();
  }
});
tv.setContentProvider(new ObservableListContentProvider());
tv.setInput(mProp.observeDetail(master));

The code shown here is not much different from the one showing how to setup a TreeViewer though much simpler for a none hierarchical widget. We are also using a default LabelProvider here instead of one who observes the attributes shown. Now let’s take a look at the dialog which gets opened when the Add … button is pressed.
Filter Dialog
The Eclipse-RCP-Framework provides a Dialog implementation named FilteredItemsSelectionDialog we can subclass and customize it

/**
 * Dialog to find persons
 */
public class PersonFilterDialog extends 
  FilteredItemsSelectionDialog
{
  private final IModelResource resource;

  /**
   * Create a new dialog
   * @param shell the parent shell
   * @param resource the model resource
   */
  public PersonFilterDialog(Shell shell, 
    IModelResource resource)
  {
    super(shell);
    this.resource = resource;

    setListLabelProvider(new LabelProvider()
      {
        @Override
        public String getText(Object element)
        {
          if (element == null)
          {
            return "";
          }
          return PersonFilterDialog.this.getText(
            (Person)element
          );
        }
      });

    setDetailsLabelProvider(new LabelProvider()
      {
        @Override
        public String getText(Object element)
        {
          if (element == null)
          {
            return "";
          }
          return PersonFilterDialog.this.getText(
            (Person)element
          );
        }
      });
  }

  private String getText(Person p)
  {
    return p.getLastname() + " " + p.getFirstname();
  }

  @Override
  protected IStatus validateItem(Object item)
  {
    return Status.OK_STATUS;
  }

  @Override
  protected Comparator< ? > getItemsComparator()
  {
    return new Comparator<Person>()
      {

        public int compare(Person o1, Person o2)
        {
          return getText(o1).compareTo(getText(o2));
        }
      };
  }

  @Override
  public String getElementName(Object item)
  {
    Person p = (Person)item;
    return getText(p);
  }

  @Override
  protected IDialogSettings getDialogSettings()
  {
    IDialogSettings settings = Activator.getDefault()
      .getDialogSettings().getSection("committerdialog");

    if (settings == null)
    {
      settings = Activator.getDefault()
        .getDialogSettings()
        .addNewSection("committerdialog");
    }
    return settings;
  }

  @Override
  protected void fillContentProvider(
    AbstractContentProvider contentProvider, 
    ItemsFilter itemsFilter, 
    IProgressMonitor progressMonitor) throws CoreException
  {
    for (Person p : resource.getFoundation().getPersons())
    {
      if (progressMonitor.isCanceled())
      {
        return;
      }

      contentProvider.add(p, itemsFilter);
    }
  }

  @Override
  protected ItemsFilter createFilter()
  {
    return new ItemsFilter()
      {

        @Override
        public boolean isConsistentItem(Object item)
        {
          return true;
        }

        @Override
        public boolean matchItem(Object item)
        {
          Person p = (Person)item;
          return matches(p.getLastname() + " " 
            + p.getFirstname()
          );
        }

      };
  }

  @Override
  protected Control createExtendedContentArea(
    Composite parent)
  {
    return null;
  }
}

and use it in our code like this

Button b = toolkit.createButton(
  buttonContainer, 
  "Add ...", 
  SWT.FLAT);

gd = new GridData(SWT.DEFAULT, SWT.DEFAULT);
gd.horizontalAlignment = SWT.FILL;
b.setLayoutData(gd);
b.addSelectionListener(new SelectionAdapter()
{
  @Override
  public void widgetSelected(SelectionEvent e)
  {
    PersonFilterDialog dialog = new PersonFilterDialog(
      parent.getShell(), 
      resource
    );

    if (dialog.open() == IDialogConstants.OK_ID)
    {
      Command cmd = AddCommand.create(
        editingDomain,
        master.getValue(),
        ProjectPackage.Literals.PROJECT__PROJECTLEADS,
        dialog.getFirstResult());
      if (cmd.canExecute())
      {
        resource.executeCmd(cmd);
      }
    }
  }
});

That’s it for today in the next part we’ll take a look at setting up of TableViewer with different columns and how the nice validation stuff is working.