Boost your JavaFX applications with Griffon

The Griffon team recently released version 2.11.0 live on stage at Gr8Conf EU, and with it comes the latest batch of updates and features that make it the best choice for writing JavaFX applications. In this post I'd like to showcase some of the features you may want to explore for your next JavaFX project. And don't worry, you can still use these features in your application even if you do not use Griffon as your application framework of choice. Scroll to the end of the page to figure out how it can be done.

JavaFX delivers a brand new host of capabilities and features when compared to other UI toolkits in the Java space, arguably bindings is one of the most eye catching as it can reduce the amount of boilerplate code required in order to keep two data elements in sync. Bindings are used by JavaFX widgets everywhere, thus it makes sense to use them in model classes to keep track of data and be able to update the UI when said data changes. Let's assume we have a simple JavaFX application that's capable of displaying read-only data coming from an external source, such as temperature or pressure sensor. The following screenshots show how this simple application may look like

Figure 1. Application screen upon startup
Figure 2. Data displayed in tabular view
Figure 3. Data displayed in diagram view

The full source code for the application can be found here. You may run it from the command line by invoking gradlew run or running the org.kordamp.griffon.examples.bindings.Launcher class from your IDE. The application has a couple of buttons (add, clear) to simulate sensor data coming into, in order to keep the application self-contained. The main responsibility of the application is to keep track a list of "events" or samples and visualize them in different ways. Notice that the gauges (from the Medusa widget library) display the minimum, average, and maximum values for the amount property of each measurement. Both the table and diagram views display the same data but in a different way. Clicking on the Add button will insert a new measurement while clicking on the Clear button will remove all measurements.

Data will be fed to the application on a background thread, as we have to follow the golden rule of Java UI programming, which is

All UI related operations, such as painting, redraw, read/write widget properties, must occur inside the UI thread. All non-UI related operations, such as disk access, network calls, computations, must occur outside of the UI thread.

Failure to follow this rule will result in UI glitches, corrupted data, unresponsive applications and more. We definitely want to avoid these headaches right away thus the measurement list is defined as a regular JavaFX ObservableList; the application makes sure to mutate its content in a thread that's not the UI thread. This leads to the measurements list to be defined on a model class like follows

package org.kordamp.griffon.examples.bindings;

import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import javafx.collections.ObservableList;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;

import javax.annotation.Nonnull;
import static javafx.collections.FXCollections.observableArrayList;

@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
    private final ObservableList<Measurement> measurements = observableArrayList();

    @Nonnull
    public ObservableList<Measurement> getMeasurements() {
        return measurements;
    }
}

But then again those measurements must be displayed by the UI which means updates must be triggered inside the UI thread. What to do? What if there was a way to ensure updates made to the list were published inside the UI thread? This is where the first JavaFX feature appears in the form of UIThreadAwareBindings. This class provides a set of methods that can create new bindings and ObservableValues that guarantee that their updates will be pushed inside the UI thread. We can safely wrap the measurements list with UIThreadAwareBindings.uiThreadAwareObservableList() and continue with the new list. This code should be placed inside the View class. Now, for finding out the minimum, average, maximum, and total values we have the choice of manipulating this brand new UI Thread aware list of measurements which means data transformations must be defined in the View class, or we can keep them in the Model class. I personally prefer the latter as we can test and inspect the data transformations completely outside of any UI concerns, that makes it easier for testing too. We'll have to revisit this decision when it comes to visualizing those values. Don't worry, we have that part covered too.

Calculating a single value out of a list of elements is akin to applying a reduction on the list. Java8 added new capabilities on top of the Collections API such as Streams and extension methods like map, filter, and reduce. These are exactly the type of operations we need to invoke on the list of measurements. We can apply the required transformations as follows

package org.kordamp.griffon.examples.bindings;

import griffon.core.GriffonApplication;
import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;

import javax.annotation.Nonnull;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Function;

import static griffon.javafx.beans.binding.CollectionBindings.averageInList;
import static griffon.javafx.beans.binding.MappingBindings.mapObject;
import static griffon.javafx.beans.binding.ReducingBindings.mapToIntegerThenReduce;
import static griffon.javafx.beans.binding.ReducingBindings.reduce;
import static javafx.beans.binding.Bindings.createIntegerBinding;
import static javafx.collections.FXCollections.observableArrayList;

