从一个单元格编辑到另一个单元格时触发有效取消 CellEditEvent 的问题

Issue firing a valid cancel CellEditEvent when editing from one cell to another

我正在实现一个可编辑的 TableView,它依赖于 CellEditEvents 来取消、启动和提交事件。

在下面的例子中,城市列是可编辑的,相应的事件被触发时:

当我从编辑单元格移动到 RadioButton 时,开始和取消事件正确触发。但是从一个单元格遍历到另一个单元格时会抛出错误。

请检查下面的 gif(步骤)和控制台输出。

On City edit start :: TableDataObj{firstName=First Name 0, lastName=Last Name 0, city=City 0}
On City edit cancel :: TableDataObj{firstName=First Name 0, lastName=Last Name 0, city=City 0}
On City edit start :: TableDataObj{firstName=First Name 1, lastName=Last Name 1, city=City 1}
On City edit cancel :: TableDataObj{firstName=First Name 1, lastName=Last Name 1, city=City 1}
On City edit start :: TableDataObj{firstName=First Name 0, lastName=Last Name 0, city=City 0}
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at javafx.scene.control.TableColumn$CellEditEvent.getTableView(TableColumn.java:772)
    at javafx.scene.control.TableColumn$CellEditEvent.getRowValue(TableColumn.java:829)
    at com.thales.javafx.tableview.CancelTableEditDemo.lambda$buildTable(CancelTableEditDemo.java:84)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
    at javafx.event.Event.fireEvent(Event.java:198)
    at javafx.scene.control.TableCell.cancelEdit(TableCell.java:400)
    at com.thales.javafx.tableview.CancelTableEditDemo$EditingCell.cancelEdit(CancelTableEditDemo.java:105)
    at javafx.scene.control.TableCell.updateEditing(TableCell.java:565)
    at javafx.scene.control.TableCell.lambda$new(TableCell.java:142)
    at javafx.beans.WeakInvalidationListener.invalidated(WeakInvalidationListener.java:83)
    at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:349)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.beans.property.ReadOnlyObjectWrapper$ReadOnlyPropertyImpl.fireValueChangedEvent(ReadOnlyObjectWrapper.java:176)
    at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:142)
    at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
    at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
    at javafx.scene.control.TableView.setEditingCell(TableView.java:1145)
    at javafx.scene.control.TableView.edit(TableView.java:1457)
    at com.sun.javafx.scene.control.behavior.TableCellBehavior.edit(TableCellBehavior.java:106)
    at com.sun.javafx.scene.control.behavior.TableCellBehavior.edit(TableCellBehavior.java:38)
    at com.sun.javafx.scene.control.behavior.CellBehaviorBase.handleClicks(CellBehaviorBase.java:269)
    at com.sun.javafx.scene.control.behavior.TableCellBehaviorBase.simpleSelect(TableCellBehaviorBase.java:218)
    at com.sun.javafx.scene.control.behavior.TableCellBehaviorBase.doSelect(TableCellBehaviorBase.java:148)
    at com.sun.javafx.scene.control.behavior.CellBehaviorBase.mousePressed(CellBehaviorBase.java:150)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase.handle(BehaviorSkinBase.java:95)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase.handle(BehaviorSkinBase.java:89)
    at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    at javafx.event.Event.fireEvent(Event.java:198)
    at javafx.scene.Scene$MouseHandler.process(Scene.java:3757)
    at javafx.scene.Scene$MouseHandler.access00(Scene.java:3485)
    at javafx.scene.Scene.impl_processMouseEvent(Scene.java:1762)
    at javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2494)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:352)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:275)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent5(GlassViewEventHandler.java:388)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:387)
    at com.sun.glass.ui.View.handleMouseEvent(View.java:555)
    at com.sun.glass.ui.View.notifyMouse(View.java:937)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null9(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)

我期望的是:当从 Cell-0 遍历到 Cell-1 时,它必须在开始编辑 Cell-1 之前为 Cell-0 触发有效的 Cancel 事件。

你们谁能帮我弄清楚 where/what 我失踪了吗?

下面是问题的完整工作代码:

import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.*;
import javafx.stage.Stage;

