Bind a JavaFx property to more than one observable

1.4k Views Asked by At

I am writing a JavaFx control that consists of a sub-control that gets user input, e.g. a TextField. The main component holds a property which represents a parsed representation of the text, e.g. a LocalDateTime. When the user enters something, this property should be updated, so I bind it to the child's value property. It should also be possible to change the current value from outside via binding the value property, so this has to be a bidirectional binding to update the child automatically. The control works fine until client code binds to the property. The following code shows my problem:

import javafx.beans.property.SimpleIntegerProperty;

public class Playbook {

    // The internal control with a property p
    static class Child {
        public SimpleIntegerProperty p = new SimpleIntegerProperty();

        public void set(int i) {p.set(i);}
    }

    // My new control with a property value which is bound
    // bidirectionally to Child.p
    static class Parent {
        public Child a1;

        public SimpleIntegerProperty value = new SimpleIntegerProperty();

        public Parent() {
            a1 = new Child();

            value.bindBidirectional(a1.p);
        }
    }

    public static void main(String[] args) {
        Parent p = new Parent();

        // some client code wants to keep the 
        // value updated and thus binds to it
        SimpleIntegerProperty outside = new SimpleIntegerProperty();
        p.value.bind(outside);

        // simulate a change in the child control
        p.a1.p.set(10);         
    }
}

When I run the code, I get an exception that a bound property cannot be set:

Caused by: java.lang.RuntimeException: A bound value cannot be set.
    at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:143)
    at com.sun.javafx.binding.BidirectionalBinding$BidirectionalIntegerBinding.changed(BidirectionalBinding.java:467)

I am sure this must be a common problem and I am just not seeing the obvious solution. I am using ReactFx, so any solution with plain JavaFx or ReactFx would be welcome. The real code uses a Var.mapBidirectional to bind the Parent and Child properties internally.

What I want to achieve is the following: 1. If outside's value changes, this should be propagated to p.value and then to p.a1.p 2. If p.a1.p changes, this should be propagated to p.value

From this I concluded that Parent.value and Parent.a1.p are always identical (plus some tranformation applied in the mapping) and this I use a bidrectional mapping. outside can change independently and can be different to value, so I use a unidirectional binding.

1

There are 1 best solutions below

0
On

After understanding that JavaFx Bindings are not what I thought it would be and that the semantics do not allow this, I switched to ReactFx's EventStream/Val/Var. ReactFx's model seems to be a better match to my expectation which allows me to create a system that behaves as follows:

  1. If outside's value changes, this should be propagated to p.value and then to p.a1.p
  2. If p.a1.p changes, this should be propagated to p.value

The following code implements this. It uses two Vars which are bound bidrectionally and thus changes in one of them are fed to the other Var. Changes from outside are defined as an EventStream which is fed to one of the Vars.

import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.value.Var;

import javafx.beans.property.SimpleIntegerProperty;

public class Playbook {

    static class Child {
        public Var<Integer> p = Var.newSimpleVar(0);

        public void set(int i) {this.p.setValue(i);}
    }

    static class Parent {
        public Child a1;

        public Var<Integer> value = Var.newSimpleVar(0);

        public Parent() {
            this.a1 = new Child();

            this.value.bindBidirectional(this.a1.p);
        }
    }

    public static void main(String[] args) {
        Parent p = new Parent();

        // some client code wants to keep the
        // value updated and thus binds to it
        SimpleIntegerProperty outside = new SimpleIntegerProperty();

        // A SimpleIntegerProperty is not a Property<Integer> but a 
        // Property<Number> so we have to use map to translate to Integer
        EventStream<Integer> stream = EventStreams.valuesOf(outside).map(Number::intValue);

        stream.feedTo(p.value);

        // simulate a change in the child control
        p.a1.p.setValue(10);
        System.out.println( p.value.getValue() );
        System.out.println(p.a1.p.getValue() );
        System.out.println(outside.get());

        // feed new value from outside
        outside.set(42);

        System.out.println( p.value.getValue() );
        System.out.println(p.a1.p.getValue() );
        System.out.println(outside.get());
    }
}

The program runs and prints

10
10
0
42
42
42