@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
    private final ObservableList<Measurement> measurements = observableArrayList();

    private final NumberBinding minAmount;
    private final NumberBinding maxAmount;
    private final NumberBinding avgAmount;
    private final ObjectBinding<Measurement> minMeasurement;
    private final ObjectBinding<Measurement> maxMeasurement;
    private final ObjectBinding<String> minName;
    private final ObjectBinding<String> maxName;
    private final ObjectBinding<String> minTimestamp;
    private final ObjectBinding<String> maxTimestamp;
    private final IntegerBinding total;

    public SampleModel() {
        minAmount = mapToIntegerThenReduce(measurements, 0, Measurement::getAmount, Math::min);
        maxAmount = mapToIntegerThenReduce(measurements, 0, Measurement::getAmount, Math::max);
        avgAmount = averageInList(measurements, 0, Measurement::getAmount);

        Measurement defaultMeasurement = new Measurement("", 0);
        minMeasurement = reduce(measurements, defaultMeasurement, measureReductor(Math::min));
        maxMeasurement = reduce(measurements, defaultMeasurement, measureReductor(Math::max));

        minName = mapObject(minMeasurement, Measurement::getName);
        maxName = mapObject(maxMeasurement, Measurement::getName);

        Function<Measurement, String> timestampMapper = m -> m != defaultMeasurement ? m.getTimestamp().toString() : "";
        minTimestamp = mapObject(minMeasurement, timestampMapper);
        maxTimestamp = mapObject(maxMeasurement, timestampMapper);

        total = createIntegerBinding(measurements::size, measurements);
    }

    private static BinaryOperator<Measurement> measureReductor(BinaryOperator<Integer> reductor) {
        return (m1, m2) -> m1.getAmount() == reductor.apply(m1.getAmount(), m2.getAmount()) ? m1 : m2;
    }

    // getters
}

Obtaining the minimum and maximum values is just a matter of mapping each measurement element to its amount property, then applying a reduction using Math::min or Math::max accordingly by leveraging ReducingBindings. Calculating the average value requires a different transformation provided by CollectionBindings. Notice that the application also displays the name and timestamp for the minimum and maximum measurements; these values are calculated by applying additional mapping and reduction transformations via MappingBindings paired with ReducingBindings. The advantage of creating bindings this way is that any changes made to the measurement list will trigger updates in these bindings.

Going back the View we must make sure that the newly created bindings are visualized correctly, that is, their updates are sent inside the UI thread. We can use UIThreadAwareBindings again to wrap any ObservableValue with another Binding that performs the update in the right thread, like the following code shows

public class SampleView extends AbstractJavaFXGriffonView {
    @MVCMember private SampleController controller;
    @MVCMember private SampleModel model;

    @FXML private Gauge minAmountGauge;
    @FXML private Gauge avgAmountGauge;
    @FXML private Gauge maxAmountGauge;
    @FXML private Label minNameLabel;
    @FXML private Label maxNameLabel;
    @FXML private Label minTimestampLabel;
    @FXML private Label maxTimestampLabel;
    @FXML private TableView<Measurement> measurementsTableView;
    @FXML private Label totalLabel;

    @Override
    public void initUI() {
        Stage stage = (Stage) getApplication()
            .createApplicationContainer(Collections.<String, Object>emptyMap());
        stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
        stage.setScene(init());
        stage.sizeToScene();
        getApplication().getWindowManager().attach("mainWindow", stage);
    }

    @SuppressWarnings("unchecked")
    private Scene init() {
        Node node = loadFromFXML();

        uiThreadAwareBind(minAmountGauge.valueProperty(), model.minAmountBinding());
        uiThreadAwareBind(avgAmountGauge.valueProperty(), model.avgAmountBinding());
        uiThreadAwareBind(maxAmountGauge.valueProperty(), model.maxAmountBinding());
        uiThreadAwareBind(minNameLabel.textProperty(), model.minNameBinding());
        uiThreadAwareBind(maxNameLabel.textProperty(), model.maxNameBinding());
        uiThreadAwareBind(minTimestampLabel.textProperty(), model.minTimestampBinding());
        uiThreadAwareBind(maxTimestampLabel.textProperty(), model.maxTimestampBinding());

        updateTotalLabel();
        model.totalBinding().addListener((v, o, n) -> updateTotalLabel());

        Scene scene = new Scene(new Group());
        scene.setFill(Color.WHITE);
        scene.getStylesheets().add("bootstrapfx.css");
        if (node instanceof Parent) {
            scene.setRoot((Parent) node);
        } else {
            ((Group) scene.getRoot()).getChildren().addAll(node);
        }
        connectActions(node, controller);
        connectMessageSource(node);

        toolkitActionFor(controller, "clear").enabledProperty()
            .bind(mapAsBoolean(model.totalBinding(), n -> n.intValue() > 0));

        return scene;
    }

