Leveraging the observer pattern in JavaFX GUI design

As noted here, the JavaFX architecture tends to favor binding GUI elements via classes that implement the Observable interface. Toward this end, Irina Fedortsova has adapted the original Converter to JavaFX in Chapter 5 of JavaFX for Swing Developers: Implementing a Swing Application in JavaFX.

image

I’ve recapitulated the program below, updating to Java 8 and removing the dependency on the now deprecated builder API. In the variation below,

  • A DoubleProperty named meters functions as the application’s Observable model.

  • Instances of Control, such as TextField, ComboBox and Slider, each function as a view of the model, as well as providing a way for the user to control the interaction.

  • Within a ConversionPanel, an InvalidationListener added to the ComboBox updates the TextField view of the model as needed to reflect the currently selected Unit; a similar listener added to the TextField updates the model itself as the user types.

  • The same model is shared between instances of ConversionPanel by the Slider, linking the sliders and any controls listening to the model.

      slider.valueProperty().bindBidirectional(meters);
    
  • Each ComboBox also has a model, ObservableList, from which the user can select among instances of Unit.

Code: converter/Unit.java

/*
 * Copyright (c) 1995, 2013, Oracle and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Oracle or the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package converter;

/**
 * @see http://docs.oracle.com/javafx/2/swing/port-to-javafx.htm
 */
public class Unit {

    String description;
    double multiplier;

    Unit(String description, double multiplier) {
        super();
        this.description = description;
        this.multiplier = multiplier;
    }

    @Override
    public String toString() {
        String s = "Meters/" + description + " = " + multiplier;
        return s;
    }
}

Code: converter/ConversionPanel.java

/*
 * Copyright (c) 2012, 2013 Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 */
package converter;

/**
 * @see http://docs.oracle.com/javafx/2/swing/port-to-javafx.htm
 */
import java.text.NumberFormat;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;

public class ConversionPanel extends TitledPane {

    private static final int MAX = 10000;
    private static final int DIGITS = 3;

    private final TextField textField = new TextField();
    private final Slider slider = new Slider(0, MAX, 0);
    private final ComboBox<Unit> comboBox;
    private NumberFormat numberFormat = NumberFormat.getNumberInstance();
    private DoubleProperty meters;

    {
        numberFormat.setMaximumFractionDigits(DIGITS);
    }

    private InvalidationListener fromMeters = (Observable o) -> {
        if (!textField.isFocused()) {
            textField.setText(numberFormat.format(meters.get() / getMultiplier()));
        }
    };

    private InvalidationListener toMeters = (Observable o) -> {
        if (textField.isFocused()) {
            try {
                Number n = numberFormat.parse(textField.getText());
                meters.set(n.doubleValue() * getMultiplier());
            } catch (Exception ignored) {
            }
        }
    };

    public ConversionPanel(String title, ObservableList<Unit> units, DoubleProperty meters) {
        setText(title);
        setCollapsible(false);
        comboBox = new ComboBox<>(units);
        comboBox.getSelectionModel().select(0);
        comboBox.setConverter(new StringConverter<Unit>() {

            @Override
            public String toString(Unit t) {
                return t.description;
            }

            @Override
            public Unit fromString(String string) {
                throw new UnsupportedOperationException("Not supported yet.");
            }
        });
        setContent(new HBox(new VBox(textField, slider), comboBox));

        this.meters = meters;
        meters.addListener(fromMeters);
        comboBox.valueProperty().addListener(fromMeters);
        textField.textProperty().addListener(toMeters);
        slider.valueProperty().bindBidirectional(meters);
        fromMeters.invalidated(null);
    }

    /**
     * Returns the multiplier for the currently selected unit of measurement.
     */
    public double getMultiplier() {
        return comboBox.getValue().multiplier;
    }
}

Code: converter/Converter.java

/*
 * Copyright (c) 2012, 2013 Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 */
package converter;

import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 * @see https://stackoverflow.com/a/31909942/230513
 * @see http://docs.oracle.com/javafx/2/swing/port-to-javafx.htm
 */
public class Converter extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    private final ObservableList<Unit> metricDistances;
    private final ObservableList<Unit> usaDistances;
    private final DoubleProperty meters = new SimpleDoubleProperty(1);

    public Converter() {
        //Create Unit objects for metric distances, and then
        //instantiate a ConversionPanel with these Units.
        metricDistances = FXCollections.observableArrayList(
            new Unit("Centimeters", 0.01),
            new Unit("Meters", 1.0),
            new Unit("Kilometers", 1000.0));

        //Create Unit objects for U.S. distances, and then
        //instantiate a ConversionPanel with these Units.
        usaDistances = FXCollections.observableArrayList(
            new Unit("Inches", 0.0254),
            new Unit("Feet", 0.3048),
            new Unit("Yards", 0.9144),
            new Unit("Miles", 1609.34));
    }

    @Override
    public void start(Stage stage) {
        stage.setScene(new Scene(new VBox(
            new ConversionPanel("Metric System", metricDistances, meters),
            new ConversionPanel("U.S. System", usaDistances, meters))));
        stage.show();
    }
}

Leave a Comment