Eclipse Databinding and QxWT

If you followed my blog closely you already know that I invested some time into makeing Eclipse Databinding available for GWT (as part of the Eclipse UFaceKit project) and because I couldn’t find a GWT-UI-Library who suited my needs I started on writing a GWT wrapper for Qooxdoo.

In my last blog entry I show a very minimal example of a UFaceKit-TableViewer. One of the major problem of the code base by then was that I had to load myriads of small tiny JavaScript files which made the start up extremely slow and unusable in a none local environment.

Though I haven’t solved the problem completely I improved the situation by using the YUI-Compressor and merging the all necessary files into as few as possible (I ended up with 6 yet). This still makes the download around 1MB (which is cached by the browser once loaded) so still a “bit” too high but for Intranet Applications acceptable.

Besiding doing this necessary downsizing of the download size I also improved the UFaceKit-Viewer support and implemented some databinding properties to observe some standard widget attributes (currently only text attributes of various widgets).

Example code

So time to show of some demo and the accompanying code. Let’s at first take a look on the following screencast I created:

It’s a simply application but it the important thing is to see how it is created.

The first important thing when it comes to databinding an GWT is that we need a domain object which informs us about attribute changes. In case of Java and Eclipse one has 2 main choices:

  • JavaBeans(tm) with reflection and PropertyChangeSupport
  • EMF with it’s reflective EObject-API and Adapter

where I would preferr the latter.

The problem in GWT is that one can’t use the JavaBeans because of the (out of the box) missing reflection support nor one can use EMF because there’s no port available yet (long ago I worked on such a thing but its simply a very huge task) but there’s a very minimal EMF-like domain model implementation named UBean (part of the UFaceKit project) which provides a similar reflective API than EObject does.

Here’s the example of a domain model object when using UBean:

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.ufacekit.core.ubean.UArrayBean;

public class MailFolder extends UArrayBean {
  public static final int MAILS = 0;
  public static final int SUBFOLDERS = 1;
  public static final int LABEL = 2;
  public static final int LAST_SELECTION = 3;

  private static final HashSet<Integer> SET 
    = new HashSet<Integer>();
  static {
    SET.add(MAILS);
    SET.add(SUBFOLDERS);
    SET.add(LABEL);
  }

  public MailFolder(String label) {
    set(LABEL, label);
  }

  @Override
  public Set<Integer> getSupportedFeatureIds() {
    return SET;
  }

  @SuppressWarnings("unchecked")
  @Override
  public <T> T getValueType(int featureId) {
    switch (featureId) {
    case MAILS:
      return (T) List.class;
    case LAST_SELECTION:
      return (T) Mail.class;
    case SUBFOLDERS:
      return (T) List.class;
    default:
      return (T) String.class;
    }
  }
}

At the moment one has to hand craft those UBean classes but in future I want to provide a JET-Template to generate them from your Ecore-Model. The complete domain model has 2 more classes named Account and Mail.

The UI itself is made up from 3 classes until now:

  • UMail: Which is simply the Application with its main start point
  • MailFolderPart: Representing the Accounts and the folders as a Tree
  • MailDetailPart: Representing the MailFolder contents and displaying the mail content

Let’s start with UMail class which is fairly simple:

public class UMail extends QooxdooApp {
  @Override
  protected void run(final QxAbstractGui application) {
    Realm.runWithDefault(QxObservables.getRealm(), 
      new Runnable() {

      public void run() {
        QxPane pane = new QxPane(Orientation.HORIZONTAL);

        MailFolderPart folderPart = new MailFolderPart();
        MailDetailPart detailPart = new MailDetailPart(
          folderPart.getSelection()
        );

        application.getRoot().add(
          pane, 
          QxOption.top(0), 
          QxOption.left(0),
          QxOption.bottom(0), 
          QxOption.right(0)
        );

       pane.add(folderPart, 0);
       pane.add(detailPart, 1);
     }
   });
 }
}

I think it’s not really a miracle what the code above this. It simply splits the main window area into 2 parts so let’s take a look at the second class which makes up the left hand side folder structure.

public class MailFolderPart extends QxComposite {
  private WritableValue value = new WritableValue();

  public MailFolderPart() {
    super();
    setWidth(200);
    setLayout(new QxGrow());

    TreeViewer<UBean, Collection<UBean>> viewer = new TreeViewer<UBean, Collection<UBean>>();
    viewer.setLabelConverter(new LabelConvertImpl());
    viewer.setContentProvider(new ContentProviderImpl());
    viewer.setInput(getAccounts());
    viewer.addSelectionChangedListener(new ISelectionChangedListener<UBean>() {
      public void selectionChanged(SelectionChangedEvent<UBean> event) {
        if( ! event.getSelection().isEmpty() ) {
          if( event.getSelection().getElements().get(0) instanceof MailFolder ) {
            value.setValue(event.getSelection().getElements().get(0));
          } else {
            value.setValue(null);
          }
        } else {
          value.setValue(null);
        }
      }
    });
    add(viewer.getWidget());
  }

  public IObservableValue getSelection() {
    return value;
  }

