4

We are running a JavaFX application that contains some editable table views. A new requested feature is: a button that adds a new row below the currently selected one and immediately starts to edit the first cell of the row.

We implemented this feature, which was not so complicated, but we experience a very strange behavior and after a couple of days investigating the issue we still have no idea what goes wrong.

What happens is that when one clicks the button it adds a new row but starts to edit to first cell not of the newly created row but on an arbitrary other row. Unfortunately, this issue is not 100% reproduceable. Sometimes it's working as expected but most often the row below the newly added row gets edited, but sometimes even completely different rows before and after the currently selected one.

Below you can find the source code of a stripped down version of a JavaFX TableView that can be used to see the issue. As already mentioned it is not 100% reproduceable. To see the issue you have to add a new row multiple times. Some times the issue occurs more often when scrolling the table up and down a couple of times.

Any help is appreciated.

Hint: we already played around with Platform.runlater() a lot, by placing the action implementation of the button inside a runlater(), but although the issue occurs less often then, it never disappeared completely.

The TableView:

package tableview;

import java.util.ArrayList;
import java.util.List;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Callback;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class SimpleTableViewTest extends Application {

    private final ObservableList<Person> data = FXCollections.observableArrayList(createData());

    private final TableView table = new TableView();

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

    private static List<Person> createData() {
        List<Person> data = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            data.add(new Person("Jacob", "Smith", "jacob.smith_at_example.com", "js_at_example.com"));
        }

        return data;
    }

    @Override
    public void start(Stage stage) {

        Scene scene = new Scene(new Group());
        stage.setTitle("Table View Sample");
        stage.setWidth(700);
        stage.setHeight(550);

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

        // Create a customer cell factory so that cells can support editing.
        Callback<TableColumn, TableCell> cellFactory = (TableColumn p) -> {
            return new EditingCell();
        };

        // Set up the columns
        TableColumn firstNameCol = new TableColumn("First Name");
        firstNameCol.setMinWidth(100);
        firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
        firstNameCol.setCellFactory(cellFactory);

        TableColumn lastNameCol = new TableColumn("Last Name");
        lastNameCol.setMinWidth(100);
        lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
        lastNameCol.setCellFactory(cellFactory);
        lastNameCol.setEditable(true);

        TableColumn primaryEmailCol = new TableColumn("Primary Email");
        primaryEmailCol.setMinWidth(200);
        primaryEmailCol.setCellValueFactory(new PropertyValueFactory<Person, String>("primaryEmail"));
        primaryEmailCol.setCellFactory(cellFactory);
        primaryEmailCol.setEditable(false);

        TableColumn secondaryEmailCol = new TableColumn("Secondary Email");
        secondaryEmailCol.setMinWidth(200);
        secondaryEmailCol.setCellValueFactory(new PropertyValueFactory<Person, String>("secondaryEmail"));
        secondaryEmailCol.setCellFactory(cellFactory);

        // Add the columns and data to the table.
        table.setItems(data);
        table.getColumns().addAll(firstNameCol, lastNameCol, primaryEmailCol, secondaryEmailCol);
        table.setEditable(true);

        // --- Here comes the interesting part! ---
        //
        // A button that adds a row below the currently selected one
        // and immediatly starts editing it.
        Button addAndEdit = new Button("Add and edit");
        addAndEdit.setOnAction((ActionEvent e) -> {
            int idx = table.getSelectionModel().getSelectedIndex() + 1;

            data.add(idx, new Person());
            table.getSelectionModel().select(idx);
            table.edit(idx, firstNameCol);
        });

        final VBox vbox = new VBox();
        vbox.setSpacing(5);
        vbox.getChildren().addAll(label, table, addAndEdit);
        vbox.setPadding(new Insets(10, 0, 0, 10));
        ((Group) scene.getRoot()).getChildren().addAll(vbox);

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

}

The editable Table Cell:

package tableview;

import javafx.event.EventHandler;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public class EditingCell extends TableCell<Person, String> {
    private TextField textField;

    public EditingCell() {
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setText(getItem());
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }

    @Override
    public void startEdit() {
        super.startEdit();
        if (textField == null) {
            createTextField();
        }
        setGraphic(textField);
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    }

    @Override
    public void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                if (textField != null) {
                    textField.setText(getString());
                }
                setGraphic(textField);
                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            } else {
                setText(getString());
                setContentDisplay(ContentDisplay.TEXT_ONLY);
            }
        }
    }

    private void createTextField() {
        textField = new TextField(getString());
        textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
        textField.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent t) {
                if (t.getCode() == KeyCode.ENTER) {
                    commitEdit(textField.getText());
                } else if (t.getCode() == KeyCode.ESCAPE) {
                    cancelEdit();
                }
            }
        });
    }

    private String getString() {
        return getItem() == null ? "" : getItem().toString();
    }
}

