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 ...

Sunday, July 26, 2015

Using the Task Class for a Responsive FX UI

In order for your FX desktop app to work properly, all of your FX control manipulation is single-threaded, running on the FX system thread.  However, for longer-running operations -- and even operations that take as little as a second -- this can produce a UI with frequent pauses, cursors, and blank screens.

This post enlists the Java FX Task class to display a complete form as soon as the user presses a button.  The Task-based implementation is contrasted with a single sequence of calls (no Task) showing a brief blank screen and an ineffectual progress bar.

Without Threading

This video shows a Stage that is displayed when the user presses the Configure button.  As the Stage is being displayed the Java keytool command is run to retrieve a list of values for an FX ChoiceBox.

video
Each time the Configure button is pressed, there is a brief pause that shows a blank screen.  This is despite running the program on a fast computer.  If the operation were more time-consuming or my computer were slower, the delay may become intolerable.  In a worst-case scenario, the user attempts to exit the application.  This screenshot shows the effect of such a delay.

The User Waiting at a Blank Form

Threading

The solution is to use Java Threads to work on the long-running operation (the keytool command in my case) and allow the portion of the UI not waiting for the operation to display.  This works well in my example because most of the UI is not waiting for the operation.  So, the user can start viewing and working with the form right away while the ChoiceBox values are prepared.

This video shows the revised version.

video
The FX mechanism that allows this multi-threading is a special class calls Task.  Combined with the Platform.runLater() method, this class will ensure that FX updating code is run on the FX thread.

The method loadAliases() runs my keytool command in the Task which is started as a plain java.lang.Thread.  The method sets up the progress HBox "hboxAliasProgress", manipulates the slow-loading ChoiceBox "cbAlias", and retrieves the keystore and password from the FX controls.  All of the preceding steps are done before the Task is created and started.


private void loadAliases() {

    if( StringUtils.isNotEmpty(tfKeystore.getText()) && 
      StringUtils.isNotEmpty(pfStorepass.getText()) ) {

        hboxAliasProgress.setVisible( true );
        cbAlias.valueProperty().unbindBidirectional(activeProfile.jarsignerConfigAliasProperty());
        cbAlias.getItems().clear();

        final String ks = tfKeystore.getText();
        final String sp = pfStorepass.getText();

        Task<Void> t = new Task<Void>() {
            public Void call() {

                try {

                    updateMessage("Loading...");
                    updateProgress( 0.1d, 1.0d );

                    final List<String;gt& aliases = keytoolCommand.findAliases(
                        "C:\\Program Files\\Java\\jdk1.8.0_40\\bin\\keytool",
                        ks,
                        sp
                    );

                    updateMessage("Updating...");
                    updateProgress(0.8d, 1.0d);

                    Platform.runLater( () -> {
                        if (CollectionUtils.isNotEmpty(aliases)) {

                            cbAlias.getItems().addAll(aliases);
                            cbAlias.valueProperty().bindBidirectional(
                             activeProfile.jarsignerConfigAliasProperty()
                             );

                        }

   // might be an empty list for empty keystore
   cbAlias.setDisable(false);  
                        hboxAliasProgress.setVisible( false );
                    });

                } catch(CommandExecutionException exc) {
                    if( logger.isWarnEnabled() ) {
                        logger.warn("error getting aliases", exc);
                    }
                    Platform.runLater(() -> {
                        cbAlias.setDisable( true );
                        hboxAliasProgress.setVisible( false );
                    });
                } finally {
                    updateMessage("");
                    updateProgress( 0.0d, 1.0d );
                }

                return null;
            }
        };

        lblAliasProgress.textProperty().bind( t.messageProperty() );
        pbAlias.progressProperty().bind( t.progressProperty() );

        new Thread(t).start();

    } else {

        cbAlias.getItems().clear();
        cbAlias.setDisable( true );
    }
}

You can find the code on GitHub.  Clone the https://github.com/bekwam/resignator-repos-1.git project and look for the class "JarsignerConfigController".

Progress

updateMessage() and updateProgress() are Task methods that can be overridden to manipulate FX controls.  For example, updateMessage() can update a Label while updateProgress() can update a ProgressBar.  I'm doing that in this example, however, I'm using binding to bind to the tasks message and progress properties.  When I call the update() methods on the Task, I update a Task property and the binding makes a corresponding change in my UI controls (lblAliasProgress and pbAlias which are a Label and a ProgressBar, respectively.

Other UI Manipulations

You'll notice Platform.runLater() used in several place in the call method.  While updateMessage() and updateProgress() will be run on the FX thread, by contract, call() does not offer any such protection.  So, when I disable the progress HBox and the ChoiceBox, I have to wrapthat in runLater().

ChoiceBox

The ChoiceBox is filled with the long-running operation.  I'm using binding to associate the ChoiceBox with a property of an active record object "jarsignerConfigAliasProperty".  In order for the ChoiceBox to be set after being filled with fresh data, I break the binding with an unbind() call before the Task runs.  I then add all the items.  Then, I restore the binding which puts my business property's value (jarsignerConfigAliasProperty) into the control.

The ChoiceBox manipulation appears in the runLater() command.

The Work

The long-running operation "findAliases" does not appear in the runLater().  That's because the call is thread safe and doesn't need any FX toolkit threading protection.

I mention long-running operations in this post, but I really mean operations over 1 sec in duration.  I've found working on FX apps that if you don't take a hard line on these performance metrics, 1 second turns into 2 seconds (and beyond).  Everything may "work" in this scenario, but as you force the user to wait for what they perceive as simple processing (ex, the display of a window), there's a chance that they might abort your application prematurely.



1 comment:

  1. This is nice information,, Thanks for sharing this information,, Its very useful to me
    mobile app development

    ReplyDelete