  private Collection<UBean> getAccounts() {
    Collection<UBean> accounts = new ArrayList<UBean>();

    Account account = new Account("tom.schindl@ufacekit.org");
    MailFolder folder = new MailFolder("Inbox");
    account.add(Account.FOLDERS, folder);

    Mail mail = new Mail(
      "QxWT example",
      "examples@ufacekit.org", 
      "tom.schindl@ufacekit.org", 
      new Date()
    );
    /*  CREATE TEST MAIL CONTENT */
    folder.add(MailFolder.MAILS, mail);

    mail = new Mail(
      "Qt for UFaceKit",
      "tom.schindl@bestsolution.at", 
      "tom.schindl@ufacekit.org", 
      new Date(new Date().getTime()+60*60*1000*10)
    );
    /*  CREATE TEST MAIL CONTENT */
    folder.add(MailFolder.MAILS, mail);
		
    MailFolder subfolder = new MailFolder("QxWT");
    folder.add(MailFolder.SUBFOLDERS, subfolder);

    subfolder = new MailFolder("UFaceKit");
    folder.add(MailFolder.SUBFOLDERS, subfolder);
    folder = new MailFolder("Templates");
    account.add(Account.FOLDERS, folder);

    folder = new MailFolder("Sent");
    account.add(Account.FOLDERS, folder);

    folder = new MailFolder("Trash");
    account.add(Account.FOLDERS, folder);

    accounts.add(account);
    account = new Account("info@ufacekit.org");
    accounts.add(account);
    return accounts;
  }

  private class LabelConvertImpl extends LabelConverter<UBean> {
     @Override
     public String getText(UBean element) {
       if( element instanceof Account ) {
         return element.get(Account.LABEL);
       } else {
         return element.get(MailFolder.LABEL);
       }
     }
  }

  private class ContentProviderImpl implements ITreeContentProvider<UBean, Collection<UBean>> {
    public Collection<UBean> getChildren(UBean parentElement) {
      if (parentElement instanceof Account) {
        return ((Account) parentElement).get(Account.FOLDERS);
      } else if (parentElement instanceof MailFolder) {
        return ((MailFolder) parentElement).get(MailFolder.SUBFOLDERS);
      }
      return Collections.emptyList();
    }

    public UBean getParent(UBean element) {
      return null;
    }

    public boolean hasChildren(UBean element) {
      if (element instanceof Account) {
        List<MailFolder> rv = ((Account) element).get(Account.FOLDERS);
        return rv != null && rv.size() > 0;
      } else if (element instanceof MailFolder) {
        List<MailFolder> rv = ((MailFolder) element).get(MailFolder.SUBFOLDERS);
        return rv != null && rv.size() > 0;
      }
      return false;
    }

    public void dispose() {
      // TODO Auto-generated method stub
    }

    public Collection<UBean> getElements(Collection<UBean> inputElement) {
      return inputElement;
    }

    public void inputChanged(IViewer<UBean> viewer, Collection<UBean> oldInput, Collection<UBean> newInput) {
      // TODO Auto-generated method stub
    }
  }
}

This code is a bit more interesting because in:

  • Lines 9 – 12: Standard setting up of a TreeViewer
  • Lines 13 – 25: Modifying the observable value to use in Master-Detail-Observables
  • Lines 78 – 87: Setting up of a LabelConverter
  • Lines 89 – 125: Setting up of a standard ITreeContentProvider

And in the end the last class:

public class MailDetailPart extends QxComposite {
  private IObservableValue mailObservable;

  public MailDetailPart(IObservableValue master) {
    super();
    setLayout(new QxGrow());
    QxPane pane = new QxPane(Orientation.VERTICAL);

    QxComposite selectionList = createList(master);
    QxComposite messageDetail = createDetail();

    pane.add(selectionList, 0);
    pane.add(messageDetail, 1);

    add(pane);
  }

  private QxComposite createList(final IObservableValue master) {
    QxComposite comp = new QxComposite(new QxGrow());
    TableViewer<Mail, IObservableList> table = new TableViewer<Mail, IObservableList>();
    table.setContentProvider(new ObservableListContentProvider<Mail, IObservableList>());

    TableViewerColumn<Mail> subject = new TableViewerColumn<Mail>(table,"Subject");
    subject.setLabelConverter(new LabelConverter<Mail>() {
      @Override
      public String getText(Mail element) {
        return element.get(Mail.SUBJECT);
      }
    });
    subject.setWidth(300);
		
    TableViewerColumn<Mail> from = new TableViewerColumn<Mail>(table,"From");
    from.setLabelConverter(new LabelConverter<Mail>() {
      @Override
      public String getText(Mail element) {
        return element.get(Mail.FROM);
      }
    });
    from.setWidth(200);

    TableViewerColumn<Mail> date = new TableViewerColumn<Mail>(table,"Date");
    date.setLabelConverter(new LabelConverter<Mail>() {
      @Override
      public String getText(Mail element) {
        return DateTimeFormat.getShortDateTimeFormat().format(
          (Date) element.get(Mail.DATE));
      }
    });
    date.setWidth(200);

    table.setInput(
      UBeansObservables.observeDetailList(
        Realm.getDefault(),
        master, 
        MailFolder.MAILS, 
        Mail.class
      )
    );

    mailObservable = ViewersObservables.observeSingleSelection(Realm.getDefault(), table);

    comp.add(table.getTable());
    comp.setHeight(200);

    return comp;
  }

