0

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.

GenerationLost
  • 179
  • 1
  • 1
  • 15
  • Consider [_Expression Binding_](https://openjfx.io/javadoc/19/javafx.fxml/javafx/fxml/doc-files/introduction_to_fxml.html#expression_binding), seen [here](https://stackoverflow.com/search?tab=votes&q=%5bjavafx%5d%20%22Expression%20Binding%22). – trashgod Sep 28 '22 at 22:34
  • 2
    It would be best to provide a [mcve] that demonstrates the issue, though I think your description of your issue is pretty good – jewelsea Sep 29 '22 at 01:00
  • A [mre] would really help. I suspect the Java equivalent of the expression binding you’re using is something along the lines of `Bindings.select(“controller”, “model”, “someString”)`, though I don’t know that for sure. JavaFX 19 introduces `Property.map(…)` and `Property.flatMap(…)` but I don’t think the FXML expression bindings have been updated to use them. – James_D Sep 29 '22 at 01:25
  • I've added an actual example and tried to explain my point a bit further, hope it's clear now. Sorry for the initial confusion. – GenerationLost Sep 29 '22 at 13:58
  • “Manually writing four members per property”. Why do this manually? Any IDE worth using at a minimum has a plug-in that will generate the methods for you. Lombok doesn’t work here because it assumes one particular and very specific implementation of a property (an assumption which defeats the entire purpose of encapsulation, imo). – James_D Sep 30 '22 at 02:45
  • @James_D "Why do this manually" is *exactly* my point. If you know of any way to generate these methods in IntelliJ IDEA, I would be glad if you could share. So far I haven't found anything, hence this post. I'm aware that Lombok is not for my particular use case (or more specifically JavaFX), but I chose to use it anyway for convenience. – GenerationLost Sep 30 '22 at 11:08
  • 1
    Assuming you are using the JavaFX template in IntelliJ, define the properties with no methods, e.g. `private StringProperty someString = new SimpleStringProperty("Test");`. Then in the menu, choose "Code", "Generate...", "Getters and Setters". Select all the properties for which you want to generate the methods, and it will generate the `getSomeString()`, `setSomeString()`, and `someStringProperty()` methods. – James_D Sep 30 '22 at 14:56
  • *"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. "*. In JavaFX 19 and later, assuming you have an `ObjectProperty model`, you can do `label.textProperty().bind(model.flatMap(Model::someStringProperty))`. – James_D Sep 30 '22 at 15:47
  • I certainly did not expect generating getters and setters will do it the proper way for property types as well, thanks a bunch! Now I see why you mentioned Lombok not working here, as it did prevent me from finding out myself that IntelliJ does indeed what I need. I'm currently also on JavaFX 17, but this seems like a nice addition, so I might just upgrade, thanks for this as well! – GenerationLost Sep 30 '22 at 16:06

1 Answers1

2

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.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • I wanted to summarize the recent comments myself to provide an answer, but I think you've done a better job providing a comprehensive overview than I could have. So thanks, I really appreciate it. – GenerationLost Sep 30 '22 at 20:32