在 TableView 中编辑单元格后更新 ObservableList

Updating ObservableList after editing cell in TableView

我正在尝试使用 Oracle 糟糕的教程创建可编辑的单元格。我发现他们的 EditCell class 仅在我单击当前编辑的同一行或任何行之外时更新。如果我点击另一行,编辑将被取消。这是本教程的 link,在它的末尾你可以找到 EditCell class,但这不是这个问题的重点:

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

此 class 创建 TextField 用于编辑目的。单击另一行会启动 cancel() 方法。还有这个代码行:

setText((String( getItem());

这会阻止编辑。我将其替换为:

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

现在可以编辑了。但再次编辑此单元格后,旧值将加载到 TextField。我猜 ObservableList 在第一次编辑后没有更新。

这是 FXML 代码:

<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>

在控制器中我声明 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代码:

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

Main 中声明控制器和可编辑列:

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

我需要用 ObservableList 绑定单元格吗?还是刷新一下?如何更新 data 让 TextField 始终填充实际值?

这是全部EditCellclass:

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

正在编辑

编辑时,onEditCommit 处理程序会在编辑已提交时收到通知(不足为奇)。该处理程序负责将新值写入模型(在您的例子中,Person)。发生这种情况时,TableView 将自动更新以显示新值。

您在取消编辑时将 Cell 的文本设置为 TextField 的值的解决方案将不起作用。最终,一旦以某种方式触发更新,Cell 将刷新以显示模型提供的 真实 数据(由 cellValueFactory 获得)。除此之外,您还没有实际更新模型,因此所谓的编辑只是一种视觉效果。


关于教程

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: 。该问题的答案提供了许多 破解 解决该问题的方法。一些还指向错误报告,表明 no-commit-on-lost-focus 行为实际上是无意的;但是,从 JavaFX 11.0.2 开始,这些错误尚未得到修复。

这意味着:

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

永远不会提交编辑。您(但实际上是教程)没有提供提交新值的有效方法,因为在调用 if (!arg2) { commitEdit(...); } 时取消了编辑。由于编辑被取消,因此没有触发提交编辑事件,并且您的 TableColumn 无法将新值写入模型项。您可以做的是,尽管这不会解决 no-commit-on-lost-focus 问题,但可以向您的 TextField 添加一个 onAction 处理程序来提交编辑。您可能还想提供一种通过键盘取消编辑的方法。这看起来像:

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

这将在按下 Enter 键时提交编辑,并在按下 Esc 键时取消编辑。

注意 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 (或其 linked/related 问题)并尝试使用给定的解决方案之一(技巧)。

此外,如以下文档所述,您不必提供自己的 onEditCommit 处理程序即可将新值写入模型——TableColumn 默认情况下会这样做(假设 cellValueFactory returns 一个 WritableValue).


文档

也许阅读 TableView 的文档比您正在阅读的教程更有益,或者至少是对您正在阅读的教程的补充:

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.