0

I have researched this for a couple hours now, and even though there is a lot about "multiple controllers", none of the solutions worked for my problem. I don't know if I am just structuring my program wrong, so I need some input.

I got a "MainView" (App class with AppView and AppController) and a "SubView" (SubView and SubViewController). In App, I am loading the mainView as primaryStage and loading the AppController. I created a stageManager class to handle different views. My problem is: Where and how do I create/load my subViewController?

public class App extends Application {

    private Stage primaryStage;
    private AnchorPane rootLayout;
    private AppController appController;

    MainModel model = new MainModel();
    StageManager stageManager;

    @Override
    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;
        this.primaryStage.setTitle("App");

        initRootLayout();
        appController.initModel(model);
        appController.setStageManager(stageManager);
    }

    public void initRootLayout() {
        try {
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(getClass().getResource("mainView.fxml"));
            rootLayout = (AnchorPane) loader.load();
            appController = loader.<AppController>getController();

            Scene scene = new Scene(rootLayout);
            primaryStage.setScene(scene);
            primaryStage.show();

            stageManager = new StageManager(primaryStage, rootLayout, appController);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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




public class AppController implements Initializable {

    private MainModel model;
    private StageManager stageManager;

    @FXML
    private Button btn;

    @FXML
    private void handleButtonAction(ActionEvent event) {
        stageManager.changeScene("subView.fxml");
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {
    }   

    public void initModel(MainModel model) {
        this.model = model;
    }

    public void setStageManager(StageManager stageManager){
        this.stageManager = stageManager;
    }
}


public class StageManager {

    private Stage primaryStage;
    private AnchorPane rootLayout;
    private AppController appController;

    public StageManager(Stage primaryStage, AnchorPane rootLayout, AppController appController){
        this.primaryStage = primaryStage;
        this.rootLayout = rootLayout;
        this.appController = appController;
    }

    public void changeScene(String scene){
        try {
            Parent parentPane = FXMLLoader.load(getClass().getResource(scene));
            primaryStage.getScene().setRoot(parentPane);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


public class SubController implements Initializable {

    private MainModel model;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
    }    

    public void setModel(MainModel model){
        this.model = model;
    }    
}


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

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.text.Font?>

<AnchorPane id="AnchorPane" prefHeight="524.0" prefWidth="850.0" xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="app.SubController">
   <children>
      <Label fx:id="lbl" layoutX="52.0" layoutY="13.0" prefHeight="38.0" prefWidth="214.0" text="just a label">
         <font>
            <Font name="Consolas" size="24.0" />
         </font>
      </Label>
   </children>
</AnchorPane>

I tried the following things to load the other controller: First I tried doing it the same way I am loading the appController (since that works), but without setting the view as the scene. So in the initRootLayout() method I simply added:

FXMLLoader subloader = new FXMLLoader();
subloader.setLocation(getClass().getResource("subView.fxml"));
subController = loader.<SubController>getController();

This didn't work. In fact something really weird happened. When I tried calling a method from subController, I got a NullPointerException. Debugging revealed that the subController is being loaded, but right after its loaded it disappears and subController Object is just "null". Why is that?

The next thing I tried after reading about this approach was to add the subController as a FXML variable. In the AppController I added @FXML private SubController subController; and in the AppController's initialize() method I tried making a call from the controller:

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        subController.setModel(model);
    } 

In this case, I am getting a NullPointerException, but it is actually pointing to the line where I call appController.initModel(model); in the App class. Even though this line works fine when I am not trying to use the subController. So, I am extremely confused and I think I need a whole new structure for this. Any kinda help would be greatly appreciated! :)

EDIT: This seems to be working: I changed the changeScenes() method to this:

public void changeScene(String scene){
    try {
     FXMLLoader loader = new FXMLLoader(getClass().getResource(scene));
     Parent root = (Parent) loader.load();
     subViewController = loader.getController();
     subViewController.setModel(mainModel);
     Scene newScene = new Scene(root);
     Stage newStage = new Stage();
     newStage.setScene(newScene);
     newStage.show();
  } catch (Exception e) {
     e.printStackTrace();
  }
}
kattapillar
  • 136
  • 12
  • James_D has a very simple example of this type of setup. I would try to find that If I were you. – SedJ601 Jan 04 '18 at 20:24
  • If it's not 100% necessary for you to take this approach, I recommend that you don't. – SedJ601 Jan 04 '18 at 20:25
  • The last approach only works with a `` element with a `fx:id` of `sub` in the fxml used with the controller containing the `initialize` method. In the first approach you construct a `FXMLLoader` instance that is never used to load the fxml and then proceed to get the controller of ***a different*** `FXMLLoader` instance... – fabian Jan 04 '18 at 20:25
  • @Sedrick could you point me to it? Like I said I have been researching this... It's not like I am asking without trying to figure it out myself. Also what approach should I not use if not necessary? – kattapillar Jan 04 '18 at 20:39
  • @fabian makes sense. But how should I do this? I am thinking of some kind of genereal create-all-controllers class or something. But I really don't know. Like I said all the examples I found either don't work (as described) or don't look anything like what I have. Pls, I need some help! – kattapillar Jan 04 '18 at 20:39
  • Is your app supposed to switch from the `mainView` to different `subViews` or is the `subView` apart of the` mainView` and the `subViews` change within the `mainView`? – SedJ601 Jan 04 '18 at 20:44
  • For nested fxmls [`controllerFactory`](https://docs.oracle.com/javase/8/javafx/api/javafx/fxml/FXMLLoader.html#setControllerFactory-javafx.util.Callback-) could be a solution. Note that it needs to be set every time you use a new instance of `FXMLLoader` so if you call `load` multiple times you need to take care of that. A the factory pattern would be beneficial in this case. – fabian Jan 04 '18 at 20:47
  • @Sedrick It's supposed to switch to different subViews, so they are not supposed to be there at the same time if that's what you mean? – kattapillar Jan 04 '18 at 20:54
  • @fabian Okay, I'll look into controllerFactory! Thank you! – kattapillar Jan 04 '18 at 20:54
  • It's not really clear what you are missing here. The `FXMLLoader` creates the controller when you load the FXML file. So if you're loading that in the `changeScene()` method you would have to retrieve it from the `FXMLLoader` in that method. You could return it from that method if you need access to it from where you call `changeScene()` – James_D Jan 04 '18 at 21:01
  • https://stackoverflow.com/questions/18619394/loading-new-fxml-in-the-same-scene and https://stackoverflow.com/questions/36463725/changing-scenes-in-javafx-nullpointerexception/36463793#36463793 – SedJ601 Jan 04 '18 at 21:02
  • @James_D Yes, I also tried that (loading it from the changeScenes method). I have the same problem here that the controller object basically "disappears". I create it like this: subController = loader.SubController>getController(); and in the next like, when I call subController.setModel(model);, I get a NullPointerException. Why is my object just disappearing? – kattapillar Jan 04 '18 at 21:12
  • That just sounds like you didn't declare the controller class in the "sub" FXML. – James_D Jan 04 '18 at 21:13
  • @Sedrick Thank you. I saw this earlier too, but I didn't want to use this approach (Controller controller = new Controller) because in an earlier post, that approach was the reason why my appController didn't work. I was told the controller shouldn't be initialized like regular objects but instead should be loaded by FXMLLoader. – kattapillar Jan 04 '18 at 21:15
  • @kattapillar I don't understand "shouldn't" in the last sentence of that comment. If you have a `fx:controller` attribute in the root element of your FXML file, the `FXMLLoader` will create the controller for you. If you *want* to create the controller yourself, omit the `fx:controller` attribute and call `setController` on the `FXMLLoader`, before loading the FXML. There is no should/shouldn't about it. – James_D Jan 04 '18 at 21:33
  • In the original post, I added the code of SubView FXML, just to confirm that fx:controller is declared. – kattapillar Jan 04 '18 at 23:36
  • @kattapillar Ok, actually in the code you posted, you create an `FXMLLoader` and set its location, but you never actually load the FXML. So the controller will be null: the controller is created when the FXML is loaded. It doesn't really make any sense to do that if you're not going to display the UI defined in the FXML file anyway. – James_D Jan 05 '18 at 03:47
  • Thank you @James_D! Your last comment made me research "loading fxml" again and I think I found the fix. I'm adding the changes I just made to the original post. – kattapillar Jan 06 '18 at 13:43

0 Answers0