双向JavaFX绑定由不相关的代码销毁 [英] Bidirectional JavaFX Binding is destroyed by unrelated code

查看:206
本文介绍了双向JavaFX绑定由不相关的代码销毁的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

更新:找到更简单的方式来重现错误行为

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

When I setup a bidirectional JavaFX binding between three variables this binding is sometimes destroyed by unrelated code.

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

I created a small example program which is capable of reproducing the buggy behavior:

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

In the MainController the binding is setup and three listeners are added to output the new value of the variable:

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的值不会改变,这样就可以预期。但是之后,每个数字只会输出一次。

The FooController changes one of the variables using a counter triggered by a button click. Pressing the button should output the same value three times because we setup three listeners. This works as expected as long the value of the DatePicker is not changed. But after that each number is only outputted once.

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控制器的基类>

Base class of the Foo and Bar controller

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

}

BarController:

BarController:

package bug;

public class BarController extends Base {

}

Foo:

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

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

Actual Output (after four button clicks):

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

Java Version:1.8.0_20

JavaFX版本:8.0.20-b26

JavaFX Version: 8.0.20-b26

推荐答案

为什么会发生这种情况

双向绑定通过创建监听器和regi用它们的特性对它们进行灭菌。当属性被标记为无效时,将调用这些侦听器,并修改相关属性的值。

The bi-directional bindings work by creating listeners and registering them with the properties. When the properties are marked as invalid, these listeners are invoked, and the dependent properties' values are changed.

绑定使用的侦听器是 WeakListener s 。这些监听器只保留对它们所观察到的对象的弱引用。因此,如果在范围中没有其他对这些属性的引用,那么这些属性就有资格进行垃圾回收。一旦收集垃圾,听众就不再需要观察,约束基本消失了。这通常是一件好事,因为它可以防止难以追踪的内存泄漏,但偶尔(如在您的示例中),它会产生混乱的情况。

The listeners that the bindings use are WeakListeners. These are listeners that only retain weak references to the objects they are observing. Thus if there are no other references to those properties in scope, the properties are eligible for garbage collection. Once they are garbage collected, the listeners no longer have anything to observe and the binding basically disappears. This is in general a good thing, because it prevents memory leaks that would be difficult to track down, but occasionally (as in your example) it creates confusing situations.

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

In your example, the references to the properties are held by the MainController. This controller is instantiated by the FXMLLoader (presumably in a start() method somewhere) when you invoke load(), but you almost certainly don't retain a reference to it beyond the start() method, which completes and exits long before the application terminates. Hence your properties are eligible for garbage collection, and when the garbage collector runs, they are cleared from the heap, along with the bindings. I suspect that when you invoke the listener on the DatePicker, the memory requirements force the garbage collector to run. If you press the button enough times (it may be many, many times), you should see the same thing happen, even without the DatePicker.

一个更简单的例子

这是一个更简单的例子。有三个 IntegerProperty s,其值绑定在一起,并且在每个示例中都有一个监听器。按增量按钮将直接增加一个,因此应调用其中每个的侦听器。如果强制垃圾收集,按运行GC按钮,您将中断执行。

Here's a simpler example. There are three IntegerPropertys whose values are bound together, and a listener on each of them as in your example. Pressing the "Increment" button will increment one directly, and so the listener on each of them should be invoked. If you force garbage collection, by pressing the "Run GC" button, you will "break" the implementation.

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组件是场景图的一部分。在我的例子中,如果我们向场景添加一个标签,并使其文本取决于属性:

In a real application, you rarely create properties that aren't used somewhere in the UI. Typically you would observe a property and when it changes, update the UI in response. This forces the UI component to hold (indirectly) a reference to the property, making it ineligible for garbage collection as long as the UI component is part of the scene graph. In my example, if we add a label to the scene and make its text dependent on the properties:

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

然后,即使在垃圾收集之后,绑定仍然保存。

then the bindings still hold even after garbage collection.

如果您仍然需要解决方法

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

Just occasionally, you do want properties that aren't observed by UI components. In this case, you must make sure they stay in scope as long as they are needed. In your code, try holding a reference to the MainController as an instance variable (not a local variable) in your application class.

这篇关于双向JavaFX绑定由不相关的代码销毁的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