javafx

ReadOnlyObjectWrapper in TableColumn.setCellValueFactory()


Looking at the JavaDocs for TableColumn you see this:

 firstNameCol.setCellValueFactory(new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
     public ObservableValue<String> call(CellDataFeatures<Person, String> p) {
         return new ReadOnlyObjectWrapper(p.getValue().getFirstName());
     }
  });

The idea being to convert a String value into a read only Observable that can be passed to a cell.

However, looking at the JavaDocs for ReadOnlyObjectWrapper, you see this:

This class provides a convenient class to define read-only properties. It creates two properties that are synchronized. One property is read-only and can be passed to external users. The other property is read- and writable and should be used internally only.

The class itself extends from SimpleObjectProperty and only adds one method, getReadOnlyProperty(), which you can see is not called in the TableColumn example.

Looking at the TableColumn source code (JFX 21) you can see this:

public final ObservableValue<T> getCellObservableValue(S var1) {
        Callback var2 = this.getCellValueFactory();
        if (var2 == null) {
            return null;
        } else {
            TableView var3 = this.getTableView();
            if (var3 == null) {
                return null;
            } else {
                CellDataFeatures var4 = new CellDataFeatures(var3, this, var1);
                return (ObservableValue)var2.call(var4);
            }
        }
    }

There's nothing in this code that checks to see if the return value is a ReadOnlyObjectWrapper (or typed equivalent) and then call getReadOnlyProperty(). It just (effectively) casts the result to ObservableValue<T> and then returns it.

Is this just a mistake in the JavaDocs? Or am I missing something important? Because I'm confused now.

More than anything, I'm at a loss to understand the use case for ReadOnlyObjectWrapper. Any SimpleObjectProperty can be cast to ReadOnlyObjectProperty or ObservableValue just the way that the TableColumn source code does. When would you use ReadOnlyObjectWrapper.getReadOnlyProperty()?????


Solution

  • Read-Only Wrapper Classes

    The use case for all the ReadOnlyXXXWrapper classes is to expose a truly read-only property while still keeping it writable internally. You can see its use throughout the implementation of JavaFX itself. Often, when you see a class that exposes a ReadOnlyXXXProperty, the respective ReadOnlyXXXWrapper class is used as the implementation. Though of course that's not always the case, and is an implementation detail regardless.

    Here's a non-exhaustive list of places where ReadOnlyXXXWrapper is used (at least in JavaFX 22):

    In all these cases, the property is read-only because it depends on other state, it must always be settable and thus must never be bound, and/or it simply must not be settable from outside code. JavaFX provides the ReadOnlyXXXWrapper classes as a ready-made mechanism to meet such requirements. Interestingly, the properties Task inherits from Worker are not implemented with read-only wrappers for some reason.

    Note the so-called "property getter" methods don't return the read-only wrapper object directly. Instead, they return the result of getReadOnlyProperty(). That method returns a read-only view of the wrapper—hence "wrapper" in the name. The class of the view does not implement WritableValue or Property (a subinterface), which means you cannot cast it to circumvent the read-only nature of the property. But you can still listen/bind/subscribe to the property as needed.

    The TableColumn Example

    That all being said, I agree with you and James_D. The example you're talking about is a bad one. Conceptually, it's not a bad idea. The way the property is created means writing to it doesn't modify the underlying Person object. So, why not make the ObservableValue truly read-only? But it seems the documentation author forgot that the ReadOnlyXXXWrapper classes are themselves still writable, or at least forgot to add a call to getReadOnlyProperty() (which is likely overkill anyway), so the example doesn't make sense practically.

    All the cell-value factory cares about is that the returned object is some implementation of ObservableValue. It doesn't care if this value is writable or not, because that's not important to being able to display the value. Some other options the example could have used include returning:

    Of all these options, the first one would probably be the best for this simple example. Of course, generally it would be best if the model class exposed JavaFX properties itself. Then the cell-value factory would just return those properties, as demonstrated by other examples of the Javadoc.

    I will note that the default TableColumn.onEditCommit handler will write the new value to the ObservableValue returned by the cell-value factory if it also implements WritableValue. I suppose it could make sense to avoid this possibility by not returning an implementation of WritableValue. However, it would make even more sense to simply set the editable property of the TableColumn to false in this case (assuming the TableView is even editable in the first place) and not use a TableCell implementation that allows for editing.