Search code examples
listviewjavafxcontrollerinitializationfxml

setting ListViews, FXML controller load order. (JavaFX)


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


Solution

  • 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).