如何将侦听器添加到双向绑定的对象 [英] How to add listeners to bidirectionally bound objects

查看:76
本文介绍了如何将侦听器添加到双向绑定的对象的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试将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;

公共类Main扩展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/fxmlalignment =centerhgap =10vgap =10
prefHeight =300prefWidth =400>
< TextArea fx:id =textArea/>
< / GridPane>

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



这是控制器。



Controller.java

 包裹样本; 

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

公共类控制器{

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

使用此控制器,当我输入序列'abcd '到textarea,我得到:

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

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

然后我尝试在textArea的Listener中打印toBind的值。



新代码是:

 包样本; 

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

公共类控制器{

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

// -----新语句.-----
System.out.print(textArea中的toBind值:);
System.out.println(toBind.get());
// -----新语句.-----
});

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

然后我得到了:

  toBind:
textArea:textArea中的
toBind值:a
toBind:ab
textArea: textArea中ab
toBind值:ab
toBind:abc
textArea:abc
textBrea中的绑定值:abc
toBind:abcd
textArea:abcd
在textArea中的绑定值:abcd

为什么会发生这种情况?该事件被正确触发。

解决方案

您的绑定,以及 toBind 财产,正在收集垃圾。



Tomas Mikula在他的博客






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

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

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






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



在示例代码中,控制器及其属性 toBind 有资格进行垃圾回收。



start()之后方法完成,保证引用的所有内容都是在调用 launch()时创建的 Application 实例,显示的 Stage ,以及从中引用的任何内容。这当然包括 Scene (由 Stage 引用),其 root root 的子元素,它们的子元素等,这些属性以及任何这些属性上的(非弱)侦听器。



所以阶段引用场景,其中有一个引用 GridPane 这是它的根,并且引用了 TextArea



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



(在您的代码的第二个版本中,附加到 ChangeListener > textArea.textProperty()引用了 toBind 。所以在那个版本中, ChangeListener 阻止 toBind 进行GC,你会看到监听器的输出。)



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



在大多数实际场景中,这不是一个问题。你不太可能创建这个额外的 StringProperty ,除非你打算在某个地方使用它。因此,如果您添加任何以自然方式使用此代码的代码,您可能会看到该问题消失。



因此,例如,假设您添加了标签:

 < Label fx:id =labelGridPane.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'd和标签的 textProperty 通过绑定到 toBind 而有一个弱引用。由于标签不是GC,因此弱引用仍然存在垃圾回收,并且 toBind 无法进行GC,所以你看到了你期望的输出。



或者,如果你在其他地方引用 toBind 属性,例如在应用程序实例中:

 公共类控制器{

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

然后

 包样本; 

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;

公共类Main扩展Application {

private StringProperty boundProperty;

@Override
public void start(Stage primaryStage)抛出异常{
FXMLLoader loader = new FXMLLoader(getClass()。getResource(sample.fxml));
父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);
}
});

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


I am trying to bind a TextArea's textProperty to a StringProperty in controller's initialize() method.

Both of them are listened by listeners to perform some behavior when value changes.

But something weird happens.

I build a simple model to reproduce the situation.

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>

I don't think the above code is relevant to this question. But just in case, I put it here.

Here is the Controller.

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

With this controller, when I input the sequence 'abcd' to the textarea, I get:

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

It seems that the change event for the toBind object is not fired.

So then I tried to print the value of toBind in textArea's Listener.

The new code is:

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

Then I got:

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

Why does this happen? The event is fired properly.

解决方案

Your binding, and the toBind property, are getting garbage collected.

A succinct description of the "premature garbage collection" problem is provided by Tomas Mikula on his blog.


First, a quick aside for anyone trying to reproduce this issue. Since the behavior described depends on garbage collection occurring, it may not always occur (it depends on memory allocation, the GC implementation being used, and other factors). If you add the line

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

to the start() method, then clicking on the blank area in the scene will request garbage collection, and the issue will (at least be more likely to) manifest itself after that (if it hasn't already).


The problem is that bindings use WeakListeners to listen to changes in properties and propagate those changes to the bound properties. A weak listener is designed not to prevent the property to which it is attached from being garbage collected if there are no other live references to that property. (The rationale is to avoid having to force programmers to manually clean up bindings when properties are no longer in scope.)

In your example code, the controller and its property toBind are eligible for garbage collection.

After the start() method completes, all that you are guaranteed to have references to are the Application instance created when you call launch(), the Stage that is shown, and anything referenced from those. This of course includes the Scene (referenced by the Stage), its root, the children of the root, their children, etc, properties of those, and (non-weak) listeners on any of those properties.

So the stage has a reference to the scene, which has a reference to the GridPane which is its root, and that has a reference to the TextArea.

The TextArea has a reference to the listener that is attached to it, but that listener keeps no additional references.

(In the second version of your code, the non-weak ChangeListener attached to the textArea.textProperty() has a reference to toBind. So in that version, the ChangeListener prevents toBind from being GC'd, and you see the output from the listener on it.)

When you load the FXML, the FXMLLoader creates the controller instance. While that controller instance has references to the string property and the text area, the reverse is not true. So once loading is complete, there are no live references to the controller, and it is eligible for garbage collection, along with the StringProperty it defines. The text area's textProperty() has only a weak reference to a listener on toBind, so the text area cannot prevent toBind being garbage collected.

In most real scenarios, this won't be a problem. You are unlikely to create this additional StringProperty unless you are going to use it somewhere. So if you add in any code that uses this in a "natural" way, you are likely to see the issue disappear.

So, e.g., suppose you add a label:

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

and bind its text to the property:

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

Then the scene has a reference to the label, etc, so it is not GC'd, and the label's textProperty has a weak reference via its binding to toBind. Since the label is not GC'd, the weak reference survives garbage collection, and toBind cannot be GC'd, so you see the output you expect.

Alternatively, if you reference the toBind property elsewhere, e.g. in the Application instance:

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

and then

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

you again see the expected behavior (even after garbage collection).

Finally (and this last point gets very subtle), if you replace the listener on textArea.textProperty() with an anonymous inner class:

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

then this also prevents GC of toBind. The reason here is that instances of anonymous inner classes contain implicit references to the enclosing instance (i.e. the instance of the controller in this case): and here the controller keeps a reference to toBind. Lambda expressions, by contrast, don't do this.

这篇关于如何将侦听器添加到双向绑定的对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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