How to pass parameters to a controller's constructor when using `<fx:include>`?

て烟熏妆下的殇ゞ 提交于 2021-01-28 17:52:01

问题


I am learning JavaFX, and I have come across an issue involving the instantiation of controllers that I can not seem to solve. Essentially, I am wondering if it is possible to do one of the following:

  1. Pass a parameter to the constructor of the controller when including FXML with <fx:include>; or
  2. Specify a custom controller instance to use when including an FXML file with <fx:include>.

Note that these issues are related. In fact, the reason I am asking about option (2) is because it would solve option (1).


My Setup


I have the following, "main" FXML file:

<!-- XML declaration, imports, etc. removed for brevity -->
<BorderPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml">
    <!-- ... -->
    <center>
        <!-- Note that PageSwitcher is a custom control that is capable of switching between pages — you should be able to ignore it here. -->
        <PageSwitcher fx:id="mainPageSwitcher" currentPageIndex="0">
            <!-- ... -->
            <fx:include source="dashboard.fxml" fx:id="dashboard" />
        </PageSwitcher>
    </center>
</BorderPane>

It has an associated controller, MainPaneController. I won't display it here, but I can, if necessary.

You may have noticed that my main FXML file does not have an fx:controller attribute on its BorderPane, despite the fact that I said that it had an associated controller. This is because, rather than allowing the FXMLLoader to create a controller for me (and thus, leaving me with no way to pass parameters to the constructor of the controller class), I chose, when loading the main FXML page in my main application class (i.e. the class that extends Application), to create my own instance of the MainPaneController class. You can see the start() method of my main application class:

@Override
public void start(Stage primaryStage) throws IOException {
    FXMLLoader mainPaneLoader;
    MainPaneController mainPaneController;
    Parent mainPane;

    // Initialize the project manager.
    projectManager = new ProjectManager(primaryStage);

    // Initialize the main pane loader.
    mainPaneLoader = new FXMLLoader();

    // Initialize the main pane controller.
    mainPaneController = new MainPaneController(projectManager);

    // Load the main pane.
    mainPaneLoader.setController(mainPaneController);
    mainPaneLoader.setLocation(getClass().getResource(MAIN_PANE_FXML_PATH));
    mainPane = mainPaneLoader.load();

    Scene mainScene;

    // Create the main scene and add it to the primary scene.
    mainScene = new Scene(mainPane);
    primaryStage.setScene(mainScene);

    // Initialize the primary stage.
    primaryStage.setTitle(APPLICATION_TITLE);

    // Show the primary stage.
    primaryStage.show();
}

Note that the "project manager" object defined above and passed into the constructor of the main pane controller is actually the primary motivation behind this entire question; it is (in addition to being passed to the main controller) the object that I need to pass to the controller of the FXML file that I included into the main FXML file using <fx:include>.

Now, this approach of creating my own controller instance and giving it to the FXMLLoader works very well for me. It allows me to easily, without any messy reflection, pass parameters to the constructor of the controller. However, it only works when I have an FXMLoader object to give the controller instance to.

In the other case, where I include an FXML file from the main FXML file using <fx:include>, JavaFX creates the controller for me, providing no way for me to either (1) pass parameters to the controller's constructor, or (2) use my own controller instance.


What I've Tried


While researching this issue, I cam across this StackOverflow question which seemed to have at least some bearing on the issue. From it, I learned about FXMLLoader.setControllerFactory(), which seemed at first like it could solve this problem. However, in order to use it, I was forced to use some rather messy reflection to check if the type's constructor could accept my object, and then use more reflection to create the controller, all the while hoping that no errors were going to be thrown because of a loophole in my code. I was forced to concede that this was not going to work.

I have also experimented with, rather than passing my object to the constructor of the controller, setting the object on the controller after the controller has been initialized. However, this did not work well because I needed to use the object in my controller's initialize() method, which is called before I would set the object on the controller. This could potentially be worked around by adding another initialize method where any functionality requiring the object could be located, perhaps called objectInitialized(); but then I would have to add this method to every single controller that needs this functionality, and I would have to remember to call all these methods at some point. Also, I wanted the object to be a final field in the controller class; obviously, it can't be final if it needs to be set externally.

