Zomono
Zomono

Reputation: 852

InvalidationListener only executed in debug mode with breakpoint

I have a problem with my InvalidationListener. It is set as Listener on a SimpleStringProperty. But it is only called for the first change of the SimpleStringProperty. I entered the debug-mode and made a break-point on the line which calls SimpleStringProperty::set and it started working until I removed the break-point again.

I made a short executable example program which simulates the modification of a SimpleStringProperty with a timer. You may run the program one time without break points and one time having a breakpoint at this line: property.set(value);

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;


public class Main extends Application {

    private SimpleStringProperty property;
    private int counter;

    @Override
    public void start(Stage stage) {
        // open a window to avoid immediately termination
        stage.setWidth(800);
        stage.setHeight(600);
        BorderPane pane = new BorderPane();
        stage.setScene(new Scene(pane));
        stage.show();

        // create a SimpleObjectProperty
        property = new SimpleStringProperty();
        property.addListener(observable ->
            System.out.println("New value is: " + counter)
        );
        counter = 0;

        // create timer to change 'property' every second
        Timeline timeline = new Timeline();
        KeyFrame keyFrame = new KeyFrame(Duration.seconds(2), event ->{
            String value = "" + ++counter;
            System.out.println("Set property to: " + value);
            property.set(value);
        });
        timeline.getKeyFrames().add(keyFrame);
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.playFromStart();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Output on my machine (Linux Mint 16.04 64bit, Oracle-Java 1.8.0_111):

Set property to: 1
New value is: 1
Set property to: 2
Set property to: 3
Set property to: 4
...

Please explain to me:

  1. Why is the listener not called on every change?
  2. Why is the listener called, when I set a break-point?
  3. What should I do to make it working without break-points?

Upvotes: 1

Views: 362

Answers (1)

James_D
James_D

Reputation: 209358

An observable value has two different states whose changes can trigger listeners. There is its value, and there is the state of whether or not it is currently valid.

In general, the value of an observable value may be something that is computed, rather than simply stored in a field. Once the value has been "realized" (my terminology), by being computed if it is computed, or by being retrieved if it is simply stored in a field, then the observable value is in a "valid" state. If the value changes (or may have changed), then the observable value becomes "invalid", indicating it may need to be recomputed or looked up again.

The invalidation listener is triggered only when the observable value transitions from a valid state to an invalid one. So in your code, the first time you call

property.set(value);

the property transitions to an invalid state (because the most recently retrieved value, if any, is not its current value).

Since you don't ever call property.get() (or property.getValue()), the property never gets validated. Consequently, the next time you call property.set(value), the property does not transition to an invalid state (it is already in that state), and so the listener is not fired.

If you replace your listener code with

property.addListener(observable ->
    System.out.println("New value is: " + property.get())
);

this listener will cause the property to become valid again, and so the listener will get fired each time.

The real issue is that you are using the wrong kind of listener here. If you want to perform an action every time the value changes, use a ChangeListener, not an InvalidationListener:

property.addListener((observable, oldValue, newValue) -> 
    System.out.println("New value is: " + newValue)
);

The observation that running in debug mode with a breakpoint causes the invalidation listener to get invoked every time is an interesting one. I'm guessing a bit, but what I suspect is happening is that when you hit the breakpoint, the debugger is showing the current values of the variables. This inevitably involves calling getValue() on the property (as part of its toString() implementation, perhaps), and so the property becomes validated.

You are unlikely to explicitly use an InvalidationListener often. Their main use is in bindings. Consider the following example:

DoubleProperty x = new SimpleDoubleProperty(3);
DoubleProperty y = new SimpleDoubleProperty(4);

DoubleBinding hyp = new DoubleBinding() {
    {
        bind(x);
        bind(y);
    }

    @Override
    protected double computeValue() {
        System.out.println("Computing distance");
        return Math.sqrt(x.get()*x.get() + y.get()*y.get());
    }
};

Label hypLabel = new Label();
hypLabel.textProperty().bind(hyp.asString("Hypotenuse: %f"));

The call to bind(x) in the binding implementation means: when x becomes invalid, consider this binding invalid. Similarly for y. Of course, the implementation of bind uses InvalidationListeners under the hood.

The point here is that the computation of hyp's value is pretty expensive. If you were to make multiple changes to x or y, then hyp becomes invalid and needs to be recomputed when you need it again. Since the text property of the label is bound to hyp, this also means the label's text becomes invalid. However, you only actually need the new value when the label is repainted in a rendering pulse; it would be overkill to compute the value for every change of x and y.

Upvotes: 6

Related Questions