仅检查项目的 JavaFX 属性 isNotNull()

ala*_*han 3 java javafx

我有几个地方想首先检查selectedItemProperty()a 的是否为ChoiceBoxselectionModel,如果不为空,则检查所选项目的某些属性。

我理想中想要的是这样的:

button.disableProperty().bind(
    choiceBox.getSelectionModel().selectedItemProperty().isNull().or(
        choiceBox.getSelectionModel().selectedItemProperty().get().myProperty()
    )
);
Run Code Online (Sandbox Code Playgroud)

但是,这会失败,因为the return value of "javafx.beans.property.ReadOnlyObjectProperty.get()" is null(即,它正在尝试获取所选项目进行评估myProperty())。

相反,我可以做类似的事情

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

但是,仅当所选项目发生更改时才会更新;它完全不知道底层的变化myProperty()。我希望能够传播底层属性的更改。我真正想要的是or短路,即它看到 为selectedItemProperty空并且不继续,但如果它不为空,则检查基础属性。

Jam*_*s_D 6

在 JavaFX 19 及更高版本中,您可以执行以下操作

button.disableProperty().bind(
    choiceBox.getSelectionModel().selectedItemProperty()
             .flatMap(selection -> selection.myProperty())
             .orElse(Boolean.TRUE)
);
Run Code Online (Sandbox Code Playgroud)

flatMap()有关、orElse()和相关方法的更多信息,请参阅此 Q/A

在 JavaFX 19 之前,您需要类似以下内容(未经测试):

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());
}
Run Code Online (Sandbox Code Playgroud)


Sla*_*law 5

选择绑定 (JavaFX < 19)

James_D 的答案非常适合 JavaFX 19+。对于旧版本的 JavaFX,我想提供一种替代方案:

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);
}
Run Code Online (Sandbox Code Playgroud)

注意:有关 的定义Foo及其flag属性,请参阅下面的完整示例。

如果您知道选择模型永远不会改变,那么您可以简单地执行以下操作:

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

// ...
Run Code Online (Sandbox Code Playgroud)

通常选择模型不会改变。当然,在这种情况下,使用 James_D 的答案中所示的侦听器方法可能是首选。特别是因为当使用“选择绑定”时,您依赖于反射,因此会遇到与为什么我应该避免在 JavaFX 中使用 PropertyValueFactory? 中描述的相同缺点。

这是一个完整的示例:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

选择绑定的类型安全替代方案

我一直对如何创建 JavaFX 提供的“选择绑定”的类型安全版本有一个想法。决定将这个想法实际编码。请注意,我尚未测试此代码,因此可能存在错误。另请注意,以下类没有原始特化。

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用Chain,您可以disable像这样绑定属性:

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);
}
Run Code Online (Sandbox Code Playgroud)

当然,可能有一个现有的库可以做类似的事情。