ListView is very eager to clear the entire selection when it receives a ListChangeListener replace change. Whereas permutations and additions/removals take care to preserve as much as possible of the selection, replacing a list (full or in part) with similar items (or even the same items) clears the selection, or exhibits other incorrect behavior.
Furthermore, this behavior depends on how the list was set. If one replaces the entire list (with ListView.setItems()) the behavior differs from replacing all elements (with ListView.getItems().setAll()). This is due to some state being tracked that is not set correctly when the entire list is replaced.
Finally, the "replace" logic can be fooled by doing individual `set` calls on items. If an "Orange" is selected and I replace the item at that exact position with "Kiwifruit", it remains selected.
Here are failing test cases:
@Test
public void whenNewListInstalledAndAllIemsAreReplacedShouldClearSelection() {
listView.setItems(FXCollections.observableArrayList("Apple", "Orange", "Banana"));
listView.getSelectionModel().select(1);
assertEquals(1, listView.getSelectionModel().getSelectedIndex());
assertEquals("Orange", listView.getSelectionModel().getSelectedItem());
listView.getItems().setAll("Kiwifruit", "Pineapple", "Grape");
assertEquals(-1, listView.getSelectionModel().getSelectedIndex());
assertNull(listView.getSelectionModel().getSelectedItem());
}
Fails because of how the list was created.
@Test
public void whenOnlyTheSelectedItemIsReplacedShouldClearItsSelection() {
listView.getItems().setAll("Apple", "Orange", "Banana");
listView.getSelectionModel().select(1);
assertEquals(1, listView.getSelectionModel().getSelectedIndex());
assertEquals("Orange", listView.getSelectionModel().getSelectedItem());
listView.getItems().set(1, "Kiwifruit");
assertEquals(-1, listView.getSelectionModel().getSelectedIndex());
assertNull(listView.getSelectionModel().getSelectedItem());
}
Fails, the 2nd item remains selected.
@Test
public void whenASelectedItemIsReplacedShouldPreserveRestOfSelection() {
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
listView.getItems().setAll("Apple", "Orange", "Banana");
listView.getSelectionModel().selectIndices(1, 2);
assertEquals(List.of(1, 2), listView.getSelectionModel().getSelectedIndices());
assertEquals(List.of("Orange", "Banana"), listView.getSelectionModel().getSelectedItems());
listView.getItems().set(1, "Kiwifruit");
assertEquals(List.of(2), listView.getSelectionModel().getSelectedIndices());
assertEquals(List.of("Banana"), listView.getSelectionModel().getSelectedItems());
}
Fails, both items remain selected.
@Test
public void replaceWithSameItemsShouldNotClearSelection() {
listView.getItems().setAll("Apple", "Orange", "Banana");
listView.getSelectionModel().select(1);
assertEquals(1, listView.getSelectionModel().getSelectedIndex());
assertEquals("Orange", listView.getSelectionModel().getSelectedItem());
listView.getItems().setAll("Apple", "Orange", "Banana");
assertEquals(1, listView.getSelectionModel().getSelectedIndex());
assertEquals("Orange", listView.getSelectionModel().getSelectedItem());
}
Fails, selection is cleared.
@Test
public void replaceRangeShouldNotAffectSelectedItemsOutsideOfRange() {
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
listView.getItems().setAll("A", "B", "C", "D", "E", "F", "G", "H");
listView.getSelectionModel().selectIndices(0, 2, 4, 6);
assertEquals(List.of(0, 2, 4, 6), listView.getSelectionModel().getSelectedIndices());
assertEquals(List.of("A", "C", "E", "G"), listView.getSelectionModel().getSelectedItems());
listView.getItems().replaceRange(2, 6, List.of("D", "C", "M", "N"));
assertEquals(List.of(0, 3, 6), listView.getSelectionModel().getSelectedIndices());
assertEquals(List.of("A", "C", "G"), listView.getSelectionModel().getSelectedItems());
}
Fails, selection is unchanged.
Note that the permutation logic also contains problems (it assumes the whole list is permutated, while it could be a range). JavaFX provides no code that permutates a range, but 3rd parties can...
Furthermore, this behavior depends on how the list was set. If one replaces the entire list (with ListView.setItems()) the behavior differs from replacing all elements (with ListView.getItems().setAll()). This is due to some state being tracked that is not set correctly when the entire list is replaced.
Finally, the "replace" logic can be fooled by doing individual `set` calls on items. If an "Orange" is selected and I replace the item at that exact position with "Kiwifruit", it remains selected.
Here are failing test cases:
@Test
public void whenNewListInstalledAndAllIemsAreReplacedShouldClearSelection() {
listView.setItems(FXCollections.observableArrayList("Apple", "Orange", "Banana"));
listView.getSelectionModel().select(1);
assertEquals(1, listView.getSelectionModel().getSelectedIndex());
assertEquals("Orange", listView.getSelectionModel().getSelectedItem());
listView.getItems().setAll("Kiwifruit", "Pineapple", "Grape");
assertEquals(-1, listView.getSelectionModel().getSelectedIndex());
assertNull(listView.getSelectionModel().getSelectedItem());
}
Fails because of how the list was created.
@Test
public void whenOnlyTheSelectedItemIsReplacedShouldClearItsSelection() {
listView.getItems().setAll("Apple", "Orange", "Banana");
listView.getSelectionModel().select(1);
assertEquals(1, listView.getSelectionModel().getSelectedIndex());
assertEquals("Orange", listView.getSelectionModel().getSelectedItem());
listView.getItems().set(1, "Kiwifruit");
assertEquals(-1, listView.getSelectionModel().getSelectedIndex());
assertNull(listView.getSelectionModel().getSelectedItem());
}
Fails, the 2nd item remains selected.
@Test
public void whenASelectedItemIsReplacedShouldPreserveRestOfSelection() {
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
listView.getItems().setAll("Apple", "Orange", "Banana");
listView.getSelectionModel().selectIndices(1, 2);
assertEquals(List.of(1, 2), listView.getSelectionModel().getSelectedIndices());
assertEquals(List.of("Orange", "Banana"), listView.getSelectionModel().getSelectedItems());
listView.getItems().set(1, "Kiwifruit");
assertEquals(List.of(2), listView.getSelectionModel().getSelectedIndices());
assertEquals(List.of("Banana"), listView.getSelectionModel().getSelectedItems());
}
Fails, both items remain selected.
@Test
public void replaceWithSameItemsShouldNotClearSelection() {
listView.getItems().setAll("Apple", "Orange", "Banana");
listView.getSelectionModel().select(1);
assertEquals(1, listView.getSelectionModel().getSelectedIndex());
assertEquals("Orange", listView.getSelectionModel().getSelectedItem());
listView.getItems().setAll("Apple", "Orange", "Banana");
assertEquals(1, listView.getSelectionModel().getSelectedIndex());
assertEquals("Orange", listView.getSelectionModel().getSelectedItem());
}
Fails, selection is cleared.
@Test
public void replaceRangeShouldNotAffectSelectedItemsOutsideOfRange() {
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
listView.getItems().setAll("A", "B", "C", "D", "E", "F", "G", "H");
listView.getSelectionModel().selectIndices(0, 2, 4, 6);
assertEquals(List.of(0, 2, 4, 6), listView.getSelectionModel().getSelectedIndices());
assertEquals(List.of("A", "C", "E", "G"), listView.getSelectionModel().getSelectedItems());
listView.getItems().replaceRange(2, 6, List.of("D", "C", "M", "N"));
assertEquals(List.of(0, 3, 6), listView.getSelectionModel().getSelectedIndices());
assertEquals(List.of("A", "C", "G"), listView.getSelectionModel().getSelectedItems());
}
Fails, selection is unchanged.
Note that the permutation logic also contains problems (it assumes the whole list is permutated, while it could be a range). JavaFX provides no code that permutates a range, but 3rd parties can...