Thursday, February 12, 2009

Bind a viewer in one statement with ViewerSupport

We've added the ViewerSupport class to simplify setting up viewers (including tree viewers), usually in just one statement.

Before:

ObservableListContentProvider contentProvider =
new ObservableListContentProvider();
viewer.setContentProvider( contentProvider );
viewer.setLabelProvider(
new ObservableMapLabelProvider(
BeansObservables.observeMaps(
contentProvider.getKnownElements(),
new String[] {
"title", "releaseDate", "director", "writer" } ) ) );
viewer.setInput(BeansObservables.observeList(model, "movies"));


After:

ViewerSupport.bind(
viewer,
BeansObservables.observeList(model, "movies"),
BeanProperties.values(new String[] {
"title", "releaseDate", "director", "writer" } ) );

22 comments:

Chris Aniszczyk (zx) said...

Very cool!

Let's get those snippets out there and updated ;)

Matthew Hall said...

The snippets in the databinding examples project are already updated to use ViewerSupport where appropriate. :)

Frank said...

Assuming that model.movies is a WritableList it seems that the code

ViewerSupport.bind(
viewer,
BeansObservables.observeList(model, "movies"),
BeanProperties.values(new String[] {
"title", "releaseDate", "director", "writer" } ) );

track changes in the movies property, if I do a model.setMovies(new Writable(...)),
but not if I do a model.getMovies.add(...)

If I change the example to

ViewerSupport.bind(
viewer,
model.getMovies(),
BeanProperties.values(new String[] {
"title", "releaseDate", "director", "writer" } ) );

then it tracks changes to the list (eg. add(...)), but not if I do a model.setMovies(new Writable(...)).

Is there a way to do both?

Matthew Hall said...

Frank, you are correct. Observables will not be able to pick up on changes in that situation since you are modifying the model's internal list directly. BeansObservables only know about changes that the bean tells us about through the java.beans.PropertyChangeListener interface.

To catch these kinds of uses you would need to either (a) add methods to the model class to support atomic add/remove actions, and fire change events from those methods, or (b) wrap the model's list in something that traps modifications and notifies the bean.

In the case of (a) it would be a good precaution to return an unmodifiable list from getMovies() to force clients to use the appropriate API.

Frank said...

I thought that I used option (b) by using a WritableList? And it also works if I bind directly to the list instead of creating an observable on the movies property of the model.

Doesn't BeansObservables.observeList(model, "movies") listen for events from the underlying list also? Or does it just listen to model events (being more like a observeValue)?

Matthew Hall said...

Not exactly. BeansObservables is only aware that the model object is a bean, and assumes that the bean's getter returns either a java.util.List or an Object[]. It is not expecting to get an IObservableList and so doesn't try to listen to it.

So for BeansObservables to fire changes with the usage that you seem to want, the bean must be able to catch changes to the list and fire events accordingly.

One solution is to wrap the bean's internal List in some type of gatekeeper decorator List which fires a change event through the bean's change support whenever the List is changed. e.g.

class BeanListDecorator {

  public BeanListDecorator(

      List decorated,

      PropertyChangeSupport pcs,

      String propertyName) {

    // save to private fields

  }



  public Object get(int index) {

    return decorated.get(index);

  }



  // reimplement all mutator methods to fire a

  // property after returning from the delegate

  // (don't forget mutator methods in iterators)



  public void add(int index, Object o) {

    decorated.add(index, o);

    pcs.firePropertyChange(propertyName, null, null);

  }



  // We also need a method for substituting the decorated

  // list, if e.g. clients call model.setMovies()

  public void setDecorated(List decorated) {

    this.decorated = decorated;

    pcs.firePropertyChange(propertyName, null, null);

  }

}

Matthew Hall said...

Also BeanListDecorator should extend AbstractList, sorry I left that out.

kiko said...

Hi,
I have a problem with deeper boxing.

This is my model: I have a ViewModel with a Finding selected. The finding has an element icd10findings which is a ICD10List. This holds an ArrayList icd10list with ICD10 elements.

Now what I want to do is bind a TableViewer to the ArrayList using the following Code:

ViewerSupport.bind(icd10TableViewer, BeansObservables
.observeDetailList(BeansObservables.observeDetailValue(
BeansObservables.observeValue(viewModel,
"findingSelected"), "icd10findings",
ICD10List.class), "icd10list", ICD10.class),
BeanProperties.value("name"));

So: viewModel.findingSelected.icd10findings.icd10list

The Problem though is, that the Table shows the entries, but if the finding selected is changed, it shows the ICD10s of all findings ever selected.

Do you have an idea how to solve this?

Boris Bokowski said...

Hi Kiko,

It's hard to tell, but it could be a bug in the framework. Could you file a bug, and ideally attach a snippet that shows the problem? Writing a snippet is going to be a little work, since you couldn't just use your existing domain classes, but it's probably worth the effort.

kiko said...

Hi,
I solved the Problem... It was due to the fact, that my ICD10 class was overrriding the equals method...

Thanks for the quick reply!

Regards, Kilian

Justin_R said...

Is there a way to customize the binding using the ViewSupport, such us use a UpdateValueStrategy?

My current case is like :
I have a domain Message bean class with a creationTime property in java.util.Date type.

When I bind a list of Message to the tableviewer, I'd like their dates to be formatted. Currently I use a LabelProvider to do that. But if I use the ViewerSupport approach, I can't figure a way to do the same thing.

Thanks,

Boris Bokowski said...

Justin, you may want to inline the code from ViewerSupport.bind so that you can customize. The trick is to subclass ObservableMapLabelProvider and to override methods like getText.

Matthew Hall said...

Another option is to introduce new value properties that take an IConverter or java.text.Format, and chain the converter/formatter property to the original property:

IValueProperty dateProp = BeanProperties.value(Message.class, "creationTime", Date.class);

// This API does not exist (yet) but would not be hard to add
IValueProperty formattedProp = Properties.formattedValue(new SimpleDateFormat("yyyy-MM-dd"));

IValueProperty formattedDateProp = dateProp.value(formattedProp);

IObservableMap columnLabels = formattedDateProp.observeDetail(contentProvider.getKnownElements());

Justin_R said...

Thanks Boris and Matthew

I used the Boris's approach to make it work. If a the ViewerSupport.bind could somehow take converters, it will make the code much simpler.

I basically re-use the code in the ViewerSupport.bind method
and replaced the ObservableMapLabelProvider class to my own LabelProvider class that extends it and adds format code within "setColumnText" method


Here's my code, in case anyone may want to see it.
ObservableListContentProvider contentProvider = new ObservableListContentProvider();
viewer.setContentProvider(contentProvider);
viewer.setLabelProvider( new MessageTableViewLabelProvider(
Properties.observeEach( contentProvider.getKnownElements(), BeanProperties.values(new String[] { "source", "date", "content", "read"} ) )
));
viewer.setInput(BeansObservables.observeList(presenter, "messages"));



// Lable Provider class definition

class MessageTableViewLabelProvider extends ObservableMapLabelProvider implements ITableLabelProvider {

public MessageTableViewLabelProvider(IObservableMap[] iObservableMaps) {
super(iObservableMaps);
}

@Override
public String getColumnText(Object obj, int index) {
Message message = (Message) obj;
switch (index) {
case 0:
return message.getSource();
case 1:
return TAConstants.DATETIME_FORMAT.format(message.getDate());
case 2:
return message.getContent();
default:
throw new IllegalArgumentException("Central Panel Mesage Table does not have column index:" + index);
}
}

}

laluz said...

Hi

this is very cool, you made my day!

But personally, I don't like the idea of using the subclassed map label provider.

From my point of view, its much better having an IConverter for IValueProperty, thus there's no need to know the exact column in the label provider implementation.
So you don't have that many occurences to be changed, when changing the columns (maybe switching col1 with col3).

Secondly, it seems to geeting really complex, if you use a non-homogeneous tree utilizing DelegatingValueProperty.

What do you think.
Sebastian

fluminis said...

Realy nice article! You save my day.

Just out of curiosity, how can you use databinding with save and cancel buttons ?

Databinding update the model in real time, but it is common to have two buttons (save and cancel) on an editing dialog to let the users cancel theirs modifications if needed.

Boris Bokowski said...

@fluminis: There are two approaches for this, you either make the edits on a working copy of the model, or you record changes as they are made so you can undo.

Matthew Painter said...

Hey :) It seems like if I bind a writable list to a table, removing items from the writable list does not remove them from the table - however, adding them does.

Here's how I am hacking around it currently:

final IChangeListener listener = new IChangeListener() {

@Override
public void handleChange(ChangeEvent event) {
Display.getDefault().asyncExec( new Runnable() {

@Override
public void run() {
viewer.refresh();
}
});
}
};

iobservable.addChangeListener( listener);
table.addDisposeListener( new DisposeListener() {

@Override
public void widgetDisposed(DisposeEvent e) {
iobservable.removeChangeListener(listener);
}
});

Any pointers on whether this is a genuine bug?

Thanks :)

microgaming said...

I thought that I used option (b) by using a WritableList? And it also works if I bind directly to the list instead of creating an observable on the movies property of the model.

Boris Bokowski said...

@Matthew: Could you please file a new bug and attach a simple snippet so that we can investigate? Thank you!

Mau said...

@Boris & @Matthew:
I know this post has few years, but I'm facing the same problem when removing all the elements of a WritableList. I did look for a bug in bugzilla but didn't find anything. Is there an alternative to Matthew's hack to solve this problem? I'm trying a custom viewer (PFGrid.SWT) and has strange behavior when I try this solution, so maybe if there's a way to make it work properly in JFace the grids also act as TableViewer.

HELEN SANDRA said...

Use our Registration system to maximize staff productivity and increase response rates. The days of handling registrations on paper are over, along with the confusion, headaches, and mistakes that often accompany those archaic methods.