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.