Customized Bidirectional Bindings in JavaFX

Bidirectional bindings are a useful means of keeping two values synchronized. When creating interactive visualizations, a property of a GUI element often depends on a model property and vice versa. This article shows how two properties of different types can be bound bidirectionally and what pitfalls to avoid.

Consider a custom chart component. I want to allow for zoom and pan by the usual mouse gestures. In addition, I want a scrollbar that gives an impression of the size and location of the currently visible part of the chart (clip region). When the user manipulates the scroll bar, I want the clip region to change. When changing the clip region using mouse gestures, I want the scroll bar to adapt accordingly.

Synchronizing the clip region of my custom chart with a scrollbar.

The problem here is that I want to bind two different things: A clip region and a scroll bar position (and its slider size). Minor transformations like conversion to string are frequently used when binding e.g. a text field values to a model property. JavaFX Properties and Bindings provide plenty of useful built-in mechanisms, the most basic being the bindBidirectional()  methods. For more complex transformations, however, I haven’t found an elegant built-in method.

So I implemented my bidirectional binding manually, which is really not much effort. All you need is a listener on to the scrollbar value that updates the clip region and a listener on the clip region that updates the scroll bar value. The only problem we need to take care of here are infinite update loops: Updating the scroll bar updates the clip region, updating the clip region updates the scroll bar, which again updates the clip region, etc.
The solution is to use an “already called” flag to avoid update loops. Set the flag to true before calling the update logic and set it to false afterwards. If the flag is true, discard any update request. Figure 1 shows what happens at runtime. The update of A (in red) caused by the update of B is discarded because the “already called” flag is on.

callStack
Figure 1. Call stack of an avoided update loop.

In the beginning, I was avoiding update loops by checking whether the new value was significantly different from the old value.

// bad idea
if(Math.abs(newValue - oldValue) > 1e-15) // perform update

Although this works most of the times, it is not very robust. Furthermore, even for a simple object like a BoundingBox, this involves a lot of boiler plate code, for comparing all the individual fields. Using a boolean flag solves the problem for any object type and independently of arbitrary precision thresholds. I refactored a global function from the code that covers similar use cases.

public class BidirectionalBinding {

    /** Executes updateB when propertyA is changed. Executes updateA when propertyB is changed.
     * Makes sure that no update loops are caused by mutual updates.
     */
    public static <A,B> void bindBidirectional(ObservableValue<A> propertyA, ObservableValue<B> propertyB, ChangeListener<A> updateB, ChangeListener<B> updateA){

        addFlaggedChangeListener(propertyA, updateB);
        addFlaggedChangeListener(propertyB, updateA);

    }

    /**
     * Adds a change listener to a property that will not react to changes caused (transitively) by itself (i.e. from an update call in the call tree that is a descendant of itself.)
     * @param property the property to add a change listener to
     * @param updateProperty the logic to execute when the property changes
     * @param <T> the type of the observable value
     */
    private static <T> void addFlaggedChangeListener(ObservableValue<T> property, ChangeListener<T> updateProperty){
        property.addListener(new ChangeListener<T>() {

        private boolean alreadyCalled = false;
        
        @Override public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {
            if(alreadyCalled) return;
            try {
                alreadyCalled = true;
                updateProperty.changed(observable,oldValue,newValue);
            }
            finally { alreadyCalled = false; }
        }
        });
    }

}

The java compiler had some problems inferring the generic types, so I gave it a hint using the <Number, Bounds> notation in front of the static method call.

BidirectionalBinding.<Number,Bounds>bindBidirectional(
    scrollBar.valueProperty(),
    axisBoundsDCProperty(),
    this::updateAxisBounds,
    this::updateScrollBarSlider
);

The updateAxisBounds and updateScrollBarSlider methods have a signature compatible with the ChangeListener<T> interface, as required by the bindBidirectional signature. The axisBoundsDC property is a SimpleObjectProperty managing a BoundingBox object.

private final ObjectProperty<Bounds> axisBoundsDC = 
    new SimpleObjectProperty<>(new BoundingBox(0,0,0,0));
protected void updateAxisBounds(ObservableValue<? extends Number> observable, Number oldValue, Number newValue){...}
protected void updateScrollBarSlider(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newBounds){...}

The boolean flag trick to avoid update loops was taken from this post on stackoverflow, which in turn took it from a decompilation of JavaFX code.

Leave a Reply

Your email address will not be published. Required fields are marked *