1

I'm new to JavaFX and was wondering if the Bindings API allowed an easier way to achieve the following. Consider a model that contains a database that may be null (because the database loads asynchronously) and a view that displays a label status reflecting the state of the database. If it is null it should say something like "Loading..." and if it isn't it should display how many items are in the database. It also would be great if the status could reflect the size of the database as it grows or shrinks.

So far, I understand that I could bind an integer property (size of the database) to the text property of the label by using a converter. This is fine, but I want the label to display more than the number. A localized string like "Loaded {0} items" precisely. And let's not forget that the database may still be null.

This is the solution I have in place

@Override
public void initialize(URL url, ResourceBundle bundle) {
    // Initialize label with default value
    status();
    model.databaseProperty().addListener((obs, old, neu) -> {
        // Update label when database is no longer null
        status();
        // Update label when size of database changes
        neu.sizeProperty().addListener(x -> status());
    });
}

public void status() {
    if (model.database() == null) {
        status.setText(bundle.getString("status.loading"));
    } else {
        String text = bundle.getString("status.ready");
        int size = model.database().size();
        text = new MessageFormat(text).format(size);
        status.setText(text);
    }
}

It works, but is there a way to do it with a chain of bindings, or at least part of it? I've seen how powerful (and lenghty) boolean bindings can be but I'm not sure something as flexible is possible with string bindings.

Xunkar
  • 115
  • 1
  • 10

1 Answers1

4

You can use Bindings.when, which is essentially a dynamic if/then binding:*

status.textProperty().bind(
    Bindings.when(model.databaseProperty().isNull())
        .then(bundle.getString("status.loading"))
        .otherwise(
            Bindings.selectInteger(model.databaseProperty(), "size").asString(
                bundle.getString("status.ready")))
);

However, the above assumes bundle.getString("status.ready") returns a java.util.Formatter string, not a MessageFormat string. In other words, it would need to be "Loaded %,d items" rather than "Loaded {0,number,integer} items".

Bindings doesn’t have built-in support for MessageFormat, but if you really want to stick with MessageFormat (which is a legitimate requirement, as there are things MessageFormat can do which Formatter cannot), you can create a custom binding with Bindings.createStringBinding:

MessageFormat statusFormat = new MessageFormat(bundle.getString("status.ready"));

status.textProperty().bind(
    Bindings.when(model.databaseProperty().isNull())
        .then(bundle.getString("status.loading"))
        .otherwise(
            Bindings.createStringBinding(
                () -> statusFormat.format(new Object[] { model.getDatabase().getSize() }),
                model.databaseProperty(),
                Bindings.selectInteger(model.databaseProperty(), "size")))
);

* Actually, it’s more like the ternary ?: operator.

VGR
  • 40,506
  • 4
  • 48
  • 63
  • Great answer thank you. From what I understand `select` will only match a `getSize()` method however and not a `size()` method, correct? Any way to change that behavior? – Xunkar Jul 20 '18 at 19:22
  • If the database class has a `public IntegerProperty sizeProperty()` method added to it, that might be sufficient. If adding methods to the database class is not possible, you can replace Bindings.selectInteger with `Bindings.createIntegerBinding`, similar to the way createStringBinding works, but if you don’t pass some Observable value representing the size to createIntegerBinding, the custom binding will not know to update itself when the size changes. – VGR Jul 20 '18 at 19:25
  • Upon further consideration, I realize that should be `public ReadOnlyIntegerProperty sizeProperty()`. And there is one other possibility for denoting the size property, though it’s a little more effort: you can create a custom Beaninfo for the database class, as described in [the Introspector documentation](https://docs.oracle.com/javase/10/docs/api/java/beans/Introspector.html), to tell the bean property discovery process what methods correspond to each of the class’s properties. – VGR Jul 20 '18 at 19:50
  • This might be required form some properties. I'm currently stumped by the fact that `select` simply cannot match the `sorted()` method of `ObservableList`. Specifically `select(object, "list", "sorted")` will raise an exception even if `object` has a `listProperty()` and `getList()` method. – Xunkar Jul 20 '18 at 21:27
  • Just to clarify, it is still possible to do `((ObservableList) Bindings.select(object, "list")).get().sorted()` but it's a bit verbose – Xunkar Jul 20 '18 at 21:53
  • There is a specific definition for what qualifies as a property. A property is not the same as a method. The JavaBeans specification states that, unless customized BeanInfo is provided, a property must have a get*CapitalizedPropertyName* method (though ‘get’ can be replaced with ‘is’ for a property whose type is primitive boolean). A writable property needs a corresponding set*CapitalizedPropertyName*. JavaFX follows that convention, but can additionally track changes if there is also a *propertyName*Property method which returns a javafx.beans.property.Property object. – VGR Jul 20 '18 at 21:57