Adam
Adam

Reputation: 36703

JavaFX bindings not passing on values

In the past I've used the following support class to wrap an existing ObservableValue with one that fires changes on the FX event thread... and this works fine.

class AsyncBinding<T> implements ObservableValue<T> {
    private List<InvalidationListener> invalidationListeners = new ArrayList<>(1);
    private List<ChangeListener<? super T>> changeListeners = new ArrayList<>(1);
    private ObservableValue<T> original;
    private ChangeListener<T> changeListener;
    private InvalidationListener invalidationListener;
    public AsyncBinding(ObservableValue<T> original) {
        super();
        this.original = original;
        changeListener = (obs, oldValue, newValue) -> {
            Runnable job = () -> {
                for (ChangeListener<? super T> listener : changeListeners) {
                    listener.changed(obs, oldValue, newValue);
                }
            };
            Platform.runLater(job);
        };
        original.addListener(changeListener);
        invalidationListener = obs -> {
            Runnable job = () -> {
                for (InvalidationListener listener : invalidationListeners) {
                    listener.invalidated(obs);
                }
            };
            Platform.runLater(job);
        };
        original.addListener(invalidationListener);
    }
    @Override
    public void addListener(InvalidationListener arg0) {
        invalidationListeners.add(arg0);
    }
    @Override
    public void removeListener(InvalidationListener arg0) {
        invalidationListeners.remove(arg0);
    }
    @Override
    public void addListener(ChangeListener<? super T> arg0) {
        changeListeners.add(arg0);
    }
    @Override
    public void removeListener(ChangeListener<? super T> arg0) {
        changeListeners.remove(arg0);

    }
    @Override
    public T getValue() {
        return original.getValue();
    }
}

However, if it is used with an intermediate StringBinding it stops updating. The following cases show the different behaviour. I've ruled out garbage collection. I'm just after an explanation/understanding of the different behaviours.

Case 1 - property -> async -> label. Works fine.

public class Case1 extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    // hold references to prevent garbage collection...
    private AsyncBinding<String> asyncValue;

    @Override
    public void start(Stage primaryStage) throws Exception {
        Label label = new Label();
        primaryStage.setScene(new Scene(label));
        primaryStage.show();

        ObjectProperty<String> property = new SimpleObjectProperty<>();

        asyncValue = new AsyncBinding<>(property);

        label.textProperty().bind(asyncValue);

        new Thread(() -> {
            for (int i = 0; i < 1_000_000; i++) {
                property.set("a" + i);
            }
        }, "background").start();
    }
}

Case 2 - property -> string -> async -> label. Stops updating randomly...

public class Case2 extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    // hold references to prevent garbage collection...
    private StringBinding stringBinding;
    private AsyncBinding<String> asyncValue;

    @Override
    public void start(Stage primaryStage) throws Exception {
        Label label = new Label();
        primaryStage.setScene(new Scene(label));
        primaryStage.show();

        ObjectProperty<String> property = new SimpleObjectProperty<>();

        stringBinding = Bindings.createStringBinding(() -> {
            return "b" + property.get();
        }, property);

        asyncValue = new AsyncBinding<>(stringBinding);

        label.textProperty().bind(asyncValue);

        new Thread(() -> {
            for (int i = 0; i < 1_000_000; i++) {
                property.set("a" + i);
            }
        }, "background").start();
    }
}

Case 3 - property -> async -> string -> label. Works fine.

public class Case3 extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    // hold references to prevent garbage collection...
    private StringBinding stringBinding;
    private AsyncBinding<String> asyncValue;

    @Override
    public void start(Stage primaryStage) throws Exception {
        Label label = new Label();
        primaryStage.setScene(new Scene(label));
        primaryStage.show();

        ObjectProperty<String> property = new SimpleObjectProperty<>();

        asyncValue = new AsyncBinding<>(property);

        stringBinding = Bindings.createStringBinding(() -> {
            return "b" + property.get();
        }, asyncValue);

        label.textProperty().bind(stringBinding);

        new Thread(() -> {
            for (int i = 0; i < 1_000_000; i++) {
                property.set("a" + i);
            }
        }, "background").start();
    }
}

Upvotes: 2

Views: 108

Answers (1)

fabian
fabian

Reputation: 82461

After some checks I found out that the problem seems to be caused by the fact that after some time java creates a local copy of the AsyncBinding.original binding for the background thread. The background thread keeps updating it's local copy but the value for the JavaFX application thread remains unmodified.

To fix this you could use the value you get in the ChangeListener and assign it to a field. Return this value from the getValue method instead:

// field with updated value on the javafx application thread
private T value;

public AsyncBinding(ObservableValue<T> original) {
    super();
    this.original = original;
    changeListener = (obs, oldValue, newValue) -> {
        Runnable job = () -> {
            value = newValue; // make sure the value on the application thread is the new value
            for (InvalidationListener listener : invalidationListeners) {
                listener.invalidated(obs);
            }
            for (ChangeListener<? super T> listener : changeListeners) {
                listener.changed(obs, oldValue, newValue);
            }
        };
        Platform.runLater(job);
    };
    original.addListener(changeListener);
}

...

@Override
public T getValue() {
    return value;
}

This way the value should be updated eventually. This could take a while however, since you're doing 1000000 Platform.runLater calls which could take a while to complete freezing the application until it's done.

Upvotes: 1

Related Questions