0

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.

Ric97
  • 109
  • 1
  • 6
  • 1
    Does this answer your question? [Passing Parameters JavaFX FXML](https://stackoverflow.com/questions/14187963/passing-parameters-javafx-fxml) – Giovanni Contreras Jan 31 '23 at 18:08
  • Unfortunately i don't think so. Maybe it's just me, i've been trying to fix this problem for hours and my head isn't fully connected right now – Ric97 Jan 31 '23 at 18:12
  • 3
    Can you create and post a [mre]? I don't really understand the issue here. What do `openA()` and `openB()` do? Usually a controller is not responsible for opening a window to display the UI that the controller is controlling; that is not part of its responsibility. – James_D Jan 31 '23 at 18:53
  • 2
    See how [spring handles this](https://stackoverflow.com/a/3485429/1155209). You could do the same thing. Create both controllers (by invoking their constructors), store the references to the controllers created, then call set methods on each so that each can know about the other, create an fxml loader for each and for each loader, you set the controllers you made, then invoke the loader load method to inject the fxml fields. Theoretically, it would work, but I don't necessarily recommend it. In the end I think you may be trying to solve an [xy problem](https://xyproblem.info). – jewelsea Jan 31 '23 at 19:42
  • It sounds like you are following terrible tutorials to me. Try learning [this](https://stackoverflow.com/questions/32342864/applying-mvc-with-javafx) or the link posted by @GiovanniContreras. – SedJ601 Jan 31 '23 at 20:37
  • 1
    Thanks everyone for your comments. I will look into them and study more. Furthermore, never heard about an 'xy problem' but it made me giggle. Sounds too accurate! – Ric97 Feb 01 '23 at 14:52

2 Answers2

3

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();
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
0

Loading a new fxml, so calling root = loader.load(); instances a new controller for the specified path. So no, you cant stick to the same instance because you wont be able to access the @FXML stuff on your declared controller or you could be calling and old instance for the page you are visualizing.

helvete
  • 2,455
  • 13
  • 33
  • 37
Matteo
  • 1
  • 1