Mark Mark - 1 month ago 14
Java Question

How to bind a property to properties of objects in a list property?

I have these classes:

class Parent {

BooleanProperty total = new SimpleBooleanProperty();
SimpleListProperty<Child> children = new SimpleListProperty<>(FXCollections.observableArrayList());
}

class Child {

BooleanProperty single = new SimpleBooleanProperty();
}


What I want is that
total
will be false if and only if all of the children's
single
s are false. In other words that if and only if there is one child with
single
as true then
total
is true.

I came up with this binding

total.bind(Bindings.createBooleanBinding(() -> {
return children.stream().filter(c -> c.isSingle()).findAny().isPresent();
}, children.stream().map(c -> c.single).collect(Collectors.toCollection(FXCollections::observableArrayList))));


Is this the best way?

Also should i make
total
read only because writing to a bound property will throw an exception?

Answer

One possible problem with binding explicitly to every child's single property, is that these bindings are made when the total.bind(...) statement is invoked. Consequently, if new Child objects are subsequently added to the list, those child's single properties will not be observed and the binding will not be invalidated if they change. Similarly, if a child is removed from the list, its single property will not be unbound.

As an alternative, you can create the list with an extractor. Note that for most use cases, you have no need for a ListProperty: just using an ObservableList directly is sufficient. So you can just do

ObservableList<Child> children = 
    FXCollections.observableArrayList(c -> new Observable[]{c.singleProperty()});

This list will now fire update notifications (and become invalid) if any of the elements single properties are invalidated (as well as when elements are added to and from the list). In other words, the list itself observes each child's single property, and the list becomes invalid (causing anything bound to it to become invalid) if any of the children's single properties become invalid. The list also makes sure it observes new children's single properties when they are added to the list, and stops observing them when children are removed from the list.

Then you just need

total.bind(Bindings.createBooleanBinding(() -> 
    children.stream().anyMatch(Child::isSingle), children);

Finally, consider using a ReadOnlyBooleanWrapper in order to expose the property as read only. Here's a properly-encapsulated version of the whole thing:

public class Parent {

    private ReadOnlyBooleanWrapper total = new ReadOnlyBooleanWrapper();
    private ObservableList<Child> children = 
        FXCollections.observableArrayList(c -> new Observable[] {c.singleProperty()});

    public Parent() {
        total.bind(Bindings.createBooleanBinding(() -> 
            children.stream().anyMatch(Child::isSingle), children);
    }

    public ReadOnlyBooleanProperty totalProperty() {
        return total.getReadOnlyProperty();
    }

    public ObservableList<Child> getChildren() {
        return children ;
    }

}

and

public class Child {
    private final BooleanProperty single = new SimpleBooleanProperty();

    public BooleanProperty singleProperty() {
        return single ;
    }

    public final boolean isSingle() {
        return singleProperty().get();
    }

    public final void setSingle(boolean single) {
        singleProperty().set(single);
    }
}