It's a left-over from some old (fixed) issues: if we need to listen to changes in list-valued properties, we need to register an InvalidationListener (_not_ a ChangeListener) to reliably get notified on changes to the property: the property doesn't fire to ChangeListeners on replacing the value with an equal but not-same list.
ListCell still uses a ChangeListener to the itemsProperty, so it's listening to the wrong list on replacing that list with an equal but not same list. Simply look at the code ;-)
Anyway, example below demonstrates the issue, run and:
- click replace-items
- click on update-previous
- expected: no call to updateItem
- actual: call to updateItem
on the bright side, changing the current items updates the cell as expected:
- click on update-current
- expected and actual: updateItem called and visuals updated
This might indicate that the items listener on the cell isn't needed at all (seems to be a singularity in the cell zoo, anyway) - any update now seems to be triggered reliably by listSkin.
public class ListCellListeningToOldItems extends Application {
public static void main(String[] args) {
launch(args);
}
int idCount;
@Override
public void start(Stage stage) {
ObservableList<String> items = createItems(null);
ListView<String> listView = new ListView<>(items);
listView.setCellFactory(lv -> {
System.out.println("created");
ListCell<String> cell = new ListCell<String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(item);
if (!empty) {
System.out.println("item updated to '" + item + "'" + "for " + getId());
// new RuntimeException("who's calling? \n").printStackTrace();
}
}
};
cell.setId("cell-id: " + idCount++);
return cell;
});
Button reset = new Button("Replace items to equal list");
reset.setOnAction(e -> {
previous = listView.getItems();
listView.setItems(createItems(listView));
});
Button updateOld = new Button("update previous list at 0");
updateOld.setOnAction(e -> {
if (previous == null || previous == listView.getItems()) {
System.out.println("same items");
return;
}
previous.set(0, "changed old");
});
Button updateCurrent = new Button("Update current list at 0");
updateCurrent.setOnAction(e -> {
Object old = listView.getItems().get(0);
listView.getItems().set(0, old + "X");
});
Parent content = new VBox(listView, reset, updateOld, updateCurrent);
stage.setScene(new Scene(content, 200, 180));
stage.setTitle(System.getProperty("java.version"));
stage.show();
}
int count;
private ObservableList<String> previous;
/**
* Creates and returns a list the is equal to but not the same as
* the current items of the list.
*/
protected ObservableList<String> createItems(ListView lv) {
ObservableList<String> items = lv != null ? FXCollections.observableArrayList(lv.getItems())
: FXCollections.observableArrayList("Foo");
return items;
}
}
ListCell still uses a ChangeListener to the itemsProperty, so it's listening to the wrong list on replacing that list with an equal but not same list. Simply look at the code ;-)
Anyway, example below demonstrates the issue, run and:
- click replace-items
- click on update-previous
- expected: no call to updateItem
- actual: call to updateItem
on the bright side, changing the current items updates the cell as expected:
- click on update-current
- expected and actual: updateItem called and visuals updated
This might indicate that the items listener on the cell isn't needed at all (seems to be a singularity in the cell zoo, anyway) - any update now seems to be triggered reliably by listSkin.
public class ListCellListeningToOldItems extends Application {
public static void main(String[] args) {
launch(args);
}
int idCount;
@Override
public void start(Stage stage) {
ObservableList<String> items = createItems(null);
ListView<String> listView = new ListView<>(items);
listView.setCellFactory(lv -> {
System.out.println("created");
ListCell<String> cell = new ListCell<String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(item);
if (!empty) {
System.out.println("item updated to '" + item + "'" + "for " + getId());
// new RuntimeException("who's calling? \n").printStackTrace();
}
}
};
cell.setId("cell-id: " + idCount++);
return cell;
});
Button reset = new Button("Replace items to equal list");
reset.setOnAction(e -> {
previous = listView.getItems();
listView.setItems(createItems(listView));
});
Button updateOld = new Button("update previous list at 0");
updateOld.setOnAction(e -> {
if (previous == null || previous == listView.getItems()) {
System.out.println("same items");
return;
}
previous.set(0, "changed old");
});
Button updateCurrent = new Button("Update current list at 0");
updateCurrent.setOnAction(e -> {
Object old = listView.getItems().get(0);
listView.getItems().set(0, old + "X");
});
Parent content = new VBox(listView, reset, updateOld, updateCurrent);
stage.setScene(new Scene(content, 200, 180));
stage.setTitle(System.getProperty("java.version"));
stage.show();
}
int count;
private ObservableList<String> previous;
/**
* Creates and returns a list the is equal to but not the same as
* the current items of the list.
*/
protected ObservableList<String> createItems(ListView lv) {
ObservableList<String> items = lv != null ? FXCollections.observableArrayList(lv.getItems())
: FXCollections.observableArrayList("Foo");
return items;
}
}