-1

I'm currently looking into the easiest way to save my SQL Connection within my JavaFX project in order to use it in all of my Controllers. Since I'm creating the Controller in a SideBar FXML file it is not possible to pass the Object from one Controller to the other.

Therefore I wanted to use the Node.setUserData() method and just save the Connection Object to the root node. Unfortunately I get NullPointers when I want to call it.

Saving it works fine:

myStage.getScene().getRoot().setUserData(con);

And calling it from the same stage variable works fine as well:

... = (Connection) myStage.getScene().getRoot().getUserData();

But I'm accessing the stage within my Sidebar.fxml via

Stage stage = (Stage) myButton.getScene().getWindow();

what then leads to NullPointers while accessing the UserData via

stage.getScene().getRoot().getUserData();

I see that the reason for this is, that it is not the "exact same" stage variable. But it has to be the same stage (when I display a new view there it is displayed on the same stage as before).

How can I find the exact same Node I've saved the UserData before? Or is there a way to access the same Node from another context where I do not have the stage?

EDIT: I've put a MCVE here to show what my Problem is: https://github.com/lud-hu/myJavaFxMcve/ EDIT: Code is working in Github now, I'll post the Code with the initial problem here:

MyMcveStarter.java

    package myMcve;

import myMcve.controller.LoginController;
import javafx.application.Application;
import javafx.stage.Stage;

public class MyMcveStarter extends Application {

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        LoginController controller = new LoginController(primaryStage);
        controller.displaySceneOn(primaryStage);
    }
}

LoginController.java

    package myMcve.controller;

import myMcve.view.LoginSceneView;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;


public class LoginController {

    private LoginSceneView view;
    private Parent scene;
    Stage myStage;

    String defaultUrl;
    String defaultName;
    String defaultPassword;



    public LoginController(Stage stage) {

            defaultUrl = "jdbc:mysql://localhost:3306/db";
            defaultName = "root";
            defaultPassword = "localhost";

        myStage = stage;

        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("../view/LoginScene.fxml"));
        try {
            scene = loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
        view = loader.getController();
    }

