JDK-8201286 : TreeTableView.TreeTableViewSelectionModel nulls and unseen multi-select
  • Type: Bug
  • Component: javafx
  • Sub-Component: controls
  • Affected Version: 8
  • Priority: P4
  • Status: Closed
  • Resolution: Won't Fix
  • OS: generic
  • CPU: x86_64
  • Submitted: 2018-04-09
  • Updated: 2024-10-30
  • Resolved: 2024-10-30
Related Reports
Relates :  
Description
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 
Comments
This issue is reproducible in 8u172-b11 but not in JDK 9.0.4, 10+46, 11-ea+6. Looks like duplicate of JDK-8088194.
09-04-2018