Properties Extractor: Best way to get the ListView instantly updating its elements

This post is about how to deal with JavaFX ListViews and TableViews and how these controls are getting informed about changed content of the contained elements.
I wonder why I didn’t find anything about the following pattern in the relevant books as this is a really crucial mechanism.
Many posts out there suggest to force triggering a ChangeEvent getting a ListView to refresh by calling

list.remove(POJO);
list.add(index,POJO);

after each commited change! Brrr ;-)!

But there is a much better way:
Enable your list to report changes on the element by providing an properties extractor.

The Demo App
I have created a small demo app to play with for giving it a try.
Basically two TableViews and one ListView all sharing the same data.
To change properties of the elements one TableView is editable:
PropertiesExtractor Demo1

The DataModel

The compulsive PersonBean folling the JavaFX Bean Pattern/Convention

public class PersonBean {

    private StringProperty firstName;
    private StringProperty lastName;
    private ObjectProperty<LocalDate> birthday;
    private ObjectBinding<Long> age;

    public PersonBean() {
    }

    public PersonBean(String firstName, String lastName, LocalDate birthday) {
        setFirstName(firstName);
        setLastName(lastName);
        setBirthday(birthday);
    }

    public final StringProperty firstNameProperty() {
        if (firstName == null) {
            firstName = new SimpleStringProperty();
        }
        return firstName;
    }

    public final String getFirstName() {
        return firstNameProperty().get();
    }

    public final void setFirstName(final java.lang.String firstName) {
        firstNameProperty().set(firstName);
    }

    public final StringProperty lastNameProperty() {
        if (lastName == null) {
            lastName = new SimpleStringProperty();
        }
        return lastName;
    }

    public final java.lang.String getLastName() {
        return lastNameProperty().get();
    }

    public final void setLastName(final java.lang.String lastName) {
        lastNameProperty().set(lastName);
    }

    public final ObjectProperty<LocalDate> birthdayProperty() {
        if (birthday == null) {
            birthday = new SimpleObjectProperty<>();
        }
        return birthday;
    }

    public final LocalDate getBirthday() {
        return birthdayProperty().get();
    }

    public final void setBirthday(final java.time.LocalDate birthday) {
        birthdayProperty().set(birthday);

    }

    public String stringValue() {
        return String.format("%s %s %s", getFirstName(), getLastName(), getBirthday().format(DateTimeFormatter.ISO_LOCAL_DATE));
    }

    public final ObjectBinding<Long> ageBinding() {
        if (age == null) {
            age = new ObjectBinding<Long>() {
                {
                    bind(birthdayProperty());
                }

                @Override
                protected Long computeValue() {
                    if (getBirthday() == null) {
                        return null;
                    }
                    return getBirthday().until(LocalDate.now(), ChronoUnit.YEARS);
                }
            };
        }
        return age;
    }

    public static Callback<PersonBean, Observable&#91;&#93;> extractor() {
        return (PersonBean p) -> new Observable[]{p.lastNameProperty(), p.firstNameProperty(), p.birthdayProperty(), p.ageBinding()};
    }
}

DataModel containing a List of randomly created PersonBeans:

public class DataModel {

    private ObservableList<PersonBean> personFXBeans;

    public DataModel() {
        init();
    }

    private void init() {
        personFXBeans = DataSource.getRandomPersonBeansList(100);
    }

    public ObservableList<PersonBean> getPersonFXBeans() {
        return personFXBeans;
    }
}

As you may know to assign a DataModel e.g. to a TableView or a ListView in JavaFX you just have to use the setItems(ObvervableList) method.

@FXML
public void onFillWithDemoDataFXBeans() {
  readOnlyListView.setItems(model.getPersonFXBeans());
  readOnlyTableView.setItems(model.getPersonFXBeans());
  editableTableView.setItems(model.getPersonFXBeans());
}

Now getting a TableView informed about property changes of contained elements is already done you for by the binding either in two ways:
via a PropertyValueFactory and by more or less direct property binding:

readOnlyFirstNameColumn.setCellValueFactory(new PropertyValueFactory<>("firstName"));
readOnlyLastNameColumn.setCellValueFactory(new PropertyValueFactory<>("lastName"));
readOnlyBirthdayColumn.setCellValueFactory(new PropertyValueFactory<>("birthday"));
readOnlyAgeColumn.setCellValueFactory(i -> i.getValue().ageBinding());

editableFirstNameColumn.setCellValueFactory(i -> i.getValue().firstNameProperty());
editableLastNameColumn.setCellValueFactory(i -> i.getValue().lastNameProperty());
editableBirthdayColumn.setCellValueFactory(i -> i.getValue().birthdayProperty());
ageColumn.setCellValueFactory(i -> i.getValue().ageBinding());

But the ListView basically only observes the list and not the properties of each element of that list.

When using a ObservableList created by FXCollections.observableArrayList() the ListView will only refresh on ListChange Events like remove() an add() of elements. Therefore

list.remove(POJO);
list.add(index,POJO);

after each commited change.

But there is a much better way:
Enable your list to report changes on the element by providing an properties extractor.
You don’t have to care about refreshing then!

ObservableList persons = FXCollections.observableArrayList(PersonBean.extractor());

See DataSource.getRandomPersonBeansList(int length):

public static ObservableList<PersonBean> getRandomPersonBeansList(int length) {
        ObservableList<PersonBean>  persons = FXCollections.observableArrayList(PersonBean.extractor());
        for (int i = 0; i < length; i++) {
            persons.add(new PersonBean(getRandomName(), getRandomLastname(), getRandomLocalDate()));
        }
return persons;
}

This Extrator is basically a Callback containing an array of Obvervables which are then observed by the Obervablelist (more precicely: ObservableListWrapper):

