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
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.
THE CODE PLEASE!
You can find the complete code at my BitBucket Repo
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
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 thatprotected 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
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(item);
if (item == null || empty) {
setText(null);
setGraphic(null);
}
}
};
Jens
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.
Have a look at the “DataSource” class. When the ObservableList is created the extractor is set: persons = FXCollections.observableArrayList(PersonBean.extractor());
ObservableList
Did you mean that?
Jens
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!
Well it actually is on the post (below “But there is a much better way:”).
But I will add a hint which class contains the code 😉
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. 🙂
done 😉
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ú.
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
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
THANK YOU
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
Hi,
Thank you for this posting! I downloaded and ran you sample app and I’m intrigue! I was looking through some JavaFX 8+ books and the web for this issue and none of the ones I’ve seen really explained this issue like you did!
I ran into this problem and I was looking for alternatives to using TableView’s refresh method. I can get my application to work when using TableView’s refresh method but I wanted to hear your opinion on what you thought about using this method instead of doing what you did?
Any thoughts?
Thanks again
Hi I download the code files. But it says “the j2se platform is not correctly set up”, what is the version you used ? Thank you