Reputation: 36703
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.
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();
}
}
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();
}
}
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
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