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.

Advertisement
This entry was posted in EMF, Tutorials. Bookmark the permalink.

12 Responses to Galileo: EMF-Databinding – Part 4

  1. Pingback: Galileo: Improved EMF-Databinding-Support « Tomsondev Blog

  2. Fred says:

    Hi Tom,

    Thanks for this nice series !

    I have to use DB without EMF (for now). Do you have any pointer to the documentation on the new databinding API ?

    Thanks

    Fred

    • tomeclipsedev says:

      no but in the end you can substitute EMFProperties against BeanProperties and the structural feature through “attribute” name and Class-tyües and you are done.

  3. Fred says:

    Thanks 🙂

  4. SeB.fr says:

    Great post, keep up the good eclipse vibes….
    I am using databinding with emf 2.3 (meaning beta databinding plugins) and I wish I could switch to this release…, but I think it would require to much upgrade to do.
    Anyway great work.
    Although I find these apis very flexible but I find them very verbose, it requires actually a number of lines of codes that is consequently large for what it does.
    Don’t you think ?

    • tomeclipsedev says:

      Well if you have ideas how to make it less verbose we are happy to discuss it. There’s always a trade of between flexibility and verbosity and it’s designed in a way that people can write their own framework on top of it e.g. I don’t UFaceKit is such a project and I have an internal project where you describe the binding itself in EMF so the whole binding code is gone 🙂

  5. Pingback: Galileo: EMF-Databinding – Part 5 « Tomsondev Blog

  6. aappddevv says:

    Is there anyway to set the observable value to be used for observing the viewer single selection directly instead of asking ViewerProperties to create yet another observable value to be used? Or can the observable return from createMasterObs() in your example just be used directly as the master for the detail? This avoids creating the listeners on the other “master” that is really acting as a passthru from the observable value from the viewer’s selection. It may just work.

    This is essentially the idea behind jgoodies use of observables in SelectionInList where you can set the observable bean “channel” through which when the viewer changes selection, the bean channel is changed and if you are using that to work on the detail, the detail is automatically updated.

    • tomeclipsedev says:

      Yes if your setup is easier you can use it directly. The problem in my case is that when selecting a person in the tree I don’t want the detail part to react and so I need a mediator in between.

  7. Pingback: 2010 in review « Tomsondev Blog

  8. CoffeeJunkie says:

    Great post Tom, i have a similar problem like you but i want the master observable not to always contain a Project (in your example) but also the committer (in your example) where the comitter is a list property of the projet (in my example) 🙂
    The problem is that i ran into the following. It would be great if you could take a quick look into this. I am kind of lost by now :/

    http://stackoverflow.com/questions/17452716/emf-treeviewer-master-detail-databinding-with-mixed-content-and-multiple-details

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.