A Data Bean:

package tableview;

import javafx.beans.property.SimpleStringProperty;

public class Person {
    private final SimpleStringProperty firstName;
    private final SimpleStringProperty lastName;
    private final SimpleStringProperty primaryEmail;
    private final SimpleStringProperty secondaryEmail;

    public Person() {
        this(null, null, null, null);
    }

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

    public SimpleStringProperty firstNameProperty() {
        return firstName;
    }

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

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

    public String getPrimaryEmail() {
        return primaryEmail.get();
    }

    public SimpleStringProperty getPrimaryEmailProperty() {
        return primaryEmail;
    }

    public String getSecondaryEmail() {
        return secondaryEmail.get();
    }

    public SimpleStringProperty getSecondaryEmailProperty() {
        return secondaryEmail;
    }

    public SimpleStringProperty lastNameProperty() {
        return lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName.set(firstName);
    }

    public void setLastName(String lastName) {
        this.lastName.set(lastName);
    }

    public void setPrimaryEmail(String primaryEmail) {
        this.primaryEmail.set(primaryEmail);
    }

    public void setSecondaryEmail(String secondaryEmail) {
        this.secondaryEmail.set(secondaryEmail);
    }
}
NoMoreBugs
  • 141
  • 2
  • 7
  • It seems duplicated see here https://stackoverflow.com/questions/29707487/javafx-tableview-edit-with-single-click-and-auto-insert-row – Towfik Alrazihi Mar 28 '18 at 10:13
  • Possible duplicate of [How to make JavaFX TableView cells editable?](https://stackoverflow.com/questions/19335196/how-to-make-javafx-tableview-cells-editable) – Towfik Alrazihi Mar 28 '18 at 10:14
  • It's definetly ***not*** a duplicate of [How to make JavaFX TableView cells editable?](https://stackoverflow.com/questions/19335196/how-to-make-javafx-tableview-cells-editable) – fabian Mar 28 '18 at 10:23
  • 1
    This could be a issue with the cells/rows not being properly initialized at the time you call `edit`. Since this happens during layout `Platform.runLater` could fail if the `Runnable` is run before the next layout pass. You could check, if calling `applyCss()` + `layout()` on the `TableView` helps... – fabian Mar 28 '18 at 10:32
  • I don't know how to solve this problem but, can it be happening because TableCells are reused in JavaFX? Scrolling down after clicking the Add and Edit button, it seems cells getting edited have a fixed interval like a cell in every 16 cells. Probably there is only one cell getting edited, but it is being reused again and again, resulting in getting rendered at different indexes when the table is scrolled. – Eralp Sahin Mar 28 '18 at 11:02
  • I have change the implementation of the button action in the sample code by adding a table.layout() before calling the edit method. This definitely improves the behavior. We had experimented with table.requestLayout() beforehand (cause we overlooked layout()) without success. I guess layout() is doing an immediate layout while requestLayout() comes to late. That would explain why it works better. – NoMoreBugs Mar 28 '18 at 11:38
  • It will require some more testing but for me it looks like a table.layout() before calling the table.edit() method fixes this issue. Many thanks to fabian so far. – NoMoreBugs Mar 28 '18 at 11:41
  • Probably unrelated, but I get null pointer exceptions from committing by pressing Enter. I don't remember why, but you need to use `setOnAction()` on the text field to capture an "Enter" key press and commit the action. Is there any advantage to your custom cell implementation over the library `TextFieldTableCell` one? – James_D Mar 28 '18 at 12:07
  • The custom cell implementation is a leftover from our real life table cell implementation that is a bit more complicated then shown in the example. – NoMoreBugs Mar 28 '18 at 12:22
  • After clicking around in the the table for half an hour I'm more or less sure that the problem is fixed by adding a table.layout() to the button action. One minor thing I had to change was to always initialize the editors text field in the startEdit() method, otherwise, in very rare cases, the the cell in the newly added line got already a value taken from another row. After fixing this. I'm happy with what we have right now. Many thanks to everyone. – NoMoreBugs Mar 28 '18 at 12:26

1 Answers1

4

The correct code of the buttons action implementation has to look like below. The important line to fix the described issue is 'table.layout()'. Many thanks to fabian!

addAndEdit.setOnAction((ActionEvent e) -> {
    int idx = table.getSelectionModel().getSelectedIndex() + 1;

    data.add(idx, new Person());
    table.getSelectionModel().select(idx);

    table.layout();

    table.edit(idx, firstNameCol);
 });
NoMoreBugs
  • 141
  • 2
  • 7