5

I make a simple JavaFX application. In this application there is a treetable with 2 columns and a check box. If check box is selected column 2 will be visible, otherwise not visible. To do this I bound tree table column visible property to checkbox selected property. When I click the check box column state change but at the same time gives.

Caused by: java.lang.RuntimeException: TreeTableColumn.visible : A bound value cannot be set.

If I use bidirectional binding I don't get this error. But I don't need bidirectional binding. Is it a bug or I don't use bind correctly? Please use below code to reproduce this error. I use jdk1.8.0_111.

JavaFXApplication.java

package test;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class JavaFXApplication extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

FXMLDocumentController.java

package test;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;

public class FXMLDocumentController implements Initializable {

    @FXML
    private TreeTableView<?> table;
    @FXML
    private CheckBox box;
    @FXML
    private TreeTableColumn<?, ?> c2;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // TODO
        c2.visibleProperty().bind(box.selectedProperty());
    }    

}

FXMLDocument.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.TreeTableColumn?>
<?import javafx.scene.control.TreeTableView?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane id="AnchorPane" prefHeight="390.0" prefWidth="452.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.65" fx:controller="javafxapplication2.FXMLDocumentController">
    <children>
      <TreeTableView fx:id="table" layoutX="2.0" prefHeight="390.0" prefWidth="149.0">
        <columns>
          <TreeTableColumn prefWidth="75.0" text="C1" />
          <TreeTableColumn fx:id="c2" prefWidth="75.0" text="C2" visible="false" />
        </columns>
      </TreeTableView>
      <CheckBox fx:id="box" layoutX="234.0" layoutY="77.0" mnemonicParsing="false" text="CheckBox" />
    </children>
</AnchorPane>
sancho
  • 598
  • 2
  • 8
  • 22
  • What happens if you remove the `visible="false"` attribute from the FXML? I would have assumed FXML properties are set before calling the `initialize` method, but perhaps it's the other way around... – Itai Nov 16 '16 at 06:25
  • @sillyfly I also think this and remove from fxml file but result the same – sancho Nov 16 '16 at 06:39
  • 1
    `TableView` and `TreeTableView` support a drop-down menu where you can choose which columns are visible. Probably supporting this, even if you don't use it, causes a call to `setVisible` on the columns somewhere (and hence the error). I would just replace the binding with a listener on the check box selection state, even though the binding is a bit more elegant. – James_D Nov 16 '16 at 14:23
  • @James_D yes you are write I got this error because of column visible property already bound implicitly to menu button even if you do not use this button. – sancho Nov 16 '16 at 14:31
  • so we can not use bind method? – sancho Nov 16 '16 at 14:55
  • @sakit It appears not. Just use a listener instead. – James_D Nov 16 '16 at 18:17
  • ok. Thanks @James_D – sancho Nov 16 '16 at 18:18
  • 1
    Does a bugreport exist for this already? I think that this behaviour should not exists. Bindings are there to be used and not throw errors where none should be imo. – geisterfurz007 Jul 20 '17 at 14:30
  • @geisterfurz007 I don't know – sancho Jul 20 '17 at 14:56
  • I got the same problem, but only with Java 8, and could not reproduce the problem with Java 11. It seems there is a bug report for this problem that was fixed in Java 9. https://bugs.openjdk.java.net/browse/JDK-8136468. – DJViking Nov 14 '19 at 09:26

1 Answers1

0

I think this actually is not a bug. It's definitely mysterious and unexpected behavior though. Part of the problem is that the binding conflict is coming from TableHeaderRow, which is created by the skin at display time.

The TableHeaderRow is responsible for rendering all the column headers, and it includes this built-in button for a menu that, by default, is a radio selection list of columns to show/hide:

TableView with column header button

As a result, the TableHeaderRow creates a bidirectional binding between these menu entries' selection state and each column's visible property.

It actually is possible to undo this binding but I found it annoying since the TableHeaderRow is null until the TableView is displayed. But, adapting this solution to access the TableHeaderRow, we can do something like:

      TableHeaderRow headerRow = ((TableViewSkinBase) tableView.getSkin()).getTableHeaderRow();
      try {

        // get columnPopupMenu field
        Field privateContextMenuField = TableHeaderRow.class.getDeclaredField("columnPopupMenu");

        // make field public
        privateContextMenuField.setAccessible(true);

        // get context menu
        ContextMenu contextMenu = (ContextMenu) privateContextMenuField.get(headerRow);

        for (MenuItem menuItem : contextMenu.getItems()) {
          // Assuming these will be CheckMenuItems in the default implementation
          BooleanProperty selectedProperty = ((CheckMenuItem) menuItem).selectedProperty();

          // In theory these menu items are in parallel with the columns, but I just brute forced it to test
          for (TableColumn<?, ?> tableColumn : tableView.getColumns()) {
            // Unlink the column's visibility with the menu item
            tableColumn.visibleProperty().unbindBidirectional(selectedProperty);
          }
        }

      } catch (Exception ex) {
        ex.printStackTrace();
      }

      // Not strictly necessary but we don't want to be misleading :-p
      tableView.setTableMenuButtonVisible(false);

But here's the kicker: you have to do this every time a column's visibility changes, because the TableHeaderRow has a listener that rebuilds the menu, and re-links the properties, every time a column is shown.

You could, of course, find a way to disable that listener.. but clearly this is already ridiculous and probably unnecessary.

Anyway, as you noted:

If I use bidirectional binding I don't get this error. But I don't need bidirectional binding.

I think the second statement isn't technically true: your use case doesn't require bidirectional binding, but it is actually appropriate here since there are other properties already linked with a column's visibility.

So, I would use bidirectional binding.

hineroptera
  • 769
  • 3
  • 10