0

I'm making a mod manager for a game, and I'm running into issues where I'm trying to call methods, but they cant be called because the controllers havent been initialized in time.

I use SceneBuilder wherever I can, I have a mix of manual loading because I am inserting entire FXMLs into the scene.

This is where the problem occurs: (in my ModListController)

    protected void handleModDoubleClick() {
        selectionList.clear();
        selectedMod = LV_ModList.getSelectionModel().getSelectedItem();
        if (selectedMod != null) {
            System.out.println("Selected mod: " + selectedMod); //TEMP @TODO
            ModFile mf = new ModFile(selectedMod);
            System.out.println("LOADING...");
            mf.retrieveData();
            xlc.setXmlListController(this); //THIS IS THE 'PROBLEM LINE'
            xlc.setXmlList(mf);
            selectionList.clear();
            System.out.println("DONE");
        }
    }

Somehow modListController is returning null when setting it.

in my XmlListController, I use this to set the controller instance:

    public void setXmlListController(ModListController mlc){
        mlc.setXmlListController(this);
    }

and it's called here in my MainController:

    private void initialize() { //This always loads last.

            System.out.println("MainContoller loaded");
            Platform.runLater(() -> {
                System.out.println("MainContoller loaded - runlater");
                ModListController mlc = new ModListController();
                uiHbox.getChildren().clear();
                uiHbox.getChildren().add(mlc.getListView()); //Adds ModList-View to scene

                FXMLLoader xmlUiLoader = new FXMLLoader(getClass().getResource("XmlList-View.fxml"));

                try {
                    Parent xmlListViewRoot = xmlUiLoader.load();
                    anchorPane.getChildren().add(xmlListViewRoot);
                    uiHbox.getChildren().add(anchorPane); //Adds XmlList-View to scene
                } catch (IOException e){
                    e.printStackTrace();
                }
                XmlListController xlc = xmlUiLoader.getController(); // Get the controller instance
                modListController.setXmlListController(xlc); //THIS IS WHERE IT'S CALLED.

                mlc.resetModList();
                handleModListClicks(mlc);

            });

I've put this inside of my ModListController to see if it was ever called:

    @FXML
    public void initialize(){
        System.out.println("Modlistcontroller loaded");

    }

And for reference, this is my method where the problem originated:

    public void setXmlList(ModFile mf){
        LV_XmlList.setDisable(false);
        mf.populateXmlList();
        xmlFileNames.clear();
        for (XmlFile xml : mf.modifiableXmlList){
            xmlFileNames.add(xml.fileName);
        }
        LV_XmlList.setItems(xmlFileNames);
    }

I was making another instance of the XmlListController in an attempt to make a nasty workaround, but the listview does not populate, likely because of the second instance.

here is the output to the console:

MainController loaded
MainController loaded - runlater
XmlListController loaded
XmlListController loaded - runlater

As you can tell, the ModListController is never initialized.

here is the Exception:

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException: Cannot invoke "com.rechaa.fs22mm.ModListController.setXmlListController(com.rechaa.fs22mm.XmlListController)" because "this.modListController" is null
    at com.rechaa.fs22mm/com.rechaa.fs22mm.MainController.lambda$initialize$0(MainController.java:79)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
    at java.base/java.lang.Thread.run(Thread.java:833)

despite all this, my modListController loads as expected in the UI if I remove the line that causes the exception, but it says this.xlc is null.

Any input is appreciated. I am stumped. I'm a novice at programming so if you see any room for improvement in general, I'll welcome the recommendations. I'd be happy to include any other code if needed. Obviously, I really want to avoid sharing the entirety of it though. Thanks again

I've tried: getting and setting controller instances double checked FXML paths commenting out bulks of code

Ryan
  • 35
  • 5
  • 1
    Create and post a [mre]. That said, it is pretty much always a mistake to instantiate an FXML controller yourself (as you do with `ModListController mlc = new ModListController()`). You haven't posted enough information for us to know what `mlc.getListView()` does but it is fairly reasonable to assume it returns `null` in that context. In any circumstances, it is a sign of bad design if controllers have references to each other. It is usually better to use some kind of [MVC design](https://stackoverflow.com/questions/32342864/applying-mvc-with-javafx). – James_D Aug 28 '23 at 01:21
  • Thank you for the advice! the getListView was the only way I could think of how to move the listView from the ModListController to the mainController scene. Believe it or not, it won't return null unless the arrayList is empty, which I've managed to handle the exceptions for. The whole FXML controller instantiation is party my problem to begin with, as I keep hitting wall trying to find a way to use methods from other controllers. For example, I have a separate FXML to get the path of the mod folder, then after accepted, it needs to repopulate the window on the main screen. I'll do better ty – Ryan Aug 28 '23 at 02:01
  • 1
    Your controllers simply shouldn’t need to communicate with each other at all. Consider using a [MVC] approach. See, for example, https://stackoverflow.com/questions/32342864/applying-mvc-with-javafx – James_D Aug 28 '23 at 10:52
  • Unpopular opinion, but everything would be simpler if you ditched the FXML and just coded up your layouts. All of this, "What controller is instantiated when?" silliness just disappears. – DaveB Aug 28 '23 at 13:23
  • @DaveB Maybe. But that would still leave the opportunity for poorly-designed code, which is the root cause of all these problems. I agree that the reflective nature of the `FXMLLoader` implementation obscures what is going on to a degree. Proper documentation of the `FXMLLoader` class and a clear and detailed description of what actually happens when you call `FXMLLoader::load` would go a very long way. (I actually requested this years ago, and the JavaFX team deemed it not necessary.) – James_D Aug 28 '23 at 13:29
  • @James_D. I mostly agree, but we see hundreds of questions here where people are confused about how to link up various layouts simply because they are using FXML. Add to that the fact that too many people think that the FXML Controller is the same thing as an MVC Controller, and your suggestion about "Consider using a [MVC] approach" also becomes problematic. Theoretically, there shouldn't be any difference between coded and FXML'd layouts with regards to framework, but in practice it's a big stumbling block. – DaveB Aug 28 '23 at 13:41
  • (Referring back to the actual question): @Ryan Where is the field `modListController` in `MainController` actually initialized? The bottom line here is that you have a null pointer exception because that field is null and you try to invoke `modListController.setXmlListController(...)`. Nowhere in the code snippets you've posted is anything that would suggest `modListController` is not null. – James_D Aug 28 '23 at 14:32
  • As previously stated, you should not instantiate the controllers yourself (i.e. you should not write `modListController = new ModListController()`). Instances of the controller class are created by the `FXMLLoader` when you call `load()`. You can retrieve them after they are created by calling `getController()` on the `FXMLLoader`. – James_D Aug 28 '23 at 14:36
  • @DaveB *"people are confused about how to link up various layouts simply because they are using FXML"*. I'm not sure this is true. I don't think the OP in this question has all this tight coupling between controllers because they are using FXML; I think they have the tight coupling because they haven't learned that it's a bad way to design the code. If the code is structured using any of the MV* family of design patterns then the question "What controller is instantiated when?" becomes irrelevant. (Which still leaves me trying to figure out if this question is answerable.) – James_D Aug 28 '23 at 14:44
  • @DaveB IIRC you had a blog post about MVC and FXML. Please feel free to add a link to it to my answer. – James_D Aug 28 '23 at 16:44

1 Answers1

2

This is a partial answer which addresses the parts of the question that can be answered.

The exception

Your stack trace indicates there is a NullPointerException on line 79 of MainController.java, where you try to invoke

modListController.setXmlListController(xlc);

The exception occurs because modListController is null, which means it has either never been assigned a value (most likely) or it has explicitly been assigned null (either the literal, or the result of an expression that evaluates to null).

You don't post any code that assigns anything to that variable (or otherwise would cause it to be assigned a value, such as via injection), so it's not possible to diagnose this further from the information you have provided.

Somehow modListController is returning null when setting it.

This statement is very confused. Setting the value of something typically doesn't return anything. And, as stated above, nowhere in the code you posted do you set the value of modListController (or otherwise cause it to be set).

Creation of controllers and invocation of initialize()

This is an attempt to address

I'm running into issues where I'm trying to call methods, but they can't be called because the controllers haven't been initialized in time.

Your code shows signs of having tried to fix problems that you (probably mistakenly) think are caused by the order of creation of controller instances (for example, the misuse of Platform.runLater(...) in the MainController's initialize() method).

Controller instances are created by the FXMLLoader when you call the load() method, and the FXML has a fx:controller attribute on the root element.

(You can also instantiate controllers in code and pass them to the FXMLLoader's setController() method, prior to calling load(). In this case you must not have a fx:controller attribute in the FXML. It doesn't look like you are doing this in your code.)

In general, the FXMLLoader performs the following when you call load():

  1. It reads the XML processing instructions (e.g. <?import ... ?>)
  2. It parses the root element, which is either an "instance element" (the element name is a class name) or <fx:root>.
    • If the root element has a fx:controller attribute
      • If there is already a controller (via a prior call to setController(...)), throw an exception
      • Otherwise
        • If there is a controllerFactory, pass the class specified in fx:controller to the controllerFactory, and set the controller to the result
        • Otherwise call the no-argument constructor on the class specified in fx:controller, and set the controller to the result
  3. For each instance element, create an instance of the specified class.
    • If the element has an fx:id attribute, and there is a controller, set the value of the field in the controller whose name matches the fx:id to the instance created
  4. When parsing is complete, if there is a controller and if it has an initialize() method, invoke the initialize() method
  5. Return the instance corresponding to the root element.

So the bottom line here is that if your FXML files have fx:controller attributes (and there is no controller factory, which is quite an advanced use), the controllers are created near the beginning of the load() process via a call to their default constructor, and the initialize() method is called near the end of the load() process, after any @FXML-annotated fields have been injected.

This means the order of creation of the controllers is (usually) controlled simply by the order in which you load the FXML files.

Advice

if you see any room for improvement in general, I'll welcome the recommendations

Some general rules to follow:

  1. Don't instantiate controller classes yourself. Use fx:controller attributes in the FXML file and let the FXMLLoader instantiate the controllers. The only exception to this is the following idiom:

     FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/view.fxml"));
     MyController controller = new MyController();
     loader.setController(controller);
     Parent root = loader.load();
    

    In this case the FXML file must not have a fx:controller attribute.

  2. Avoid direct communication between different controllers. The FXML-controller pair should stand alone and should completely encapsulate a unit of the UI.

The second point can be difficult for new programmers (and this is really separate from considerations about FXML and controllers; it's about how to separate concerns in your application). My recommendation is to use some kind of Model-View-XXX pattern. If you follow this approach, the controllers all communicate via a shared model instance, which contains the application data and logic. (The one common theme shared by all these patterns, and arguably the most important part of them, is the existence of a separate model class.) There are several different variants of these patterns. The best known (in the sense of the one that more people have heard of) is Model-View-Controller (MVC). In my opinion, the pattern that best fits FXML and FXML "controllers" is Model-View-Presenter, in which the FXML is a passive view and the FXML "controller" is really a presenter. Other people advocate thinking of the FXML-controller pair as comprising the View in MVC and having a separate controller class.

The canonical reference for all these patterns is Martin Fowler's UI Architecture Blog, despite the fact that it is quite old and in something of a draft form. See also this example using FXML (it is really more of an example of Model-View-Presenter, even though I refer to it as MVC).

James_D
  • 201,275
  • 16
  • 291
  • 322
  • Undoubtedly, MVP is the best fit if you want the FXML Controller to take one of the roles in MV-XXX. But, if you are going to do this, then you have to control the direction of the dependencies involved because the MV-XXX frameworks are ALL about reducing coupling. Since you cannot do anything useful with FXML without having @FXML instances of the controls, which makes the Controller dependent on the FXML file, you need to avoid having those "#" references back to the Controller in the FXML file. That's IFF you want the FXML Controller to act as Presenter in MVP. – DaveB Aug 29 '23 at 15:40
  • @DaveB There are [flavors of MVP](https://martinfowler.com/eaaDev/PassiveScreen.html) though where this bidirectional dependency between the view and presenter/controller are part of the design. Swing was actually implemented under the hood like this: the control classes (`JButton`, for example) really comprised the model, and behind the scenes was a "UIDelegate" which was effectively a tightly-coupled view and controller pair. I see these patterns as fairly dynamic, evolving as the general nature of requirements and user expectations change, subject to some basic immutable principles. – James_D Aug 29 '23 at 16:18
  • @DaveB *"Since you cannot do anything useful with FXML without having @FXML instances of the controls"* In theory you can, right? Since you can delegate the change in any observable property in a `Node` (or indeed any object) to the controller (via [property change handlers](https://openjfx.io/javadoc/20/javafx.fxml/javafx/fxml/doc-files/introduction_to_fxml.html#collections_and_property_handlers)). Some things (cell [value] factories, selection listeners) get too unwieldy to do this way, but they're technically possible. – James_D Aug 29 '23 at 16:26
  • I'm trying to think about how you would do something like load values into Labels/TextFields in response to the Model completing a service call to a database. Since you can't do the service call on the FXAT, you'll most likely have an Event on Task completion, or a call to Platform.Runlater(), neither of which directly are going to change properties (that they don't know about) in FXML fields. – DaveB Aug 29 '23 at 16:34
  • None of which changes the fact the the View in MVP is supposed to be passive layout only. Which IMHO means you can't define Event handlers in any case. I'm struggling to see how you would flip the dependency from Controller->FXML to FXML->Controller completely. Otherwise you have each somewhat dependent on the other, and the result is strong coupling. – DaveB Aug 29 '23 at 16:36
  • @DaveB You can do either (again, in theory). Application code: Given a `Model model`, do `FXMLLoader loader = new FXMLLoader(...);`, then `loader.getNamespace().put("model", model);` before `Parent node = loader.load();`, and then in the FXML something like `` and `"`. The controller's `search(...)` method can invoke a `Service` or submit a `Task` to an executor, with the `onSucceeded` handler updating the model. That's basically "classical MVC", with no need for `@FXML` fields. (There are corner cases, I admit.) – James_D Aug 29 '23 at 17:07
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/255102/discussion-between-james-d-and-daveb). – James_D Aug 29 '23 at 17:08