Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Sunday, June 28, 2015

Binding an Integer to a TextField in JavaFX

I've spent a lot of my programming career moving objects in and out of UI controls for use in the service or data layers.  JavaFX binding is a capability that can remove trivial getter and setter calls, replacing them with a declarative linkage.  The primary benefit of this is a separation that allows the bound property to be manipulated outside of the UI control. For instance, you can update the "firstName" property without having to know if the UI control is a Label (ex, "lblFirstName") or a TextField (ex, "tfFirstName") or both.



No Binding

If you were retrieving a value for display without binding, you might do something like this to load the UI (TextField tfNumUpdates) with a value from the data model (ds for "data source").

Integer numUpdates = ds.getNumUpdates();
tfNumUpdates.setText( String.valueOf(numUpdates) );

When you need to adjust the model -- increment the number of updates -- you would do something like this.

ds.incrNumUpdates();  // increase by one
numUpdates = ds.getNumUpdates();
tfNumUpdates.setText( String.valueOf(numUpdates) );

And, if you wanted to pull the current value out of the UI, say for a manual edit of the TextField, you would do

String numUpdates_s = tfNumUpdates.getText();
Integer numUpdates = -1;
if( s != null && s.length() > 0 ) {
  numUpdates = new Integer(s);
}
ds.setNumUpdates( numUpdates );

The interaction between the model and the UI is straightforward and is used in plenty of applications, JavaFX ones included.  However, it doesn't scale particularly well because this code grows for additional fields and model interactions (more ds calls and more setters on the UI).  Also, it's up to the programmer to determine how he or she wants to convert mismatched types (String versus Integer).  The problem becomes worse when you need to serialize between the model in more than one place.

Binding

Binding lets you declare your UI controls' relationships to model fields.  This statement will keep a model property "numUpdates" in sync with a UI control "tfNumUpdates".

tfNumUpdates.textProperty().bindBidirectional(ds.getNumUpdates(), new NumberStringConverter());

tfNumUpdates will reflect the datasource "ds".  This means that in my business logic, I can make model changes and not worry about the exact fields that need to be updated.  On the UI side, I don't have to worry about packing and unpacking my UI controls into data structures for use on the backend.

Example

This code can be found in the following public Bitbucket Git repository:  https://bitbucket.org/bekwam/jfxbop-repos-1.

The application DemoApp binds a model element represented as a SimpleIntegerProperty called numUpdates to a TextField.  When the Update Button is pressed, the model is manipulated an the UI automatically updated.

See the following video


Model


I'm coding the model using a framework I'm working on called JFXBop.  See the above Git repository for the latest.  The JavaFX binding concept isn't dependent on this framework.

public class DemoAppDataSource extends BaseManagedDataSource {

    private final static Logger logger = LoggerFactory.getLogger(DemoAppDataSource.class);

    /***
     * Property that tracks initialization of object
     *
     * Used in ManagedDataSource framework
     */
    private Boolean initialized = false;

    /**
     * A "business property" representing a link to back-end services and data*
     */
    private final IntegerProperty numUpdates = new SimpleIntegerProperty(-1);

    /**
     * Called by the framework (a Google Guice AOP Interceptor)
     *
     * @throws Exception
     */
    @Override
    public void init() throws Exception {
        super.init();

        numUpdates.set(0);
        initialized = true;

        if( initCB != null ) {
            initCB.accept(null);
        }
    }

    @Override
    public boolean isInitialized() {
        return initialized;
    }

    public IntegerProperty getNumUpdates() {
        return numUpdates;
    }

    public void incrNumUpdates() {
        numUpdates.set( numUpdates.get() + 1 );
    }
}

numUpdates is a final IntegerProperty that is manipulated by the class DemoAppDataSource.  I've made the field final to fend of NPEs that might occur in the rest of the program if someone tries to call a get() or set() on the property.  This means that I have to provide the field with an initial value (-1).  In the init() method, I later set this to the initial value of 0 for the start of the program.

The back-end is unlikely to send or receive serialized JavaFX objects with JavaFX Property fields.  The back-end will probably send POJOs or JSON as a transport.  The DemoAppDataSource class would manage the conversion.  What's nice about the class is that it simply exposes a property for binding and doesn't need references to the overall UI for getting and setting with refreshes.

View

The View is created in SceneBuilder as an FXML file called DemoApp.fxml.  It refers to a JavaFX Controller called DemoAppController.

SceneBuilder Screenshot of the FXML File

@Viewable(
        fxml="/DemoApp.fxml",
        stylesheet = "/DemoApp.css",
        title = "DemoApp - Num Refreshes"
)
public class DemoAppController extends GuiceBaseView {

    private final static Logger logger = LoggerFactory.getLogger(DemoAppController.class);

    @FXML
    TextField tfNumUpdates;

    @Inject
    DemoAppDataSource ds;

    @FXML
    public void initialize() {

        tfNumUpdates.textProperty().bindBidirectional(ds.getNumUpdates(), new NumberStringConverter());
    }

    @FXML
    public void update() {

        if( logger.isDebugEnabled() ) {
            logger.debug("[UPDATE] updating");
        }

        ds.incrNumUpdates();
    }

}

The View layer binds a UI control TextField tfNumUpdates to the model field numUpdates.  Since there is a datatype difference that needs conversion, I'm using an expanded form of bindBidirectional that will convert Integers to Strings.

What's nice about the binding example is that the update() method is very clean.  It invokes the business operation which adjusts the model.  The UI is then refreshed with an explicit refresh call in the Controller update() method or a call made from the back-end.

If you get a bunch of POJOs from the back end and have a JavaFX Controller loaded up with getText() and setText() calls, I won't hold it against you.  A lot of code does this.  However, if you have the opportunity to add JavaFX binding, a number of defensive strategies such as the final Property object and converts can help reduce the number of NPEs that will surface during later maintenance.

1 comment:

  1. Hi Carl - just wanted to let you know that you helped a group of CS students from Denmark immensely :)

    ReplyDelete