Search code examples
javajavafxfxml

Updating ObservableList after editing cell in TableView


I'm trying to create editable cells using Oracle poor tutorial. I figured out that their EditCell class only updates when I click on the same row I currently edit or outside any row. In case when I click on another row, edit is cancelled. Here is the link to this tutorial and on the end of it you can find EditCell class, but it's not the point of this question:

https://docs.oracle.com/javase/8/javafx/user-interface-tutorial/table-view.htm

This class creates TextField for edit purposes. Clicking on another row launches cancel() method. And there is this code line:

setText((String( getItem());

that blocks edit. I replaced it with:

setText((String) textField.getText());

and edit works now. But after editing this cell again old value is loaded to TextField. I guess that ObservableList is not updated after first edit.

Here is FXML code:

<GridPane fx:controller="sample.Controller"
      xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">

    <TableView GridPane.columnIndex="0" GridPane.rowIndex="1" items="${controller.data}" editable="true">
        <columns>
            <TableColumn fx:id="colName" text="name">
                <cellValueFactory>
                    <PropertyValueFactory property="Name"/>
                </cellValueFactory>
            </TableColumn>

            <TableColumn fx:id="colSurname" text="surname">
                <cellValueFactory>
                    <PropertyValueFactory property="Surname"/>
                </cellValueFactory>
            </TableColumn>
        </columns>
    </TableView>
</GridPane>

In controller I declare ObservableList:

public class Controller {

    @FXML
    private TableColumn<Person, String> colName;
    @FXML
    private TableColumn<Person, String> colSurname;

    @FXML
    private ObservableList<Person> data;

    public Controller(){
        data = FXCollections.observableArrayList(
                new Person("John", "S."),
                new Person("Jane", "S.")
        );
    }

    public TableColumn<Person, String> getColName() {
        return colName;
    }

    public void setColName(TableColumn<Person, String> colName) {
        this.colName = colName;
    }

    public TableColumn<Person, String> getColSurname() {
        return colSurname;
    }

    public void setColSurname(TableColumn<Person, String> colSurname) {
        this.colSurname = colSurname;
    }

    public ObservableList<Person> getData() {
        return data;
    }

    public void setData(ObservableList<Person> data) {
        this.data = data;
    }
}

Person.java code:

public class Person {

    private final SimpleStringProperty name;
    private final SimpleStringProperty surname;

    public Person(String name, String surname){
        this.name = new SimpleStringProperty(name);
        this.surname = new SimpleStringProperty(surname);
    }

    public String getName() {
        return name.get();
    }

    public SimpleStringProperty nameProperty() {
        return name;
    }

    public void setName(String name) {
        this.name.set(name);
    }

    public String getSurname() {
        return surname.get();
    }

    public SimpleStringProperty surnameProperty() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname.set(surname);
    }
}

In Main I declare controller and editable column:

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));

        Parent root = (Parent) loader.load();
        primaryStage.setScene(new Scene(root, 300, 275));

        Controller controller = loader.getController();
        TableColumn<Person, String> colName = controller.getColName();

        Callback<TableColumn<Person, String>, TableCell<Person, String>> cellFactory =
            (TableColumn<Person, String> p) -> new sample.EditCell();

        colName.setCellFactory(cellFactory);
        colName.setOnEditCommit(
                (TableColumn.CellEditEvent<Person, String> t) -> {
                    ((Person) t.getTableView().getItems().get(
                            t.getTablePosition().getRow())
                    ).setName(t.getNewValue());
                });


        primaryStage.show();
    }

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

Do I need bind cell with ObservableList? Or refresh it? How to update data to have TextField always filled with actual value?

Here is whole EditCell class:

class EditCell extends TableCell<Person, String> {

    private TextField textField;

    public EditCell() {
    }

    @Override
    public void startEdit() {
        if (!isEmpty()) {
            super.startEdit();
            createTextField();
            setText(null);
            setGraphic(textField);
            textField.selectAll();
        }
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();

        setText((String) getItem());

        //setText((String) textField.getText());
        //This line updates cell, but textField keeps old value after next edit.

        setGraphic(null);
    }

    @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());
                }

                setText(null);
                setGraphic(textField);
            } else {
                setText(getString());
                setGraphic(null);
            }
        }
    }

    private void createTextField() {
        textField = new TextField(getString());
        textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
        textField.focusedProperty().addListener(
                (ObservableValue<? extends Boolean> arg0,
                 Boolean arg1, Boolean arg2) -> {
                    if (!arg2) {
                        commitEdit(textField.getText());
                    }
                });
    }

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

