lumberajackshaw lumberajackshaw - 7 months ago 138
Java Question

Trouble passing arguments to controller class in JavaFx

I'm trying to build a simple Login panel in JavaFX. I've reviewed several other posts about this same problem using nearly the same approach here on stackoverflow but, as of yet, have not been able to figure out what I've done wrong. I'm using eclipse.

This part of my code works until I try to access

login
variable in my LoginController class. With in the
initLogon
method, the variable has data and is assigning it but when I try to access the variable from elsewhere it's null. My guess is that I'm somehow working in 2 separate instances of the LoginController class, one that is getting initialized by
initdata
and the other that is not, but I can't to figure out where I've failed to connect the dots.

Here are the relevant portions of my code:

Login class:



public class Login {
private Scene scene;
private GsonBuilder gsonBuilder;
private Config config;
private Token token; //auth token from json server

public Login(Scene scene, GsonBuilder gsonBuilder, Config config, Token token){
this.scene = scene;
this.gsonBuilder = gsonBuilder;
this.config = config;
this.token = token;
}
public void showLoginScreen(){
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("LoginScreen.fxml"));
scene.setRoot((Parent) loader.load());
LoginController controller = loader.<LoginController>getController();
controller.initLogin(this);
} catch (Exception ex) {
Logger.getLogger(Login.class.getName()).log(Level.SEVERE, null, ex);
}
}
public Config getConfig(){ return this.config; }
public Token getToken(){ return this.token; }
public GsonBuilder getGsonBuilder(){ return this.gsonBuilder; }
}


LoginController class:



public class LoginController implements Initializable {

//Removed FXML stuff for brevity
private Login login;

public void initLogin(final Login login){
this.login = login;
//Troubleshooting line **works**
System.out.println("After initLogin, but from initLogin " +
this.login.getConfig().getAppUsername());
}
@Override
public void initialize(URL location, ResourceBundle resources) {
assert txtUsername != null : "fx:id=\"txtUsername\" was not injected: check your FXML file 'LoginScreen.fxml'.";
assert txtPassword != null : "fx:id=\"txtPassword\" was not injected: check your FXML file 'LoginScreen.fxml'.";
assert btnLogin != null : "fx:id=\"btnLogin\" was not injected: check your FXML file 'LoginScreen.fxml'.";
assert btnCancel != null : "fx:id=\"btnCancel\" was not injected: check your FXML file 'LoginScreen.fxml'.";
System.out.println(this.getClass().getSimpleName() + ".initialize");
// Troubleshoot line **Does not work**
System.out.println("During initialization " +
this.login.getConfig().getAppUsername());

//other stuff happens below here
}
// Other functions are here


}

TestApplication class:



// class extends Application above this (removed for brevity)
Stage loginStage = new Stage(StageStyle.UNDECORATED);
Scene scene = new Scene(new AnchorPane());
Login appLogin = new Login(scene, this.gsonBuilder, this.config, this.token);
appLogin.showLoginScreen();
loginStage.setScene(scene);
loginStage.show();

Answer

The issue lies solely in the order things happen.

When you call FXMLLoader.load(), the FXMLLoader loads and parses the fxml, creates the controller instance, injects the @FXML-annotated fields, and calls initialize() on the controller instance. Then it returns.

In your code, you are (necessarily) calling controller.initLogin() after the load method completes, and consequently after the initialize() method has been invoked. So login is null in the initialize() method.

A quick fix is just to perform the initialization that depends on login in the initLogin() method.

Possibly a more robust fix is to control the instantiation of the controller. You can do this by removing the fx:controller attribute from the FXML file, instantiating the controller yourself, and setting the controller on the FXMLLoader:

public void showLoginScreen(){
    try {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("LoginScreen.fxml"));
        LoginController controller = new LoginController();
        controller.initLogin(this);
        loader.setController(controller);
        scene.setRoot((Parent) loader.load()); 
    } catch (Exception ex) {
        Logger.getLogger(Login.class.getName()).log(Level.SEVERE, null, ex);
    }
}

Alternatively, keep the fx:controller attribute in the FXML file and use a controller factory. This approach is more useful if you want to reflectively inspect the controller class and set a value if an appropriate method or constructor exists. However, for this simple case it might look like:

public void showLoginScreen(){
    try {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("LoginScreen.fxml"));
        loader.setControllerFactory((Class<?> controllerType) -> {
            if (controllerType == LoginController.class) {
                LoginController controller = new LoginController();
                controller.initLogin(this);
                return controller ;
            } else {
                try {
                    return controllerType.newInstance();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
        scene.setRoot((Parent) loader.load()); 
        LoginController controller = loader.<LoginController>getController();
        controller.initLogin(this);
    } catch (Exception ex) {
        Logger.getLogger(Login.class.getName()).log(Level.SEVERE, null, ex);
    }
}

Note that both of these examples allow you to define LoginController with a constructor parameter, instead of with a method specifically initializing the Login, which can lead to more robust code.

Comments