JavaFX provides a huge amount of internal support for managing the scene graph. That includes navigating from nodes, as well as events, to the Scene and Stage.

In JavaFX, the Stage can be navigated to by using a node’s getScene() method, followed by retrieving the Window using getWindow(). As the JavaFX Stage extends the Window class, this can be cast to Stage.

So, getting the Stage from the current controller is incredibly easy. In fact, I’ll take you through three ways to get to it. But, this comes with a warning.

Warning: there are very limited use cases where using a controller to directly control a window is a good idea. The second half of this article goes into a little more detail about these dangers. I’ll also go through some design ideas for how you can set Stage behaviour in a stable and controllable way.

Usually, it’s a better idea to keep the code for interacting with the window lifecycle in the calling class rather than the controller.

We’ll go through the simple ways to access the Stage, and then talk through some more advanced design ideas for Stage control.

What we’ll achieve in this tutorial

There are three main ways to get access a Stage from within a controller:

  • Access the Stage through a node
  • Set the Stage using a custom controller method.
  • Get the Stage through an ActionEvent

As promised, I’ll also talk through the dangers of using these strategies and best practice for controlling Stage and Window behaviour

  • NullPointerExceptions during Stage access
  • Race conditions from Window creation

Finally, I’ll talk about some basic design principles that should help you set Behaviour in your Stages and Windows controllably.

Accessing the Stage from your Controller

So, in an imaginary program, we have a Project Wizard. It’s pretty simple: all we’re asking the user to do is to add a Project name, and a Project lead. We’re not winning awards for our UI design here, but we still want to satisfy two user expectations:

  1. When they click the OK button, they commit their input to a database and the window should close
  2. They should also be able to commit their input and close the window by pressing enter on either text box.

We won’t go into detail about validating the information the user’s provided – this article’s not about that. But our initial plan is that we need to be able to access the Stage from both the projectNameTextField and whenever the okButton is pressed.

We’ll do these each in turn.

1. Access the Stage through a node

All nodes in a scene are nested inside a scene graph that runs from the Scene to each element – each node – in it. Our scene is organised as a tree, with the Scene element at the top, and our leaf nodes (like TextField) at the bottom.

When it displays a Scene, JavaFX attaches it to a Window object that is accessible through the scene.getWindow() method.

To traverse this tree, we need only two commands, regardless of which node we start at:

  1. We use node.getScene() to get the Scene from the node.
  2. Then, we use scene.getWindow() to get the Window from the scene.
Window window = projectNameTextField.getScene().getWindow();

As Stage extends the Window class, we can cast getWindow() to Stage:

Stage thisStage = (Stage) projectNameTextField.getScene().getWindow();

Warning 1: This can be an unstable way to get the Stage from the controller, especially if you try to do so during the creation or initialization of your controller. The FXMLLoader creates, initializes and loads your controller in a very specific order, all of which happens before we create our Stage:

So, running any of the code above in either the controller constructor (assuming you’re using some custom controller factory) or initialize() method, you’ll generate a NullPointerException because at those points in time, there is no window.

Warning 2: It’s also worth knowing that stage.show() sets off a bunch of different function calls in the quantum toolkit and the glass rendering system – in the background. So any attempt to do things in the controller straight after stage.show() dumps you immediately into a race condition.

The only stable way to do this is to inject the Stage into the controller after the load() method has completed, using a custom method.

We’ll cover that in “Set the Stage using a custom controller method” below

2. Accessing the Stage through an ActionEvent

Although I’ve said ActionEvent, we can do this for any Event – MouseEvents, GestureEvents and so on. They all represent actions on the scene, and we can use them to access the scene graph.

When we define the onAction button click in the FXML file, we have to provide a method that is called whenever the ActionEvent is fired. We’ll need one extra line of code to access the scene graph from the event and cast the source of the event as a Node.

From there, we traverse the scene graph in the same way as we did before.

public void handleCommitAddProject(ActionEvent event) {
    Node node = (Node) event.getSource();
    Stage thisStage = (Stage) node.getScene().getWindow();
    commitToDatabase();
    thisStage.hide();
}

Because this code isn’t executed until the button is pressed, we are guaranteed to have a Scene and a Window at this point – so the NullPointerException issues are more easily avoided.

That being said, it’s still not best practice.

Warning 3: the eagle-eyed among you will have noticed that our controller just got a bunch of new code. Now in addition to controlling the user interactions with the View, it’s got to run database code and hide the Stage? We’ll get to that later.

