javajavafx

JavaFX best way to manage different controllers using each others methods?


I'm new to JavaFX. I'm having troubles with controllers.

I have two GUIs, called A and B, each one with its controller (ControllerA and ControllerB).

My program is pretty simple: it starts by opening A, and there's a button that opens B when pressed. Viceversa, B has a button that opens A.

ControllerA has one method, called "openA", and ControllerB has one method called "openB".

So, A needs a ControllerB to open B, and viceversa again.

I watched a tutorial and the way he deals with controller communication is the following:


public class ControllerA{

public void onPressingButtonB(ActionEvent e) throws IOException{
        FXMLLoader loaderB = new FXMLLoader(getClass().getResource("class-b.fxml"));
        root = loaderB.load();
        ControllerB controllerB = loaderB.getController();
        controllerB.openB(e);
}

But this seems 'not optimal' to me. Everytime i'm in A and want to go to B, i need to reistantiate the ControllerB. So, i declared that ControllerA has a ControllerB, and used the following code:


public class ControllerA{

private ControllerB controllerb;
    {
        try {
            controllerb = loadControllerB();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    public ControllerB loadControllerB() throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("class-b.fxml"));
        root = loader.load();
        return loader.getController();
    }

public void onPressingButtonB(ActionEvent e) throws IOException{
        controllerb.openB(e);
    }

This way, my action listener can be resolved to one line, having istantiated the controller directly in my class, and it works like a charm.

Thing is... of course i need to do it specularly with ControllerB, but this leads to a major problem: if ControllerA istantiate a ControllerB when created, and ControllerB istantiate a ControllerA when created... it's a loop. In fact, it loops and gives me error on the load method.

My question is: is there a way to fix my code and creating controllers just one time (so my action listener can be just one line of code), or i have to reistantiate controllers every time i have to use them?

Thank you very much.


Solution

  • None of what is described seems like a particularly good design to me. Controllers should not be opening their own views in any sense. The de-facto industry-standard way to communicate between two controllers is via a model (i.e. use a MVC design).

    In this case, your model can include some state that represents what the current view should be. Usually, this is just a function of natural properties that your model would include anyway. For example, if you have an application where the user has to log in, your model might have a user property. The controller for a login screen would validate the user credentials and change the user property if they were correct. Other screens might have a logout button which just set the user to null. An observer on the model's user property would display the login screen if the user changes to null, and display the main screen if the user changes to non-null.

    For simple demonstration purposes, here's a model class which just directly has a property representing the current view:

    package org.jamesd.examples.switchviews;
    
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleObjectProperty;
    
    public class Model {
        public enum View {A,B}
    
        private final ObjectProperty<View> currentView = new SimpleObjectProperty<>(View.A);
    
        public View getCurrentView() {
            return currentView.get();
        }
    
        public ObjectProperty<View> currentViewProperty() {
            return currentView;
        }
    
        public void setCurrentView(View currentView) {
            this.currentView.set(currentView);
        }
    
        
    }
    

    Now all your controllers should do is update the state of a (shared) model:

    A.fxml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Button?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.VBox?>
    <VBox xmlns="http://javafx.com/javafx"
          xmlns:fx="http://javafx.com/fxml"
          fx:controller="org.jamesd.examples.switchviews.ControllerA"
          alignment="CENTER"
          spacing="20"
          fx:id="root">
        <Label text="This is view A"/>
        <Button text="Go to view B" onAction="#goToB"/>
    
    </VBox>
    

    and ControllerA:

    package org.jamesd.examples.switchviews;
    
    import javafx.fxml.FXML;
    
    public class ControllerA {
        private Model model;
        public void setModel(Model model) {
            this.model = model;
        }
        
        @FXML
        private void goToB() {
            model.setCurrentView(Model.View.B);
        }
    }
    

    and similarly, B.fxml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Button?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.VBox?>
    <VBox xmlns="http://javafx.com/javafx"
          xmlns:fx="http://javafx.com/fxml"
          fx:controller="org.jamesd.examples.switchviews.ControllerB"
          alignment="CENTER"
          spacing="20"
          fx:id="root">
        <Label text="This is view B"/>
        <Button text="Go to view A" onAction="#goToA"/>
    
    </VBox>
    

    and ControllerB:

    package org.jamesd.examples.switchviews;
    
    import javafx.fxml.FXML;
    
    public class ControllerB {
        private Model model;
        public void setModel(Model model) {
            this.model = model;
        }
    
        @FXML
        private void goToA() {
            model.setCurrentView(Model.View.A);
        }
    }
    

    The responsibility for changing the actual view when the model changes belongs elsewhere. In this simple example, we can just do it in the application class, which has access to the scene:

    package org.jamesd.examples.switchviews;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    import java.io.IOException;
    
    public class HelloApplication extends Application {
    
        private Parent viewA ;
        private Parent viewB ;
    
        @Override
        public void start(Stage stage) throws IOException {
            Model model = new Model();
    
            // load both views:
            FXMLLoader loaderA = new FXMLLoader(getClass().getResource("A.fxml"));
            viewA = loaderA.load();
            ControllerA controllerA = loaderA.getController();
            controllerA.setModel(model);
    
            FXMLLoader loaderB = new FXMLLoader(getClass().getResource("B.fxml"));
            viewB = loaderB.load();
            ControllerB controllerB = loaderB.getController();
            controllerB.setModel(model);
    
            // create scene with initial view:
            Scene scene = new Scene(viewFromModel(model.getCurrentView()),320,200);
    
            // change view when model property changes:
            model.currentViewProperty().addListener((obs, oldView, newView) ->
                scene.setRoot(viewFromModel(newView))
            );
    
            stage.setScene(scene);
            stage.show();
        }
    
        private Parent viewFromModel(Model.View view) {
            return switch(view) {
                case A -> viewA ;
                case B -> viewB ;
            };
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    A more extensive example is shown here