  private QxComposite createDetail() {
    DataBindingContext ctx = new DataBindingContext();

    QxDock dock = new QxDock();
    dock.setSeparatorY("separator-vertical");

    QxComposite comp = new QxComposite();
    comp.setPadding(10);
    comp.setLayout(dock);
    comp.setBackgroundColor("white");

    QxGrid layout = new QxGrid(10);
    layout.setColumnAlign(0, HAlign.RIGHT, VAlign.MIDDLE);
    layout.setSpacing(3);

    QxComposite header = new QxComposite(layout);
    header.setAllowGrowY(true);
    header.setMarginLeft(10);
    header.setMarginBottom(10);
    header.setMarginTop(10);

    comp.add(header, QxOption.edge(Edge.NORTH));

    QxFont font = QxFont.fromString("12px sans-serif bold");
    IQxValueProperty textProperty = QxWidgetProperties.text();

    QxLabel label = new QxLabel();
    label.setContent("Subject: ");
    label.setFont(font);
    label.setTextColor("#a0a0a0");
    header.add(label, QxOption.rowColumn(0, 0));

    label = new QxLabel();
    label.setFont(font);
    label.setMarginLeft(10);
    ctx.bindValue(
      textProperty.observe(label), 
      UBeansObservables.observeDetailValue(
        Realm.getDefault(), 
        mailObservable,
        Mail.SUBJECT, 
        String.class
      )
    );
    header.add(label, QxOption.rowColumn(0, 1));

    // ------------------------------------------------
    label = new QxLabel();
    label.setContent("From: ");
    label.setFont(font);
    label.setTextColor("#a0a0a0");
    header.add(label, QxOption.rowColumn(1, 0));

    label = new QxLabel();
    label.setMarginLeft(10);
    ctx.bindValue(
      textProperty.observe(label), 
      UBeansObservables.observeDetailValue(
        Realm.getDefault(), 
        mailObservable,
        Mail.FROM, 
        String.class
      )
    );
    header.add(label, QxOption.rowColumn(1, 1));

    // ------------------------------------------------
    label = new QxLabel();
    label.setContent("Date: ");
    label.setFont(font);
    label.setTextColor("#a0a0a0");
    header.add(label, QxOption.rowColumn(2, 0));

    label = new QxLabel();
    label.setMarginLeft(10);

    Converter date2string = new Converter(Date.class, String.class) {
      public Object convert(Object fromObject) {
        return fromObject == null ? "" : DateTimeFormat.getShortDateTimeFormat().format((Date) fromObject);
      }
    };

    ctx.bindValue(
      textProperty.observe(label), 
        UBeansObservables.observeDetailValue(
          Realm.getDefault(), 
          mailObservable,
          Mail.DATE, 
          Date.class
        ), 
      null, 
      new UpdateValueStrategy().setConverter(date2string)
    );
    header.add(label, QxOption.rowColumn(2, 1));

    // ------------------------------------------------
    label = new QxLabel();
    label.setContent("To: ");
    label.setFont(font);
    label.setTextColor("#a0a0a0");
    header.add(label, QxOption.rowColumn(3, 0));

    label = new QxLabel();
    label.setMarginLeft(10);
    ctx.bindValue(
      textProperty.observe(label), 
      UBeansObservables.observeDetailValue(
        Realm.getDefault(), 
        mailObservable,
        Mail.TO, String.class
      )
    );
    header.add(label, QxOption.rowColumn(3, 1));

    // ------------------------------------------------
    QxHtml html = new QxHtml("");
    html.setOverflow(Overflow.AUTO, Overflow.AUTO);
    ctx.bindValue(
      textProperty.observe(html), 
      UBeansObservables.observeDetailValue(
        Realm.getDefault(), 
        mailObservable,
        Mail.TEXT, 
        String.class
      )
    );
    comp.add(html, QxOption.edge(Edge.CENTER));
    return comp;
  }
}

This code is a bit longer but in the end for those familiar with Eclipse Databinding and JFace it should look quite straight forward:

  • Line 18 – 66: Creates a TableViewer and binds the detail list (=list of mails) of the selected folder to it and observes the current selection to bind their detail values to the controls afterwards
  • Line 68 – 196: Binds labels and widgets using QxWidgetProperties and UBeanObservables

UFaceKit Development

A new homepage is available which is going to example backgrounds and more about UFaceKit.

Licenses
I was not sure about this in my last blog so here’s the current status:

  • Qooxdoo-Wrapper: Released under EPL
  • Eclipse Databinding, Viewer and UFaceKit-API: Released under GPLv3 and UFaceKit.org Commercial License

The decision not to release everything under EPL made is necessary to setup my own server with SVN and a bug tracker. Beside the current code available already yet there’s much more we are going to provide which is currently on my workstation.

Give a try yourself

All code shown is available from the above mentionned Subversion repository but if you are not familiar with setting this up you can give it a try your own here (Please note that as mentionned above there’s still ~1 MB JS-code so application startup could be better)

Now that’s cool

If you are following my blog you may have noticed that I invested some hours this weekend in Eclipse-Databinding and GWT.

