Issue
Yes, the problem is very similar to an old question asked a few years ago. In fact, my sample code is based on that one, too. I also tried the answer given there but it didn't work.
package com.example.bindingssize;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.IntegerBinding;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class HelloApplication extends Application {
private IntegerBinding sizeBinding;
private BooleanBinding isEven;
@Override
public void start(Stage primaryStage) {
final ObservableList<String> list = FXCollections.observableArrayList();
primaryStage.setTitle("Demonstrator");
// Button
Button addButton = new Button();
addButton.setText("Add Element");
addButton.setOnAction(event -> list.add("TEST"));
// ListView
ListView<String> lv = new ListView<>();
lv.setItems(list);
// Add elements to root
VBox root = new VBox();
root.getChildren().add(addButton);
root.getChildren().add(lv);
// Show scene
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
sizeBinding = Bindings.size(list);
isEven = Bindings.createBooleanBinding(() -> {
System.out.println(sizeBinding.get()); // key line 1 of 2
System.out.println("inside isEven");
return list.size() % 2 == 0;
}, sizeBinding);
// key line 2 of 2
isEven.addListener((obs, o, n) -> System.out.println(isEven.get() ? "Even" : "Odd"));
}
public static void main(String[] args) {
launch();
}
}
The above code works. Each time the add button is clicked, the print statements inside 'isEven' print as expected. However, this behavior completely depends on the 2 key lines. The print statement won't print anything if one or both of the key lines is missing. Here I made everything a field of the class, so they shouldn't be garbage collected. So what's the reason of this behavior?
This is JavaFX 11.0.2, AdoptOpenJDK-11.0.11+9, on macOS Catalina.
Solution
Bindings Are Lazy
In JavaFX, a binding has a "valid" state. When the value is potentially changed while the binding is in a valid state then the binding fires an invalidation event. But if the binding is not currently valid then no invalidation event will be fired. The way to "validate" a binding is to query its value. This setup allows a binding to compute its value only when that value is needed.
Depending On Bindings
Your isEven
binding is dependent on the sizeBinding
binding. This is implemented by having the isEven
binding observe the sizeBinding
binding via an InvalidationListener
. As noted above, invalidation events are only fired when a binding goes from a valid state to an invalid state.
The problem is, except for your println(sizeBinding.get())
call, you never query the value of sizeBinding
. That means you never validate the binding, and that means the isEven
binding never knows to recompute its value. If you had:
isEven = Bindings.createBooleanBinding(() -> sizeBinding.get() % 2 == 0, sizeBinding);
Then it would work. Notice it queries the sizeBinding
value.
Change Listeners
If a ChangeListener
is added to a binding then the value is computed eagerly. The reason for this is simple: The listener is passed both the old and new values. Without the ChangeListener
you add to the isEven
binding, the binding remains lazy. And since you never validate the isEven
binding it never needs to (re)compute its value.
With the ChangeListener
added, however, the value is (re)computed eagerly. But note the isEven
binding will only recompute its value if it knows that value has changed. In other words, you still have to make sure the sizeBinding
binding is validated when appropriate.
Answered By - Slaw
Answer Checked By - Candace Johnson (JavaFixing Volunteer)