3

I have a few places where I want to check first if the selectedItemProperty() of a ChoiceBox's selectionModel is null, and if it is not null, to check some property of the selected item.

What I would ideally like is something like:

button.disableProperty().bind(
    choiceBox.getSelectionModel().selectedItemProperty().isNull().or(
        choiceBox.getSelectionModel().selectedItemProperty().get().myProperty()
    )
);

However, this fails, because the return value of "javafx.beans.property.ReadOnlyObjectProperty.get()" is null (ie, it's trying to fetch the selected item to evaluate myProperty()).

Instead, I can do something like

button.disableProperty().bind(
    Bindings.createBooleanBinding(
        () -> {
            if (choiceBox.getSelectionModel().selectedItemProperty().isNull())
                return true;
            return choiceBox.getSelectionModel().selectedItemProperty().get().myProperty().get();
            },
            choiceBox.getSelectionModel().selectedItemProperty()
        }
    )
);

However, this only updates if the selected item changes; it's completely ignorant of changes in the underlying myProperty(). I'd like to be able to propagate changes in the underlying property. What I really want here is for the or to be short-circuiting, that is for it to see that the selectedItemProperty is null and to not carry on, but if it is not null, to check the underlying property.

alan ocallaghan
  • 3,116
  • 17
  • 37

2 Answers2

6

In JavaFX 19 and later you can do

button.disableProperty().bind(
    choiceBox.getSelectionModel().selectedItemProperty()
             .flatMap(selection -> selection.myProperty())
             .orElse(Boolean.TRUE)
);

For more information on flatMap(), orElse(), and related methods, see this Q/A.

Prior to JavaFX 19 you need something like the following (this is not tested):

choiceBox.getSelectionModel().selectedItemProperty().addListener(
    (obs, oldSelection, newSelection) -> {
        if (newSelection == null) {
            button.disableProperty().unbind();
            button.setDisable(true);
        } else {
            button.disableProperty().bind(newSelection.myProperty());
        }
    }
);
if (choiceBox.getSelectionModel().getSelectedItem() == null) {
    button.setDisable(true);
} else {
    button.disableProperty().bind(choiceBox.getSelectionModel().getSelectedItem().myProperty());
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Nice, thanks. Project is currently on 19.0.2 but hopefully can bump it without breaking anything – alan ocallaghan Aug 22 '23 at 14:24
  • 1
    @alanocallaghan I was just looking at the Javadocs for `flatMap()`, and apparently it was introduced in JavaFX 19 (not 20 as I had originally stated). So you won't need to update your JavaFX version (though you should be able to with no issues anyway). – James_D Aug 23 '23 at 17:22
  • Indeed, thanks again – alan ocallaghan Aug 23 '23 at 18:29
5

Select Bindings (JavaFX < 19)

James_D's answer is perfect for JavaFX 19+. For older versions of JavaFX, I wanted to offer an alternative:

void bindDisableProperty(Node node, ChoiceBox<Foo> choiceBox) {
    BooleanBinding selectionNotNull = Bindings
            .select(choiceBox, "selectionModel", "selectedItem")
            .isNotNull();

    BooleanBinding flag = Bindings
            .selectBoolean(choiceBox, "selectionModel", "selectedItem", "flag");
    
    BooleanBinding disable = Bindings
            .when(selectionNotNull)
            .then(flag)
            .otherwise(true);
    
    node.disableProperty().bind(disable);
}

Note: See the full example below for the definition of Foo and its flag property.

If you know the selection model will never change, then you can simply do:

ReadOnlyObjectProperty<Foo> selectedItem = choiceBox
        .getSelectionModel()
        .selectedItemProperty();
BooleanBinding selectionNotNull = selectedItem.isNotNull();
BooleanBinding flag = Bindings.selectBoolean(selectedItem, "flag");

// ...

And typically the selection model won't be changed. Of course, in that case, using the listener approach shown in James_D's answer might be preferred. Especially since when using a "select binding" you rely on reflection, and thus run into the same disadvantages as described in Why should I avoid using PropertyValueFactory in JavaFX?.

Here's a full example:

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        ChoiceBox<Foo> choiceBox = new ChoiceBox<>();
        choiceBox.getItems().addAll(new Foo(true), new Foo(true), new Foo(false), new Foo(false), new Foo(true));
        choiceBox.setConverter(new StringConverter<Foo>() {
            @Override
            public String toString(Foo object) {
                return object == null ? "<no selection>" : "Foo (" + object.isFlag() + ")";
            }

            @Override
            public Foo fromString(String string) {
                throw new UnsupportedOperationException();
            }
        });

        Button clearSelectionBtn = new Button("Clear Selection");
        clearSelectionBtn.setOnAction(e -> {
            e.consume();
            choiceBox.setValue(null);
        });

        TextField toDisable = new TextField("I am a text field!");
        bindDisableProperty(toDisable, choiceBox);

        VBox root = new VBox(10, clearSelectionBtn, choiceBox, toDisable);
        root.setPadding(new Insets(10));
        root.setAlignment(Pos.TOP_CENTER);

        primaryStage.setScene(new Scene(root, 600, 400));
        primaryStage.show();
    }

    private void bindDisableProperty(Node node, ChoiceBox<Foo> choiceBox) {
        BooleanBinding selectionNotNull =
                Bindings.select(choiceBox, "selectionModel", "selectedItem").isNotNull();
        BooleanBinding flag = Bindings.selectBoolean(choiceBox, "selectionModel", "selectedItem", "flag");
        BooleanBinding disable = Bindings.when(selectionNotNull).then(flag).otherwise(true);
        node.disableProperty().bind(disable);
    }

    public static class Foo {

        private final BooleanProperty flag = new SimpleBooleanProperty(this, "flag");

        public final void setFlag(boolean flag) {
            this.flag.set(flag);
        }

        public final boolean isFlag() {
            return flag.get();
        }

        public final BooleanProperty flagProperty() {
            return flag;
        }

        public Foo() {}

        public Foo(boolean flag) {
            setFlag(flag);
        }
    }
}