It was Saturday in the evening when I got the Eclipse-Databinding working inside GWT and I started searching for a decent Table and TableTree-Widget (I looked at the code in the gwt-incubator but that didn’t made me happy). So I thought the RAP people are using Qooxdoo as the underlying JavaScript-UI library what would be if there was a GWT-Binding to this JavaScript-Lib using the GWT-native calling interface and so I started search for such a thing but apparently noone ever tried to write such a thing. So I started reading docs and studying libraries who use this native-calling interface – I’ve never written such a thing before – and voilá it was yesterday around lunch time when I had the first success because one of the GWT-Demos showed up.

The Java code for this small application looks like this:

public class Button extends QooxdooApp {
	@Override
	protected void run(QxAbstractGui application) {
		QxHBox box = new QxHBox();
		box.setSpacing(10);
		
		QxComposite container = new QxComposite(box);
		container.setPadding(20);
		
		application.getRoot().add(container, QxOption.left(0), QxOption.top(0));
		
		QxButton btn1 = new QxButton("Button A", "/icon/22/apps/media-video-player.png");
		container.add(btn1);
		
		QxButton btn2 = new QxButton("Button B", "/icon/22/apps/internet-mail.png");
		btn2.setEnabled(false);
		container.add(btn2);
		
		QxToggleButton btn3 = new QxToggleButton("Toggle Button", "/icon/22/apps/internet-web-browser.png");
		btn3.focus();
		container.add(btn3);
		
		QxRepeatButton btnRepeat = new QxRepeatButton(null,"/icon/22/actions/list-add.png");
		
		container.add(btnRepeat);
		
		final QxLabel l1 = new QxLabel("0");
		l1.setDecorator("main");
		l1.setPadding(2, 4);
		l1.setBackgroundColor("white");
		container.add(l1);
		
		btnRepeat.addExecuteHandler(new ExecuteHandler() {

			public void execute(ExecuteEvent executeEvent) {
				int tmp = Integer.parseInt(l1.getContent()) + 1;
				l1.setContent(tmp+""); 
			}
		});
	}

}

and now compare it too the original source:

/* ************************************************************************

   qooxdoo - the new era of web development

   http://qooxdoo.org

   Copyright:
     2004-2008 1&1 Internet AG, Germany, http://www.1und1.de

   License:
     LGPL: http://www.gnu.org/licenses/lgpl.html
     EPL: http://www.eclipse.org/org/documents/epl-v10.php
     See the LICENSE file in the project's top-level directory for details.

   Authors:
     * Sebastian Werner (wpbasti)
     * Fabian Jakobs (fjakobs)

************************************************************************ */

/* ************************************************************************

#asset(qx/icon/${qx.icontheme}/22/apps/media-video-player.png)
#asset(qx/icon/${qx.icontheme}/22/apps/internet-mail.png)
#asset(qx/icon/${qx.icontheme}/22/apps/internet-web-browser.png)
#asset(qx/icon/${qx.icontheme}/22/actions/list-add.png)

************************************************************************ */

qx.Class.define("demobrowser.demo.widget.Button",
{
  extend : qx.application.Standalone,

  members :
  {
    main: function()
    {
      this.base(arguments);

      var box = new qx.ui.layout.HBox();
      box.setSpacing(10);

      var container = new qx.ui.container.Composite(box);
      container.setPadding(20);

      this.getRoot().add(container, {left:0,top:0});


      // Two normal buttons
      var btn1 = new qx.ui.form.Button("Button A", "icon/22/apps/media-video-player.png");
      container.add(btn1);

      var btn2 = new qx.ui.form.Button("Button B", "icon/22/apps/internet-mail.png");
      btn2.setEnabled(false);
      container.add(btn2);



      // Toggle Button
      var btn3 = new qx.ui.form.ToggleButton("Toggle Button", "icon/22/apps/internet-web-browser.png");
      btn3.focus();
      container.add(btn3);

      btn3.addListener("changeChecked", function(e) {
        this.debug("Checked: " + e.getData());
      }, this);



      // Repeat Button
      var img1 = "icon/22/actions/list-add.png";
      var btnRepeat = new qx.ui.form.RepeatButton(null, img1);
      container.add(btnRepeat);

      // Label
      var l1 = new qx.ui.basic.Label("0");
      l1.setDecorator("main");
      l1.setPadding(2, 4);
      l1.setBackgroundColor("white");
      container.add(l1);

      // Listener
      btnRepeat.addListener("execute", function()
      {
        var tempValue = parseInt(l1.getContent()) + 1;
        l1.setContent(tempValue.toString());
      });
    }
  }
});

Afterwards I went outdoors because the weather was just too nice to stay inside but because of my great success in the morning I sat down and implemented a few more widgets to show you the GWT-Databinding example from Saturdays post running on Qooxdoo-GWT.

public class TextLabelBinding extends QooxdooApp {

