Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle data bindings that are related?

I often find myself running into problems where two (related) values of a control get updated, and both would trigger an expensive operation, or possibly the control could get temporarily in an inconsistent state.

For example, consider a data binding where two values (x,y) get substracted from each other, and the end result is used as a divisor for some other property z:

z / (x - y)

If x and y are bound to some external value, then updating them one at a time could lead to unexpected division by zero errors, depending on which property gets updated first and what the old value of the other property is. The code that updates property z just listens on changes in both x and y -- it has no way of knowing in advance that another update is coming for the other property.

Now this problem is fairly easy to avoid, but there are other similar cases, like setting width and height... do I resize the window right away or wait for another change? Do I allocate memory for the specified width and height right away or do I wait? If the width and height was 1 and 1 million, and then gets updated to 1 million and 1, then temporarily I'd have a width and height of 1 million x 1 million...

This can be a fairly general question, although for me specifically it would apply to JavaFX Bindings. I'm interested in ways how to deal with these situations without running into undefined behaviour or doing expensive operations that need to be redone as soon as another binding changes.

The thing I've done so far to avoid these situations is to clear bindings first to known values before setting new values, but this is a burden on the code updating the bindings that it really shouldn't have to know about.

like image 655
john16384 Avatar asked Nov 03 '13 02:11

john16384


1 Answers

I am only now learning JavaFX so take this answer with a grain of salt... and any corrections are welcome. I was interested myself with this, so did a little research.

Invalidation listeners

The answer to this problem is partially the InvalidationListener. You can read the docs in detail here, but the essence is that a ChangeLister propagates the change immediately, while the InvalidationListener takes a note that a value is invalid but defers the computation until it is needed. An example demonstrating the two cases based on the "z / (x - y)" calculation:

First, the trivial stuff:

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableNumberValue;
import javafx.beans.value.ObservableValue;

public class LazyExample
{
    public static void main(String[] args) {
        changeListenerCase();
        System.out.println("\n=====================================\n");
        invalidationListenerCase();
    }
    ...
}

The 2 cases (change and invalidation listener) will set up 3 variables, x, y, z, the computed expression z / (x - y) and the appropriate listener. Then they call a manipulate() method to change the values. All steps are logged:

    public static void changeListenerCase() {
        SimpleDoubleProperty x = new SimpleDoubleProperty(1);
        SimpleDoubleProperty y = new SimpleDoubleProperty(2);
        SimpleDoubleProperty z = new SimpleDoubleProperty(3);

        NumberBinding nb = makeComputed(x,y,z);

        nb.addListener(new ChangeListener<Number>() {
            @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                System.out.println("ChangeListener: " + oldValue + " -> " + newValue);
            }
        });

        // prints 3 times, each after modification
        manipulate(x,y,z);

        System.out.println("The result after changes with a change listener is: " + nb.doubleValue());
    }

    public static void invalidationListenerCase() {
        SimpleDoubleProperty x = new SimpleDoubleProperty(1);
        SimpleDoubleProperty y = new SimpleDoubleProperty(2);
        SimpleDoubleProperty z = new SimpleDoubleProperty(3);

        NumberBinding nb = makeComputed(x,y,z);

        nb.addListener(new InvalidationListener() {
            @Override public void invalidated(Observable observable) {
                System.out.println("Invalidated");
            }
        });

        // will print only once, when the result is first invalidated
        // note that the result is NOT calculated until it is actually requested
        manipulate(x,y,z);

        System.out.println("The result after changes with an invalidation listener is: " + nb.doubleValue());
    }

And the common methods:

    private static NumberBinding makeComputed(final ObservableNumberValue x, final ObservableNumberValue y, final ObservableNumberValue z) {
        return new DoubleBinding() {
            {
                bind(x,y,z);
            }
            @Override protected double computeValue() {
                System.out.println("...CALCULATING...");
                return z.doubleValue() / (x.doubleValue()-y.doubleValue());
            }
        };
    }

    private static void manipulate(SimpleDoubleProperty x, SimpleDoubleProperty y, SimpleDoubleProperty z) {
        System.out.println("Changing z...");
        z.set(13);
        System.out.println("Changing y...");
        y.set(1);
        System.out.println("Changing x...");
        x.set(2);
    }

The output from this is:

...CALCULATING...
Changing z...
...CALCULATING...
ChangeListener: -3.0 -> -13.0
Changing y...
...CALCULATING...
ChangeListener: -13.0 -> Infinity
Changing x...
...CALCULATING...
ChangeListener: Infinity -> 13.0
The result after changes with a change listener is: 13.0

=====================================

...CALCULATING...
Changing z...
Invalidated
Changing y...
Changing x...
...CALCULATING...
The result after changes with an invalidation listener is: 13.0

So in the first case there is an excessive number of calculations and an infinity case. In the second the data is marked invalidated on the first change and then recalculated only when needed.

The Pulse

What about binding graphical properties, e.g. the width and height of something (as in your example)? It seems that the infrastructure of JavaFX does not apply changes to graphical properties immediately, but according to a signal called the Pulse. The pulse is scheduled asynchronously and, when executed, will update the UI based on the current state of the properties of the nodes. Each frame in an animation and each change of UI properties will schedule a pulse to be run.

I do not know what happens in your example case where, having initial width=1px and height=106px, the code sets width=106px (in one step, schedules pulse) and then height=1px (second step). Does the second step emit another pulse, if the first hasn't been processed? The reasonable thing to do from JavaFX's point of view is for the pipeline to process only 1 pulse event, but I need some reference for that. But, even if two events are processed, the first should process the entire state change (both width and height) so changes occur in one visual step.

The developer will have to be consious of the architecture I believe. Suppose a separate task does (pseudocode):

width = lengthyComputation();
Platform.runLater(node.setWidth(width));
height = anotherLengthyComputation();
Platform.runLater(node.setHeight(height));

I guess if the first pulse event has a chance to run, then the user will see a change of width - pause - a change of height. It would be better to write this as (again, always in a background task) (pseudocode):

width = lengthyComputation();
height = anotherLengthyComputation();
Platform.runLater(node.setWidth(width));
Platform.runLater(node.setHeight(height));

UPDATE (Comment from john16384): According to this it is not possible to listen to the pulse directly. However, one can extend certain methods of javafx.scene.Parent that are run once per pulse and achieve the same effect. So you either extend layoutChildren(), if no changes to the children tree is required or either of computePrefHeight(double width)/computePrefWidth(double height), if the children tree will be modified.

like image 64
Nikos Paraskevopoulos Avatar answered Sep 23 '22 01:09

Nikos Paraskevopoulos