33

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?

assylias
  • 321,522
  • 82
  • 660
  • 783
user1828169
  • 331
  • 1
  • 3
  • 4
  • can you post u r POJO class ? – invariant Dec 16 '12 at 23:57
  • 1
    There is an approach outlined at the Oracle Forum [here](https://forums.oracle.com/thread/2244635) that outlines a solution to this issue. – ThomasMH Jul 28 '13 at 23:04
  • The approach given by kleopatra is the latest and simplest solution. I want to only add that the `extractor` can return whatever observables it wants to. And that it can be very succinctly written with lambdas. – HRJ Feb 25 '15 at 12:28
  • @HRJ The thrust of the original question is how to update the `ListView` when the contents of an element in the backing `ObservableList` changes. Expanding on kleopatra's answer it would also be necessary to add a `ListChangeListener` to the list that would capture and handle `wasUpdated()` changes. Or did I miss something? – jfr May 30 '20 at 11:01

7 Answers7

22

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);
    }
}
Abra
  • 19,142
  • 7
  • 29
  • 41
kleopatra
  • 51,061
  • 28
  • 99
  • 211
7

You can manually trigger a ListView.EditEvent—which will cause the ListView to update—by calling the ListView::fireEvent method inherited from javafx.scene.Node. For example,

/**
 * Informs the ListView that one of its items has been modified.
 *
 * @param listView The ListView to trigger.
 * @param newValue The new value of the list item that changed.
 * @param i The index of the list item that changed.
 */
public static <T> void triggerUpdate(ListView<T> listView, T newValue, int i) {
    EventType<? extends ListView.EditEvent<T>> type = ListView.editCommitEvent();
    Event event = new ListView.EditEvent<>(listView, type, newValue, i);
    listView.fireEvent(event);
}

Or as a one liner,

listView.fireEvent(new ListView.EditEvent<>(listView, ListView.editCommitEvent(), newValue, i));

Here is a sample application to demonstrate its use.

/**
 * An example of triggering a JavaFX ListView when an item is modified.
 * 
 * Displays a list of strings.  It iterates through the strings adding
 * exclamation marks with 2 second pauses in between.  Each modification is
 * accompanied by firing an event to indicate to the ListView that the value
 * has been modified.
 * 
 * @author Mark Fashing
 */
public class ListViewTest extends Application {

    /**
     * Informs the ListView that one of its items has been modified.
     *
     * @param listView The ListView to trigger.
     * @param newValue The new value of the list item that changed.
     * @param i The index of the list item that changed.
     */    
    public static <T> void triggerUpdate(ListView<T> listView, T newValue, int i) {
        EventType<? extends ListView.EditEvent<T>> type = ListView.editCommitEvent();
        Event event = new ListView.EditEvent<>(listView, type, newValue, i);
        listView.fireEvent(event);
    }

    @Override
    public void start(Stage primaryStage) {
        // Create a list of mutable data.  StringBuffer works nicely.
        final List<StringBuffer> listData = Stream.of("Fee", "Fi", "Fo", "Fum")
                .map(StringBuffer::new)
                .collect(Collectors.toList());
        final ListView<StringBuffer> listView = new ListView<>();
        listView.getItems().addAll(listData);
        final StackPane root = new StackPane();
        root.getChildren().add(listView);
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
        // Modify an item in the list every 2 seconds.
        new Thread(() -> {
            IntStream.range(0, listData.size()).forEach(i -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(listData.get(i));
                Platform.runLater(() -> {
                    // Where the magic happens.
                    listData.get(i).append("!");
                    triggerUpdate(listView, listData.get(i), i);
                });            
            });
        }).start();
    }

    public static void main(String[] args) {
        launch(args);
    }

}
Mark
  • 3,057
  • 1
  • 14
  • 5
6

Using Francis idea I did:

   list.set(list.indexOf(POJO), POJO);

May not be the best solution but worked.

emorgado
  • 61
  • 2
  • 2
3

Since Java 8u60 ListView officially supports a method refresh() to update the view manually. JavaDoc:

This is useful in cases where the underlying data source has changed in a way that is not observed by the ListView itself.

I successfully used this method for this issue here to update the content of items in the ListView.

2

You should take the observable list and update the object using list.set(selectedIndex, object); My example showing button with the handle method. In this I edited list users in fx viewtable

Button commit = new Button("Commit");
    commit.setOnAction(new EventHandler<ActionEvent>() {
        public void handle(ActionEvent evt) {
            int selectedIndex = tableView.getSelectionModel().getSelectedIndex();
            User user = tableView.getSelectionModel().getSelectedItem();
            user.setId(Integer.parseInt(idTF.getText()));
            user.setName(nameCB.getValue());
            user.setSurname(srnameTF.getText());
            user.setAddress(addressTF.getText());
            service.getUsers().set(selectedIndex, user);
            tableView.toFront();
        }
    });
milosz
  • 650
  • 8
  • 14
-1
ObservableList<String> items = FXCollections.observableArrayList();
ListView lv;
lv.setItems(items);
items.add();
items.remove;
Cole Tobin
  • 9,206
  • 15
  • 49
  • 74
WeiChao
  • 11
  • 1
-5

try this

  list.remove(POJO);
  list.add(index,POJO);
Francis
  • 603
  • 1
  • 6
  • 17