The question is published on by Tutorial Guruji team.
I want my included/embedded fxml-based controls to inform their containing objects (parent views) about changes in their states/properties. As you can see below I wrote a Container.fxml including a View.fxml plus the according to controller classes, named “Container.java” and “View.java”.
When the textField of ViewController.java changes, its StringProperty ‘textProperty_View’ is updated. That seems to be working properly as the OnAction-handler is invoked as expected.
But the listener in the parent ContainerController.java does not fire though I bound its StringProperty to the StringProperty of ViewController.java and added a ChangeListener for this property.
What am I doing wrong?
P.S.:
- I simplified the example by doing the same thing without
fx:include
to see if the binding works. It works. But not when I embed the view like in the described problem (see code below) - Am using javafx-15-ea+3 and java11
AppStarter.java
public class AppStarter extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage stage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("/fxml/Container.fxml")); Scene scene = new Scene(root, 300, 275); stage.setScene(scene); stage.show(); } }
ContainerController.fxml
<HBox xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="gui.ContainerController"> <fx:include source="View.fxml" fx:id="view"/> <!-- <TextField fx:id="textFieldMain" prefWidth="200" onAction="#onActionMain"/> --> </HBox>
ContainerController.java
public class ContainerController implements Initializable { private final StringProperty textProperty_Main = new SimpleStringProperty(); @FXML private ViewController viewController; // @FXML // void onActionMain(ActionEvent event) { // System.out.println("onActionMain " + viewController.getTextProperty_View()); // } @Override public void initialize(URL location, ResourceBundle resources) { textProperty_Main.bind(viewController.textProperty_ViewProperty()); textProperty_MainProperty().addListener((observable, oldValue, newValue) -> System.out.println("textProperty of Container changed to: " + getTextProperty_Main()) ); } public String getTextProperty_Main() { return textProperty_Main.get(); } public StringProperty textProperty_MainProperty() { return textProperty_Main; } }
View.fxml
<HBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="gui.ViewController"> <TextField fx:id="textField" prefWidth="200" onAction="#onAction">Unset</TextField> </HBox>
ViewController.java
public class ViewController { private final StringProperty textProperty_View = new SimpleStringProperty(); @FXML private TextField textField; @FXML void onAction(ActionEvent event) { textProperty_ViewProperty().setValue(textField.getText()); System.out.println("textProperty of View changed to: " + textProperty_ViewProperty().getValue()); } public String getTextProperty_View() { return textProperty_View.get(); } public StringProperty textProperty_ViewProperty() { return textProperty_View; } }
Answer
The issue you are seeing is a really frustrating one: JavaFX bindings use WeakListener
s to implement the bindings. This means that if the bindings go out of scope, the listeners are eligible for garbage collection, and consequently may stop working. (In my setup, your code actually works, but if I invoke System.gc()
somewhere, it immediately stops working.)
The only fix I can find for this is to explicitly retain a reference to the ContainerController
in the application class (since the ContainerController
has a reference to the ViewController
, and the ViewController
has a reference to the text field, this creates a path to the binding’s listener via the listeners on the text field, etc.):
public class AppStarter extends Application { private ContainerController controller ; public static void main(String[] args) { launch(args); } @Override public void start(Stage stage) throws Exception { FXMLLoader loader = new FXMLLoader(getClass().getResource("Container.fxml")); Parent root = loader.load(); this.controller = loader.getController(); Scene scene = new Scene(root, 300, 275); stage.setScene(scene); stage.show(); } }