Adding a CheckBox column to an existing TableView

Summary: As noted here, this is likely a bug; steps to avoid the pitfall include these:

  • Verify that the data model exports properties correctly, as shown here.
  • Critically examine the value of replacing PropertyValueFactory with an explicit Callback, when possible, as outlined here, here, here, here and here.

The problem is that CheckBoxTableCell can’t find or bind the ObservableProperty<Boolean> based on the parameter supplied:

active.setCellFactory(CheckBoxTableCell.forTableColumn(active));

The CheckBoxTableCell defers to the table column for access to the target Boolean property. To see the effect, replace the active parameter with a Callback that returns the ObservableValue<Boolean> for row i explicitly:

active.setCellFactory(CheckBoxTableCell.forTableColumn(
    (Integer i) -> data.get(i).active));

While this makes the checkboxes work, the underlying problem is that the Person class needs an accessor for the active property. Using JavaFX Properties and Binding discusses the property method naming conventions, and the Person class of the Ensemble8 tablecellfactory illustrates a working model class with a property getter for each attribute, also shown below.

With this change PropertyValueFactory can find the newly added BooleanProperty, and the original form of forTableColumn() works. Note that the convenience of PropertyValueFactory comes with some limitations. In particular, the factory’s fall-through support for the previously missing property accessor goes unnoticed. Fortunately, the same accessor allows substitution of a simple Callback for each column’s value factory. As shown here, instead of PropertyValueFactory,

active.setCellValueFactory(new PropertyValueFactory<>("active"));

Pass a lamda expression that returns the corresponding property:

active.setCellValueFactory(cd -> cd.getValue().activeProperty());

Note also that Person can now be private. Moreover, the use of explicit type parameters affords stronger type checking during compilation.

image

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

/**
 * https://stackoverflow.com/a/68969223/230513
 */
public class TableViewSample extends Application {

    private final TableView<Person> table = new TableView<>();
    private final ObservableList<Person> data
        = FXCollections.observableArrayList(
            new Person("Jacob", "Smith", "[email protected]"),
            new Person("Isabella", "Johnson", "[email protected]"),
            new Person("Ethan", "Williams", "[email protected]"),
            new Person("Emma", "Jones", "[email protected]"),
            new Person("Michael", "Brown", "[email protected]")
        );

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

    @Override
    public void start(Stage stage) {
        stage.setTitle("Table View Sample");
        stage.setWidth(600);
        stage.setHeight(400);

        final Label label = new Label("Address Book");
        label.setFont(new Font("Arial", 20));

        table.setEditable(true);

        TableColumn<Person, Boolean> active = new TableColumn<>("Active");
        active.setCellValueFactory(cd -> cd.getValue().activeProperty());
        active.setCellFactory(CheckBoxTableCell.forTableColumn(active));

        TableColumn<Person, String> firstName = new TableColumn<>("First Name");
        firstName.setCellValueFactory(cd -> cd.getValue().firstNameProperty());

        TableColumn<Person, String> lastName = new TableColumn<>("Last Name");
        lastName.setCellValueFactory(cd -> cd.getValue().lastNameProperty());

        TableColumn<Person, String> email = new TableColumn<>("Email");
        email.setCellValueFactory(cd -> cd.getValue().emailProperty());

        table.setItems(data);
        table.getColumns().addAll(active, firstName, lastName, email);

        final VBox vbox = new VBox();
        vbox.setSpacing(5);
        vbox.setPadding(new Insets(8));
        vbox.getChildren().addAll(label, table);

        stage.setScene(new Scene(vbox));
        stage.show();
    }

    private static class Person {

        private final BooleanProperty active;
        private final StringProperty firstName;
        private final StringProperty lastName;
        private final StringProperty email;

        private Person(String fName, String lName, String email) {
            this.active = new SimpleBooleanProperty(true);
            this.firstName = new SimpleStringProperty(fName);
            this.lastName = new SimpleStringProperty(lName);
            this.email = new SimpleStringProperty(email);
        }

        public BooleanProperty activeProperty() {
            return active;
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public StringProperty lastNameProperty() {
            return lastName;
        }

        public StringProperty emailProperty() {
            return email;
        }
    }
}

Leave a Comment