Tuesday, February 03, 2009

Introducing the Properties API

Observables are hard to implement. There are a handful of rules, guidelines and corner cases that observables implementors must pay attention to for data binding to work properly. Observables must call ObservableTracker.getterCalled() for things like ComputedValue or MultiValidator to work; observables must ensure that all invocations are made from within the realm; a ChangeEvent must be fired before ValueChangeEvent; the list goes on. To an extent we've been able to isolate these concerns within the framework, but over time I couldn't help noticing that many of our observables followed a template--a lengthy, complicated one.

In June 2008 I stumbled across bug 194734 and I thought Tom was on to something. I started working on a prototype, which we've been evolving and refining for the past six months. I'm very pleased with the result, and if you've tried implementing your own observables before, I think you will be too.

There are four types of properties: IValueProperty, IListProperty, ISetProperty and IMapProperty. Properties serve two main purposes:
  • They act as convenient, portable observable factories for a particular property.
  • They act as strategy objects for the observables they create. That is, the observables use the properties to access, modify, and listen to the property source object.
Suppose you want to observe the "name" property of a bean object:

Person person = new Person("Joe");
IValueProperty nameProperty = BeanProperties.value("name");
IObservableValue personName =
nameProperty.observe(person);

One of the strengths of properties is how easy it is to combine them to observe nested attributes:

IListProperty childrenProperty = BeanProperties.list("children");
IValueProperty nameProperty = BeanProperties.value("name");
IObservableList namesOfChildren =
childrenProperty.values(nameProperty).observe(person);

or, a shorter version:

IObservableList namesOfChildren = BeanProperties
.list("children").values("name").observe(person);

Properties are reusable:

IValueProperty selection = WidgetProperties.selection();
IObservableValue buttonSelection = selection.observe(button);
IObservableValue comboSelection = selection.observe(combo);
IObservableValue spinnerSelection = selection.observe(spinner);

But most importantly, they are easy to implement. Let's close with a real-world example:

DataBinding does not come with built-in support for observing the selection of a DateTime widget (see bug 169876 for an explanation why). However a useful use case is to observe the date portion (year+month+day) and set the time of day to midnight.

The following is a complete property implementation for the DateTime#selection property, that you are free to use in your own projects:

public class DateTimeSelectionProperty extends SimpleValueProperty {
public Object getValueType() {
return Date.class;
}

// One calendar per thread to preserve thread-safety
private static final ThreadLocal calendar =
new ThreadLocal() {
protected Object initialValue() {
return Calendar.getInstance();
}
};

protected Object doGetValue(Object source) {
Calendar cal = (Calendar) calendar.get();

DateTime dateTime = (DateTime) source;
cal.clear();
cal.set(dateTime.getYear(), dateTime.getMonth(),
dateTime.getDay());
return cal.getTime();
}

protected void doSetValue(Object source, Object value) {
Calendar cal = (Calendar) calendar.get();
cal.setTime((Date) value);

DateTime dateTime = (DateTime) source;
dateTime.setYear(cal.get(Calendar.YEAR));
dateTime.setMonth(cal.get(Calendar.MONTH));
dateTime.setDay(cal.get(Calendar.DAY_OF_MONTH));
}

public INativePropertyListener adaptListener(
ISimplePropertyListener listener) {
return new WidgetListener(listener);
}

private class WidgetListener implements INativePropertyListener,
Listener {
private final ISimplePropertyListener listener;

protected WidgetListener(ISimplePropertyListener listener) {
this.listener = listener;
}

public void handleEvent(Event event) {
listener.handlePropertyChange(
new SimplePropertyEvent(event.widget,
DateTimeSelectionProperty.this, null));
}
}

protected void doAddListener(Object source,
INativePropertyListener listener) {
((DateTime) source).addListener(
SWT.Selection, (Listener) listener);
}

protected void doRemoveListener(Object source,
INativePropertyListener listener) {
((DateTime) source).removeListener(
SWT.Selection, (Listener) listener);
}

public IObservableValue observe(Object source) {
// For widgets, we want the display realm instead of
// Realm.getDefault()
Realm realm = SWTObservables.getRealm(
((Widget) source).getDisplay());
return observe(realm, source);
}
}


Now we can observe the selection of a DateTime:

IObservableValue selection =
new DateTimeSelectionProperty().observe(dateTime);


Today we released WidgetValueProperty as public API, but did not make it in time for M5. However in the future (starting in the next integration build), implementing properties for widgets will be much simpler:

  • Omit the adaptListener, doAddListener, and doRemoveListener methods
  • Omit the WidgetListener class
  • Omit the observe() method
  • Add a no-arg constructor with a call to super(eventType) in the constructor.

In the example the constructor would call "super(SWT.Selection)". As long as the the widget supports untyped listeners, WidgetValueProperty will handle the listener parts for you.

4 comments:

bocker said...

That rocks.

Finally I can replace my ugly workarounds for setting a property dynamically.

Thank you all !

Sebastian Fuchs

John Conlon said...

Great work. I had similar requirements for property observations and I just happened to also be looking for something for dataTime observation as well.

thanks,

Chris Aniszczyk (zx) said...

Thanks for your hard work on this one!

This is good stuff and I look forward to covering it in the RCP book.

suzana martin said...

Events By us is directing the Wedding and we could not be more excited! our Venue will host our event and we are honored to have an amazing group of special guests will have the opportunity to see at work! So, Save the Date!