	@Override
	protected void run(QxAbstractGui application) {
		final QxLabel titleLabel = new QxLabel("");
		final QxTextField firstNameField = new QxTextField("");
		final QxTextField lastNameField = new QxTextField("");
				
		QxVBox box = new QxVBox();
		box.setSpacing(10);
		
		QxComposite container = new QxComposite(box);
		container.setPadding(20);
		
		container.add(titleLabel);
		container.add(firstNameField);
		container.add(lastNameField);
		
		application.getRoot().add(container, QxOption.left(0), QxOption.top(0));

		final Person p = new Person();
		p.setFirstname("Tom");
		p.setLastname("Schindl");
		
		DataBindingContext dbc = new DataBindingContext(QxObservables.getRealm());
		QxIWidgetValueProperty uiProp = QxWidgetProperties.text();
		
		dbc.bindValue(uiProp.observe(firstNameField), UBeansObservables.observeValue(QxObservables.getRealm(), p, Person.FIRST_NAME));
		dbc.bindValue(uiProp.observe(lastNameField), UBeansObservables.observeValue(QxObservables.getRealm(), p, Person.LAST_NAME));
		
		ComputedValue titleValue = new ComputedValue(QxObservables.getRealm()) {
			private IObservableValue last = UBeansObservables.observeValue(QxObservables.getRealm(), p, Person.LAST_NAME);
			private IObservableValue first = UBeansObservables.observeValue(QxObservables.getRealm(), p, Person.FIRST_NAME);
			
			@Override
			protected Object calculate() {
				return last.getValue().toString().toUpperCase() + ", " + first.getValue();
			}
			
		};
		dbc.bindValue(uiProp.observe(titleLabel), titleValue);		
	}

}

I’m going to look at the more complex controls when having the next free time slot but for now this is just plain cool, isn’t it?

Eclipse-Databinding 3.5 for GWT

I’m a big fan of GWT but one of the real pain points when developing GWT applications is that there’s no databinding framework [*] available to synchronize your domain objects and UI-Widgets.

As many of you know I’m an even bigger fan of Eclipse-Databinding because it is written so that it can be used with ANY Model-Technology you want and even more important ANY UI-Technology you want and in fairly every environment you want (or at least porting to it is possible without too much trouble).

I’m happy to announce that the days without a databinding solution are gone forever now because yesterday in the night I made Eclipse-Databinding 3.5 compile under GWT and already wrote the first WidgetProperty implementations for Text and Label-Widgets.

Take a look at this video to see it in action

The code used to create this small presentation looks like this:

import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.observable.value.ComputedValue;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.ufacekit.core.ubean.databinding.observables.UBeansObservables;
import org.eclipse.ufacekit.ui.gwt.databinding.GWTObservables;
import org.eclipse.ufacekit.ui.gwt.databinding.IWidgetValueProperty;
import org.eclipse.ufacekit.ui.gwt.databinding.WidgetProperties;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;

/**
 * Entry point classes define <code>onModuleLoad()</code>.
 */
public class GWTDatabindingExample implements
		EntryPoint {
  /**
    * This is the entry point method.
    */
  public void onModuleLoad() {
    final Label titleLabel = new Label();
    final TextBox firstNameField = new TextBox();
    final TextBox lastNameField = new TextBox();
		
    final Person p = new Person();
    p.setFirstname("Tom");
    p.setLastname("Schindl");
		
    DataBindingContext dbc = new DataBindingContext(
      GWTObservables.getRealm()
    );
    IWidgetValueProperty uiProp = WidgetProperties.text();
		
    dbc.bindValue(
      uiProp.observe(firstNameField), 
      UBeansObservables.observeValue(
        GWTObservables.getRealm(), p, Person.FIRST_NAME
      )
    );

    dbc.bindValue(
      uiProp.observe(lastNameField), 
      UBeansObservables.observeValue(
        GWTObservables.getRealm(), p, Person.LAST_NAME
      )
    );
		
    ComputedValue titleValue = 
      new ComputedValue(GWTObservables.getRealm()) {
        private IObservableValue last = 
          UBeansObservables.observeValue(
            GWTObservables.getRealm(), p, Person.LAST_NAME
        );

       private IObservableValue first = 
         UBeansObservables.observeValue(
           GWTObservables.getRealm(), p, Person.FIRST_NAME
       );
			
      @Override
      protected Object calculate() {
        return last.getValue().toString().toUpperCase() 
          + ", " + first.getValue()
        ;
      }
    };

    dbc.bindValue(uiProp.observe(titleLabel), titleValue);

    RootPanel.get("titleContainer").add(titleLabel); 
    RootPanel.get("firstNameFieldContainer").add(
      firstNameField
    );
    RootPanel.get("lastNameFieldContainer").add(
      lastNameField
    );
  }
}

If you take a close look to the sources above the first thing you’ll spot is the usage of org.eclipse.ufacekit.core.ubean which is very light weight domain model implementation part of UFaceKit. The domain model has it’s own notification concept and a reflective API similar to EMF but is as light weight as possible.

A domain object implementation looks like this:

import org.eclipse.ufacekit.core.ubean.UBaseBean;
import org.eclipse.ufacekit.core.ubean.notify.Notification;

public class Person extends UBaseBean {
  public static final int FIRST_NAME = 1;
  public static final int LAST_NAME = 2;

  private String lastname;
  private String firstname;

  public void add(int featureId, Object value) {
    throw new IllegalArgumentException(
      "No multi feature with id '"+featureId+"'"
    );
  }

  @SuppressWarnings("unchecked")
  public <V> V get(int featureId) {
    if (featureId == FIRST_NAME) {
      return (V) getFirstname();
    } else if (featureId == LAST_NAME) {
      return (V) getLastname();
    }
    throw new IllegalArgumentException(
      "No feature with id '"+featureId+"'"
    );
  }

