-1

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;
    }
}
  • This works as expected for me: I see updates for the text property in the `ContainerController` when the text changes. – James_D Jun 21 '20 at 16:02
  • java naming conventions, please .. – kleopatra Jun 21 '20 at 16:05
  • @James_D: Yes, I tried, but I did not work. The problem is, that the listener does NOT even get invoked. So changes within the handler-function are useless. Thanks anyway. – NeilArmstrong9000 Jun 21 '20 at 16:09
  • @kleopatra: Do you mean I missed one setter for the properties?! I left them out to save space in this post. Having them included did not make any difference. Or what convention do you mean?! – NeilArmstrong9000 Jun 21 '20 at 16:10
  • I was seeing the listener getting invoked. But I see the problem now. Answer imminent. – James_D Jun 21 '20 at 16:13
  • no, I mean exactly what I said: __do follow java naming conventions__ (hints: you don't and any search engine is your friend ;) – kleopatra Jun 21 '20 at 21:31

1 Answers1

2

The issue you are seeing is a really frustrating one: JavaFX bindings use WeakListeners 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();
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thank you very very much for (a) the explanation and (b) your solution, James! It worked! Though I am wondering if there is maybe a different solution as I have have several levels of embedded controls and think that I generate a lot of "boilerplate" code just to pass values to the top level control. – NeilArmstrong9000 Jun 21 '20 at 16:38
  • @NeilArmstron9000 Use a MVC approach: share a single model instance with each controller and bind to properties in the model, instead of binding between different controllers. See, e.g. https://stackoverflow.com/questions/32342864/applying-mvc-with-javafx/32343342#32343342 – James_D Jun 21 '20 at 16:40
  • Thanks again for your advice! I tried it out and it works BUT I am not sure if and how it really solves my problem. This is because my Container.fxml is containing an included fxml which is including another fxml. Is it possible to pass the model argument from my AppStarter.java to the control at the bottom level in one step without attaching controllers for every level? Or am I totally wrong and should I better use something like a depency injection framework? – NeilArmstrong9000 Jun 22 '20 at 08:18
  • @NeilArmstron9000 DI frameworks are always an option worth considering. Also note that if you set a `controllerFactory` on the FXML loader, the same factory will be used for loading included FXML files. This gives you the opportunity to provide a model to controllers and nested controllers (though likely requires using reflection in the code for the controller factory). See https://stackoverflow.com/a/37071740/2189127 – James_D Jun 22 '20 at 16:31
  • I will try it out! From my point of view my main question in this post was answered. And as DI is a different topic this question could be closed. Thanks so much again for your advices! – NeilArmstrong9000 Jun 23 '20 at 13:05