Creating aggregate JavaFX bindings

The standard JavaFX API provides a class named Bindings that can be used to create all kinds of bindings your application may need. Methods in this class can be used to translate any Observable, ObservableValue, Binding, Expression, and/or Property into another Binding, regardless of their original type. Here’s for example how you could translate a StringProperty into an IntegerBinding, by exposing the length of the contained String:

StringBinding sourceBinding = ... // initialized elsewhere
IntegerBinding lengthBinding = Bindings.createIntegerBinding(
    () -> sourceBinding.get().length(),
    sourceBinding);

While quick and to the point we’re forced to read the value directly from the inputs (in this case sourceProperty) instead of receiving the value as is. Here’s another typical scenario: calculate an aggregate binding based on a list of observable values. Say you have an observable class that exposes a BooleaProperty to indicate its valid state; perhaps this class looks like the following one

public class ObservableBean {
    private final BooleanProperty valid = new SimpleBooleanProperty(this, "valid", false);
    // additional properties + getter/setters

    public boolean isValid() {
        return valid.get();
    }

    public BooleanProperty validProperty() {
        return valid;
    }

    public void validate() {
        // ...
    }
}

Finding out if all of the entries of type ObservableBean are valid and exposing that result as a BooleanBinding can be done in this way

ObservableList<ObservableBean> beans = FXCollections.observableArrayList();

BooleanBinding allValid = Bindings.createBooleanBinding(
    () -> beans.stream().allMatch(ObservableBean::isValid), beans);

There are a couple of issues with this code. First we’re forced to extract the values again; second we’ve got lucky as the resulting type is a boolean, this means we don’t have to worry about the case when the list is empty as the resulting BooleanBinding will contain a false value. But what if we’re aggregating over type that’s not a boolean and we need a non-null value? How do we specify such default value? This is where Griffon’s JavaFX Binding support comes in.

We can rewrite the first sample code in a more concise way using Griffon’s MappingBindings utility class:

StringProperty sourceProperty = new SimpleStringProperty();

IntegerBinding lengthBinding = MappingBindings.mapAsInteger(
    sourceProperty, String::length);

We can now make use of the actual value, which in this case is supplied to a Function, masked as a method reference. You'll find other variants of this mapping method supporting all basic binding types. What's even better if that there are overloaded versions that take an observable Function, thus you can update the calculating function at any time you so desire, for example

StringProperty sourceProperty = new SimpleStringProperty();

ObjectProperty<Function<String, Integer>> string2int = new SimpleObjectProperty<>();
string2int.set(String::length);

IntegerBinding lengthBinding = MappingBindings.mapAsInteger(
    sourceProperty, string2int);

string2int.setValue(s -> s.length() * 2);  // double it up!

Moving on to the aggregate binding, there's another utility class named ReducingBindings that offers map/reduce capabilities on an observable collection of values (yes, this includes observable variants of List, Set, and Map).

ObservableList<ObservableBean> beans = FXCollections.observableArrayList();

BooleanBinding allValid = ReducingBindings.mapToBooleanThenReduce(beans, false,
    ObservableBean::isValid, (a, b) -> a && b);

We cal also rewrite this code by applying a function/matcher combination, like this

ObservableList<ObservableBean> beans = FXCollections.observableArrayList();

BooleanBinding allValid = MatchingBindings.allMatch(beans,
             ObservableBean::isValid, // map to boolean
             bool -> bool);  // predicate

However if you recall the first version of the code relied on the stream capabilities added in Java 8. Wouldn't it be great to use those same capabilities in an observable manner? If you think that's a good thing to have then I have news for you: Griffon exposes an ObservableStream class that offers the same contract as a Stream, this being said it is not a subtype of Stream.

ObservableList<ObservableBean> beans = FXCollections.observableArrayList();

BooleanBinding allValid = GriffonFXCollections.observableStream(beans)
    .allMatch(ObservableBean::isValid);

One more thing, all these aggregate bindings will trigger an update whenever an element is added or removed from the collection but not when the internal state of an element is updated, that is, if an ObservableBean changes valid state from false to true the aggregate will fail to see the update. This is clearly not what we want. We need a way to listen to internal changes within elements, with this use case in mind Griffon's JavaFX support exposes ElementObservableList, an implementation of ObservableList that's aware of internal element updates. You simply must implement a marker interface, ElementObservableList.ObservableValueContainer, or provide a custom strategy that implements ElementObservableList.ObservableValueExtractor, for example

ObservableList<ObservableBean> beans = FXCollections.observableArrayList();

ElementObservableList ebeans = new ElementObservableList<>(beans,
    new ElementObservableList.ObservableValueExtractor<ObservableBean>() {
        @Nonnull
        @Override
        public ObservableValue<?>[] observableValues(@Nullable ObservableBean instance) {
            return new ObservableValue[]{instance.validProperty()};
        }
    });

BooleanBinding allValid = GriffonFXCollections.observableStream(ebeans)
    .allMatch(ObservableBean::isValid);

You can find other utility methods and classes in Griffon's JavaFX Binding support:

You may use these classes with any JavaFX application, you're not required to use the full features of the Griffon framework to do so, you only have to configure the griffon-javafx artifact in your build, for example

Maven

<dependency>
    <groupId>org.codehaus.griffon</groupId>
    <artifactId>griffon-javafx</artifactId>
    <version>2.15.0</version>
</dependency>

Gradle

dependencies {
    compile 'org.codehaus.griffon:griffon-javafx:2.15.0'
}

Et voilà! That is all. Enjoy writing JavaFX applications that take advantage of bindings without having to write so much low level transformations.

Keep on coding!

2 comments

  • Hi Andres. I’m doing my thesis and I needed some advice. I want to develop a desktop application and my fist choice was Griffon. But in a thesis from last year the following was said:

    “One of the biggest flaws of Griffon, is, that it is primarily targeted at the Swing UI toolkit. It can however be used with JavaFX, considering that features are not as integrated as with swing. It is a problem for the controls community, as they want to use JavaFX as the main UI toolkit. Besides this flaw, Griffon enforces the use of the model, view, controller pattern. It follows the Swing application framework, which also defines the application’s life cycle.”

    My question is, this is a quote from 2017, can you tell me if this “problem” still persists?

    Thank you, Inês

    • Hi Inês,

      Do you have a link to the full thesis from where the comment was lifted? Would like to read it in context.

      Setting the record straight: Griffon 1.x was created with Swing support in mind, JavaFX support was added later as a bolt-on. Griffon 2.x was redesigned from scratch to better accommodate the differences between different UI toolkits, there’re no Swing abstractions leaking into JavaFX and viceversa, also you may use Java, Groovy, Kotlin, either in isolation or in conjuction. My personal preference when writing Griffon applications is to follow the PMVC design pattern however Griffon does not enforce a explicit pattern, you can combine artifacts in any way you deem fit, as shown at http://griffon-framework.org/tutorials/5_mvc_patterns.html; you can even pick a totally different pattern, bearing in mind that additional configuration may be required.

Trackbacks/Pingbacks

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

ˆ Back To Top