3. Set the Stage using a custom controller method.

As some prep-work for setting the Stage manually, we’ll need to create a method in our controller that will accept the Stage we pass it and set up the listener on the text field. We’ll use that instead of digging around in the scene graph to get it from a node.

//in controller
public void setStage(Stage stage) {
    this.stage = stage;

    projectNameTextField.setOnAction(event -> {
        commitToDatabase();
        stage.hide();
    });
}

Now, once we’ve loaded the view, we can ask the FXMLLoader for the controller, and use that controller to run our custom method.

public class Main extends Application {

    @Override
    public void start(Stage stage) throws IOException {
        //Set stage appearance stuff
        stage.setTitle("Project Wizard");
        stage.getIcons().add(new Image(getClass().getResourceAsStream("EdenCodingIcon.png")));

        //Load the view
        FXMLLoader loader = new FXMLLoader(getClass().getResource("NewProject.fxml"));

        //Create the scene
        Parent root = loader.load();
        stage.setScene(new Scene(root));

        //Set the Stage
        NewProjectController newProjectController = loader.getController();
        newProjectController.setStage(stage);

        //show the stage
        stage.show();
    }

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

Warning 4: Again, we have to run database code in the controller? And, of course, whenever we create a new View that does the same thing, we’ll just end up copying and pasting that database code there too…

Controlling Stage behaviour sustainably

If you’re trying to create larger applications, it can be a good idea to design your code to define and encapsulate the responsibilities of every class really clearly.

So, whenever I’m designing a controller, I try to remember what they were originally designed to do!

JavaFX’s basic MVC design pattern

In the JavaFX MVC design pattern, the model, view and controller are designed to act together. The only responsibility of the controller in this MVC design pattern is to maintain the model that the user interacts with through the view. And of course to update the view as necessary.

The Window – the Viewport through which we interact with the view – is created by a calling class. In our case, Main.java. It does this through the FXMLLoader.

Now, in the code above, we asked the controller to use the values in the project name and lead TextField to populate a database, and close the Stage. That means adding more models to our MVC.

Adding models to the MVC

When I first started with JavaFX I really didn’t see the problem with adding some extra code to my controller – what’s a little bit of database code between friends? The Project needs to go in the database, so the controller should put it there, right?

I think about the database an an extra model. It has a database location and connection properties. You can store these in a static helper class like DatabaseProperties.java but it doesn’t fundamentally change the fact that you’re creating a model in the controller.

The sniff test for any model in an MVC pattern is: do we require this model in order to update the View? There are use cases where we want the user to select a database so they can add the project to a particular destination, but in this case, the database is completely separate.

And, sure enough, when we create a different window later, we’ll have to copy and paste the code.

Creating models in the calling class

An alternative design for this is to pass the model information (the intended Project model) back to the calling class. In my opinion, the responsibility for updating the database and determining future actions doesn’t lie with the controller.

I like to think of it this way: The calling class opened a window because it wanted the user to input certain data. So the calling class should determine whether the input it gets is acceptable.

There are several ways to do this, but the easiest is to expose the OK button from the controller and set that code in the calling class (you could do this through a getter, or even an Observer patter)

newProjectController.okButton.setOnAction(event -> {
    if(newProjectController.inputIsValid()){
        Project newProject = newProjectController.getResult();
        commitToDatabase(newProject);
        stage.hide();
    } else {
        notifyUserResultNotAcceptable();
    }
});

Notice: we still delegated to the controller to work out whether the user’s input was valid by calling newProjectController.inputIsValid(). From my point of view, the controller maintains the Project model so that’s the controller’s responsibility fair and square.

Now the calling class can ask the controller for the model using newProjectController.getResult(). It then interacts with the database and – importantly – it decides what to do next.

If we later create another Window with a different controller, we can reuse the database code easily.

Conclusions

Accessing the Stage in JavaFX can be achieved by navigating the scene graph using the getScene() and getWindow() methods. As Stage extends Window, the resulting Window can be cast to Stage, where needed.

When accessing a Stage, timing is important, as the Stage is not created until the very end of a View-creation process.

Code designers should also be careful when using the Stage within a controller. Typically, the controller is responsible only for updating the model and view, and is shouldn’t really be responsible for the Window lifecycle. This responsibility more comfortably fits with whichever class created the Stage in the first place.