Data binding 双向JavaFX绑定被不相关的代码破坏

Data binding 双向JavaFX绑定被不相关的代码破坏,data-binding,javafx,Data Binding,Javafx,更新:找到了一种更容易重现错误行为的方法 当我在三个变量之间设置双向JavaFX绑定时,这个绑定有时会被不相关的代码破坏 我创建了一个小的示例程序,它能够再现错误行为: 在主控制器中,设置绑定并添加三个侦听器以输出变量的新值: package bug; import java.nio.file.Path; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty

更新:找到了一种更容易重现错误行为的方法

当我在三个变量之间设置双向JavaFX绑定时,这个绑定有时会被不相关的代码破坏

我创建了一个小的示例程序,它能够再现错误行为:

在主控制器中,设置绑定并添加三个侦听器以输出变量的新值:

package bug;

import java.nio.file.Path;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;

public class MainController {

    @FXML
    private Foo foo;

    @FXML
    private Bar bar;

    private ObjectProperty<Path> pathProperty = new SimpleObjectProperty<>();

    @FXML
    private void initialize() {

    pathProperty.addListener((observablePath, oldPath,
        newPath) -> {
        System.out.println(newPath);
    });

    foo.pathProperty().addListener((observablePath, oldPath,
        newPath) -> {
        System.out.println(newPath);
    });

    bar.pathProperty().addListener((observablePath, oldPath,
        newPath) -> {
        System.out.println(newPath);
        });

    bar.pathProperty()
        .bindBidirectional(pathProperty);
    foo.pathProperty()
        .bindBidirectional(pathProperty);
    }

}
傅:

实际输出(点击四次按钮后):

Java版本:1.8.0_20


JavaFX版本:8.0.20-b26

为什么会发生这种情况

双向绑定通过创建侦听器并使用属性注册它们来工作。当属性标记为无效时,将调用这些侦听器,并更改依赖属性的值

绑定使用的侦听器是。这些侦听器只保留对它们正在观察的对象的弱引用。因此,如果范围中没有其他对这些属性的引用,则这些属性可以进行垃圾收集。一旦它们被垃圾收集,监听器就不再需要观察任何东西,绑定基本上消失了。这通常是一件好事,因为它可以防止难以追踪的内存泄漏,但偶尔(如您的示例中)会造成混乱的情况

在您的示例中,对属性的引用由
MainController
保存。当您调用
load()
时,该控制器由
fxmloader
实例化(可能在某个地方的
start()
方法中),但几乎可以肯定的是,除了
start()
方法之外,您不会保留对它的引用,该方法在应用程序终止之前很久就完成并退出了。因此,您的属性可以进行垃圾收集,当垃圾收集器运行时,它们将与绑定一起从堆中清除。我怀疑当您在
日期选择器上调用侦听器时,内存需求会迫使垃圾收集器运行。如果您按下按钮足够多次(可能多次),您应该会看到同样的情况发生,即使没有
日期选择器

一个更简单的例子

这里有一个简单的例子。有三个值绑定在一起的
IntegerProperty
s,每个值上都有一个侦听器,如您的示例所示。按下“递增”按钮将直接递增一个,因此应调用每个按钮上的侦听器。如果您通过按“rungc”按钮强制垃圾收集,您将“中断”实现

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class BidirectionalBindingDemo extends Application {

    @Override
    public void start(Stage primaryStage) {
        IntegerProperty x = new SimpleIntegerProperty();
        IntegerProperty y = new SimpleIntegerProperty();
        IntegerProperty z = new SimpleIntegerProperty();
        y.bindBidirectional(x);
        z.bindBidirectional(x);
        ChangeListener<Number> listener = (obs, oldValue, newValue) -> System.out.println(x.get()) ;
        x.addListener(listener);
        y.addListener(listener);
        z.addListener(listener);

        Button incrementButton = new Button("Increment");
        incrementButton.setOnAction(event -> x.set(x.get()+1));

        Button gcButton = new Button("Run GC");
        gcButton.setOnAction(event -> System.gc());

        HBox root = new HBox(5, incrementButton, gcButton);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(10));

        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
即使在垃圾回收之后,绑定仍然有效

如果您仍然需要解决方案


只是偶尔,您确实需要UI组件无法观察到的属性。在这种情况下,您必须确保只要需要它们,它们就在范围内。在您的代码中,尝试将对
MainController
的引用作为应用程序类中的实例变量(而不是局部变量)保存。

回答得很好。我现在不能测试它,但我很确定它会解决我的问题。不过我还是需要更多的名声来支持你。
package bug;

import java.nio.file.Path;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

public abstract class Base {

    protected ObjectProperty<Path> pathProperty = new SimpleObjectProperty<>();

    public ObjectProperty<Path> pathProperty() {
    return pathProperty;
    }

}
package bug;

public class BarController extends Base {

}
package bug;

import java.io.IOException;
import java.nio.file.Path;

import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.BorderPane;

public class Foo extends BorderPane {

    private final FooController controller;

    public Foo() {
    controller = new FooController();
    FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(
        "Foo.fxml"));
    fxmlLoader.setRoot(this);
    fxmlLoader.setController(controller);
    try {
        fxmlLoader.load();
    } catch (IOException exception) {
        throw new RuntimeException(exception);
    }
    }

    public ObjectProperty<Path> pathProperty() {
    return controller.pathProperty();
    }    

}
package bug;

import java.nio.file.Path;

import javafx.beans.property.ObjectProperty;
import javafx.scene.layout.BorderPane;

public class Bar extends BorderPane {

    private final BarController controller;

    public Bar() {
    controller = new BarController();
    }

    public ObjectProperty<Path> pathProperty() {
    return controller.pathProperty();
    }

}
0
0
0
1
1
1
2
2
2
3
3
3
4
4
4
5
5
5
6
6
6
7
7
7
8
8
8
9
9
9
10
10
10
11
11
11
0
0
0
1
1
1
2
2
2
3
3
3
4
4
4
5
5
5
6
6
6
7
7
7
8
8
8
(Select date with DatePicker)
9
10
11
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class BidirectionalBindingDemo extends Application {

    @Override
    public void start(Stage primaryStage) {
        IntegerProperty x = new SimpleIntegerProperty();
        IntegerProperty y = new SimpleIntegerProperty();
        IntegerProperty z = new SimpleIntegerProperty();
        y.bindBidirectional(x);
        z.bindBidirectional(x);
        ChangeListener<Number> listener = (obs, oldValue, newValue) -> System.out.println(x.get()) ;
        x.addListener(listener);
        y.addListener(listener);
        z.addListener(listener);

        Button incrementButton = new Button("Increment");
        incrementButton.setOnAction(event -> x.set(x.get()+1));

        Button gcButton = new Button("Run GC");
        gcButton.setOnAction(event -> System.gc());

        HBox root = new HBox(5, incrementButton, gcButton);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(10));

        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
    Label label = new Label();
    label.textProperty().bind(Bindings.format("x: %s y: %s z:%s", x, y, z));

    HBox root = new HBox(5, button, gcButton, label);