如何将侦听器添加到双向绑定的对象

时间:2018-01-13 07:00:17

标签: java javafx

我正在尝试将TextArea的textProperty绑定到控制器的initialize()方法中的StringProperty。

当值发生变化时,侦听器会监听它们以执行某些行为。

但奇怪的事情发生了。

我构建了一个简单的模型来重现这种情况。

Main.java

package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setScene(new Scene(root, 400, 300));
        primaryStage.show();
    }


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

sample.fxml

<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.TextArea?>
<GridPane fx:controller="sample.Controller"
          xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10"
          prefHeight="300" prefWidth="400">
  <TextArea fx:id="textArea"/>
</GridPane>

我不认为上述代码与此问题相关。但为了以防万一,我把它放在这里。

这是控制器。

Controller.java

package sample;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;

public class Controller {

  @FXML
  TextArea textArea;

  private StringProperty toBind = new SimpleStringProperty();

  public void initialize() {
    textArea.textProperty().bindBidirectional(toBind);

    textArea.textProperty().addListener((observable, oldValue, newValue) -> {
      System.out.print("textArea: ");
      System.out.println(newValue);
    });

    toBind.addListener((observable, oldValue, newValue) -> {
      System.out.print("toBind: ");
      System.out.println(newValue);
    });
  }
}

使用此控制器,当我输入序列&#39; abcd&#39;到了textarea,我得到了:

textArea: a
textArea: ab
textArea: abc
textArea: abcd

似乎没有触发toBind对象的change事件。

然后我尝试在textArea的监听器中打印toBind的值。

新代码是:

package sample;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;

public class Controller {

  @FXML
  TextArea textArea;

  private StringProperty toBind = new SimpleStringProperty();

  public void initialize() {
    textArea.textProperty().bindBidirectional(toBind);

    textArea.textProperty().addListener((observable, oldValue, newValue) -> {
      System.out.print("textArea: ");
      System.out.println(newValue);

      // ----- New statements. -----
      System.out.print("toBind value in textArea: ");
      System.out.println(toBind.get());
      // ----- New statements. -----
    });

    toBind.addListener((observable, oldValue, newValue) -> {
      System.out.print("toBind: ");
      System.out.println(newValue);
    });
  }
}

然后我得到了:

toBind: a
textArea: a
toBind value in textArea: a
toBind: ab
textArea: ab
toBind value in textArea: ab
toBind: abc
textArea: abc
toBind value in textArea: abc
toBind: abcd
textArea: abcd
toBind value in textArea: abcd

为什么会这样?该事件被正确解雇。

2 个答案:

答案 0 :(得分:4)

您的绑定和toBind属性正在收集垃圾。

&#34;过早垃圾收集的简洁描述&#34; Tomas Mikula在blog上提供了问题。

首先,对于试图重现此问题的任何人来说,请快速放弃。由于所描述的行为取决于发生的垃圾收集,因此可能并不总是发生(它取决于内存分配,正在使用的GC实现以及其他因素)。如果添加行

root.setOnMouseClicked(e -> System.gc());

start()方法,然后单击场景中的空白区域将请求垃圾收集,并且问题将(至少更有可能)在此之后显现(如果它没有&t; t的话)。

问题在于绑定使用WeakListener来侦听属性中的更改并将这些更改传播到绑定属性。如果没有对该属性的其他实时引用,则弱侦听器被设计为不阻止其附加的属性被垃圾收集。 (理由是避免在属性不再在范围内时强制程序员手动清理绑定。)

在您的示例代码中,控制器及其属性toBind符合垃圾回收的条件。

start()方法完成后,保证引用的所有内容都是在您调用Application时创建的launch()实例,即显示的Stage,以及那些引用的任何东西。这当然包括Scene(由Stage引用),其rootroot的子女,他们的子女等,这些属性以及(非-weak)任何这些属性的监听器。

因此stage引用了scene,它引用了GridPane作为其根,并且引用了TextArea

TextArea引用了附加到它的侦听器,但该侦听器不保留其他引用。

(在您的代码的第二个版本中,ChangeListener附带的非弱textArea.textProperty()引用了toBind。因此在该版本中,ChangeListener阻止toBind成为GC&#39; d,你会看到监听器的输出。)

加载FXML时,FXMLLoader会创建控制器实例。虽然该控制器实例具有对字符串属性和文本区域的引用,但反之则不然。因此,一旦加载完成,就没有对控制器的实时引用,它有资格进行垃圾收集,以及它定义的StringProperty。文本区域textProperty()只有{em>弱引用给toBind上的侦听器,因此文本区域无法阻止toBind被垃圾回收。< / p>

在大多数实际情况中,这不会成为问题。除非您打算在某处使用它,否则您不太可能创建此额外的StringProperty。因此,如果您在&#34;自然&#34;中添加任何使用此代码的代码。你可能会看到这个问题消失了。

因此,例如,假设你添加了一个标签:

<Label fx:id="label" GridPane.rowIndex="1"/>

并将其文本绑定到属性:

  public void initialize() {
    textArea.textProperty().bindBidirectional(toBind);

    textArea.textProperty().addListener((observable, oldValue, newValue) -> {
      System.out.print("textArea: ");
      System.out.println(newValue);
    });

    toBind.addListener((observable, oldValue, newValue) -> {
      System.out.print("toBind: ");
      System.out.println(newValue);
    });

    label.textProperty().bind(toBind);
  }

然后场景中有一个对标签等的引用,因此它不是GC&#39; d标签的textProperty通过绑定到toBind而具有弱引用。由于label不是GC&#39; d,弱引用仍然存在垃圾回收,而toBind不能是GC,因此您可以看到预期的输出。

或者,如果您在其他位置引用toBind属性,例如在Application实例中:

public class Controller {

  @FXML
  TextArea textArea;

  private StringProperty toBind = new SimpleStringProperty();

  public void initialize() {
    textArea.textProperty().bindBidirectional(toBind);

    textArea.textProperty().addListener((observable, oldValue, newValue) -> {
      System.out.print("textArea: ");
      System.out.println(newValue);
    });

    toBind.addListener((observable, oldValue, newValue) -> {
      System.out.print("toBind: ");
      System.out.println(newValue);
    });

  }

  public StringProperty boundProperty() {
      return toBind ;
  }
}

然后

package sample;

import javafx.application.Application;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    private StringProperty boundProperty ;

    @Override
    public void start(Stage primaryStage) throws Exception{
        FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
        Parent root = loader.load();
        Controller controller = loader.getController();
        boundProperty = controller.boundProperty();
        root.setOnMouseClicked(e -> System.gc());
        primaryStage.setScene(new Scene(root, 400, 300));
        primaryStage.show();
    }


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

你再次看到预期的行为(即使在垃圾收集之后)。

最后(最后一点非常微妙),如果用{匿名内部类替换textArea.textProperty()上的监听器:

textArea.textProperty().addListener(new ChangeListener<String>() {

  @Override
  public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
    System.out.print("textArea: ");
    System.out.println(newValue);
  }
});

然后这也会阻止toBind的GC。这里的原因是匿名内部类的实例包含对封闭实例的隐式引用(在这种情况下是控制器的实例):这里控制器保留对toBind的引用。相比之下,Lambda表达式不会这样做。

答案 1 :(得分:1)

我找到了2个解答你的答案

<强> 1

将StringProperty声明为static

或2:

在TextArea Listener中,只需致电toBind.get()

但说实话,我不知道为什么会这样做。