JavaFX: Update of ListView if an element of ObservableList changes

user1828169 picture user1828169 · Dec 16, 2012 · Viewed 35.7k times · Source

I would like to display a list of persons (coded in POJOS, and containing a name and surname property) using a JavaFX ListView control. I created the ListView and added the list of persons as an ObservableList. Everything works fine if I delete or add a new person to the ObservableList, but changes in the POJO do not trigger an update of the ListView. I have to remove and add the modified POJO from the ObservableList to trigger the update of the ListView. Is there any possibility to display changes in POJOS without the workaround described above?

Answer

kleopatra picture kleopatra · Jan 29, 2014

There are several aspects to your question (and I'm not entirely which is the problem :-) I'll assume that your POJO somehow notifying listeners about changes, could be by being a full-fledged JavaBean, that is complies to its notification contract via firing propertyChange events as needed or some other means - otherwise, you would need some manual push of the change anyway.

The basic approach to make an FX-ObservableList notify its own listeners on mutations of contained elements is to configure it with a custom Callback that provides an array of Observables. If the elements have fx-properties you would do something like:

Callback<Person, Observable[]> extractor = new Callback<Person, Observable[]>() {

    @Override
    public Observable[] call(Person p) {
        return new Observable[] {p.lastNameProperty(), p.firstNameProperty()};
    }
};
ObservableList<Person> teamMembers = FXCollections.observableArrayList(extractor);
// fill list

If the pojo is-a full-fledged core javaBean, its properties have to be adapted to fx-properties, f.i. by using JavaBeanProperty:

Callback<PersonBean, Observable[]> extractor = new Callback<PersonBean, Observable[]>() {
    List<Property> properties = new ArrayList<Property>();
    @Override
    public Observable[] call(PersonBean arg0) {
        JavaBeanObjectProperty lastName = null;
        JavaBeanObjectProperty age = null;
        try {
            lastName = JavaBeanObjectPropertyBuilder.create()
                    .bean(arg0).name("lastName").build();
            age = JavaBeanObjectPropertyBuilder.create()
                    .bean(arg0).name("age").build();
            // hack around loosing weak references ... 
            properties.add(age);
            properties.add(lastName);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return new Observable[] {lastName, age};
    }

};
ObservableList<Person> teamMembers = FXCollections.observableArrayList(extractor);
// fill list

Note a caveat: without keeping a strong reference to the adapted properties somewhere, they will be quickly garbage-collected - and then appear to have no effect at all (falling into the trap again and again, not sure how if there's a good strategy to avoid it).

For any other means of (possibly coarse-grained) notification, you can implement a custom adapter: the adapter below listens to all propertyChanges of a bean, listening to other types of events would be quite analogous.

/**
 * Adapt a Pojo to an Observable.
 * Note: extending ObservableValue is too much, but there is no ObservableBase ...
 *
 * @author Jeanette Winzenburg, Berlin
 */
public class PojoAdapter<T> extends ObservableValueBase<T> {

    private T bean;
    private PropertyChangeListener pojoListener;
    public PojoAdapter(T pojo) {
        this.bean = pojo;
        installPojoListener(pojo);
    }

    /**
     * Reflectively install a propertyChangeListener for the pojo, if available.
     * Silently does nothing if it cant.
     * @param item
     */
    private void installPojoListener(T item) {
        try {
            Method method = item.getClass().getMethod("addPropertyChangeListener", 
                  PropertyChangeListener.class);
            method.invoke(item, getPojoListener());
        } catch (NoSuchMethodException | SecurityException | IllegalAccessException | 
                  IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
    /**
     * Returns the propertyChangeListener to install on each item.
     * Implemented to call notifyList.
     * 
     * @return
     */
    private PropertyChangeListener getPojoListener() {
        if (pojoListener == null) {
            pojoListener = new PropertyChangeListener() {

                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    fireValueChangedEvent();
                }
            };
        }
        return pojoListener;
    }

    @Override
    public T getValue() {
        return bean;
    }

}

It's usage just the same as above (getting boring, isn't it :-)

Callback<PersonBean, Observable[]> extractor = new Callback<PersonBean, Observable[]>() {

    @Override
    public Observable[] call(PersonBean arg0) {
        return new Observable[] {new PojoAdapter<PersonBean>(arg0)};
    }

};
ObservableList<Person> teamMembers = FXCollections.observableArrayList(extractor);
// fill list

Unfortunately, automatic updates of a ListView with such cool list won't work reliably due to a bug that's fixed only in jdk8. In earlier versions, you are back at square 1 - somehow listening to the change and then manually update the list:

protected void notifyList(Object changedItem) {
    int index = list.indexOf(changedItem);
    if (index >= 0) {
        // hack around RT-28397
        //https://javafx-jira.kenai.com/browse/RT-28397
        list.set(index, null);
        // good enough since jdk7u40 and jdk8
        list.set(index, changedItem);
    }
}