Collapsing / expanding TreeView randomly checks CheckBoxes

653 views Asked by At

I'm having a TreeView with a few nodes having CheckBoxes (see MWE).

When collapsing / expanding some node, the CheckBoxes of other nodes are checked or unchecked.

To reproduce the behaviour, just expand all the nodes, check ChildA, collapse Block1 and ChildC will be automatically checked.

package treeviewexample;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.CheckBoxTreeCell;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;


public class TreeViewExample extends Application {

  @Override
  public void start(Stage primaryStage) {

    StackPane root = new StackPane();

    /* example Treeview */
    TreeView tw = new TreeView();
    TreeItem rootNode = new TreeItem("Root");
    TreeItem blockOne = new TreeItem("Block1");
    TreeItem childA = new TreeItem("ChildA");
    TreeItem childB = new TreeItem("ChildB");
    blockOne.getChildren().add(childA);
    blockOne.getChildren().add(childB);
    TreeItem blockTwo = new TreeItem("Block2");
    TreeItem childC = new TreeItem("ChildC");
    TreeItem childD = new TreeItem("ChildD");
    blockTwo.getChildren().add(childC);
    blockTwo.getChildren().add(childD);
    rootNode.getChildren().add(blockOne);
    rootNode.getChildren().add(blockTwo);
    tw.setRoot(rootNode);

    /* add CheckBoxes */
    tw.setCellFactory(CheckBoxTreeCell.<String>forTreeView());

    root.getChildren().add(tw);
    Scene scene = new Scene(root, 300, 250);
    primaryStage.setTitle("Hello World!");
    primaryStage.setScene(scene);
    primaryStage.show();
  }

  public static void main(String[] args) {
    launch(args);
  }

}

How can I prevent this behaviour? At a later point in my program, I want to go through the TreeView and get the status (checked or not) of the nodes to work with them.

From https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/TreeCell.html I know the following:

Due to the fact that TreeCell extends from IndexedCell, each TreeCell also provides an index property. The index will be updated as cells are expanded and collapsed, and therefore should be considered a view index rather than a model index.

So is this intended behaviour? Why would anyone want to have that?

1

There are 1 answers

0
James_D On BEST ANSWER

The behavior you are seeing is nothing to do with the index of the cell, but solely because you haven't provided any mechanism for the CheckBoxTreeCell to "know" whether or not it is supposed to be checked. Consequently, when you expand or collapse nodes in the tree, and the cells are reused for other items, they will likely maintain their checked status even though they are now supposed to be representing new data.

The basic issue here is that the CheckBoxTreeCell is just the view: it does not maintain the selected state. You need to provide a mechanism for the cell to know whether the item it represents is selected or not. The API provides two ways to do this: either use a CheckBoxTreeItem as the items in the tree, or use a model class that has a BooleanProperty and provide a mapping to that boolean property.

The first version looks like this (I also got rid of all your raw types: you really should not post code here that generates warnings and just ignore them):

public void start(Stage primaryStage) {

    StackPane root = new StackPane();

    /* example Treeview */
    TreeView<String> tw = new TreeView<>();
    CheckBoxTreeItem<String> rootNode = new CheckBoxTreeItem<>("Root");
    CheckBoxTreeItem<String> blockOne = new CheckBoxTreeItem<>("Block1");
    CheckBoxTreeItem<String> childA = new CheckBoxTreeItem<>("ChildA");
    CheckBoxTreeItem<String> childB = new CheckBoxTreeItem<>("ChildB");
    blockOne.getChildren().add(childA);
    blockOne.getChildren().add(childB);
    CheckBoxTreeItem<String> blockTwo = new CheckBoxTreeItem<>("Block2");
    CheckBoxTreeItem<String> childC = new CheckBoxTreeItem<>("ChildC");
    CheckBoxTreeItem<String> childD = new CheckBoxTreeItem<>("ChildD");
    blockTwo.getChildren().add(childC);
    blockTwo.getChildren().add(childD);
    rootNode.getChildren().add(blockOne);
    rootNode.getChildren().add(blockTwo);
    tw.setRoot(rootNode);

    /* add CheckBoxes */
    tw.setCellFactory(CheckBoxTreeCell.<String>forTreeView());

    root.getChildren().add(tw);
    Scene scene = new Scene(root, 300, 250);
    primaryStage.setTitle("Hello World!");
    primaryStage.setScene(scene);
    primaryStage.show();
} 

The second option looks like:

@Override
public void start(Stage primaryStage) {

    StackPane root = new StackPane();

    /* example Treeview */
    TreeView<Item> tw = new TreeView<>();
    CheckBoxTreeItem<Item> rootNode = new CheckBoxTreeItem<>(new Item("Root"));
    CheckBoxTreeItem<Item> blockOne = new CheckBoxTreeItem<>(new Item("Block1"));
    CheckBoxTreeItem<Item> childA = new CheckBoxTreeItem<>(new Item("ChildA"));
    CheckBoxTreeItem<Item> childB = new CheckBoxTreeItem<>(new Item("ChildB"));
    blockOne.getChildren().add(childA);
    blockOne.getChildren().add(childB);
    CheckBoxTreeItem<Item> blockTwo = new CheckBoxTreeItem<>(new Item("Block2"));
    CheckBoxTreeItem<Item> childC = new CheckBoxTreeItem<>(new Item("ChildC"));
    CheckBoxTreeItem<Item> childD = new CheckBoxTreeItem<>(new Item("ChildD"));
    blockTwo.getChildren().add(childC);
    blockTwo.getChildren().add(childD);
    rootNode.getChildren().add(blockOne);
    rootNode.getChildren().add(blockTwo);
    tw.setRoot(rootNode);

    StringConverter<TreeItem<Item>> itemStringConverter = new StringConverter<TreeItem<Item>>() {

        @Override
        public String toString(TreeItem<Item> item) {
            return item.getValue().getName();
        }

        @Override
        public TreeItem<Item> fromString(String string) {
            return new TreeItem<>(new Item(string));
        }

    };

    /* add CheckBoxes */
    tw.setCellFactory(
            CheckBoxTreeCell.forTreeView(treeItem -> treeItem.getValue().selectedProperty(), itemStringConverter));

    root.getChildren().add(tw);
    Scene scene = new Scene(root, 300, 250);
    primaryStage.setTitle("Hello World!");
    primaryStage.setScene(scene);
    primaryStage.show();
}

public static class Item {

    private final String name;
    // use something with a more meaningful name here:
    private final BooleanProperty selected = new SimpleBooleanProperty();

    public Item(String name, boolean selected) {
        this.name = name;
        setSelected(selected);
    }

    public Item(String name) {
        this(name, false);
    }

    public String getName() {
        return name;
    }

    public BooleanProperty selectedProperty() {
        return selected;
    }

    public final boolean isSelected() {
        return selectedProperty().get();
    }

    public final void setSelected(boolean selected) {
        selectedProperty().set(selected);
    }
}