【问题标题】:Bidirectional JavaFX Binding is destroyed by unrelated code双向 JavaFX 绑定被无关代码破坏
【发布时间】:2014-10-11 08:25:44
【问题描述】:

更新:找到了一种更简单的方法来重现错误行为

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

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

在 MainController 中设置绑定并添加三个监听器以输出变量的新值:

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);
    }

}

FooController 使用由单击按钮触发的计数器更改其中一个变量。因为我们设置了三个监听器,所以按下按钮应该输出相同的值三次。只要不更改 DatePicker 的值,这将按预期工作。但是之后每个数字只输出一次。

package bug;

import java.nio.file.Paths;
import java.time.LocalDate;

import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.DatePicker;

public class FooController extends Base {

    int counter = 0;

    @FXML
    private DatePicker startDatePicker;

    private ChangeListener<LocalDate> breakThings;

    @FXML
    private void onBugClicked(ActionEvent event) {
    for (int i = 0; i < 3; i++) {
        pathProperty.set(Paths.get(String.valueOf(counter++)));
    }
    }

    @FXML
    private void initialize() {

    breakThings = (observableDate, oldDate, newDate)->{
        System.out.println("Triggered");
    };  

    startDatePicker.valueProperty().addListener(breakThings);
    }
}

Foo 和 Bar 控制器的基类

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

Java 版本:1.8.0_20

JavaFX 版本:8.0.20-b26

【问题讨论】:

    标签: data-binding javafx


    【解决方案1】:

    为什么会这样

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

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

    在您的示例中,对属性的引用由MainController 持有。当您调用load() 时,该控制器由FXMLLoader 实例化(可能在某处的start() 方法中),但您几乎可以肯定不会在start() 方法之外保留对它的引用,该方法完成并退出很长时间在应用程序终止之前。因此,您的属性有资格进行垃圾收集,并且当垃圾收集器运行时,它们会与绑定一起从堆中清除。我怀疑当您在DatePicker 上调用侦听器时,内存要求会强制垃圾收集器运行。如果你按下按钮的次数足够多(可能很多次),你应该会看到同样的事情发生,即使没有DatePicker

    一个更简单的例子

    这是一个更简单的例子。有三个 IntegerPropertys 的值绑定在一起,并在每个上都有一个侦听器,如您的示例所示。按下“增量”按钮将直接增加一个,因此应该调用每个监听器。如果你强制垃圾回收,按下“Run GC”按钮,你将“破坏”实现。

    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 中某处使用的属性。通常你会观察一个属性,当它发生变化时,更新 UI 作为响应。这会强制 UI 组件(间接)持有对属性的引用,只要 UI 组件是场景图的一部分,它就无法进行垃圾收集。在我的示例中,如果我们向场景添加标签并使其文本依赖于属性:

        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);
    

    那么即使在垃圾回收之后绑定仍然有效。

    如果您仍需要解决方法

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

    【讨论】:

    • 很好的答案。我现在无法测试它,但我很确定它会解决我的问题。不过,我仍然需要更多声望才能投票给你。
    猜你喜欢
    • 2015-03-01
    • 2017-09-06
    • 2023-03-28
    • 2011-04-19
    • 2016-10-17
    • 2015-01-19
    • 2017-04-22
    • 2014-01-09
    相关资源
    最近更新 更多