简体   繁体   中英

Why does calling get of a bound value in the binding throw a stackoverflow error

This code throws a stackoverflowerror in a label binding when getting the value of the label being bound. I expect the label to be "test" initially then on the first press "test pressed" then "test pressed pressed" and so on. However reading the value throws a stackoverflowerror because calling the getText() method triggers the binding. I expect only button press events to trigger the binding.

Note: I've commented out the code which causes the error and added another button to better show what I'm confused about.

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application{

    @Override
    public void start(Stage primaryStage) {

        Label l = new Label("test");
        Button b = new Button("press me");

        l.textProperty().bind(Bindings.createStringBinding(() ->{
            System.out.println("changing label text");
            return "ok";
            //return l.getText() + " pressed"; //Causes a stackoverflow error
        },b.pressedProperty()));

        Button b2 = new Button("press me 2");
        b2.pressedProperty().addListener((o) -> {
            l.getText(); //Why does this not triggger the binding?
        });

        VBox root = new VBox();
        root.getChildren().addAll(l,b,b2);

        Scene scene = new Scene(root, 300, 250);
        primaryStage.setTitle("Binding test");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

My objective here is to have a binding which, on a certain condition, does not change the text. The logic in the Callable Lambda is something like:

if(condition){
    return "ok";
}else{
    return l.getText(); //if the condition is not met then use the current value.
}

I know I can use a listener on the pressed property and then set the text label that way, so I have a solution but I would like to know why the above is happening.

Just semantically, your binding expresses the rule that the text of the label is the text of the label concatenated with " pressed" . Clearly this is saying that the label's text depends on the label's text, so it's recursive.

I don't think this is the rule you want to impose anyway. I think you want the rule to be "the label's text is "test" if the button is not pressed, and "test pressed" if the button is pressed. (Right now your binding is told to recompute if the button's pressed property changes, but the value doesn't actually depend on that property.)

Technically what's happening is something along the following lines:

public class Label {

    private final StringProperty textProperty = new SimpleStringProperty() ;

    public String getText() {
        return textProperty().get();
    }

    // ...
}

and

public class SimpleStringProperty {

    private StringBinding binding ;
    private boolean bound ;
    private String value ;

    // ...

    public String get() {
        if (bound) {
            value = binding.get();
        }
        return value ;
    }

    public void bind(StringBinding binding) {
        bound = true ;
        this.binding = binding ;
        value = binding.get();
    }
}

Finally, string binding has logic along the following lines:

public abstract class StringBinding {

    private boolean valid = false;
    private String value ;

    protected void bind(ObservableStringValue dependency) {
        dependency.addListener(o -> invalidate());
    }

    private void invalidate() {
        valid = false ;
        // notify invalidation listeners...
    }

    public String get() {
        if (!valid) {
            value = computeValue();
            valid = true ;
        }
        return value ;
    }

    public abstract String computeValue();
}

And in your example, the implementation of computeValue() invokes the label's getText() method.

So when you create the binding, the value of the label's text property is set from the value of the binding. The binding isn't valid (because it hasn't been computed yet), so it is computed via the method you provide. That method invokes label.getText() , which gets the value from the property. Because the property is bound, it check the binding, which still isn't valid (because the computation of its value hasn't been completed), so it computes its value, which invokes label.getText() ...

So you probably want something like:

label.textProperty().bind(Bindings.createStringBinding(() -> {
    if (b.isPressed()) {
         return "test pressed";
    } else {
         return "test";
    }
}, b.pressedProperty());

If you want the underlying string to be able to be changed, you need to create a new property for it:

StringProperty text = new SimpleStringProperty("test");
label.textProperty().bind(Bindings.createStringBinding(() -> {
    if (b.isPressed)() {
        return text.get() + " pressed" ;
    } else {
        return text.get();
    }
}, text, b.pressedProperty());

or, equivalently

label.textProperty().bind(text.concat(
    Bindings.when(b.pressedProperty())
    .then(" pressed")
    .otherwise("")));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM