javajavafxjavafx-tableview

Is there a javafx.scene.control.TableColumn function like setOnEditCommit() that can be used for finding the property related to the changed cell?


I'm using JavaFX 17 to make an editable table. The table data comes from an observable list of MyCustomClass objects. I then made all cells editable by setting the cell factory of each column to TextFieldTableCell. So far so good. Setter function receives a CellEditEvent as expected; I can get the object that the row's data originated from, the column that was changed, the values that were changed.

@FXML
private void onEdit(TableColumn.CellEditEvent<MyCustomClass, String> editedCell) {
    MyCustomClass object = cell.getRowValue();
    String ValueBeforeUserMadeEdit = cell.getOldValue();
    String valueThatIsNowShowing = cell.getNewValue();
}

Now the bad news. The event object does not have a function for indicating which property (or ideally, which property setter) should be used to update the value inputted by the user (i.e. the property that relates to the changed column). I originally gave the property name to the cell in a PropertyValueFactory, which has a function for getting that String. However, I can't find a way to get the property value factory from the cell, and even if I did it seems like too much work to then find the property setter from that string.

It would be easier to create a subclass of TextFieldTableCell that stores a reference to the correct setter, but I am hoping someone can tell me if there is built in functionality for this. Seems like there should have been, even at version 17. I'm a student, and really trying to understand this stuff, so any help at all is really appreciated!


Solution

  • Handler per Column

    There's another approach, if you really need to define your own on-edit-commit handlers. It would look something like this:

    import javafx.fxml.FXML;
    import javafx.scene.control.TableColumn;
    
    public class Controller {
    
      @FXML private TableColumn<Foo, String> firstNameCol;
      @FXML private TableColumn<Foo, String> lastNameCol;
    
      @FXML
      private void initialize() {
        firstNameCol.setOnEditCommit(e -> e.getRowValue().setFirstName(e.getNewValue()));
        lastNameCol.setOnEditCommit(e -> e.getRowValue().setLastName(e.getNewValue()));
      }
    }
    

    When you do it this way, you know exactly which setter to call because each column gets its own on-edit-commit handler (and columns are associated with a specific property). I personally would prefer this approach.


    Get Cell's ObservableValue

    Given this method is annotated with @FXML, I assume you're trying to use this one method as the implementation for the on-edit-commit handler of multiple columns. This can complicate things, but what you want is possible:

    @FXML
    private void onEditCommit(TableColumn.CellEditEvent<MyCustomClass, String> event) {
        TableColumn<MyCustomClass, String> column = event.getTableColumn();
        MyCustomClass item = event.getRowValue();
    
        ObservableValue<String> observable = column.getCellObservableValue(item);
        if (observable instanceof WritableValue<String> writable) {
            writable.setValue(event.getNewValue());
        }
    }
    

    Note: I did not write this in an IDE, so there may be some slight syntax errors. But it should compile, at least on newer versions of Java.

    But note this is essentially what the default implementation does. And note that the existence of this default on-edit-commit handler is documented:

    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.

    So, unless you need to change the default behavior, you likely don't need to worry about implementing your own on-edit-commit handler.

    Potential Issues

    The above requires that the cellValueFactory returns an instance of WritableValue. And this WritableValue must be linked to the model's property. This should be no problem if your model class exposes JavaFX properties like so:

    public class Person {
    
       private final StringProperty name = new SimpleStringProperty(this, "name");
       public final void setName(String name) { this.name.set(name); }
       public final String getName() { return name.get(); }
       public final StringProperty nameProperty() { return name; }
    }
    

    Note: If your model uses JavaFX properties then I suggest using lambda expressions instead of PropertyValueFactory. Check out Why should I avoid using PropertyValueFactory in JavaFX?.

    Otherwise, PropertyValueFactory will return a ReadOnlyObjectWrapper that is divorced from the model's property after getting the current value. In other words, even though ReadOnlyObjectWrapper does implement WritableValue, setting the property will not forward the new value to the model item.

    If you cannot or are unwilling to modify your model to use JavaFX properties, then you can use a different cell-value factory implementation than PropertyValueFactory. For example:

    import javafx.beans.property.adapter.JavaBeanObjectPropertyBuilder;
    import javafx.beans.value.ObservableValue;
    import javafx.scene.control.TableColumn;
    import javafx.util.Callback;
    
    public class JavaBeanValueFactory<S, T> implements Callback<TableColumn.CellDataFeatures<S, T>, ObservableValue<T>> {
    
        private final String propertyName;
    
        public JavaBeanValueFactory(String propertyName) {
            this.propertyName = propertyName;
        }
    
        @Override
        @SuppressWarnings("unchecked")
        public ObservableValue<T> call(TableColumn.CellDataFeatures<S, T> param) {
            try {
                return JavaBeanObjectPropertyBuilder.create().bean(param.getValue()).name(propertyName).build();
            } catch (NoSuchMethodException ex) {
                throw new RuntimeException(ex);
            }
        }
    }
    

    Then replace new PropertyValueFactory<>("foo") with new JavaBeanValueFactory<>("foo").

    Or you could do something like this:

    // where 'firstNameCol' is e.g., a TableColumn<Person, String>
    firstNameCol.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().getFirstName()) {
      final Person item = data.getValue();
      @Override
      protected void invalidated() {
        item.setFirstName(get());
      }
    });
    

    Or anything you can think of where the property will forward new values to the model.