Custom Controls and Dialogs in JavaFX Done Right

Categories:Java, Programming
Tags:, ,
Michael

JavaFX has been a pleasure to develop GUIs with. However, I’ve had a difficult time finding help on creating custom controls and dialog boxes that don’t expose the clunky FXML code to the client. In my opinion, the client of a custom control or dialog should be able to use the control without needing to know anything about the FXML. Here, I will document my method of creating custom dialog boxes and controls that are laid out in FXML, yet hide that FXML from any client code.

The sample code used in this post can be found here.

An Example of the Wrong Way

Many JavaFX guides that I see on the internet will give an example like the following one on how to load a custom dialog. (This example was copy-pasted from this Stack Overflow question.)


FXMLLoader loader = new FXMLLoader(getClass().getResource("Sample.fxml"));
Parent root = (Parent)loader.load();

//Parent root = FXMLLoader.load(getClass().getResource("Sample.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();

SampleController controller = (SampleController)loader.getController();
controller.setStageAndSetupListeners(stage);

Notice how the client of this custom dialog needs to load the FXML, create the new Scene for the dialog, and then get the FXMLLoader‘s controller to communicate with the dialog. This is unnecessary and far too verbose. The client shouldn’t have to know which FXML form to load. The name of the FXML form is passed as a String and that creates the chance that a typo will lead to a runtime exception. The client code has to setup the FXMLLoader and get the dialog’s controller in separate steps.

The Right Way

I propose a way in which all FXML work is hidden from the client code inside the custom dialog’s controller. The way I propose can reduce all of the above code to one, maybe two, lines. Most importantly, my way reduces the potential for error by making any runtime exceptions that may happen above into compile-time errors.

The way described below embraces the Model View Controller (MVC) pattern. This allows client code to easily manipulate custom controls and dialogs through their controller classes.

MVC In JavaFX

JavaFX provides FXML to create the View portion of MVC. The Model portion is usually provided by a JavaBean. The Controller portion is often a custom Java class that provides a connection between the Model, the View, and other Controllers.

The View (FXML)

To create FXML for my custom components, I use the JavaFX Scene Builder (a JavaFX application itself). It is very nice for creating even complex GUIs. The only thing to make sure of when using this method for custom controls is to not set the controller class in the FXML file. That will be done in the controller itself.

The Controller

The controller is where all the magic goes! First, create a JavaFX controller as you normally would. The JavaFX Scene Builder can actually create one for you by clicking on View > Show Sample Controller Skeleton.

All of the FXMLLoader work goes in the constructor. I don’t like putting it in the constructor (especially since the FXMLLoader.load() method can throw an exception), however, this seems to be the conventional place to put it according to Oracle documentation. If you want, you can always move it to a load() method and give the client code some more work to do.

Here is an example constructor (notice where the controller is set for the FXML file on line 4):

public TestDialog(Parent parent)
{
    FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("TestDialog.fxml"));
    fxmlLoader.setController(this);

    // Nice to have this in a load() method instead of constructor, but this 
    // seems to be the convention.
    try
    {
        setScene(new Scene((Parent) fxmlLoader.load()));
    }
    catch (IOException e)
    {
        e.printStackTrace();
    }
}

I always implement Initializable and set all controls to a valid default state in the initialize method.

There are a couple of differences here between custom controls and custom dialogs boxes. First, for a custom dialog, I extend the Stage class. For controls I extend GridPane or whatever should be the root of the custom control. This is simply because dialog boxes usually have a complete stage to fill anyway and this allows the client code to show a dialog box by calling showAndWait() on the controller. Finally, in the controller class for custom controls, make sure to set the root element like so.

The Model

This article won’t spend much time discussing the model element of MVC in JavaFX. Models are easily provided by JavaBean classes and should usually be passed to the controller class via a constructor argument. The controller class can implement JavaFX Properties to bind the model’s data to components in the view.

Conclusion

Following these directions, custom controls are easy for client code to use. See this for how to show a custom modal dialog. It becomes only two lines!

TestDialog testDialog = new TestDialog(null);
testDialog.showAndWait();

I hope this helps JavaFX programmers. Please create issue reports/pull requests for the sample code project if you see any way to make it better.

Author:

7 Comments

  1. Sarah
    SarahReply
    May 5, 2014 at 12:55 pm

    Thanks. I’m learning JavaFX using FXML, and I wanted to open a custom dialog. Your solution is the cleanest way I’ve seen so far and saved me some aggravation.

  2. dineshramitc
    June 10, 2014 at 3:48 am

    Reblogged this on Dinesh Ram Kali..

  3. Ghan
    GhanReply
    September 23, 2015 at 2:13 am

    The following sentence saved my ass this week:
    “The only thing to make sure of when using this method for custom controls is to not set the controller class in the FXML file.”

    Thank you very much

  4. Adrian Bakker
    October 13, 2015 at 9:59 am

    Curious about the line “fxmlLoader.setController(this);” I have loved using this pattern because it simplifies window creation to be the same as C#, but I can’t get passed the “Leaking “this” in the Constructor” problem it presents. I have found a similar example on oracle’s website, so assume that it is not a huge issue, but it seems to be a design flaw to pass a class which has not yet been fully constructed.

    • Michael R. Taylor
      October 13, 2015 at 10:37 am

      Yes, I’m uncomfortable with that myself, even though it seems to be a common pattern.

      It makes the code a bit more verbose, but I’m now a fan of moving that line along with the setScene(...) call into a separate load() method. I should probably create a new blog post with that modification to this method.

Leave a Reply

Name*
Email*
Url
Your message*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>