Lastly, I also considered the option that, for every FXML file that I need to include into the main FXML file, rather than including it in the FMXL, I could do it from the Java controller. This way I could create my own FXMLLoader, set my own controller instance on it, and thus, solve the problem. However, I would, if possible, much prefer to keep all the UI code in the FXML files.


Summary


In summary, I need a way to pass parameters to the constructor of my controller when using <fx:include>.

I realize that this is a long question and a somewhat complicated issue, so I really appreciate any help you can provide. Also, please let me know in the comments if I need to clarify anything or post additional code.

Thank you all for you help!
—Jacob


回答1:


After further consideration, I have decided to simply implement Option (3) in my question under "What I've Tried". Basically, instead of using <fx:include>, I decided that it would be easier, more flexible, and more future-proof to include any controllers that require parameters directly/manually from my Java code.


Why?


As I thought more about my issue, I realized that there was absolutely no way for me to pass a parameter to the constructor of my controller "from my FXML". After all, since the object I was trying to pass was defined in my Java code (the main application class), there was not going to be a way to use it in FXML. Thus, my entire perspective on the issue changed.

Option 1

After I re-evaluated my options, I considered just using FXMLLoader.setControllerFactor() after all; this approach was confirmed by James_D in the comments he posted on my question: since all controller instantiation happens through reflection, using reflection to pass my object to the controller wouldn't necessarily be as bad of an idea as I thought. In fact, it would probably work quite well.

However, while this would work well if all I needed to do was pass a single object to the controller, what if, for some of my controller, I wanted to pass multiple objects (and perhaps of different types)? Then the controller factory would grow rather unwieldy as I would have to check for multiple parameters of potentially different types, and then pass the correct ones.

In addition, what if I wanted to pass an object to a controller, not from the application class, where the controller factory is defined, but from somewhere else? For example, imagine that I had an application class, MyApplication.java, a "main" controller, MainController.java, and a controller "under" the main one, NavigationBarController.java? Perhaps I, at some point, would want to pass an object, not from MyApplication to NavigationBarController, but from MainController to NavigationBarController. If that was the case (which is quite possible), then my controller factory would no longer work because it would not have access to the necessary object. Thus, I concluded that setControllerFactory() would not work for me, at least not very well.

Option 2 (What I Used)

The other option (and the one I ended up using) was to include my controllers from my Java code, using FXMLLoader directly; and, rather then including all the controllers from the main application class, I could include each one in the controller that it "resided" in.

For example, using the example provided in Option (1) above, rather than using a controller factory and an fx:controller attribute in the FXML file, I would remove the fx:controller attribute from any FXML file/controller pair that needed access to my object.

Instead, in MyApplication I would use FXMLLoader directly to load, initialize, and add MainController. Then, in MainController, I would use FXMLLoader to include NavigationBarController.

Now, the biggest potential weakness with this approach is the fact that I would need to basically repeat the loading code every time I wanted to include an FXML file/controller pair. To solve this, I created a sort of "utility class" called FXMLQuickLoader that contains methods to easily and quickly load FXML file/controller pairs, but with custom controller objects. If you are interested in seeing the code, I created a GitHub Gist containing FXMLQuickLoader.java.

Option 3 (Untested)

Now, James_D pointed out that I could potentially use something called a "dependency injection factory". If I understood him correctly, it would automatically initialize my controllers' fields, rather than me needing to pass objects into constructors. This solution seems like it would work perfectly; however, because this is my first JavaFX project, and because I am under some time constraints, I decided that it would likely take too long to learn it.

However, for my next JavaFX project, I will probably look into it and (strongly) consider using it. I will try to update this answer later if I do.


Summary


In summary, for me, I determined that it would be best to include my controllers manually using FXMLLoader, rather than trying to use one of the alternatives. However, I am just learning JavaFX, and I am not exactly knowledgeable on the subject, so you should take this answer with a grain of salt. It may help other users who come along, though, and at least outline some potential solutions. I am also still open to further suggestions about the issue.



来源:https://stackoverflow.com/questions/60794477/how-to-pass-parameters-to-a-controllers-constructor-when-using-fxinclude

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!