Type-Safe Alternative to Select Bindings

I've always had an idea on how to create a type-safe version of the "select bindings" offered by JavaFX. Decided to actually code the idea up. Note I have not tested this code, so there may be bugs. Also note there are no primitive specializations of the below class.

import java.util.Objects;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.util.Callback;

/**
 * A type-safe alternative to {@link Bindings#select(Object, String...) select} bindings. A {@code Chain} is made up
 * of one or more "links" of {@linkplain ObservableValue observable values}. Except for the "root" link of the chain,
 * the observable value of a link is computed by a {@link Callback} based on the observable value of the previous
 * link. If any link's {@code Callback} in the chain returns {@code null}, then the chain will be "broken" at that link
 * and the end value of the chain will be {@code null}.
 *
 * <p>A {@code Chain} is created via the {@link Builder} class. Use the {@link #from(ObservableValue)} and {@link
 * #fromValue(Object)} methods to create a new builder.
 *
 * <p>Unless otherwise stated, passing {@code null} to any method of this API will result in a {@code
 * NullPointerException} being thrown.
 *
 * @param <T> The value type.
 */
public class Chain<T> extends ObjectBinding<T> {

    /**
     * Creates a new builder for a chain with the given observable as its root.
     *
     * @param observable the root observable value
     * @return a new {@code Builder}
     * @param <T> The observable's value type.
     * @param <U> The observable type.
     * @see #fromValue(Object)
     */
    public static <T, U extends ObservableValue<T>> Builder<T, U> from(U observable) {
        Objects.requireNonNull(observable);
        return new Builder<>(observable);
    }

    /**
     * Creates a new builder for a chain with the given <b>value</b> as its root. This method wraps {@code value} in an
     * {@code ObservableValue} implementation whose {@code getValue()} method will always return {@code value}.
     *
     * @param value the root value; may be {@code null}
     * @return a new {@code Builder}
     * @param <T> The value type.
     * @see #from(ObservableValue)
     */
    public static <T> Builder<T, ?> fromValue(T value) {
        return from(new ImmutableObservableValue<>(value));
    }

    private final Link<?, ?, T, ?> tail;

    private Chain(Link<?, ?, T, ?> tail) {
        this.tail = tail;
        bind(tail);
    }

    @Override
    protected T computeValue() {
        ObservableValue<T> observable = tail.get();
        return observable == null ? null : observable.getValue();
    }

    @Override
    public void dispose() {
        tail.dispose();
    }

