FULL PRODUCT VERSION :
Java SE 1.8.0_162-b12
ADDITIONAL OS VERSION INFORMATION :
Duplicated on both with same jdk:
Windows 10.0.1.14393
RHEL 3.10.0-693.17.1.el7.x86_64
A DESCRIPTION OF THE PROBLEM :
TreeTableView.TreeTableViewSelectionModel gets out of date while deleting objects.
Using the given test case, the SelectionModel can be caused to contain null selected items, throw IndexOutOfBoundsExceptions, grow the selection during deletes, and have selections that are not highlighted / cannot be removed without reselecting the miscellaneous additions.
There are multiple test cases/unexpected behaviors included in this single report because it is likely they are all related.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Create the three files below in the default package (SelectionModelTestCase.java, SelectionModelTestCase.fxml, and SelectionModelTestCaseController.java)
Using the SelectionModelTestCase, each test is written as though the test was just started but can be run back-to-back.
If more tree items are needed for the test, use the add button to populate more
1 - Select "child 6" and press "Delete".
There will be two items selected after the removal.
Pressing "Delete" again will result in a third selection.
2 - Select the last item in the list and press "Delete".
There will now be a null object in the selection list.
3 - Select the last item in the list and press "Delete" to put a null object in the selection list.
Sort the "name" column to produce an IndexOutOfBoundsException
4 - Select the last item in the list and press "Delete" two or more times.
After the first deletion, each delete event no longer sends a selection event.
This can be verified by adding a print() to the delete() function: duplicate prints should be made for each delete,
but is reduced to one until a new selection is made.
5 - Select the last item in the list and press "Delete" two times, and press on an item not listed in the print-out.
More than one item will be selected, but the second will not be highlighted. This extra selection will stay until
it is directly selected (null items occupying the extra selection spot will not disappear)
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Using the same code with TableView instead of TreeTableView produces the expected results
ACTUAL -
Actual results are recorded in the "Steps to Reproduce"
ERROR MESSAGES/STACK TRACES THAT OCCUR :
Crashes do not occur, and only one of the above cases results in an IndexOutOfBoundsException while there are nulls / -1 indices in the selection
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
SelectionModelTestCase.java:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
public class SelectionModelTestCase extends Application
{
public static void main(String[] args)
{
launch(args);
}
@Override
public void start(Stage primaryStage) throws IOException
{
FXMLLoader loader = new FXMLLoader(SelectionModelTestCase.class.getResource("SelectionModelTestCase.fxml"));
BorderPane borderPane = loader.load();
Scene scene = new Scene(borderPane);
primaryStage.setScene(scene);
primaryStage.show();
primaryStage.setTitle("Selection Model");
}
}
SelectionModelTestCase.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="SelectionModelTestCaseController"
prefHeight="400.0" prefWidth="600.0">
<top>
<HBox spacing="5">
<Button text="Add" onAction="#add"/>
<Button text="Delete" onAction="#delete"/>
</HBox>
</top>
<center>
<TreeTableView fx:id="treeTable">
<columns>
<TreeTableColumn fx:id="nameColumn" text="Object Name" prefWidth="300"/>
</columns>
</TreeTableView>
</center>
</BorderPane>
SelectionModelTestCaseController.java:
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.fxml.FXML;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class SelectionModelTestCaseController
{
private static final String NULL_ITEM = "null item";
private static final String NULL_DATA = "null data";
@FXML
private TreeTableView<TestData> treeTable;
@FXML
private TreeTableColumn<TestData, String> nameColumn;
private int childNumber = 1; // the number of the added child
private TreeItem<TestData> root; // the tree root: for adding and removing items
private InvalidationListener printSelection = observable -> print(); // prints the current selection
public void initialize()
{
root = new TreeItem<>(new TestData("root"));
// add the root node and save for later
treeTable.setRoot(root);
treeTable.setShowRoot(false);
treeTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
// add printing for the current selection
treeTable.getSelectionModel().getSelectedItems().addListener(new WeakInvalidationListener(printSelection));
// set cell factory, otherwise won't render
nameColumn.setCellFactory(column -> new CustomTreeCell());
// add some starting children
for (int i = 0; i < 10; i++)
{
add();
}
}
@FXML
private void delete()
{
// remove objects from the tree and redraw. New ArrayList does not change behavior, but seems safer
root.getChildren().removeAll(new ArrayList<>(treeTable.getSelectionModel().getSelectedItems()));
}
@FXML
private void add()
{
// just adds children with incrementing numbers
root.getChildren().add(new TreeItem<>(new TestData("child " + childNumber++)));
}
private void print()
{
System.out.println(treeTable.getSelectionModel().getSelectedItems().stream()
.map(this::getStringForItem) // map to string
.collect(Collectors.joining(", "))); // to be joined
}
// Gets a string to represent each tree item. Allows for null items and data.
private String getStringForItem(TreeItem<TestData> treeItem)
{
if(treeItem == null)
{
return NULL_ITEM;
}
TestData data = treeItem.getValue();
return data == null ? NULL_DATA : data.getName();
}
// for use in this test case
private static class CustomTreeCell extends TreeTableCell<TestData, String>
{
@Override
protected void updateItem(String item, boolean empty)
{
super.updateItem(item, empty);
TestData data = getTreeTableRow().getItem();
if (empty || data == null)
{
setText("");
}
else
{
setText(data.getName());
}
}
}
// for use in this test case
private static class TestData
{
private String name;
private TestData(String name)
{
this.name = name;
}
private String getName()
{
return name;
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Filter nulls