  public void remove(int featureId, Object value) {
    throw new IllegalArgumentException(
      "No multi feature with id '"+featureId+"'"
    );
  }

  public void set(int featureId, Object value) {
    if( featureId == FIRST_NAME ) {
      setFirstname((String) value);
    } else if( featureId == LAST_NAME ) {
      setLastname((String) value);
    } else {
      throw new IllegalArgumentException(
        "No feature with id '"+featureId+"'"
      );
    }
  }

  public String getLastname() {
    return lastname;
  }

  public void setLastname(String lastname) {
    notifyListeners(
      new Notification(
        this, 
        Notification.SET, 
        LAST_NAME,
        this.lastname, 
        this.lastname = lastname
      )
    );
  }

  public String getFirstname() {
    return firstname;
  }

  public void setFirstname(String firstname) {
    notifyListeners(
      new Notification(
        this, 
        Notification.SET, FIRST_NAME,
        this.firstname, 
        this.firstname = firstname
      )
    );
  }
}

The reflective API has the advantage that it doesn’t need reflection to call methods and thus is directly usable within GWT applications where no reflection is available. This is only the start of the many things UFaceKit is going to provide to you starting from full GWT-Databinding support over JFace-Viewer like implemenations to it’s highlevel UI-Abstraction.

For UFaceKit the support of GWT marks a new milestone because now we have a solution for another Java-Developer Community. We now support:

  • SWT/JFace – including a Java5 JFace-Viewer implementation
  • Qt – Qt-Databinding and Qt-JFace-Viewers
  • Swing – Swing-Databinding and Swing-Viewers (though this needs some TLC)
  • GWT – GWT-Databinding and start of GWT-Viewers (Combo, List and Tree)
  • UBean – a very lightweight model technology compatible with GWT

The sources are available from the UFaceKit-repository (you need to have the Google-Eclipse-Plugin installed) and an Eclipse 3.5 as the target (if you don’t want to see compile errors in your IDE)

* Update there are databinding-frameworks (GXT, gwittir) but they force me to buy into their whole framework and even reach out into my Domain-Model if I understood them appropiately.

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.

Galileo: EMF-Databinding – Part 3

Setting up a TreeViewer with EMF-Databinding

The RCP-Application
With this article we are starting to write our RCP-Application which uses Eclipse-Databinding. I’m not going into detail how you create an RCP-Application (I simply use the PDE-Wizard) but assume that you are familiar with this kind of thing.

Still I’d like to describe the application design and the reason behind decisions I made a bit. The first and most important thing is that I’m not a fan of EditorPart and I think for none-file resources the IEditorInput stuff simply doesn’t make a lot of sense so all applications I’ve written since I develop RCP are ViewParts.

A misconception I often see when I say that my RCP-Applications are free from editors is that people ask: “How are you going to be part of Workbench-Dirty/Save-Lifecyle if you use a ViewPart?”.

The answer is quite simple the only thing you have to do is to make your ViewPart implement ISaveablePart2 and then your view is part of the workbench-dirty state lifecycle.

Application Parts

The screen shot from above shows that how the applications UI code is broken up in smaller logical parts:

  • ProjectAdminViewPart:
    This is the main UI area which is subclassing ViewPart and registered into the workbench usin the views-Extension-Point.
  • ProjectExplorerPart:
    This class makes up the left part of the application showing a TreeViewer to select project, create subprojects, … . The internals of this part are described in more detail in this blog post later on.
  • ProjectFormAreaPart:
    This class makes up the right upper part showing a set of input controls who act as a detail part of the ProjectExplorerPart. The internals of this part are explained in detail in Part 4 of this blog series.
  • ProjectCommittersPart:
    This class makes up the right lower part showing a TableViewer. The internals of this part are explained in detail in Part 5 of this blog series.

Before we dive into the main topic of this blog post I’d like to show some interesting code found inside the ProjectAdminViewPart because it shows some interesting RCP features and introduces a small but often forgotten databinding stuff leading to memory leaks in conjunction with databinding.

Restoring view state information
The first interesting thing is that the content of the main area is created using a SashForm to give the user the possibility to define how much space the left part should occupy. The user naturally expects that when he starts the application the next time that the UI comes up in the same state it’s been when shutting down so we need to store the sash-weights when shutting down and restore them on the next start up.

The Eclipse-RCP-Framework provides a possibility to make this happen by using an IMemento to persist informations across application launches the only thing you to do is to use the API:

public class ProjectAdminViewPart 
  extends ViewPart implements ISaveablePart2
{
  private float divider = 0.2f;
  private SashForm sashForm;

  @Override
  public void init(final IViewSite site, IMemento memento) 
    throws PartInitException
  {
    super.init(site, memento);
    if (memento != null 
      && memento.getFloat(DIVIDER_KEY) != null)
    {
      divider = memento.getFloat(DIVIDER_KEY);
    }

    listener = new PartListenerImpl(site);
    site.getPage().addPartListener(listener);
  }

  @Override
  public void saveState(IMemento memento)
  {
    super.saveState(memento);
    int total = sashForm.getWeights()[0] 
                       + sashForm.getWeights()[1];
    memento.putFloat(
      DIVIDER_KEY, 
      sashForm.getWeights()[0] * 1.f / total);
  }

  @Override
  public void createPartControl(Composite parent)
  {
    // ...
    sashForm = new SashForm(parent, SWT.HORIZONTAL);
    // ...
    int left = (int)(100 * divider);
    sashForm.setWeights(new int []{ left, 100 - left });
  }
}

