Search code examples
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):

    • The x, y, width, height, screen, and showing properties of Window.

    • The x, y, width, height, and window properties of Scene.

    • The parent property of Node and TreeItem.

    • The tableView property of TableColumn and TableCell.

    • The listView property of ListCell.

    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:

    • A SimpleObjectProperty. This keeps it simple and avoids the confusion caused by returning a type that includes "read only" in the name yet is still writable.

    • A "purer" implementation of ObservableValue that's not a full-blown [ReadOnly]Property and that always delegates to the model object. Could also be an implementation of WritableValue to write values back to the model object due to, e.g., editing. But this is likely overkill, especially since the ObservableValue won't fire any invalidation events without even more effort.

    • An immutable implementation of ObservableValue if such an implementation existed in the JavaFX library.

    • A [ReadOnly]JavaBeanObjectProperty adapter, given the example is specifically about interacting with classes not designed with JavaFX properties in mind (though still seem to follow JavaBean conventions). A downside of these adapters is that building them means handling NoSuchMethodException, a checked exception.

    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.