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:
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[]> 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…
and commit:
THE CODE PLEASE!
You can find the complete code at my BitBucket Repo.
Reference: | Properties Extractor: Best way to get the ListView instantly updating its elements from our JCG partner Jens Deters at the JavaFX Delight blog. |