    /**
     * A builder class for {@link Chain} objects. A {@code Builder} instance is not reusable. Once any of the "link"
     * methods or the {@code build} method are invoked, invoking any of them again will result in an {@code 
     * IllegalStateException} being thrown. Note that the "link" methods all return a <b>new</b> instance of {@code 
     * Builder}.
     * 
     * <p>This class provides four "link" methods for different scenarios.
     * 
     * <table>
     *     <tbody>
     *         <tr>
     *             <th>Method</th>
     *             <th>Purpose</th>
     *         </tr>
     *         <tr>
     *             <td>{@link #link(Callback)}</td>
     *             <td>To map the previous {@code ObservableValue} to another {@code ObservableValue}.</td>
     *         </tr>
     *         <tr>
     *             <td>{@link #linkFromValue(Callback)}</td>
     *             <td>To map the <b>value</b> of the previous {@code ObservableValue} to an {@code ObservableValue}.</td>
     *         </tr>
     *         <tr>
     *             <td>{@link #linkToValue(Callback)}</td>
     *             <td>To map the previous {@code ObservableValue} to a <b>value</b>.</td>
     *         </tr>
     *         <tr>
     *             <td>{@link #linkByValue(Callback)}</td>
     *             <td>To map the <b>value</b> of the previous {@code ObservableValue} to a <b>value</b>.</td>
     *         </tr>
     *     </tbody>
     * </table>
     *
     * @param <T> The observable's value type.
     * @param <U> The observable type.
     * @see Chain#from(ObservableValue)
     * @see Chain#fromValue(Object)
     */
    public static class Builder<T, U extends ObservableValue<T>> {

        private final Link<?, ?, T, U> tail;
        private boolean valid = true;

        // creates a builder for the root link
        Builder(U observable) {
            this.tail = new Link<>(observable);
        }

        // creates a builder for the next link in the chain
        Builder(Link<?, ?, T, U> tail) {
            this.tail = tail;
        }

        /**
         * Creates a link that maps the previous link's observable to a new observable.
         *
         * <p>The {@code callback} will never be invoked with a {@code null} argument.
         *
         * @param callback the callback to compute the new link's observable
         * @return a <b>new</b> {@code Builder} instance
         * @param <X> The next observable's value type.
         * @param <Y> The next observable's type.
         * @throws IllegalStateException if this builder has already been used to create the next link or build the
         * chain
         */
        public <X, Y extends ObservableValue<X>> Builder<X, Y> link(Callback<? super U, ? extends Y> callback) {
            Objects.requireNonNull(callback);
            invalidate();
            return new Builder<>(new Link<>(tail, callback));
        }

        /**
         * Creates a link that maps the <b>value</b> of the previous link's observable to a new observable.
         *
         * <p>The {@code callback} will never be invoked with a {@code null} argument.
         *
         * @param callback the callback to compute the new link's observable
         * @return a <b>new</b> {@code Builder} instance to continue building the chain
         * @param <X> The next observable's value type.
         * @param <Y> The next observable's type.
         * @throws IllegalStateException if this builder has already been used to create the next link or build the
         * chain
         */
        public <X, Y extends ObservableValue<X>> Builder<X, Y> linkFromValue(
                Callback<? super T, ? extends Y> callback) {
            Objects.requireNonNull(callback);
            invalidate();
            return new Builder<>(new Link<>(tail, observable -> {
                T value = observable.getValue();
                return value == null ? null : callback.call(value);
            }));
        }

        /**
         * Creates a new link that maps the previous link's observable to a new <b>value</b>. The new value, when not
         * {@code null}, will be wrapped in an {@code ObservableValue} implementation whose {@code getValue()} method
         * will always return said new value. If the value of the previous observable is {@code null} then the chain
         * is considered broken.
         *
         * <p>The {@code callback} will never be invoked with a {@code null} argument.
         *
         * @param callback the callback to compute the new link's value
         * @return a <b>new</b> {@code Builder} instance to continue building the chain
         * @param <X> The next observable value's value type.
         * @throws IllegalStateException if this builder has already been used to create the next link or build the
         * chain
         */
        public <X> Builder<X, ?> linkToValue(Callback<? super U, ? extends X> callback) {
            Objects.requireNonNull(callback);
            invalidate();
            return new Builder<>(new Link<>(tail, observable -> {
                X nextValue = callback.call(observable);
                return nextValue == null ? null : new ImmutableObservableValue<>(nextValue);
            }));
        }

