javafxtableview

JavaFX Button disappears from Table View


I have a table view populated with persons. Only one person (Jane) gets a button in column "Actions".

When double clicking on the column header to trigger the column auto fit the button disappears.

Why is this happening and how can I avoid it? The code is a simplified scenario of my app where I have a more complex control than a single button so I would like to keep getting the control from the class instances that are populating the table view.

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Test extends Application {

private final ObservableList<Person> data = FXCollections.observableArrayList(
        new Person("John", "Doe"),
        new Person("Jane", "Smith"),
        new Person("Bill", "Jones")
);

@Override
public void start(Stage primaryStage) {
    TableView<Person> tableView = new TableView<>();
    tableView.setItems(data);

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

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

    TableColumn<Person, Void> buttonCol = new TableColumn<>("Actions");
    buttonCol.setCellFactory(param -> new ButtonCell());

    tableView.getColumns().addAll(firstNameCol, lastNameCol, buttonCol);

    VBox vbox = new VBox(tableView);

    Scene scene = new Scene(vbox);
    primaryStage.setScene(scene);
    primaryStage.show();
}

public static class Person {
    private final SimpleStringProperty firstName;
    private final SimpleStringProperty lastName;

    public Person(String firstName, String lastName) {
        this.firstName = new SimpleStringProperty(firstName);
        this.lastName = new SimpleStringProperty(lastName);

        button = new Button();

        button.setOnAction(event -> {
            System.out.println("My name is : " + this.firstName.get());
        });
    }

    public String getFirstName() {
        return firstName.get();
    }

    public SimpleStringProperty firstNameProperty() {
        return firstName;
    }

    public String getLastName() {
        return lastName.get();
    }

    public SimpleStringProperty lastNameProperty() {
        return lastName;
    }


    private final Button button;

    public Button getButton() {
        return button;
    }
}

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

// Custom cell for the button column
private class ButtonCell extends TableCell<Person, Void> {

    @Override
    protected void updateItem(Void item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setGraphic(null);
        } else {

            Person person = getTableView().getItems().get(getIndex());
            if (person.getFirstName().equals("Jane"))
                setGraphic(person.getButton());

        }
    }
}

}


Solution

  • UI elements (for example, buttons) should not be properties of the model (the Person, in this case): they should be part of the view (the cell).

    I can't tell exactly why you are observing the behavior you are observing, but it's likely the button belonging to "Jane" is being added to multiple cells (which is not allowed), or perhaps finally ending up in a cell which is not being used. All of this depends on the internal (hidden) mechanism of cell reuse in the table.

    Instead, make the button a property of the cell and use the updateItem(...) method to choose whether or not to display it depending on the state of the model.

    The following works fine:

    import javafx.application.Application;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.TableCell;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    public class Test extends Application {
    
        private final ObservableList<Person> data = FXCollections.observableArrayList(
                new Person("John", "Doe"),
                new Person("Jane", "Smith"),
                new Person("Bill", "Jones")
        );
    
        @Override
        public void start(Stage primaryStage) {
            TableView<Person> tableView = new TableView<>();
            tableView.setItems(data);
    
            TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
            firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
    
            TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
            lastNameCol.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
    
            TableColumn<Person, Void> buttonCol = new TableColumn<>("Actions");
            buttonCol.setCellFactory(param -> new ButtonCell());
    
            tableView.getColumns().addAll(firstNameCol, lastNameCol, buttonCol);
    
            VBox vbox = new VBox(tableView);
    
            Scene scene = new Scene(vbox);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        public static class Person {
            private final SimpleStringProperty firstName;
            private final SimpleStringProperty lastName;
    
            public Person(String firstName, String lastName) {
                this.firstName = new SimpleStringProperty(firstName);
                this.lastName = new SimpleStringProperty(lastName);
    
            }
    
            public String getFirstName() {
                return firstName.get();
            }
    
            public SimpleStringProperty firstNameProperty() {
                return firstName;
            }
    
            public String getLastName() {
                return lastName.get();
            }
    
            public SimpleStringProperty lastNameProperty() {
                return lastName;
            }
    
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    
        // Custom cell for the button column
        private class ButtonCell extends TableCell<Person, Void> {
    
            private final Button button = new Button();
    
            {
                button.setOnAction(e -> System.out.println("My name is : " + getTableView().getItems().get(getIndex()).getFirstName()));
            }
    
            @Override
            protected void updateItem(Void item, boolean empty) {
                super.updateItem(item, empty);
                if (empty) {
                    setGraphic(null);
                } else {
    
                    Person person = getTableView().getItems().get(getIndex());
                    if (person.getFirstName().equals("Jane"))
                        setGraphic(button);
    
                }
            }
        }
    }