    public void displaySceneOn(Stage stage) {
        stage.setTitle("login");
        Scene myScene = new Scene(scene, 1250, 650);

        stage.setScene(myScene);
        stage.show();

        try {
            initializeDbConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void initializeDbConnection() throws SQLException {

            try {
                DriverManager.setLoginTimeout(15);
                Connection con = DriverManager.getConnection(defaultUrl, defaultName, defaultPassword);

                UserManagementController controller = new UserManagementController(myStage, con);
                controller.displaySceneOn(myStage);

            } catch (Exception e) {
            }
        }


}

SideBarController.java

package myMcve.controller;

import myMcve.controller.LevelManagementController;
import myMcve.controller.LoginController;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.stage.Stage;

public class SideBarController{
    @FXML
    private Button levelManBtn;

    public Button getLevelManBtn() {
        return levelManBtn;
    }


    @FXML
    private void levelMan(ActionEvent event){
        //start other Controller from here (SideBar)
        //how do I access the DB Connection here?
        Stage stage = (Stage) levelManBtn.getScene().getWindow();
        //LevelManagementController controller = new LevelManagementController(stage, con);
        //controller.displaySceneOn(stage);
    }


}

UserManagementController.java

package myMcve.controller;

import com.sun.prism.impl.Disposer;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;
import javafx.util.Callback;
import myMcve.view.UserManagementView;

import java.io.IOException;
import java.sql.*;

public class UserManagementController{

    private UserManagementView view;
    private Parent scene;
    Stage myStage;
    Connection con;

    public UserManagementController(Stage stage, Connection con){

        myStage = stage;
        this.con = con;

        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("../view/UserManagementScene.fxml"));

        try {
            scene = loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
        view = loader.getController();

    }

    public void displaySceneOn(Stage stage){
        stage.setTitle("user management");
        Scene myScene = new Scene(scene, 1250, 650);

        stage.setScene(myScene);

        stage.show();
    }

}

LoginSceneView.java

package myMcve.view;

import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

public class LoginSceneView {
    @FXML
    private Label label;

    public Label getLabel() {
        return label;
    }

}

LoginScene.fxml

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

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.112-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="myMcve.view.LoginSceneView">

    <Label fx:id="label" text="login Buttons etc..." />

</AnchorPane>

SideBar.fxml

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

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

<VBox fx:id="sidebar" prefHeight="650.0" prefWidth="250.0" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1" fx:controller="myMcve.controller.SideBarController">
    <children>
        <Button fx:id="levelManBtn" layoutX="10.0" layoutY="123.0" prefHeight="50.0" prefWidth="350.0" text="Level Management" onAction="#levelMan"/>
    </children>
</VBox>

UserManagementView.java

package myMcve.view;

import javafx.fxml.FXML;
import javafx.scene.control.*;

public class UserManagementView {

    @FXML
    private Label label;

    public Label getLabel() {
        return label;
    }
}

UserManagementScene.fxml

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

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>


<BorderPane xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1" fx:controller="myMcve.view.UserManagementView">
    <left>
        <!-- SideBar import -->
        <fx:include fx:id="sidebar" source="SideBar.fxml" />
    </left>
    <center>
        <Label fx:id="label" text="user managemnt tableview and Buttons etc..." />
    </center>
</BorderPane>
luhu
  • 361
  • 3
  • 12
  • "It is not possible to pass the object from one controller to another." Why not? – James_D Feb 28 '17 at 22:42
  • Also, if you really want to do this by setting it as user data on the root node, that should work (though design-wise it's pretty horrible). What is actually null? Why is it null? – James_D Feb 28 '17 at 23:05
  • 1
    Using userData does not seem to be the recommended way to do this. Related: [Passing Parameters JavaFX FXML](http://stackoverflow.com/questions/14187963/passing-parameters-javafx-fxml) and [What is the main way to connect a view and a model in JavaFX?](http://stackoverflow.com/questions/19895951/what-is-the-main-way-to-connect-a-view-and-a-model-in-javafx) – jewelsea Mar 01 '17 at 00:07
  • @James_D It's not possible since I have to instantiate the new Controllers from my SideBar where I can't pass the Connection to. At least I tried it for bunch of hours and I didn't get it to work. – luhu Mar 01 '17 at 06:39
  • @James_D the Object I get from getUserData() is null when I call it from the Sidebar Controller as mentioned above. I know it should work but unfortunately I doesn't. :( – luhu Mar 01 '17 at 06:40
  • I don't understand why you think instantiating the controllers from one particular place means you can't initialise them the way you want. You should create a [MCVE] that shows what the actual problem is. – James_D Mar 01 '17 at 06:44
  • @James_D I've put a MCVE here: https://github.com/lud-hu/myJavaFxMcve/ Hope this helps to understand what my problem is. How do I get the Connection into the SideBarController in order to call the LevelManagement from here? – luhu Mar 01 '17 at 08:09
  • You should really include the code in your question. If the link goes stale, the question will be of no use to anyone else. I answered, but please [edit] the question so that it remains useful to others. (You may need to reduce the example further: I don't think the `LevelManagement` component is really needed to make the point.) – James_D Mar 01 '17 at 13:37
  • Oh, and it's pretty obvious from your code why setting the user data doesn't work. When you load a new view, you are replacing the scene (and the root of the scene). So even though `stage` is the same object, `stage.getScene()` and `stage.getScene().getRoot()` are different objects when you call `setUserData(...)` and `getUserData()`. – James_D Mar 01 '17 at 14:40

1 Answers1

2

I tried using a design like the one you are using, in which you implement a traditional MVC architecture by using the FXML/JavaFX Controller pair as the MVC view, and create a separate MVC controller. In the end I dismissed it as too convoluted.

The architecture implicit in the FXML-controller design is really a variant of MVC called MVP ("Model-View-Presenter") and if you read the Martin Fowler article on UI architectures, it's close to the variant he calls "Passive View". In this architecture, the FXML file represents the view, which is essentially completely passive and just defines layout. The JavaFX Controller represents the "Presenter", which observes and updates the model, and responds to changes in it by modifying the view. My main recommendation would be to refactor your design so it conforms to this: so remove one of the "controller" layers entirely, and consider the "JavaFX controller" to be fulfilling the general role of controller/presenter in one of the MVC variants. (I posted a fuller answer to this here.)

In particular, your design struggles when you use <fx:include>. The issue is your design is "controller-centric", in the sense that you favor creating the controllers and letting the controllers create the views. Using <fx:include> basically creates a new view for you (by loading another FXML file), which then creates the controller for you. So this is more "view-centric". The conflict between these two makes it tricky to share a model (or services) among the controllers.

One way to get this to work is to set a controller factory on the FXMLLoader. The controller factory is a function that maps a class (defined by the fx:controller attribute in the FXML file) to the JavaFX controller instance. So you can use this factory to create the controller with the connection already initialized in the controller. (BTW, the connection is playing the role of the MVC model in your application: you might want to refactor that to something more robust at a later stage. You should at least factor all the database-specific code to a Data Access Object, and share that object instead of the raw connection.)

First, define a constructor in SideBarController that takes a Connection:

public class SideBarController{
    @FXML
    private Button levelManBtn;

    private final Connection con ;

    public SideBarController(Connection con) {
        this.con = con ;
    }

    // existing code (which obviously can now access the connection)...

}

Now when you load your UserManagementView, specify a controller factory that calls the constructor taking a Connection, if one exists, and calls the no-argument constructor otherwise. The approach shown here uses reflection, and is basically akin to the way dependency injection (which is actually what you are trying to do here) frameworks are implemented. There are other possibilities too.

public UserManagementController(Stage stage, Connection con){

    myStage = stage;
    this.con = con;

    FXMLLoader loader = new FXMLLoader();

    // note that this resource name will likely not work if you bundle the app as a jar....        
    loader.setLocation(getClass().getResource("../view/UserManagementScene.fxml"));

    loader.setControllerFactory((Class<?> controllerType) -> {

        try {
            // check constructors of controllerType to see if one takes a Connection:
            for (Constructor<?> c : controllerType.getConstructors()) {
                if (c.getParameterCount() == 1 && c.getParameterTypes()[0].equals(Connection.class)) {
                    // found matching constructor, invoke it with the connection as parameter:
                    return c.newInstance(con);
                }
            }

            // no matching constructor, just invoke default constructor:
            return controllerType.newInstance();
        } catch (Exception e) {
            // fatal...
            throw new RuntimeException(e);
        }
    });

    try {
        scene = loader.load();
    } catch (IOException e) {
        e.printStackTrace();
    }
    view = loader.getController();

}

Controller factories are propagated to the load process for <fx:include>d FXML files. So when UserManagementScene.fxml is loaded, the specified controller is UseManagementView. That has no constructor taking a Connection, so the default constructor will be invoked. When the <fx:include> for SideBar.fxml is encountered, it specifies a controller of SideBarController, which does (now) have a constructor taking a Connection, so that constructor is invoked.

Note that your design is a little different for the side bar than it is for the rest of your application, in that in the other components, your view is the FXML-controller pair with a separate controller class. For the side bar, you use the JavaFX approach where the view is the FXML file, and the controller is specified as the fx:controller. Again, I would probably refactor the application so that everything follows that second design, rather than the first.

Community
  • 1
  • 1
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thank you very much! This seems to work very good and it is easier than I thought! :) – luhu Mar 02 '17 at 10:03