My PersonBean already provides it’s extrator callback:

public static Callback<PersonBean, Observable[]> extractor() {
   return (PersonBean p) -> new Observable[]{p.lastNameProperty(), p.firstNameProperty(), p.birthdayProperty(), p.ageBinding()};
}

Following this pattern all controls are updated instantly after applying the change.

Edit data…
PropertiesExtractor Demo3

and commit:
PropertiesExtractor Demo4

THE CODE PLEASE!
You can find the complete code at my BitBucket Repo

14 thoughts on “Properties Extractor: Best way to get the ListView instantly updating its elements”

  1. Hi Jens,

    Thanks for this useful piece of code. I have modified your code a bit so that the editable table
    is set to false : editableTableView.setEditable(false);
    Then I replaced the line editableFirstNameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
    with the following code so that a double click on a cell in the editable tableview overwrites the first name.

    editableFirstNameColumn.setCellFactory(c -> {
    TableCell cell = new TableCell() {
    @Override
    public void updateItem(String item, boolean empty) {
    super.updateItem(item, empty);
    if (item == null || empty) {
    setText(null);
    setGraphic(null);
    }
    }
    };
    cell.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler() {
    @Override
    public void handle(MouseEvent event) {
    if (event.getClickCount() > 1) {
    PersonBean myPerson = (PersonBean) cell.getTableView().getItems().get(cell.getIndex());
    myPerson.setFirstName(“Got dblclick”);
    }
    }
    });
    return cell;
    });

    This works but only for the readonly tableview and the listview but no update is done in the editable tableview. What did I forget ?
    What should I do so that all 3 views show the text ?

    thanks

    1. Hi,

      I think the issue is to trust that calling super.updateItem(item, empty); will set setText(item) (actually “item” could be more complex than just a String).
      A look at the TableCell class hierarchy shows that protected void updateItem(T item, boolean empty) just sets the value of the ItemProperty but does not call setText();

      So I suggest to add this to your updateItem(); method (and mind to keep it type safe ;-)):


      TableCell cell = new TableCell() {
      @Override
      protected void updateItem(String item, boolean empty) {
      super.updateItem(item, empty);
      setText(item);
      if (item == null || empty) {
      setText(null);
      setGraphic(null);
      }
      }
      };

      Jens

  2. Maybe I’m missing something, but how did you get from the tables responding to changes on the list to it responding to each property on each PersonBean? I see that you have a static method that returns a list of properties, but where did that get hooked into the ObservableList? I cannot find anything about ObservableList supporting a static extractor method.

    1. Have a look at the “DataSource” class. When the ObservableList is created the extractor is set:
      ObservableList persons = FXCollections.observableArrayList(PersonBean.extractor());

      Did you mean that?

      Jens

      1. Ah! Below is indeed the code that brings the two together

        public static ObservableList getRandomPersonFXBeansList(int length) {
        ObservableList persons = FXCollections.observableArrayList(PersonFXBean.extractor());
        for (int i = 0; i < length; i++) {
        persons.add(new PersonFXBean(getRandomName(), getRandomLastname(), getRandomLocalDate()));
        }
        return persons;
        }

        But since this is missing from your blog text, I could not connect the two strings. Very interesting indeed!

          1. I’d copy-in that whole sniplet, because it exactly shows how it is used, without the need to go out to BitBucket, find the appropriate file in the data directory and look up the method. IMHO it will make the blog post complete on its own. 🙂

  3. Hi,
    I know this post is very old but I’m implementing this in my job and I just cannot get it work (im using javafx 8u40) using something like this:

    ObservableList users = FXCollections.observableArrayList(new Callback() { public Observable[] call(UserModel u)
    { return new Observable[] { u.activeProperty() }; }
    });

    Nonetheless I wrote the same code in a static method called extractor in the UserModel (just like you did) and now works perfectly:
    ObservableList users = FXCollections.observableArrayList(UserModel.extractor());
    Thank you for your post, but I really don’t know why it is not working using the other form, did you have a similar problem? or do you have any idea why this would happen? which javafx version did you use?

    Regards from Perú.

    1. Hm, weired.
      Tried it with my demo project and I created the values list this way

      ObservableList persons = FXCollections.observableArrayList(PersonBean.extractor());

      this way

      ObservableList persons = FXCollections.observableArrayList(new Callback<PersonBean, Observable[]>() {
      @Override
      public Observable[] call(PersonBean p) {
      return new Observable[]{
      p.lastNameProperty(),
      p.firstNameProperty(),
      p.birthdayProperty(),
      p.ageBinding()
      };
      }
      });

      and this way

      ObservableList persons = FXCollections.observableArrayList(
      (PersonBean p) -> new Observable[]{
      p.lastNameProperty(),
      p.firstNameProperty(),
      p.birthdayProperty(),
      p.ageBinding()}
      );

      It worked in all three ways no matter which JDK version I used (tried all >u20).

      Have you tried it with a typed Callback? (or did the browser skip the less-than and greater-than signs?)
      like

      new Callback<UserModel,Observable[]>() { public Observable[] call(UserModel u)
      { return new Observable[] { u.activeProperty() }; }
      }

      Regards,
      Jens

  4. Too bad this is still broken:
    If you have elements in the list more than once and remove one, or you sort the list with sort(), the listeners on the property are removed and updates will no longer be propagated.
    This is directly visible when looking at com.sun.javafx.collections.ElementObserver

  5. I see you don’t monetize your site, don’t waste
    your traffic, you can earn extra cash every month because you’ve got high quality content.
    If you want to know how to make extra bucks, search for:
    best adsense alternative Wrastain’s tools

Leave a Reply

Your email address will not be published. Required fields are marked *