        /**
         * Creates a new link that maps the <b>value</b> of the previous link's observable to a new <b>value</b>. The
         * new value, when not {@code null}, will wrapped in an {@code ObservableValue} implementation whose {@code
         * getValue()} method will always return said new value. If the value of the previous observable is {@code null}
         * then the chain is considered broken.
         *
         * <p>The {@code callback} will never be invoked with a {@code null} argument.
         *
         * @param callback the callback to compute the new link's value
         * @return a <b>new</b> {@code Builder} instance to continue building the chain
         * @param <X> The next observable value's value type.
         * @throws IllegalStateException if this builder has already been used to create the next link or build the
         * chain
         */
        public <X> Builder<X, ?> linkByValue(Callback<? super T, ? extends X> callback) {
            Objects.requireNonNull(callback);
            invalidate();
            return new Builder<>(new Link<>(tail, observable -> {
                T value = observable.getValue();
                if (value == null) {
                    return null;
                }
                X nextValue = callback.call(value);
                return nextValue == null ? null : new ImmutableObservableValue<>(nextValue);
            }));
        }

        /**
         * Builds and returns the {@code Chain}.
         *
         * @return the built {@code Chain}
         * @throws IllegalStateException if this builder has already been used to create the next link or build the
         * chain
         */
        public Chain<T> build() {
            invalidate();
            return new Chain<>(tail);
        }

        private void invalidate() {
            if (!valid) {
                throw new IllegalStateException("builder already used to create next link or build chain");
            }
            valid = false;
        }
    }

    private static class Link<T, U extends ObservableValue<T>, X, Y extends ObservableValue<X>>
            extends ObjectBinding<Y> {

        private final boolean root;
        private final Link<?, ?, T, U> previous;
        private final Callback<? super U, ? extends Y> callback;

        private Y observable;

        // creates a root link
        Link(Y observable) {
            this.root = true;
            this.previous = null;
            this.callback = null;

            this.observable = observable;
            bind(observable);
        }

        // creates a non-root link
        Link(Link<?, ?, T, U> previous, Callback<? super U, ? extends Y> callback) {
            this.root = false;
            this.previous = previous;
            this.callback = callback;

            bind(previous);
        }

        @Override
        protected Y computeValue() {
            /*
             * A link can become invalid in one of two ways:
             *
             *   - The previous link is invalidated.
             *   - The observable of this link is invalidated.
             *
             * Only in the first case do we need to recompute the observable for
             * this link. Whether or not the observable needs to be recomputed
             * upon invalidation is handled by the onInvalidating() method.
             */
            if (!root && observable == null) {
                U previousObservable = previous.get();
                if (previousObservable != null) {
                    observable = callback.call(previousObservable);
                    if (observable != null) {
                        bind(observable);
                    }
                }
            }
            return observable;
        }

        @Override
        protected void onInvalidating() {
            if (!root && !previous.isValid() && observable != null) {
                unbind(observable);
                observable = null;
            }
        }

        @Override
        public void dispose() {
            if (observable != null) {
                unbind(observable);
                observable = null;
            }
            if (!root) {
                unbind(previous);
                previous.dispose();
            }
        }
    }

    private static class ImmutableObservableValue<T> implements ObservableValue<T> {

        private final T value;

        ImmutableObservableValue(T value) {
            this.value = value;
        }

        @Override
        public T getValue() {
            return value;
        }

        /*
         * Since this observable value is immutable, there's no reason to store
         * the listeners. These methods simply check the argument for null in
         * order to minimally satisfy their contracts.
         */

        @Override
        public void addListener(ChangeListener<? super T> listener) {
            Objects.requireNonNull(listener);
        }

        @Override
        public void removeListener(ChangeListener<? super T> listener) {
            Objects.requireNonNull(listener);
        }

        @Override
        public void addListener(InvalidationListener listener) {
            Objects.requireNonNull(listener);
        }

        @Override
        public void removeListener(InvalidationListener listener) {
            Objects.requireNonNull(listener);
        }
    }
}

Using Chain, you can then bind the disable property like so:

void bindDisableProperty(Node node, ChoiceBox<Foo> choiceBox) {
    Chain<Foo> selectedItem = Chain.from(choiceBox.selectionModelProperty())
            .linkFromValue(SelectionModel::selectedItemProperty)
            .build();

    ObjectBinding<Boolean> disable = Bindings.when(selectedItem.isNotNull())
            .then(Chain.from(selectedItem).linkFromValue(Foo::flagProperty).build())
            .otherwise(Boolean.TRUE);

    node.disableProperty().bind(disable);
}

Of course, there may be an existing library that does something similar.

Slaw
  • 37,820
  • 8
  • 53
  • 80