Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8277456

Map, FlatMap and OrElse fluent bindings for ObservableValue

    XMLWordPrintable

Details

    • source, binary
    • minimal
    • No functional changes result from this change.
    • Java API
    • JDK

    Description

      Summary

      Add the methods map, flatMap and orElse to javafx.bean.value.ObservableValue which provide a new instance of ObservableValue with the given transformation applied, and the (protected) methods isObserved and allowValidation in javafx.beans.binding.ObjectBinding.

      Problem

      JavaFX currently does not provide a type-safe way to create property binding chains, instead relying on method names and reflection to create such bindings, see javafx.beans.binding.Bindings.select. Furthermore, this specific API is limited to this specific use case and does not allow for additional transformations or for dealing with a map or list property.

      Solution

      The proposed solution is to provide the ability to directly transform an ObservableValue into another by means of user provided lambda functions which are provided to methods that work similar to ones found in java.util.Optional.

      The type safe version of:

        label.textProperty().bind(
          Bindings.when(Bindings.selectBoolean(label.sceneProperty(), "window", "showing"))
            .then("Visible")
            .otherwise("Not Visible")
        );

      Would become with this proposed API:

        label.textProperty().bind(label.sceneProperty()
          .flatMap(Scene::windowProperty)  // new method 'flatMap'
          .flatMap(Window::showingProperty)  // new method 'flatMap'
          .orElse(false)  // new method 'orElse'
          .map(showing -> showing ? "Visible" : "Not Visible")  // new method 'map'
        );

      The proposal was discussed on the openjfx-dev mailinglist here: https://www.mail-archive.com/openjfx-dev@openjdk.java.net/msg21333.html and there was also some discussion on the initial PoC on Github here: https://github.com/openjdk/jfx/pull/434

      Specification

      Webrevs: Full ( https://openjdk.github.io/cr/?repo=jfx&pr=675&range=00 ) and Github diff ( https://github.com/openjdk/jfx/pull/675/files)

      The relevant parts that change public API are:

      In javafx.beans.value.ObservableValue:

      --- a/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
      +++ b/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
      @@ -137,4 +139,112 @@ public interface ObservableValue<T> extends Observable {
            * @return The current value
            */
           T getValue();
      +
      +    /**
      +     * Returns an {@code ObservableValue} that holds the result of applying the
      +     * given mapping function on this value. The result is updated when this
      +     * {@code ObservableValue} changes. If this value is {@code null}, no
      +     * mapping is applied and the resulting value is also {@code null}.
      +     * <p>
      +     * For example, mapping a string to an upper case string:
      +     * <pre>{@code
      +     * var text = new SimpleStringProperty("abcd");
      +     * ObservableValue<String> upperCase = text.map(String::toUpperCase);
      +     *
      +     * upperCase.getValue();  // Returns "ABCD"
      +     * text.set("xyz");
      +     * upperCase.getValue();  // Returns "XYZ"
      +     * text.set(null);
      +     * upperCase.getValue();  // Returns null
      +     * }</pre>
      +     *
      +     * @param <U> the type of values held by the resulting {@code ObservableValue}
      +     * @param mapper the mapping function to apply to a value, cannot be {@code null}
      +     * @return an {@code ObservableValue} that holds the result of applying the given
      +     *     mapping function on this value, or {@code null} when it
      +     *     is {@code null}; never returns {@code null}
      +     * @throws NullPointerException if the mapping function is {@code null}
      +     * @since 19
      +     */
      +    default <U> ObservableValue<U> map(Function<? super T, ? extends U> mapper) {
      +        return new MappedBinding<>(this, mapper);
      +    }
      +
      +    /**
      +     * Returns an {@code ObservableValue} that holds this value, or the given constant if
      +     * it is {@code null}. The result is updated when this {@code ObservableValue} changes. This
      +     * method, when combined with {@link #map(Function)}, allows handling of all values
      +     * including {@code null} values.
      +     * <p>
      +     * For example, mapping a string to an upper case string, but leaving it blank
      +     * if the input is {@code null}:
      +     * <pre>{@code
      +     * var text = new SimpleStringProperty("abcd");
      +     * ObservableValue<String> upperCase = text.map(String::toUpperCase).orElse("");
      +     *
      +     * upperCase.getValue();  // Returns "ABCD"
      +     * text.set(null);
      +     * upperCase.getValue();  // Returns ""
      +     * }</pre>
      +     *
      +     * @param constant the value to use when this {@code ObservableValue}
      +     *     holds {@code null}; can be {@code null}
      +     * @return an {@code ObservableValue} that holds this value, or the given constant if
      +     *     it is {@code null}; never returns {@code null}
      +     * @since 19
      +     */
      +    default ObservableValue<T> orElse(T constant) {
      +        return new OrElseBinding<>(this, constant);
      +    }
      +
      +    /**
      +     * Returns an {@code ObservableValue} that holds the value of an {@code ObservableValue}
      +     * produced by applying the given mapping function on this value. The result is updated
      +     * when either this {@code ObservableValue} or the {@code ObservableValue} produced by
      +     * the mapping changes. If this value is {@code null}, no mapping is applied and the
      +     * resulting value is {@code null}. If the mapping resulted in {@code null}, then the
      +     * resulting value is also {@code null}.
      +     * <p>
      +     * This method is similar to {@link #map(Function)}, but the mapping function is
      +     * one whose result is already an {@code ObservableValue}, and if invoked, {@code flatMap} does
      +     * not wrap it within an additional {@code ObservableValue}.
      +     * <p>
      +     * For example, a property that is only {@code true} when a UI element is part of a {@code Scene}
      +     * that is part of a {@code Window} that is currently shown on screen:
      +     * <pre>{@code
      +     * ObservableValue<Boolean> isShowing = listView.sceneProperty()
      +     *     .flatMap(Scene::windowProperty)
      +     *     .flatMap(Window::showingProperty)
      +     *     .orElse(false);
      +     *
      +     * // Assuming the listView is currently shown to the user, then:
      +     *
      +     * isShowing().getValue();  // Returns true
      +     *
      +     * listView.getScene().getWindow().hide();
      +     * isShowing().getValue();  // Returns false
      +     *
      +     * listView.getScene().getWindow().show();
      +     * isShowing().getValue();  // Returns true
      +     *
      +     * listView.getParent().getChildren().remove(listView);
      +     * isShowing().getValue();  // Returns false
      +     * }</pre>
      +     * Changes in any of the values of: the scene of {@code listView}, the window of that scene, or
      +     * the showing of that window, will update the boolean value {@code isShowing}.
      +     * <p>
      +     * This method is preferred over {@link javafx.beans.binding.Bindings#select Bindings} methods
      +     * since it is type safe.
      +     *
      +     * @param <U> the type of values held by the resulting {@code ObservableValue}
      +     * @param mapper the mapping function to apply to a value, cannot be {@code null}
      +     * @return an {@code ObservableValue} that holds the value of an {@code ObservableValue}
      +     *     produced by applying the given mapping function on this value, or
      +     *     {@code null} when the value is {@code null}; never returns {@code null}
      +     * @throws NullPointerException if the mapping function is {@code null}
      +     * @since 19
      +     */
      +    default <U> ObservableValue<U> flatMap(Function<? super T, ? extends ObservableValue<? extends U>> mapper) {
      +        return new FlatMappedBinding<>(this, mapper);
      +    }
      }

      In javafx.beans.binding.ObjectBinding:

      --- a/modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java
      +++ b/modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java
      @@ -182,6 +189,35 @@ public abstract class ObjectBinding<T> extends ObjectExpression<T> implements
               return valid;
           }
      
      +    /**
      +     * Checks if the binding has at least one listener registered on it. This
      +     * is useful for subclasses which want to conserve resources when not observed.
      +     *
      +     * @return {@code true} if this binding currently has one or more
      +     *     listeners registered on it, otherwise {@code false}
      +     * @since 19
      +     */
      +    protected final boolean isObserved() {
      +        return helper != null;
      +    }
      +
      +    /**
      +     * Checks if the binding is allowed to become valid. Overriding classes can
      +     * prevent a binding from becoming valid. This is useful in subclasses which
      +     * do not always listen for invalidations of their dependencies and prefer to
      +     * recompute the current value instead. This can also be useful if caching of
      +     * the current computed value is not desirable.
      +     * <p>
      +     * The default implementation always allows bindings to become valid.
      +     *
      +     * @return {@code true} if this binding is allowed to become valid, otherwise
      +     *     {@code false}
      +     * @since 19
      +     */
      +    protected boolean allowValidation() {
      +        return true;
      +    }
      +
           /**
            * Calculates the current value of this binding.
            * <p>

      In javafx.beans.binding.Bindings:

      --- a/modules/javafx.base/src/main/java/javafx/beans/binding/Bindings.java
      +++ b/modules/javafx.base/src/main/java/javafx/beans/binding/Bindings.java
      @@ -437,6 +437,10 @@ public final class Bindings {
            * <p>
            * Note: since 8.0, JavaBeans properties are supported and might be in the chain.
            * </p>
      +     * <p>
      +     * Since 19, it is recommended to use {@link ObservableValue#flatMap(java.util.function.Function)}
      +     * to select a nested member of an {@link ObservableValue}.
      +     * </p>
            *
            * @param <T> the type of the wrapped {@code Object}
            * @param root
      @@ -444,6 +448,7 @@ public final class Bindings {
            * @param steps
            *            The property names to reach the final property
            * @return the created {@link ObjectBinding}
      +     * @see ObservableValue#flatMap(java.util.function.Function)
            */
           public static <T> ObjectBinding<T> select(ObservableValue<?> root, String... steps) {
               return new SelectBinding.AsObject<T>(root, steps);

      Attachments

        Issue Links

          Activity

            People

              jhendrikx John Hendrikx
              jhendrikx John Hendrikx
              Kevin Rushforth
              Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved: