3

I'm using JavaFX version 15.0.1. I want to make more complex scene via injecting several FXML files into it, like this:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>

<AnchorPane fx:controller="MainFxmlController">
   <children>
      <VBox>
         <children>
            <fx:include fx:id="topMenu" source="top_menu.fxml" />
            <fx:include fx:id="navigation" source="navigation.fxml" />
            <fx:include fx:id="statusBar" source="status_bar.fxml" />
         </children>
      </VBox>
   </children>
</AnchorPane>

Here I had found that controller of included FXML is loaded automatically and injected into @FXML annotated field named <value of fx:id>Controller in the main controller (in this case MainFxmlController).

My question is: How can I in this case use my own controller factory to instantiate the corresponding controller class? I need to give controller some dependencies in a constructor.

1 Answers1

7

The same controller factory will be used for the included FXMLs as for the enclosing FXML; so your controller factory can test which controller class is passed to the callback method, create the appropriate object, and pass dependencies to it.

Something like this:

// application model class:
DataModel model = new DataModel();

FXMLLoader loader = new FXMLLoader(getClass().getResource("main.fxml"));
loader.setControllerFactory(controllerType -> {

    if (controllerType == MainController.class) {
        return new MainController(model);
    }

    if (controllerType == TopMenuController.class) {
        return new TopMenuController(model);
    }

    if (controllerType == NavigationController.class) {
        return new NavigationController(model);
    }

    if (controllerType == StatusBarController.class) {
        return new StatusBarController(model);
    }

    return null ; // or throw an unchecked exception
});

Parent mainRoot = loader.load();

If you prefer (or need more generality), you can use reflection:

loader.setControllerFactory(controllerType -> {

    try {
        for (Constructor<?> c : controllerType.getConstructors()) {
            if (c.getParameterCount() == 1 
                && c.getParameterTypes()[0] == DataModel.class) {

                return c.newInstance(model);
            }
        }
        // If we got here, there's no constructor taking a model,
        // so try to use the default constructor:
        return controllerType.getConstructor().newInstance();
    } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
        throw new RuntimeException(e);
    }
});
James_D
  • 201,275
  • 16
  • 291
  • 322
  • And what if I need to do this the other way? For example when `TopMenuController` needs access to `MainController` because of changing the text of a label in `Main`. – Ondřej Kozel Jan 21 '21 at 12:09
  • 3
    @OndřejKozel Don’t. Change the model, and let the other controller update its view in response to the change in the model. See https://stackoverflow.com/a/32343342 Generally if one controller needs access to another, it’s a bad design. – James_D Jan 21 '21 at 12:12