    private void updateTotalLabel() {
        runInsideUIAsync(() -> totalLabel.setText(msg(SampleView.class.getName() + ".label.total", asList(model.totalBinding().getValue()))));
    }
}

The View is responsible for assembling the UI; it does so by reading an FXML file from a conventional location then binding widget values to the model bindings we defined. I didn't mentioned this earlier but the application is I18N aware (with English, Spanish, and German translations so far) as you can appreciate in the code that updates the label used to display the total number of measurements. We'll cover I18N in another post to keeps things simple for now. The View defines local fields for all UI specific bindings due to an implementation detail of many of JavaFX's widgets and binding utilities: reliance on WeakChangeListener. This particular implementation detail will remove any listeners that are no longer reachable by a reference, such as those created on the fly as method arguments; defining them as fields avoids the listeners form being reclaimed by GC ahead of time.

The next is to visualize the list of measurements in tabular and diagram forms. As everyone that has worked with JavaFX in the past knows, setting up a TableView can be quite a pain, even a non-editable one like the table used by this application. Griffon has taken inspiration from GlazedLists and delivers a set of abstractions that help you configure TableViews effortlessly. Enter TableViewFormat and TableViewModel. Use these two combined with a regular TableView to setup a table that follows basic conventions, for example

TableViewFormat<Measurement> tableFormat = new DefaultTableFormat<>(
    new DefaultTableFormat.Column("name", 0.2d),
    new DefaultTableFormat.Column("amount", 0.1d),
    new DefaultTableFormat.Column("timestamp")
);
ObservableList<Measurement> measurements = uiThreadAwareObservableList(model.getMeasurements());
TableViewModel<Measurement> tableModel = new DefaultTableViewModel<>(measurements, tableFormat);
tableModel.attachTo(measurementsTableView);
measurementsTableView.setEditable(false);

As the code shows, the table contains 3 columns. Each column defines the name of the property and the default proportional width it may take. These column definitions are provided by the TableFormat. Next up is the TableModel, responsible for expanding each element of the measurement list into the appropriate data element per column in the table.

With the tabular form completed we turn our attention to the diagram view, which uses an XYChart to visualize the data. This type of chart requires that each element in its data be of a particular type, specifically XYChart.Data. We need to figure out a way to define another ObservableList that can handle this type, making sure that its elements are in sync with the measurements list. What if we were able to define a transformation list that maps one type to the other? That's exactly what MappingObservableList does. The code couldn't be simpler than

XYChart.Series<String, Number> sampleSeries = new XYChart.Series<>();        
sampleSeries.setData(new MappingObservableList<>(measurements, m -> new XYChart.Data<>(m.getName(), m.getAmount())));
measurementsChart.getData().add(sampleSeries);

Any changes made to the source list will be published to the mapping list, transforming the element into the target type as a result. That's the power of bindings and observables, one change triggers a cascade of updates, we only need to make sure to setup the right transformation pipeline for each particular value.

In summary, using JavaFX bindings and Griffon's JavaFX binding support leads to better code as data concerns are kept in the data layer while UI specifics remain at the view layer. More features such as additional controls and I18N will be covered in future posts.

I promised to show how you can leverage these features without forcing Griffon as the application framework (but hopefully you'll be convinced to do so ;-). For this you simply need to include the griffon-javafx-<version>.jar dependency on your project. The Griffon project does not publishes releases to Maven's Central repository, instead it relies on Bintray's JCenter to make its binaries available to anyone.

Happy coding.

Liked it? Take a second to support aalmiray on Patreon!
Become a patron at Patreon!

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