Issue
Coming from WPF and XAML, going back to JavaFX I'm having quite a hard time with bindings. In general, I get the gist and binding properties of a model class to GUI-elements do work if I code them in Java. But there's so much boilerplate code, it's really exhausting.
In the past I've avoided FXML and always built my GUIs in plain Java. Now I want to see what I've missed. Being already familiar with XAML provided an easier entry to FXML but I'm having issues with bindings. There's a post from someone with a similar background: How to do binding in FXML, in JavaFX 2?
To the "problem" itself: Again, coming from WPF, it seems like a whole lot of overhead and boilerplate to get a simple string binding from code to the GUI and I wonder if there's a easier, more modern or more convenient way of binding properties.
I haven't found much in regards of bindings and FXML, other than old posts and more recent answers (as of 2020) stating that the state of FXML still isn't great after all this time.
At this point I'm no longer sure what to do exactly. Writing a lot of boilerplate code for so many properties to just get a proper binding... Might actually be more feasible to update GUI components directly to reduce code (e.g. set the text property on the label itself by passing data from the model)?
Update with example (sorry for not including this in the first place); note that I'm using Lombok to generate getters and setters with annotations to already reduce the amount of boilerplate code:
Simple model class:
public class Model {
@Getter @Setter
private StringProperty someString = new SimpleStringProperty("test");
}
Simplified controller:
public class Controller {
@FXML
public Label myLabel;
@FXML
public Label myOtherLabel;
@Getter @Setter
private StringProperty someString = new SimpleStringProperty("test");
@Getter @Setter
private Model model = new Model();
public void init() {
// if I wouldn't use binding within FXML I would do the following:
myLabel.textProperty().bind(someString);
myOtherLabel.textProperty().bind(model.getSomeString());
}
public void onButtonClick(ActionEvent e) {
someString.set("clicked");
model.getSomeString().set("clicked");
}
}
Simplified FXML view:
<VBox>
<Text>Some text</Text>
<Label fx:id="myLabel" text="${controller.someString}"/>
<Label fx:id="myOtherLabel" text="${controller.model.someString}"/>
</VBox>
Correction to my initial statement: I was actually missing a detail, so I think I figured out why it doesn't work. Looks like I would always need four items for one binding to work:
// the actual property holding the data
private StringProperty someString = new SimpleStringProperty("Test");
// a method to return the above property
public StringProperty someStringProperty() {
return someString;
}
// a getter for the actual text of the property
public String getSomeString() {
return someString.get();
}
// a setter for the property string (optional)
public void setSomeString(String text) {
someString.set(text);
}
All I'm doing is generating some data that needs to be displayed on the GUI. Manually writing up to four members per property just for a binding in FXML seems a lot.
The alternative is binding in code with someProperty.bind(someOtherProperty)
for each property I wish to display. At the same time, I could just write someLabel.setText(someString);
and I'd be done with it.
Creating the bindings in code (as seen above) also comes with a drawback; In case I wanted to swap out (or re-generate) the model, the bindings won't update. I will have to re-use one instance of the model and set it's contents in order not to break the bindings.
Just to give another example where I'm coming from which might help understanding why I raised this issue. In WPF/XAML you would just have the following:
public class Model
{
public string SomeString { get; set; }
}
public partial class View ...
{
public Model DataModel { get; set; }
}
In the XAML file:
<Label Text = "{Binding DataModel.SomeString}" />
... then set the Model object as data context, done. It's probably not fair to compare both frameworks like this, and this isn't meant to complain and to be like "WPF is so much better bla bla". Again, just trying to explain where I come from.
TLDR; should I bite the bullet and go for a ton of boilerplate to have a nice FXML binding or do it the "primitive" way and set GUI contents directly, sparing myself all that additional code?
As of now, to me personally, it seems it would be more viable to ditch FXML and build the GUI purely in Java as it seems like it would drastically reduce the amount of code, since even if you define your GUI in FXML, you'd probably have to double down on re-defining controls in your controller anyway to have access to them.
Solution
Just a summary of some of the comments, which I think serves as a complete answer to the question.
The JavaFX properties pattern extends the standard Java Bean pattern by adding an additional "property accessor" method. Where the standard Java Bean pattern defines a property x
of type T
by two methods
public T getX() ; // makes x readable
public void setX(T x) ; // makes x writable
the JavaFX properties pattern adds an additional method
public Property<T> xProperty() ;
which gives access to the observable property. The contract is that
xProperty().get() == getX()
and that the effects of
setX(x)
and
xProperty().set(x)
are identical.
The additional functionality provided by the property accessor is the ability for clients to bind to or observe the property:
someOtherProperty.bind(xProperty())
xProperty().addListener(...)
Typically the pattern is implemented simply as
private final ObjectProperty<T> x = new SimpleObjectProperty<>(initialValue);
public ObjectProperty<T> xProperty() {
return x ;
}
public final T getX() {
return xProperty().get();
}
public final void setX(T value) {
xProperty().set(value);
}
It's important to note here that the property type is T
, but the field type is different (it is ObjectProperty<T>
). As in the Java Bean pattern, the name of the field can also be different to the name of the property.
Lombok is not a good fit for all of this, because it assumes the field name and type both define the property name and type. So using
@Getter @Setter
private ObjectProperty<T> x = new SimpleObjectProperty<>();
will generate the "wrong" methods:
public ObjectProperty<T> getX();
public void setX(ObjectProperty<T> x);
Note that you essentially never want to replace the ObjectProperty
itself; to change the property value you should call set()
on the existing property instance. Calling the setX(...)
method defined by Lombok will cause any listeners previously registered to fail to be notified of subsequent changes to the data.
You could do the following:
@Getter
private final ObjectProperty<T> x = new SimpleObjectProperty<>();
and clients could use this perfectly fine; they would have to do
getX().get()
to retrieve the value,
getX().set(x)
to set it, and
someProperty.bind(getX())
getX().addListener(...)
etc. This works, but doesn't conform to the normal naming pattern.
FXML expression binding uses reflection and the Binding
API to bind to properties (and properties of properties), etc.
The expression ${controller.model.value}
essentially translates to Bindings.select(controller, "model", "value")
, so
<Label fx:id="label" text="${controller.model.value}"/>
is equivalent to doing
label.textProperty().bind(Bindings.select(this, "model", "value"));
in the controller.
Bindings.select
in turn uses reflection to identify JavaFX properties to which to bind. In doing this, it assumes that methods are named according to the JavaFX properties pattern. Because of this, trying to use FXML expression binding in conjunction with Lombok won't work.
Here's an example of a complete working application, binding the text of a label to a property in a model, held by the controller.
hello-view.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jamesd.examples.binding.HelloController">
<HBox spacing="5">
<Label text="Current value:"/>
<Label text="${controller.model.value}"/>
</HBox>
<HBox spacing="5">
<Label text="Update value:"/>
<TextField fx:id="newValueTF" onAction="#updateValue"/>
</HBox>
<Button text="Reset model" onAction="#resetModel"/>
</VBox>
HelloController.java
package org.jamesd.examples.binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
public class HelloController {
private ObjectProperty<Model> model = new SimpleObjectProperty<>(new Model());
public Model getModel() {
return model.get();
}
public ObjectProperty<Model> modelProperty() {
return model;
}
public void setModel(Model model) {
this.model.set(model);
}
@FXML
private TextField newValueTF ;
@FXML
private void updateValue() {
getModel().setValue(newValueTF.getText());
}
@FXML
private void resetModel() {
setModel(new Model());
}
}
Model.java
package org.jamesd.examples.binding;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Model {
private final StringProperty value = new SimpleStringProperty("Test");
public String getValue() {
return value.get();
}
public StringProperty valueProperty() {
return value;
}
public void setValue(String value) {
this.value.set(value);
}
}
and the application class:
package org.jamesd.examples.binding;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load());
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Note this allows for the binding to persist over replacement of the model instance in the controller. This can also be achieved in Java code in the controller, using
label.textProperty().bind(Bindings.select(model, "value"));
or, preferably, in JavaFX 19 or later:
label.textProperty().bind(model.flatMap(Model::valueProperty));
Finally, while the JavaFX properties pattern is verbose, the code can be autogenerated by most IDEs. As an example, beginning with
public class Model {
private final StringProperty value = new SimpleStringProperty("Test");
}
from the menu, choose "Code", "Generate...", and "Getter/Setter". Then select the value
property (you can select as many properties as you need, and do this all in one shot for multiple properties), and "OK", and the three methods will be generated for you.
In Eclipse, install the E(fx)clipse plugin. Then right-click on the code, choose "Source", "Generate JavaFX getters and setters" and follow the wizard to select the appropriate properties.
Answered By - James_D
Answer Checked By - Timothy Miller (JavaFixing Admin)