Reputation: 13
I'm trying to set up a checkbox tree in JavaFX where the leaves are bound to their parents:
I did it using a binding with the selectedProperty
.
It works just fine, but it throws an exception each time I select or deselect the parent.
Is there an easy way around this? I hate to abandon an approach that works except for those nasty exceptions.
The relevant code is below. Please forgive any java convention errors - I'm still new to the language.
Controller:
package application;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.CheckBoxTreeCell;
public class BindingTestController {
@FXML
private TreeView<String> MainTree;
static List<CheckBoxTreeItem<String>> treeItems = new ArrayList<CheckBoxTreeItem<String>>();
CheckBoxTreeItem<String> root = new CheckBoxTreeItem<String>("Root");
CheckBoxTreeItem<String> parent1 = new CheckBoxTreeItem<String>("Parent1");
CheckBoxTreeItem<String> parent2 = new CheckBoxTreeItem<String>("Parent2");
private void AddColumns(CheckBoxTreeItem<String> item){
for (int i = 1 ; i < 5 ; i++){
CheckBoxTreeItem<String> itemColumn = new CheckBoxTreeItem<String>(item.getValue()+"_Column_"+i);
treeItems.add(itemColumn);
item.getChildren().add(itemColumn);
itemColumn.selectedProperty().bind(item.selectedProperty());
}
}
@FXML // This method is called by the FXMLLoader when initialization is complete
private void initialize() {
treeItems.add(root);
treeItems.add(parent1);
root.getChildren().add(parent1);
treeItems.add(parent2);
root.getChildren().add(parent2);
AddColumns(parent1);
AddColumns(parent2);
MainTree.setRoot(root);
MainTree.setShowRoot(true);
root.setExpanded(true);
MainTree.setCellFactory(p-> new CheckBoxTreeCell());
MainTree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
MainTree.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() {
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
CheckBoxTreeItem<String> treeItem = (CheckBoxTreeItem)newValue;
}
});
}
}
Here is the FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TreeView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.BindingTestController">
<center>
<VBox prefHeight="1049.0" prefWidth="714.0"BorderPane.alignment="CENTER">
<children>
<TreeView fx:id="MainTree" prefHeight="624.0" prefWidth="714.0"/>
</children>
</VBox>
</center>
</BorderPane>
and here is the main:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
try {
// BorderPane root = new BorderPane();
BorderPane root = (BorderPane) FXMLLoader.load(Main.class.getResource("BindingTest.fxml"));
Scene scene = new Scene(root,400,400);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Any advice would be appreciated.
Upvotes: 1
Views: 1470
Reputation: 21799
The problem is coming from this line
itemColumn.selectedProperty().bind(item.selectedProperty());
To understand why do you get an exception whatever node you select, you should take a look on the independentProperty of CheckBoxTreeItem
:
A CheckBoxTreeItem can be independent or dependent. By default, CheckBoxTreeItem instances are dependent, which means that any changes to the selection state of a TreeItem will have an impact on parent and children CheckBoxTreeItem instances. If a CheckBoxTreeItem is set to be independent, this means that any changes to that CheckBoxTreeItem will not directly impact the state of parent and children CheckBoxTreeItem instances.
This means, when you select/deselect a node in the tree, which has children, it will select/deselect all of it's children by default (and also sets the checked, unchecked, indeterminate state of its parent, but this is not important in your case). This is one of the functionality what you need and you get it out of the box.
Now you can understand why your binding is problematic:
You bind the selectedProperty()
of the child nodes to the selectedProperty()
of the parent node, then:
if you check one of the leaf nodes, you will get an exception, because the value is bound, and bound values cannot be set.
if you check one of the parent nodes, it will try to select all of its children (because of being dependent), then you will get an exception again.
Solution
You have two requirements:
select all of the children nodes, when the parent is selected, which is already done
do not allow to select child nodes manually
For the second requirement, you can disable the checkbox of these items. For this you can use the setCellFactory method of your TreeView
:
MainTree.setCellFactory((item) -> {
return new CheckBoxTreeCell<String>(){
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(item != null) {
this.disableProperty().unbind();
CheckBoxTreeItem<String> value = (CheckBoxTreeItem<String>) treeItemProperty().getValue();
this.disableProperty().bind(value.leafProperty());
}
}
};
});
This snippet will bind the disableProperty
of the displayed CheckBoxTreeCell
objects to the leafProperty of the corresponding CheckBoxTreeItem
.
public final ReadOnlyBooleanProperty leafProperty()
Represents the TreeItem leaf property, which is true if the TreeItem has no children.
Which results in a tree where all the nodes that has no children are disabled.
To make this working you should remove this line from your code:
itemColumn.selectedProperty().bind(item.selectedProperty());
and you should replace this line
MainTree.setCellFactory(p-> new CheckBoxTreeCell());
with the code posted above.
Upvotes: 2