Solution

  • Editing

    When editing, the onEditCommit handler is notified when an edit has been committed (unsurprisingly). This handler is responsible for writing the new value to the model (in your case, Person). When this happens the TableView will automatically update to display the new value.

    Your solution to set the text of the Cell to the value of the TextField when the edit is cancelled won't work. Eventually, once an update is triggered somehow, the Cell will refresh to display the real data provided by the model (obtained by the cellValueFactory). Besides that, you haven't actually updated the model and so the supposed edit is just a visual thing.


    About the Tutorial

    The tutorial you link to has issues. The biggest of which is that it assumes when the TextField loses focus you can successfully commit the new value. As you are experiencing, this is not the case. You can see many others have experienced this problem by looking the this question: TableView doesn't commit values on focus lost event. The answers to that question provide many ways to hack around the problem. Some also point to bug reports, indicating the no-commit-on-lost-focus behavior is actually unintended; however, those bugs have not been fixed as of JavaFX 11.0.2.

    What this means is that:

    textField.focusedProperty().addListener(
            (ObservableValue<? extends Boolean> arg0,
             Boolean arg1, Boolean arg2) -> {
                if (!arg2) {
                    commitEdit(textField.getText());
                }
            });
    

    Won't ever commit the edit. You (but really the tutorial) provide no working means to commit the new value because the edit is cancelled by the time if (!arg2) { commitEdit(...); } is invoked. Since the edit is cancelled there is no commit edit event fired and your TableColumn can't write the new value to the model item. What you can do, though this won't fix the no-commit-on-lost-focus problem, is add an onAction handler to your TextField that commits the edit. You'll probably want to provide a means to cancel the edit via the keyboard as well. This would look something like:

    textField.setOnAction(event -> {
        commitEdit(textField.getText());
        event.consume();
    }
    textField.setOnKeyPressed(event -> {
        if (event.getCode() == KeyCode.ESCAPE) {
            cancelEdit();
            event.consume();
        }
    }
    

    This will commit the edit when the Enter key is pressed and cancel the edit when the Esc key is pressed.

    Note that TextFieldTableCell already provides this behavior out of the box, no need to roll your own EditCell implementation. However, if you want to commit the edit when the focus is lost then you'll have to look at the answers to TableView doesn't commit values on focus lost event (or its linked/related questions) and attempt to use one of the given solutions (hacks).

    Also, as noted in the below documentation, you don't have to provide your own onEditCommit handler in order to write the new value to the model—TableColumn does that by default (assuming cellValueFactory returns a WritableValue).


    Documentation

    Perhaps reading the documentation of TableView will be more beneficial than, or at least complimentary to, the tutorial you're reading:

    Editing

    This control supports inline editing of values, and this section attempts to give an overview of the available APIs and how you should use them.

    Firstly, cell editing most commonly requires a different user interface than when a cell is not being edited. This is the responsibility of the Cell implementation being used. For TableView, it is highly recommended that editing be per-TableColumn, rather than per row, as more often than not you want users to edit each column value differently, and this approach allows for editors specific to each column. It is your choice whether the cell is permanently in an editing state (e.g. this is common for CheckBox cells), or to switch to a different UI when editing begins (e.g. when a double-click is received on a cell).

    To know when editing has been requested on a cell, simply override the Cell.startEdit() method, and update the cell text and graphic properties as appropriate (e.g. set the text to null and set the graphic to be a TextField). Additionally, you should also override Cell.cancelEdit() to reset the UI back to its original visual state when the editing concludes. In both cases it is important that you also ensure that you call the super method to have the cell perform all duties it must do to enter or exit its editing mode.

    Once your cell is in an editing state, the next thing you are most probably interested in is how to commit or cancel the editing that is taking place. This is your responsibility as the cell factory provider. Your cell implementation will know when the editing is over, based on the user input (e.g. when the user presses the Enter or ESC keys on their keyboard). When this happens, it is your responsibility to call Cell.commitEdit(Object) or Cell.cancelEdit(), as appropriate.

    When you call Cell.commitEdit(Object) an event is fired to the TableView, which you can observe by adding an EventHandler via TableColumn.setOnEditCommit(javafx.event.EventHandler). Similarly, you can also observe edit events for edit start and edit cancel.

    By default the TableColumn edit commit handler is non-null, with a default handler that attempts to overwrite the property value for the item in the currently-being-edited row. It is able to do this as the Cell.commitEdit(Object) method is passed in the new value, and this is passed along to the edit commit handler via the CellEditEvent that is fired. It is simply a matter of calling TableColumn.CellEditEvent.getNewValue() to retrieve this value.

    It is very important to note that if you call TableColumn.setOnEditCommit(javafx.event.EventHandler) with your own EventHandler, then you will be removing the default handler. Unless you then handle the writeback to the property (or the relevant data source), nothing will happen. You can work around this by using the TableColumnBase.addEventHandler(javafx.event.EventType, javafx.event.EventHandler) method to add a TableColumn.editCommitEvent() EventType with your desired EventHandler as the second argument. Using this method, you will not replace the default implementation, but you will be notified when an edit commit has occurred.