0

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!

Gavin Ray
  • 129
  • 1
  • 7
  • 1
    You don't need a direct reference to the model's property. You can use the [`CellEditEvent#getRowValue()`](https://openjfx.io/javadoc/19/javafx.controls/javafx/scene/control/TableColumn.CellEditEvent.html#getRowValue()) method to get a reference to the model item. Then, due to the fact that you registered this on-edit-commit handler with a specific column, and columns are associated with specific properties, you can simply call the appropriate setter on the model item. – Slaw Oct 10 '22 at 06:07
  • How would I call the appropriate setter on the model item? I'm looking for a way to find the property or property name on the column after calling cell.getTableColumn(), but am not finding it. – Gavin Ray Oct 10 '22 at 06:31
  • From the table column, there is a getCellValueFactory, but that does not have a get PropertyValueFactory, which stores the original string I gave indicating the property. – Gavin Ray Oct 10 '22 at 06:34
  • Added answer. Hopefully that makes what I'm trying to say clearer. – Slaw Oct 10 '22 at 06:40
  • Thanks for all of this! I really don't want to move away from FXML which, you are right, I am using to link a single method shared with all columns. (super scalable). But I did find the attribute name from the column after casting cellValueFactory to propertyValueFactory! So I might be close. – Gavin Ray Oct 10 '22 at 06:43
  • 1
    I'm not sure I understand your focus on `PropertyValueFactory`. It shouldn't be important to any implementation, unless I'm just not seeing something about your setup. Nor does `TableColumn#getCellObservableValue(...)` return the cell value factory, but rather the `ObservableValue` that said factory returns for the specified item. – Slaw Oct 10 '22 at 06:45
  • 1
    In other words, why doesn't the first implementation (i.e., essentially the default implementation) I showed in my answer work for you? – Slaw Oct 10 '22 at 06:48
  • My IDE did not like the try (observable instanceof...) so I tried casting observableValue to WritableValue first then called setValue on it. It still doesn't work. No errors, but the property is not updated. Am I missing something? – Gavin Ray Oct 10 '22 at 07:04
  • The observable value is a ReadOnlyObjectWrapper. How do I make it a WritableValue like you say? – Gavin Ray Oct 10 '22 at 07:15
  • So, decorate myClass setters with @FXML? My class has public getters and setters. – Gavin Ray Oct 10 '22 at 07:17
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/248691/discussion-between-gavin-ray-and-slaw). – Gavin Ray Oct 10 '22 at 07:21
  • 1
    No. I edited my answer to address why `PropertyValueFactory` + no JavaFX properties in the model breaks my suggested solution, and offers a way to fix it. – Slaw Oct 10 '22 at 07:33

1 Answers1

4

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.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • 1
    nice answer :) with a slight error: we _must not_ do additional work (like updating another field) in the setter of a property (because its value might be changed by binding), instead do such work in its invalidated – kleopatra Oct 10 '22 at 10:39
  • @kleopatra Updated – Slaw Oct 10 '22 at 16:17