Avoid listener leaking
There’s something I very often see in databinding code from various people (even those who have a lot of databinding knowledge) is that they are not diposing their observables once not more needed which leads to memory leaks. This means that we have to remember all observables created and dispose them manually when not needed any more.

An exception to this are observables created for SWT-Widgets because they dispose themselves when the SWT-Widget is disposed but for all others the rule is the one who created it has to dispose it. Databinding provides a class named ObservablesManager which helps you with this.

In 3.5 EMF-Databinding added new API to ObservableManager which would make collecting observables very easy but apparently a bug was found very late in the release cycle and so the API is not useable currently. The API in question is used like this:

ObservablesManager mgr = new ObservablesManager();
mgr.runAndCollect(new Runnable()
  {
    public void run()
    {
      // your code which creates observables
    }
  });

Though the default API is broken for EMF-Databinding we can add an easy fix so that the API at least collects all IEMFObservable so that we only have to deal with things like ComputedValue and such stuff because as stated above SWT-Observables dispose themselves when the SWT-Widget is disposed.

So our code looks like this:

  @Override
  public void createPartControl(Composite parent)
  {
    // ...
    /* 
     * Track the creation of observables so that we 
     * don't leak listeners when the view part is closed
     */
    mgr = new EMFObservablesManager();
    defaultMgr = new ObservablesManager();
    mgr.runAndCollect(new Runnable()
      {

        public void run()
        {
          projectExplorer = new ProjectExplorerPart(
            getViewSite(), 
            sashForm, 
            toolkit, 
            resource.getFoundation(), 
            defaultMgr);

          projectDataForm = new ProjectFormAreaPart(
            getViewSite(),
            sashForm,
            toolkit,
            resource,
            defaultMgr,
            projectExplorer.getProjectObservable());
        }
      });
    // ...
  }

Setting up a TreeViewer
Enough about the generic stuff and let’s now concentrate on the main topic of this blog.
Project Explorer
Though I guess most of you know how to set up a standard TreeViewer here a short description:

  1. Set a CellLabelProvider: Responsible to translate the model object into a visual representation (text,colors,image,…). The CellLabelProvider-API was introduced in 3.3 and on top it JFace provides many cool new features like StyledText-Support (different fonts and colors in one cell) and ToolTip-Support. It’s worth revisiting your old code and replace LabelProvider through CellLabelProvider subclasses
  2. Set an ITreeContentProvider: Responsible to translate the input into a tree-structure.
  3. Set an input: Any input the ITreeContentProvider can handle

Createing the content provider
The Eclipse-RCP-Platform comes with a plugin named org.eclipse.jface.databinding which provides databinding support and implementation classes for SWT and JFace controls. One of those classes is ObservableListTreeContentProvider which is a class implementing the ITreeContentProvider and makes setting up an observed tree quite easily.

The ObservableListTreeContentProvider can’t do all the work on its own but expects us to provide it helper classes:

  • TreeFactory: responsible to create IObservableLists to observe the childitems of a treenode
  • TreeStructureAdvisor: responsible to provide informations on how to access the model parent of an item and helping to make the decision if an item has child-items

Looking at the picture above shows us that our structure is a bit of an virtual one when we compare it to the domain instance where we would only see the project-subprojects relation on the first sight.
Project Class
Our viewer though displays to different multi-value features as children in the Tree:

  • subprojects whose elements are of type Project
  • committerships whose elements are of type CommitterShip

Displaying different Object-Types was not supported in 3.4 and is a new feature in the new Databinding implementation as well as the possibility to combine 2 multi-value features into a single observable list which is for me personally one of the coolest features in the Properties-API.

  private static class TreeFactoryImpl 
    implements IObservableFactory
  {
    private IEMFListProperty multi = EMFProperties.multiList(
      ProjectPackage.Literals.PROJECT__SUBPROJECTS,
      ProjectPackage.Literals.PROJECT__COMMITTERS);

    public IObservable createObservable(final Object target)
    {
      if (target instanceof IObservableList)
      {
        return (IObservable)target;
      }
      else if (target instanceof Project)
      {
        return multi.observe(target);
      }

      return null;
    }
  }

  private static class TreeStructureAdvisorImpl 
    extends TreeStructureAdvisor
  {
    @Override
    public Object getParent(Object element)
    {
      if (element instanceof Project)
      {
        return ((Project)element).getParent();
      }

      return null;
    }

    @Override
    public Boolean hasChildren(Object element)
    {
      if (element instanceof Project 
        && (
          ((Project)element).getCommitters().size() > 0 
          || ((Project)element).getSubprojects().size() > 0
        )
      )
      {
        return Boolean.TRUE;
      }
      return super.hasChildren(element);
    }
  }

and used like this

private TreeViewer init(Composite parent, 
  Foundation foundation)
{
  TreeViewer viewer = new TreeViewer(parent);
  ObservableListTreeContentProvider cp = 
    new ObservableListTreeContentProvider(
      new TreeFactoryImpl(), 
      new TreeStructureAdvisorImpl()
  );
  viewer.setContentProvider(cp);
  
  // rest of viewer setup
}

