Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Monday, September 7, 2015

Applying Wait / Notify to a Custom Dialog Called from a Task

If you want to display a modal dialog and have your program wait on the result, call showAndWait().  However, this approach won't work if your modal dialog is displayed in a Task.  The Task requires that FX interactions be placed on the FX thread via the runLater() method.  Your Task code will blow right past the runLater() and call the remainder of your Task before the dialog is even displayed.

All code in this post can be found on GitHub.

Without a Task showAndWait() Works

To start the post, let me show you a Task-less example where showAndWait() works as expected.


The video displays a dialog when the Launch Button is pressed.  showAndWait() is invoked in the handler.  The debug statement is executed once the dialog returns control to the caller.  Here is the code of the dialog class.

public class GetDataDialog {

    private VBox vbox;
    private String data = "";

    public void init() {
        vbox = new VBox();
        vbox.setSpacing(20.0d);
        vbox.setPadding(new Insets(10.0d));

        TextField tf = new TextField();

        Button btn = new Button("Submit");
        btn.setOnAction( (evt) -> {
            data = tf.getText();
            ((Button)evt.getSource()).getScene().getWindow().hide();
        } );

        vbox.getChildren().add(new Label("Enter Data"));
        vbox.getChildren().add(tf);
        vbox.getChildren().add(btn);
    }

    public VBox getRoot() { return vbox; }
    public String getData() { return data; }
}

GetDataDialog is created and initialized in a Button EventHandler.  Here is the code from the main program.  Notice that the showAndWait() appears before the debug() call which matches what was shown in the video (hide dialog, then write output).

Button btn = new Button("Launch Dialog");
btn.setOnAction((evt) -> {

    GetDataDialog dialog = new GetDataDialog();
    dialog.init();
    Scene dialogScene = new Scene(dialog.getRoot());
    Stage dialogStage = new Stage();
    dialogStage.initModality(Modality.APPLICATION_MODAL);
    dialogStage.setScene(dialogScene);
    dialogStage.showAndWait();

    if (logger.isDebugEnabled()) {
        logger.debug("[BTN] data=" + dialog.getData());
    }
});

No Coordination

If you call a Task from your Button EventHandler -- a good practice for long-running operations -- things don't work quite as expected.  The dialog-showing code is properly isolated in a Platform.runLater(), however, the runLater() does not hold up the calling Task.

Watch this video for repeated attempts to get the value entered from the modal dialog.  The debug() call precedes the showAndWait() call.


The same GetDataDialog class is used.  The no stopping example uses the following Button EventHandler.

Button btnTask = new Button("Launch Dialog As Task");
btnTask.setOnAction( (evt) -> {

    Task task = new Task() {

        @Override
        protected Void call() throws Exception {

            GetDataDialog dialog = new GetDataDialog();

            Platform.runLater( () -> {
                dialog.init();
                Scene dialogScene = new Scene(dialog.getRoot());
                Stage dialogStage = new Stage();
                dialogStage.initModality(Modality.APPLICATION_MODAL);
                dialogStage.setScene(dialogScene);
                dialogStage.showAndWait();
            });

            if(logger.isDebugEnabled()) {
                logger.debug("[BTN TASK] data=" + dialog.getData());
            }

            return null;
        }
    };
    new Thread(task).start();
});

Using Wait / Notify

To get the desired behavior, a synchronization mechanism is needed to halt the processing of the Task.  This can be done using Object.wait() and Object.notify().  I ask the Task to wait() on the dialog object.  As a good practice, I'm also anticipating a timeout.  The wait() will pause the thread's execution until a notify() is received (or a timeout).

The following video shows the reworked code behaving as expected.  The output isn't written until the dialog has been dismissed and a value (GetDialogData.data) has been retrieved.


This is the reworked Button EventHandler.

Button btnTaskSynced = new Button("Launch Dialog As Task (Synched)");
btnTaskSynced.setOnAction( (evt) >> {

    Task task = new Task() {

        @Override
        protected Void call() throws Exception {

            GetDataDialogSynced dialog = new GetDataDialogSynced();

            Platform.runLater( () >> {
                dialog.init();
                Scene dialogScene = new Scene(dialog.getRoot());
                Stage dialogStage = new Stage();
                dialogStage.initModality(Modality.APPLICATION_MODAL);
                dialogStage.setScene(dialogScene);
                dialogStage.showAndWait();
            });

            synchronized (dialog) {
                dialog.wait( 10000 );
            }

            if(logger.isDebugEnabled()) {
                logger.debug("[BTN TASK] data=" + dialog.getData());
            }

            return null;
        }
    };
    new Thread(task).start();
});

Unlike the previous two example, this uses a different GetDataDialog implementation, GetDataDialogSynched.  The difference is in the notify().

public class GetDataDialogSynced {

    private VBox vbox;
    private String data = "";

    public void init() {
        vbox = new VBox();
        vbox.setSpacing(20.0d);
        vbox.setPadding(new Insets(10.0d));

        TextField tf = new TextField();

        Button btn = new Button("Submit");
        btn.setOnAction( (evt) -> {
            data = tf.getText();
            ((Button)evt.getSource()).getScene().getWindow().hide();
            synchronized(GetDataDialogSynced.this) {
                GetDataDialogSynced.this.notify();
            }
        } );

        vbox.getChildren().add(new Label("Enter Data"));
        vbox.getChildren().add(tf);
        vbox.getChildren().add(btn);
    }

    public VBox getRoot() { return vbox; }
    public String getData() { return data; }
}

Notes on Synchronized

The wait() and the notify() command require locking, so I've framed the calls with a synchronized(dialog) instruction.  If you get an IllegalMonitorException, you'll need to make sure that your wait/notify matches the synchronized block.  Also, if you never see your program progressing, make sure that you're wait/notifying on the same object.

I'm using the dialog (GetDataDialog) but I could have used any object that both classes know about.

Summary

All code in this post can be found on GitHub.
showAndWait() works perfectly when you're on the FX thread.  Your processing comes to a halt as the showAndWait() is displayed which is exactly what you want from a modal dialog.  However, if you are not on the FX thread, the terms change.  You need to use a runLater() for the FX code to work and this may mean that your code is not executed right away.  For example, let's say you're in the middle of a Task and need the user's interaction to proceed.  Follow up the runLater() with a wait() and call a notify() to continue processing.

1 comment: