Tuesday, January 09, 2007

Master Detail - Selection

I find myself answering quite a few questions on the newsgroup hinting that the solution is the master detail APIs. I figured that a verbose explanation couldn't hurt the cause. The stereotypical master detail use case contains a list and upon selection in the list details will be displayed in a separate set of widgets. I'll attempt to explain the core concepts involved.

To explain we'll use the master detail snippet available in the data binding examples project. The snippet displays an SWT List filled with Persons. The Person model has one property, name. Upon selection in the List the name of the currently selected person is displayed in a Text widget.



The relevant data binding code is copied and pasted below...
// 1. Observe changes in selection.
IObservableValue selectionObservable = ViewersObservables
.observeSingleSelection(viewer);

// 2. Observe the name property of the current selection.
IObservableValue detailObservable = BeansObservables
.observeDetailValue(Realm.getDefault(), selectionObservable, "name",
String.class);

// 3. Bind the Text widget to the name detail (selection's name).
new DataBindingContext().bindValue(SWTObservables.observeText(name,
SWT.None), detailObservable, new BindSpec().setUpdateModel(false));


If you follow the comments in the snippet you'll see that we need to perform 3 steps:

  1. Observe changes of the viewer's selection.

  2. Observe the name property of the selection.

  3. Bind the text of the Text widget to the name detail (selected person's name).


1. Observe changes in the viewer's selection


IObservableValue has a value. I know, rocket science. But what IObservableValue provides us is a reference to a transient instance and we can observe changes from one instance to the next. If the selection of the viewer is a person the value of the selection observable will be this person. If the selection is null the value of the selection observable will be null. Regardless of the value we have a reference to an object that will contain the viewer's selection. Change events will fire when a new person, or null, is set as the value of this observable. This is the master.

2. Observe the detail of the master (name of the selected person)


When we observe a detail of the master we're effectively saying...
"observe the value of a property of the value contained in an observable".


Translated into our use case we're saying...
"observe the value of the name property of the person contained in the selection"


If the selection of the the Viewer is a Person with the name "mickey mouse" the following will be true...

  • selectionObservable.getValue() == Person("mickey mouse")

  • detailObservable.getValue() == "mickey mouse"


When the selection changes to a Person with the name of "daffy duck" the following will be true...

  • selectionObservable.getValue() == Person("daffy duck")

  • detailObservable.getValue() == "daffy duck"


When the selection changes to null the following will be true...

  • selectionObservable.getValue() == null

  • detailObservable.getValue() == null


When the master value changes the detail populates itself with the value from the master. This gives us the master detail behavior. To finish it off we need to...

3. Bind the detail to the Text widget


The last step is to bind the detail observable to the Text widget in order to display the detail. Enough said.

Tomorrow night I'll describe a use case that explains how this fits into using JFace Data Binding and the presentation model design pattern.

4 comments:

Freaky-namuH said...

I see plenty of examples showing simple object binding (Person.name), but how would you handle more complex objects?
Eg
class Person
{
String name;
Address address;
}
class Address
{
String suburb;
}

Modifying the Snippet010MasterDetail to:
IObservableValue detailObservable = BeansObservables.observeDetailValue(Realm.getDefault(), selection, "name.address.suburb",String.class);

Complains about
org.eclipse.core.databinding.BindingException: Could not find property with name name.address.suburb in class class org.eclipse.jface.examples.databinding.snippets.Snippet010MasterDetail$Person

How would you do this?

Brad Reynolds said...

You need to observe each object walking the graph. So it would be something like...

//Observe the address of the selected person
IObservableValue addressObservable = BeansObservables.observeDetailValue(Realm.getDefault(), selectedPerson, "address", Address.class);

//Observe the suburb of the address
IObservableValue suburbObservable = BeansObservables.observeDetailValue(Realm.getDefault(), addressObservable, "suburb", String.class);

It's not the prettiest but it's possible. You could probably write a util to walk the graph but as of 3.3M5 (which means we probably won't be able to do anything for Data Binding 1.0) we don't have a way to simplify the object traversal.

Freaky-namuH said...

Here's a quick little method to do it. It relies on BeansObservables.getPropertyDescriptor being public (it's currently private). I haven't really tested it outside of the Snippet010MasterDetails.java.

public static IObservableValue observeNestedDetailValue(Realm realm, IObservableValue master, String propertyName,
Class basePropertyType)
{
String[] propertyNames = propertyName.split("\\."); //$NON-NLS-1$
PropertyDescriptor descriptor = null;
IObservableValue observableValue = master;
for (int i = 0; i < propertyNames.length; i++)
{
String aPropertyName = propertyNames[i];
Class clazz = descriptor == null ? basePropertyType : descriptor.getPropertyType();
descriptor = BeansObservables.getPropertyDescriptor(clazz, aPropertyName);
observableValue = BeansObservables.observeDetailValue(Realm.getDefault(), observableValue, aPropertyName, descriptor.getPropertyType());
}
return observableValue;
}

Freaky-namuH said...

I should add, you call it like this

IObservableValue detailObservable1 = observeNestedDetailValue(Realm.getDefault(), selection, "address.suburb", Person.class);