Custom JavaFX Dialogs

By Carl Walker / February 24, 2024

This article presents a custom JavaFX Dialog. A custom Dialog returns a value using a function call-like syntax. This makes procedural code readable since a user interaction can be blended cleanly with other method calls.

The most important aspect of the custom Dialog subclass is the result. If the Dialog is expected to cause side effects -- say saving data as it is being operated -- it is better to use a general-purpose component. The result enables the synchronous showAndWait() to be used.

The app presented in this article allows the user to manipulate a TableView by adding and editing hyperlinks: New button and double-click on a TableRow. The user is also able to change the title of the table: Change button. The following screenshot is the main window when the app is started.

Main Window
Main Window of Demonstration App

The main window is based on the scene graph from hyperlinks-view.fxml which is paired with HyperlinksController. The Hyperlink class is a Java record domain object containing a URL with a text description. The following UML shows the static structure of the app.

UML Class Model
Class Model of App

Standard Dialogs

An off-the-shelf Dialog is used to implement the change title feature. This Dialog is a TextInputDialog. Other standard JavaFX Dialogs include Alert and ChoiceDialog. See the JavaFX Javadoc for a description of those classes. Alert is a widely used standard Dialog. Most apps will use Alert to communicate errors, information, or confirmations.

In HyperlinksController, a Label is injected as lblTitle. In an initialize() method, the Label is bound to a StringProperty.


private final static String DEFAULT_TITLE = "My Hyperlinks";

@FXML
private TableView<Hyperlink> tvHyperlinks;

@FXML
private Label lblTitle;

private final StringProperty title = new SimpleStringProperty(DEFAULT_TITLE);

public void initialize() {
    // abbreviated
    lblTitle.textProperty().bind(title);
}
HyperlinkDialogController.java

The button handler attached to the Change button sets the Label text property based on the result returned from the dialog. There are a few lines for setting the title and header of the Dialog window.


@FXML
protected void onChangeTitle() {
    var dialog = new TextInputDialog(title.get());
    dialog.setTitle("Change Title");
    dialog.setHeaderText("Enter a new title");
    dialog.showAndWait().ifPresent(title::set);
}
HyperlinkDialogController.java

The TextInputDialog result flows neatly into the StringProperty set with ifPresent from the Optional and a method reference.

Custom Dialog

To add a hyperlink to the TableView or to edit an existing hyperlink, the user enters a URL and text description in a custom Dialog. Both operations result in the instantiation of a custom Dialog "HyperlinkDialog". For the add operation, the user presses the New button. The following handler is called which will add the Hyperlink domain object result to the TableView. Note the similar call signature to what was used with the standard TextInputDialog. An Optional, chained to showAndWait(), will add to TableView if present.


@FXML
protected void onNewHyperlink() {
    try {
        new HyperlinkDialog()
                .showAndWait()
                .ifPresent(
                        response -> tvHyperlinks.getItems().add(response)
                );
    } catch (IOException exc) {
        exc.printStackTrace();
    }
}
HyperlinksController.java

There is a slight variation in the call to edit an existing hyperlink. In this case, an initial value is given to fill the UI controls. This allows the user to review the existing values. The following listing is a section of the initialize() method showing the response replacing the selected TableRow.


public void initialize() {
    // abbreviated
    tvHyperlinks.setRowFactory( tv -> {
        TableRow<Hyperlink> row = new TableRow<>();
        row.setOnMouseClicked(event -> {
            if (event.getClickCount() == 2 && (! row.isEmpty()) ) {
                Hyperlink rowData = row.getItem();
                try {
                    new HyperlinkDialog(rowData).showAndWait().ifPresent(response -> {
                        var selectedIndex = tvHyperlinks.getSelectionModel().getSelectedIndex();
                        tvHyperlinks.getItems().set(selectedIndex, response);
                    });
                } catch(IOException exc) {
                    exc.printStackTrace();
                }
            }
        });
        return row;
    });
}
HyperlinksController.java

Referring back to the class model, the custom Dialog extends the Dialog class and is called HyperlinkDialog. This extension defines a constructor that adds hyperlinks-dialog.fxml to the supplied DialogPane. The most important part of this constructor is the result converter. This is a lambda passed to setResultConverter() that knows how to retrieve a Hyperlink value from the embedded controller, HyperlinksController. The caller does not need to interact directly with the controller (or, worse, the controller does not need to interact with the caller) since the result will be available through the familiar showAndWait().


public HyperlinkDialog(Hyperlink initialValue) throws IOException {
    super();

    FXMLLoader fxmlLoader = new FXMLLoader(HyperlinksController.class.getResource("hyperlink-dialog.fxml"));

    ButtonType saveButtonType = new ButtonType("Ok", ButtonBar.ButtonData.OK_DONE);
    ButtonType cancelButtonType = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);

    this.setTitle("Hyperlink");
    this.setHeaderText("Enter hyperlink values");
    this.getDialogPane().setContent(fxmlLoader.load());

    HyperlinkDialogController c = fxmlLoader.getController();

    c.setInitialValue(initialValue); // null safe

    this.setResultConverter(p -> {
        if( p == saveButtonType ) {
            return c.getHyperlink();
        } else {
            return null;
        }
    });

    this
            .getDialogPane()
            .getButtonTypes()
            .addAll(saveButtonType, cancelButtonType);

    this.getDialogPane().lookupButton(saveButtonType).disableProperty().bind(c.validProperty().not());
}
HyperlinkDialog.java

In addition to providing the result converter, the preceding constructor sets the content for the DialogPane, configures title and header text, and adds buttons. The buttons belong to the Dialog superclass but can be retrieved by the subclass. In this code, the save Button is bound to a property of the controller "valid" which will disable the save Button if the input is invalid.

Dialog subclasses are best for side-effect-free interactions. If a Dialog needs to manipulate external resources like an API, opt for a general-purpose component. Tasks, error reporting through Alert, and ProgressBars will be easier to apply to a class built from the ground up rather than a dismantled Dialog subclass.

The code listing for HyperlinkDialogController is long and contains JavaFX code not specific to custom Dialogs. Its source file is available as a link in a new tab. The controller gathers two fields -- text and URL -- and packages them into a Hyperlink domain object for retrieval via the getHyperlink() method. There is repetition in this class that could easily be factored out. It is permitted in this example to keep the indirection to a minimum.

Use the standard JavaFX Dialogs judiciously. showAndWait() is a convenient call that allows you to blend user interactions with other function calls in your methods. Create a custom Dialog for cases where you need more than a singular piece of data (say a pair of text values). Keep the custom Dialog synchronous and result-focused. If you find a custom Dialog requiring side effects like an API, use a component that will give you more control over the button bar to report progress and messages.

A runnable Maven project with source code supporting this article can be found on GitLab under Carl Walker / javafx-hyperlink-dialog.