public class CancelTableEditDemo extends Application {
    public static void main(String... a) {
        Application.launch(a);
    }

    @Override
    public void start(final Stage primaryStage) throws Exception {
        final ObservableList<TableDataObj> items = FXCollections.observableArrayList();
        final int no = 2;
        for (int i = 0; i < no; i++) {
            final String firstName = "First Name " + i;
            final String lastName = "Last Name " + i;
            final String city = "City " + i;
            items.add(new TableDataObj(i, firstName, lastName, city));
        }

        final TableView<TableDataObj> table = buildTable();
        table.setItems(items);

        final VBox root = new VBox(new RadioButton("Use this for focus changing"), table);
        root.setSpacing(10);
        root.setPadding(new Insets(10));
        VBox.setVgrow(table, Priority.ALWAYS);

        final Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Cancel Table Edit Demo");
        primaryStage.show();
    }

    @SuppressWarnings("unchecked")
    private TableView<TableDataObj> buildTable() {
        final TableView<TableDataObj> tableView = new TableView<>();
        tableView.setEditable(true);
        final TableColumn<TableDataObj, Integer> idCol = new TableColumn<>();
        idCol.setText("Id");
        idCol.setCellValueFactory(param -> param.getValue().idProperty().asObject());

        final TableColumn<TableDataObj, String> fnCol = new TableColumn<>();
        fnCol.setText("First Name");
        fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
        fnCol.setPrefWidth(150);

        final TableColumn<TableDataObj, String> lnCol = new TableColumn<>();
        lnCol.setText("Last Name");
        lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
        lnCol.setPrefWidth(150);

        final TableColumn<TableDataObj, String> cityCol = new TableColumn<>();
        cityCol.setEditable(true);
        cityCol.setText("City");
        cityCol.setCellValueFactory(param -> param.getValue().cityProperty());
        cityCol.setPrefWidth(150);
        cityCol.setCellFactory(param -> {
            final EditingCell<TableDataObj, String> cell = new EditingCell<>();
            cell.setOnMouseClicked(e -> {
                tableView.edit(cell.getTableRow().getIndex(), cityCol);
            });
            return cell;
        });
        cityCol.setOnEditStart(e -> {
            System.out.println("On City edit start :: " + e.getRowValue());
        });
        cityCol.setOnEditCancel(e -> {
            System.out.println("On City edit cancel :: " + e.getRowValue());
        });
        cityCol.setOnEditCommit(e -> {
            System.out.println("On City edit commit :: val : " + e.getNewValue() + " :: " + e.getRowValue());
            e.getRowValue().setCity(e.getNewValue());
        });
        tableView.getColumns().addAll(idCol, fnCol, lnCol, cityCol);
        return tableView;
    }

    /**
     * Editing Cell
     */
    class EditingCell<T, S> extends TableCell<T, S> {

        private TextField textField;

        @Override
        public void cancelEdit() {
            super.cancelEdit();
            updateItem(getItem(), getItem() == null);
        }

        @Override
        public void commitEdit(final S newValue) {
            super.commitEdit(newValue);
        }

        @Override
        public void startEdit() {
            super.startEdit();
            updateItem(getItem(), getItem() == null);
            textField.selectAll();
            textField.requestFocus();
        }

        @Override
        public void updateItem(final S item, final boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setText(null);
                setGraphic(textField);
            } else {
                if (isEditing()) {
                    if (textField == null) {
                        createTextField();
                    }
                    textField.setText(getString());
                    setGraphic(textField);
                    setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                } else {
                    setText(item != null ? item.toString() : "");
                    setContentDisplay(ContentDisplay.TEXT_ONLY);
                }
            }
        }

        private void createTextField() {
            textField = new TextField(getString());
            textField.setMinWidth(getWidth() - getGraphicTextGap() * 2);

            textField.setOnKeyPressed(keyEvent -> {
                if (keyEvent.getCode() == KeyCode.ESCAPE) {
                    cancelEdit();
                    keyEvent.consume();
                } else if (keyEvent.getCode() == KeyCode.ENTER) {
                    commitEdit((S) textField.getText()); // For now casting directly for testing
                    keyEvent.consume();
                }
            });

            /* Cancel edit when loosing focus. */
            textField.focusedProperty().addListener((obs, prevFocus, focused) -> {
                if (!focused) {
                    cancelEdit();
                }
            });
        }

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

