Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX: Update of ListView if an element of ObservableList changes

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?

like image 594
user1828169 Avatar asked Dec 16 '12 22:12

user1828169


1 Answers

There are several aspects to your question (and I'm not entirely sure which aspect is the problem :-) I'll assume that your POJO is somehow notifying listeners about changes, could be by being a full-fledged JavaBean. That is, it complies with its notification contract via firing propertyChange events as needed or by 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 losing 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 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;     }  } 

Its 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 updating 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);     } } 
like image 62
kleopatra Avatar answered Oct 05 '22 22:10

kleopatra