Montag, 18. November 2013

Nested or dependant JavaFx Properties

In JavaFx the concept of bindable properties makes life pretty much easier. Here is an example: You want the text of a Label be the same, as in one of your properties: myLabel.textProperty().bind(myProperty);

But sometimes you want to bind to a property of an object, that is held in another property. Lets make an example. You have a property selectedCustomer which holds a Customer object. In that Customer object you have a String property called name. If you bind directly to name with bind(selectedCustomer.get().nameProperty()) you get in trouble, if the value of selectedCustomer changes. Of course you can fix that with implementing a ChangeListener and add it to selectedCustomer and rebind it on the change.

Here is a class, that encapsulated that for you, here is the code for your free use:


This is a Java 8 example and makes use of the new Lamda features. Of course you can use that with replacing the lambdas with anonymous inner classes.

The use is very easy:
ObjectProperty<Customer> selectedCustomer =...;
ObjectProperty<String> selectedName = new NestedObjectProperty<>(selectedCustomer, 
  (Customer customer)-> customer.nameProperty(), false);


The Parameters of the constructor are:
  • The property that delivers the property to bind against, in our example selectedCustomer
  • A Function implementation, that returns the property to bind against, in our example it gets a Customer as parameter and returns a String property
  • A boolean, if the binding should be unidirectional or bidirectional

Kommentare:

  1. Dieser Kommentar wurde vom Autor entfernt.

    AntwortenLöschen
  2. Hi,

    nice idea, but why do you not use: javafx.beans.binding.Bindings#select(ObservableValue, String ...) ?

    Regards
    JavaFX-Learner

    AntwortenLöschen
  3. Good point. There are two reasons:
    Using Bindings.select returns a Binding implementation, no property. So you cannot make a bidirectional binding.
    And the other disadvantage from my point of view is, that you pass the names of the properties as Strings. So if you make a refactoring, this may break. In my example you pass a Function which is implemented with direct use of the methods, no reflection is used. In my tests with JDK 8 it also logged a NullPointerException when setting one props in the chain to null. But of course you can use that, if it fits your needs!

    AntwortenLöschen
  4. Oh yeah, now it makes perfectly sens. Thanks.

    But something like “selectedCustomer.get().addressProperty().get().streetProperty()” would only be possible if I start to nest the “NestedObjectProperty” like this:

    ObjectProperty<Customer> selectedCustomer =...;
    ObjectProperty<Address> selectedAddress = new NestedObjectProperty<>(selectedCustomer, (Customer customer)-> customer.addressProperty(), false);
    ObjectProperty<String> selectedStreet = new NestedObjectProperty<>(selectedAddress, (Address address)-> address.streetProperty(), false);

    // or in one line:
    ObjectProperty<String> selectedStreetOneLine = new NestedObjectProperty<>(new NestedObjectProperty<>(selectedCustomer,  (Customer customer)-> customer.addressProperty(), false), (Address address)-> address.streetProperty(), false );

    Is this correct?

    AntwortenLöschen
  5. You are right, I already tried this, it works, but the constructor is not optimized for deeper nesting. The use case was a property of a property. Maybe you could optimize my class to do this more nicely for deeper nesting.

    AntwortenLöschen
  6. Hi Johannes,

    I had the same issue.

    And I do not want to work with select in production mode for the raison you said before.

    I work on a low level binding named FXBinding ( https://github.com/dooApp/FXBinding )

    Feel free to test.

    Christophe.

    AntwortenLöschen
  7. Hi,
    thanks for your work. I know this post is old but it's still in the top google search results for "javafx nested properties" so I thought I'd point out a problem I encountered.
    In line 35 you pass a WeakChangeListener with a lambda to the addListener method but nothing stops the GC from collecting the lambda because there is no strong reference to it. In my program the binding suddenly stopped working after a short period of time. So I ended up creating a change listener member method and passing a method reference instead of the lambda.

    public NestedObjectProperty(ObservableValue dependentProperty, final Function> getFunction, boolean bidirectional) {
    this.getFunction = getFunction;
    this.bidirectional = bidirectional;
    bindDependentProperty(dependentProperty.getValue());
    dependentProperty.addListener(this::onDependentPropertyChanged);
    }

    private void onDependentPropertyChanged(ObservableValue observable, D oldValue, D newValue) {
    // ...
    }

    For my program it's okay to keep the observer alive because the observer and the observable have the same lifetime. But one should probably add a close/dispose method that removes the change listener.

    AntwortenLöschen
  8. Hello,
    you are completely right. We fixed that by adding a instance reference to the listener:

    public class NestedObjectProperty extends SimpleObjectProperty {
    /**
    * Listener to the dependand property
    */
    @SuppressWarnings("FieldCanBeLocal") //mustn't be lokal due to the WeakChangeListener...
    private final ChangeListener listener;
    /**
    * The binding partner of this instance
    */
    private Property boundProperty;

    /**
    * Creates a new instance
    * @param dependandProperty the property that delivers the property to bind against
    * @param getFunc a {@link java.util.function.Function} of D and ObjectProperty of T
    * @param bidirectional boolean flag, if the binding should be bidirectional or unidirectional
    */
    public NestedObjectProperty(ObservableValue dependandProperty, final Function> getFunc, boolean bidirectional) {
    bindDependandProperty(dependandProperty.getValue(), getFunc, bidirectional);

    listener = (observable, oldValue, newValue) -> {
    if (boundProperty != null) {
    if (bidirectional) {
    unbindBidirectional(boundProperty);
    } else {
    unbind();
    }
    boundProperty = null;
    }
    bindDependandProperty(newValue, getFunc, bidirectional);
    };
    dependandProperty.addListener(new WeakChangeListener<>(listener));
    }

    /**
    * Binds this instance to the dependant property
    * @param newValue D
    * @param getFunc Function> getFunc, boolean bidirectional) {
    if (newValue != null) {
    Property newProp = getFunc.apply(newValue);
    boundProperty = newProp;
    if (newProp != null) {
    if (bidirectional){
    bindBidirectional(newProp);
    }else{
    bind(newProp);
    }
    }else{
    set(null);
    }
    }else{
    set(null);
    }
    }
    }

    Best regards,
    John

    AntwortenLöschen