    /**
     * Data object.
     */
    class TableDataObj {
        private final IntegerProperty id = new SimpleIntegerProperty();
        private final StringProperty firstName = new SimpleStringProperty();
        private final StringProperty lastName = new SimpleStringProperty();
        private final StringProperty city = new SimpleStringProperty();

        public TableDataObj(final int i, final String fn, final String ln, final String cty) {
            setId(i);
            setFirstName(fn);
            setLastName(ln);
            setCity(cty);
        }

        public StringProperty cityProperty() {
            return city;
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public String getCity() {
            return city.get();
        }

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

        public int getId() {
            return id.get();
        }

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

        public IntegerProperty idProperty() {
            return id;
        }

        public StringProperty lastNameProperty() {
            return lastName;
        }

        public void setCity(final String city1) {
            city.set(city1);
        }

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

        public void setId(final int idA) {
            id.set(idA);
        }

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

        @Override
        public String toString() {
            return "TableDataObj{" +
                    "firstName=" + firstName.get() +
                    ", lastName=" + lastName.get() +
                    ", city=" + city.get() +
                    '}';
        }
    }
}

好的..因为在升级到 JavaFX 17 之前我必须寻找解决方法,下面是我想出的更改(对于 JavaFX 8):

首先,在 onCancelEdit 事件处理程序中添加对 TablePosition 的空检查,以确保不会因内部错误而引发错误。

cityCol.setOnEditCancel(e -> {
    if (e.getTablePosition() != null) {
        System.out.println("On City edit cancel :: " + e.getRowValue());
    }
});

其次,为了触发正确的取消事件,我在条件不正确时明确触发取消事件。

@Override
public void cancelEdit() {
    TablePosition<T, ?> editingCell = getTableView().getEditingCell();
    super.cancelEdit();
    // If the editingCell is null, then the editCancelEvent fired in super method has no impact. So explicitly firing a valid editCancelEvent.
    if (editingCell == null) {
        final TablePosition<T, S> pos = new TablePosition<>(getTableView(), getTableRow().getIndex(), getTableColumn());
        Event.fireEvent(getTableColumn(), new TableColumn.CellEditEvent<>(getTableView(), pos, TableColumn.editCancelEvent(), null));
    }
    setText(getItem() != null ? getItem().toString() : "");
    setContentDisplay(ContentDisplay.TEXT_ONLY);
}

包含更改的完整工作演示如下:

import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class CancelTableEditDemo extends Application {
    public static void main(String... a) {
        Application.launch(a);
    }

    @Override
    public void start(final Stage primaryStage) throws Exception {
        final ObservableList<TableDataObj> items = FXCollections.observableArrayList();
        final int no = 2;
        for (int i = 0; i < no; i++) {
            final String firstName = "First Name " + i;
            final String lastName = "Last Name " + i;
            final String city = "City " + i;
            items.add(new TableDataObj(i, firstName, lastName, city));
        }

        final TableView<TableDataObj> table = buildTable();
        table.setItems(items);

        final VBox root = new VBox(new RadioButton("Use this for focus changing"), table);
        root.setSpacing(10);
        root.setPadding(new Insets(10));
        VBox.setVgrow(table, Priority.ALWAYS);

        final Scene sc = new Scene(root);
        primaryStage.setScene(sc);
        primaryStage.setTitle("Cancel Table Edit Demo");
        primaryStage.show();
    }

    @SuppressWarnings("unchecked")
    private TableView<TableDataObj> buildTable() {
        final TableView<TableDataObj> tableView = new TableView<>();
        tableView.setEditable(true);
        final TableColumn<TableDataObj, Integer> idCol = new TableColumn<>();
        idCol.setText("Id");
        idCol.setCellValueFactory(param -> param.getValue().idProperty().asObject());

        final TableColumn<TableDataObj, String> fnCol = new TableColumn<>();
        fnCol.setText("First Name");
        fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
        fnCol.setPrefWidth(150);

        final TableColumn<TableDataObj, String> lnCol = new TableColumn<>();
        lnCol.setText("Last Name");
        lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
        lnCol.setPrefWidth(150);

        final TableColumn<TableDataObj, String> cityCol = new TableColumn<>();
        cityCol.setEditable(true);
        cityCol.setText("City");
        cityCol.setCellValueFactory(param -> param.getValue().cityProperty());
        cityCol.setPrefWidth(150);
        cityCol.setCellFactory(param -> {
            final EditingCell<TableDataObj, String> cell = new EditingCell<>();
            cell.setOnMouseClicked(e -> {
                tableView.edit(cell.getTableRow().getIndex(), cityCol);
            });
            return cell;
        });
        cityCol.setOnEditStart(e -> {
            System.out.println("On City edit start :: " + e.getRowValue());
        });
        cityCol.setOnEditCancel(e -> {
            if (e.getTablePosition() != null) {
                System.out.println("On City edit cancel :: " + e.getRowValue());
            }
        });
        cityCol.setOnEditCommit(e -> {
            System.out.println("On City edit commit :: val : " + e.getNewValue() + " :: " + e.getRowValue());
            e.getRowValue().setCity(e.getNewValue());
        });
        tableView.getColumns().addAll(idCol, fnCol, lnCol, cityCol);
        return tableView;
    }

    /**
     * Editing Cell
     */
    class EditingCell<T, S> extends TableCell<T, S> {

        private TextField textField;

        @Override
        public void cancelEdit() {
            TablePosition<T, ?> editingCell = getTableView().getEditingCell();
            super.cancelEdit();
            // If the editingCell is null, then the editCancelEvent fired in super method has no impact. So explicitly firing a valid editCancelEvent.
            if (editingCell == null) {
                final TablePosition<T, S> pos = new TablePosition<>(getTableView(), getTableRow().getIndex(), getTableColumn());
                Event.fireEvent(getTableColumn(), new TableColumn.CellEditEvent<>(getTableView(), pos, TableColumn.editCancelEvent(), null));
            }
            setText(getItem() != null ? getItem().toString() : "");
            setContentDisplay(ContentDisplay.TEXT_ONLY);
        }

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

        @Override
        public void updateItem(final S item, final boolean empty) {
            super.updateItem(item, empty);
            setGraphic(null);
            if (empty) {
                setText(null);
            } else {
                setText(item != null ? item.toString() : "");
                setContentDisplay(ContentDisplay.TEXT_ONLY);
            }
        }

        private void createTextField() {
            textField = new TextField(getString());
            textField.setMinWidth(getWidth() - getGraphicTextGap() * 2);

            textField.setOnKeyPressed(keyEvent -> {
                if (keyEvent.getCode() == KeyCode.ESCAPE) {
                    cancelEdit();
                    keyEvent.consume();
                } else if (keyEvent.getCode() == KeyCode.ENTER) {
                    commitEdit((S) textField.getText()); // For now casting directly for testing
                    keyEvent.consume();
                }
            });

            /* Cancel edit when loosing focus. */
            textField.focusedProperty().addListener((obs, prevFocus, focused) -> {
                if (!focused && isEditing()) {
                    cancelEdit();
                }
            });
        }

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

    /**
     * Data object.
     */
    class TableDataObj {
        private final IntegerProperty id = new SimpleIntegerProperty();
        private final StringProperty firstName = new SimpleStringProperty();
        private final StringProperty lastName = new SimpleStringProperty();
        private final StringProperty city = new SimpleStringProperty();

        public TableDataObj(final int i, final String fn, final String ln, final String cty) {
            setId(i);
            setFirstName(fn);
            setLastName(ln);
            setCity(cty);
        }

        public StringProperty cityProperty() {
            return city;
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public String getCity() {
            return city.get();
        }

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

        public int getId() {
            return id.get();
        }

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

        public IntegerProperty idProperty() {
            return id;
        }

        public StringProperty lastNameProperty() {
            return lastName;
        }

        public void setCity(final String city1) {
            city.set(city1);
        }

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

        public void setId(final int idA) {
            id.set(idA);
        }

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

        @Override
        public String toString() {
            return "TableDataObj{" +
                    "firstName=" + firstName.get() +
                    ", lastName=" + lastName.get() +
                    ", city=" + city.get() +
                    '}';
        }
    }
}