Createing the CellLabelProvider
To get such a nice looking tree we can’t use a simple CellLabelProvider (e.g. ColumnLabelProvider is a simple implementation of it) but need one with more features. Another CellLabelProvider implementation is the StyledCellLabelProvider which uses owner-draw to draw StyledText-Strings in the tree.

JFace-Databinding doesn’t provide an implementation for StyledCellLabelProvider out of the box so I had to write my own one but that’s not too hard. The only thing which has to be done is to attach a listener to IObservableMap(s) to observe attributes of all tree elements and update the viewer if one of them changes.

private class TreeLabelProviderImpl 
  extends StyledCellLabelProvider
{
  private IMapChangeListener mapChangeListener = 
    new IMapChangeListener()
    {
      public void handleMapChange(MapChangeEvent event)
      {
        Set<?> affectedElements = 
          event.diff.getChangedKeys();
        if (!affectedElements.isEmpty())
        {
          LabelProviderChangedEvent newEvent = 
            new LabelProviderChangedEvent(
              TreeLabelProviderImpl.this, 
              affectedElements.toArray()
          );
          fireLabelProviderChanged(newEvent);
        }
      }
    };

  public TreeLabelProviderImpl(
    IObservableMap... attributeMaps)
  {
    for (int i = 0; i < attributeMaps.length; i++)
    {
      attributeMaps[i].addMapChangeListener(
        mapChangeListener
      );
    }
  }

  @Override
  public String getToolTipText(Object element)
  {
    return "#dummy#";
  }

  @Override
  public void update(ViewerCell cell)
  {
    if (cell.getElement() instanceof Project)
    {
      Project p = (Project)cell.getElement();

      StyledString styledString = new StyledString(
        p.getShortname()!=null ? p.getShortname():"*noname*",
        null
      );
      String decoration = " (" + 
        p.getCommitters().size() + " Committers)";
      styledString.append(
        decoration, 
        StyledString.COUNTER_STYLER
      );
      cell.setText(styledString.getString());
      cell.setImage(projectImage);
      cell.setStyleRanges(styledString.getStyleRanges());
    }
    else if (cell.getElement() instanceof CommitterShip)
    {
      Person p = (
        (CommitterShip)cell.getElement()
      ).getPerson();
      String value = "*noname*";
      if (p != null)
      {
        value = p.getLastname() + ", " + p.getFirstname();
      }
      StyledString styledString = new StyledString(
        value, null);
      cell.setText(styledString.getString());
      cell.setForeground(
        cell.getControl().getDisplay().getSystemColor(
          SWT.COLOR_DARK_GRAY
        )
      );
      cell.setImage(committerImage);
      cell.setStyleRanges(styledString.getStyleRanges());
    }
  }
}

and used like this

private TreeViewer init(Composite parent, 
  Foundation foundation)
{
  TreeViewer viewer = new TreeViewer(parent);
  ObservableListTreeContentProvider cp = 
    new ObservableListTreeContentProvider(
      new TreeFactoryImpl(), 
      new TreeStructureAdvisorImpl()
  );
  viewer.setContentProvider(cp);

  IObservableSet set = cp.getKnownElements();
  IObservableMap[] map = new IObservableMap [4];

  map[0] = EMFProperties.value(
    ProjectPackage.Literals.PROJECT__SHORTNAME
  ).observeDetail(set);

  map[1] = EMFProperties.value(
    ProjectPackage.Literals.PROJECT__COMMITTERS
  ).observeDetail(set);

  map[2] = EMFProperties.value(
      FeaturePath.fromList(
        ProjectPackage.Literals.COMMITTER_SHIP__PERSON,
        ProjectPackage.Literals.PERSON__FIRSTNAME)
      ).observeDetail(set);

  map[3] = EMFProperties.value(
      FeaturePath.fromList(
        ProjectPackage.Literals.COMMITTER_SHIP__PERSON, 
        ProjectPackage.Literals.PERSON__LASTNAME)
      ).observeDetail(set);

  viewer.setLabelProvider(new TreeLabelProviderImpl(map));
  // Further viewer setup

The last step is to set the input of the viewer which has to be an IObservableList which is in our example all top-level projects who are restored in the foundation instance.

IEMFListProperty projects = EMFProperties.list(
  ProjectPackage.Literals.FOUNDATION__PROJECTS
);
viewer.setInput(projects.observe(foundation));

There is even more code in the ProjectExplorerPart e.g. a customized ColumnViewerToolTipSupport to create the nice looking ToolTips when hovering items of the tree as well as some RCP-Code to allow plugin.xml to contribute a context menu but that’s standard JFace and RCP code.

Galileo: EMF-Databinding – Part 2

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:
Plugin 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.

Interface and Class Overview

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:
Nested Lastname Property
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.
Feature Path

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:

EMFProperties
EMFProperties-Factory

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

EMF-Edit Factory

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
value_property

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.

emf_value_property

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
list_property

The IListProperty implementations IEMFListProperty and IEMFEditListProperty provide the possibility to create observables of multi valued attribute/properties like e.g. subprojects of a project.

emf_list_property
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.