Blog-Archiv

Montag, 13. März 2017

MVC Data Binding with JavaFX

In this Blog I will approach data-binding by looking at JavaFX and how it happens there.

I could present you now the TemperatureMvc application ported on JavaFX, but I think, after having seen ports to AWT and Vaadin, you may already guess how it would look like.

Every windowing-system has its own presentation layer utility classes. For example I could have implemented the Vaadin MVC example using a FieldGroup binding, but that could not have been ported to any other windowing-system. The same applies to JavaFX, but they went new ways in data-binding, so let's try to implement the TemperatureMvc newly, using just JavaFX utilities.

Make JavaFX Run

JavaFX is contained in Java 8, so download the latest JDK and install it on your machine.

When you want to compile and run a JavaFX application with Java 8 using Eclipse, you may see this error:

Access restriction: The type 'Application' is not API (restriction on required library ..../jfxrt.jar)

What you need to do now is described here:

  • Select your Java project in Package Explorer
  • Click context menu "Properties"
  • Select "Java Build Path"
  • Focus tab "Libraries"
  • Expand the "JRE System Library" node
  • Select "Access rules"
  • Click "Edit..."
  • Click "Add..."
  • Choose Resolution: "Accessible"
  • Enter the pattern javafx/**
  • Commit all dialogs with "Ok"

Further you need to know how a JavaFX GUI application is to be created. Here is a "Hello World":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class Main extends Application
{
    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        final HBox hbox = new HBox(new Text("Hello World"));
        hbox.setPadding(new Insets(4));
        final Scene scene = new Scene(hbox);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Hello World");
        primaryStage.show();
    }
}

Constructing the Stage is hidden in Application super-class. But you need to implement the static main() method and call launch() there.

You may have noticed that abbreviations are back: HBox, VBox. Once there was a Java convention saying that we should not use acronyms but long names .... which I really appreciated .... and I guess a lot of other people also did .... wasn't that cool enough:-?

Another JavaFX beauty is the LINUX problem when debugging a JavaFX application via Eclipse. As soon as it hits a breakpoint, the whole machine freezes completely. You need to press Ctl-Alt-F1 to change to your primary terminal, log in and kill Eclipse manually there:

ps aux | grep -i eclipse
kill .... # pid

The web says you can avoid this by starting the JavaFX application with this VM-argument:

-Dsun.awt.disablegrab=true

Bindable Model

For JavaFx we need to enhance our model to contain real properties instead of fields. Then we need to have one additional method per property that returns the property itself. So we have a getXxx(), a setXxx(), and a xxxProperty(), all public. Here is the JavaFX compatible TemperatureModel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class TemperatureModel
{
    private IntegerProperty temperatureInCelsius = new SimpleIntegerProperty();
    
    public IntegerProperty temperatureInCelsiusProperty() {
        return temperatureInCelsius;
    }
    
    public Integer getTemperatureInCelsius() {
        return temperatureInCelsius.getValue();
    }
    public void setTemperatureInCelsius(Integer temperatureInCelsius) {
        this.temperatureInCelsius.setValue(temperatureInCelsius);
    }

    public Integer getTemperatureInFahrenheit() {
        return toFahrenheit(getTemperatureInCelsius());
    }
    public void setTemperatureInFahrenheit(Integer temperatureInFahrenheit) {
        setTemperatureInCelsius(toCelsius(temperatureInFahrenheit));
    }

    public Integer toFahrenheit(Integer valueInCelsius) {
        if (valueInCelsius == null)
            return null;
        return Integer.valueOf((int) Math.round(valueInCelsius * 1.8) + 32);
    }

    public Integer toCelsius(Integer valueInFahrenheit) {
        if (valueInFahrenheit == null)
            return null;
        return Integer.valueOf((int) Math.round((valueInFahrenheit - 32) / 1.8));
    }
}

These "real" properties (instead of bean fields) enable us to bind them to view-fields in two directions: read-only, or read-write (bidirectional).

Actually you don't need the traditional getters and setters any more. You can always use xxxProperty().setValue(....) instead.

View Binding

For completeness I have to mention that a JavaFX view normally is written in FXML. I did not do this here but created the two text-fields "manually" in Java code.

There is no controller. The view is the controller. It receives a model and simply binds model-properties to view-properties. Datatype-converters can be applied when using the static Bindings utility.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.control.TextField;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.*;
import javafx.util.StringConverter;
import javafx.util.converter.NumberStringConverter;

public class JfxTemperatureView
{
    private final GridPane view;
    private final TextField celsius;
    private final TextField fahrenheit;
    private TemperatureModel model;

    public JfxTemperatureView() {
        celsius = new TextField();
        fahrenheit = new TextField();

        view = new GridPane();
        view.add(new Text("Celsius:"), 0, 0);
        view.add(celsius, 1, 0);
        view.add(new Text("Fahrenheit:"), 2, 0);
        view.add(fahrenheit, 3, 0);
    }

    public Node getAddableComponent() {
        return view;
    }

    public void setModel(final TemperatureModel model) {
        if (this.model != null) {
            celsius.textProperty().unbindBidirectional(this.model.temperatureInCelsiusProperty());
            fahrenheit.textProperty().unbindBidirectional(this.model.temperatureInCelsiusProperty());
        }
        
        this.model = model;
        
        Bindings.bindBidirectional(
                celsius.textProperty(),
                model.temperatureInCelsiusProperty(),
                new NumberStringConverter()    {
                    @Override
                    public Number fromString(String text) {
                        return stringToInteger(text);
                    }
                });
        
        Bindings.bindBidirectional(
                fahrenheit.textProperty(), 
                model.temperatureInCelsiusProperty(), 
                new StringConverter<Number>()    {
                    @Override
                    public Number fromString(String text) {
                        return model.toCelsius(stringToInteger(text));
                    }
                    @Override
                    public String toString(Number number) {
                        final Integer i = model.toFahrenheit((Integer) number);
                        return (i != null) ? i.toString() : "";
                    }
                });
    }
    
    private Integer stringToInteger(String text)   {
        if (text.length() <= 0 || text.equals("-"))
            return null;
        
        try {
            return Integer.valueOf(text);
        }
        catch (NumberFormatException e) {
            errorDialog(e.toString());
            return null;
        }
    }

    private void errorDialog(String message) {
        final Text label = new Text(message);
        final HBox layout = new HBox(label);
        layout.setPadding(new Insets(8));
        
        final Stage dialog = new Stage(StageStyle.UTILITY);
        dialog.initOwner(view.getScene().getWindow());
        dialog.initModality(Modality.WINDOW_MODAL);
        dialog.centerOnScreen();
        
        final Scene scene = new Scene(layout);
        dialog.setScene(scene);
        dialog.show();
    }
}

This view holds a layout container, the two temperature fields, and a model. In the constructor these are assigned and arranged to a view. The resulting Node is exposed via getAddableComponent().

The setModel() method first releases any old model. Then it binds the new one in both directions, that means model property changes will be rendered in the view fields, and view field changes will be mirrored to the according model property.

All changes will go through the given converter (3rd parameter). I use this to display error messages about non-numeric input, and to convert from and to Fahrenheit values for the Fahrenheit field. The stringToInteger() method manages the user's input.

The errorDialog() method finally represents the JavaFX way to display a modal dialog. Meanwhile this has been improved by a class Alert in the newest JDK.

Demo Application

Here is how this is built together and shown as GUI application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import javafx.application.Application;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;

public class Demo extends Application
{
    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        // build the MVC
        final TemperatureModel model = new TemperatureModel();
        final JfxTemperatureView view = new JfxTemperatureView();
        // set an initial model value
        view.setModel(model);
        model.temperatureInCelsiusProperty().setValue(21);
        
        final VBox ground = new VBox(8);    // 8 "spacings", see http://stackoverflow.com/questions/26182460/javafx-8-hidpi-support
        ground.setAlignment(Pos.CENTER);
        ground.setPadding(new Insets(8.0));
        ground.getChildren().add(view.getAddableComponent());
        
        // add a button that sets a new model into the MVC
        final Button resetButton = new Button("Reset");
        resetButton.setOnAction(actionEvent -> view.setModel(new TemperatureModel()));
        ground.getChildren().add(resetButton);
        
        final Scene scene = new Scene(ground);
        primaryStage.setScene(scene);
        
        primaryStage.setTitle("JavaFX TemperatureMvc");
        primaryStage.show();
    }
}

Most remarkable here is the new way how to implement callbacks as lambda expressions. The line

        resetButton.setOnAction(actionEvent -> view.setModel(new TemperatureModel()));

sets an anonymous function as callback handler of the reset-button. It is the same as

        resetButton.setOnAction(new EventHandler() {
            @Override
            public void handle(ActionEvent arg0) {
                view.setModel(new TemperatureModel());
            }
        });

When you compile and run this source code, you will see that the values get changed on any keypress. Which is not a binding recommendable for server-driven web applications (e.g. Vaadin). Network traffic would explode.

Resume

Every windowing system has its own utility and helper classes around controls and widgets. I could also show you a neat example how to bind the TemperatureModel with Vaadin FieldGroup very elegantly, short and concise. But you can not change the windowing-system any more when you give away your MVC to one of these environments. Your presentation layer will then depend on that windowing toolkit. And when it expires (like Swing did), you will write everything newly, from scratch.

On the other hand you miss all those useful newbies with your old-fashioned MVC thinking. The JavaFX data binding classes are part of the Java runtime-library since 1.8 (Java 8). It is an MVC-like data binding, that means the view gets directly bound to the model, other than with MVP where the presenter would be the mediator between view and model. That kind of data-binding from property to property, where not only the model is a collection of properties but also the view is, is well-equipped for generic data models (collections of properties instead of types). You could chain several such models together, each having different responsibilities (like sorting, filtering, layout, ...) and end up in a model that actually is the view. JavaFX on rails. Or Oracle ADF?

But JavaFX is not the one-and-only data binding in this world. For example I found no setBuffered(true) method (I am sure it works somehow:-). You would need this when the view should commit all fields together on the user pressing "Save". There were already successful concepts about this in JGoodies, and Vaadin also supports such. So do we (community) have a specification-request for data-binding? Yes we have:

  1. JSR 227 (Withdrawn)
  2. JSR 295 (Withdrawn)

Who will take over? It's time for us to get active. In my next Blog I will try to specify what makes up data binding as much as I experienced it.




Keine Kommentare: