Search code examples
javajavafxfxml

Injected Controllers throw NullpointerException in JavaFX


In this application a HBox mainView contains a treeView and an overView containing a Button "A". Pressing the button will make the overView disappear and ViewA appear instead.

main-view.fxml:

<?import javafx.scene.layout.HBox?>

<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="mainView" fx:controller="com.desktop.controllers.MainController">
    <children>
        <fx:include source="tree-view.fxml" fx:id="treePane"/>
        <fx:include source="overview-view.fxml" fx:id="overviewPane"/>
        <fx:include source="A-view.fxml" visible="false" fx:id="APane"/>
    </children>
</HBox>

overview-view.fxml:

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>

<VBox xmlns:fx="http://javafx.com/fxml/1" fx:id="overviewPane" fx:controller="com.desktop.controllers.OverviewController">
    <children>
        <Button fx:id="AButton" text="A" onAction="#handleSwitchToA"/>
    </children>
</VBox>

a-view.fxml:

<?import javafx.scene.control.Label?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:id="aPane" fx:controller="com.desktop.controllers.AController">
    <children>
            <Label fx:id="aLabel" text="A"/>
    </children>
</VBox>

Since the application is very small I do not want to create multiple scenes and pass the treeView as reference. Instead I want to maintain one scene and just toggle the visibility between the Views A ... etc from the overview. I tried to copy the injection of MainController like in this example

MainController:

public class MainController implements Initializable {
    private Pane rootPane;
    @FXML
    private HBox mainView;
    @FXML
    private AController aController;
    @FXML
    private OverviewController overviewController;
    @FXML
    private VBox overviewPane;
    @FXML
    private Pane aPane;
    @FXML
    public void handleSwitchToA() {
        overviewController.getOverviewPane().setVisible(false);
        overviewController.getOverviewPane().setManaged(false);

        aController.getAPane().setVisible(true);
        aController.getAPane().setVisible(true);
    }
    public void setStartPane(Pane rootPane) {
        this.rootPane = rootPane;
    }
    @Override
    @FXML
    public void initialize(URL location, ResourceBundle resources) {
        assert overviewPane != null : "fx:id=\"overviewPane\" was not injected: check your FXML file 'main-view.fxml'.";
        overviewController.setMainController(this);
        aController.setMainController(this);
    }
}

OverViewController:

public class OverviewController implements Initializable {
    @FXML
    private VBox overviewPane;
    private MainController mainController;
    public void setMainController(MainController mainController) {
        this.mainController = mainController;
    }
    public VBox getOverviewPane() {
        return overviewPane;
    }
    @FXML
    void handleSwitchToA(ActionEvent actionEvent) {mainController.handleSwitchToA();}
    @Override
    public void initialize(URL location, ResourceBundle resources) {
    }
}

TestApplication:

public class TestApplication extends Application {
    private MainController mainController;
    private TreeController treeController;
    private Stage primaryStage;
    Pane rootPane;
    Scene rootScene;

    @Override
    public void start(Stage primaryStage) throws IOException {
        this.primaryStage = primaryStage;
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/desktop/main-view.fxml"));
        rootPane = fxmlLoader.load();
        rootScene = new Scene(rootPane);
        mainController = fxmlLoader.getController();
        mainController.setStartPane(rootPane);
        primaryStage.setScene(rootScene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

Unfortunately I run into an initialization issue here:

Caused by: java.lang.NullPointerException: Cannot invoke 
"com.desktop.controllers.OverviewController.setMainController(com.desktop.controllers.MainController)" because "this.overviewController" is null
    at com.madesktop/com.desktop.controllers.MainController.initialize(MainController.java:67)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2655)
    ... 12 more

I tried to track the issue by putting println("") into the initialize() methods of the controllers. The order in which the controllers get initialized is 1.OverViewController 2.AController 3.MainController

I cannot figure out why OverViewController is not initialized. Can anybody help out ?


Solution

  • When injecting a nested controller, the field name follows the pattern of <fx:id>Controller. Since you have:

    <fx:include source="overview-view.fxml" fx:id="overviewPane"/>
    

    That means you need to have overviewPaneController as the name of the field declared in MainController.

    You have a similar problem with aController. In the FXML file, you have fx:id="APane". So, the field name actually needs to be APaneController. Note the capital A there, because that's what you have in the FXML file. I recommend fixing the name in the FXML file to aPane and then use aPaneController as the field name. That additionally makes it align with the corresponding node field:

    @FXML 
    private Pane aPane;
    

    And it makes it so you're following standard Java naming conventions.