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

Saturday, August 8, 2015

A/B Presentation of the Java FX Task Class

Use the JavaFX Task class to keep your JavaFX app lively while your event handlers and actions are processing.  This blog post is an A/B presentation showing an unresponsive UI contrasted with a UI that keeps the user engaged.  When working with the non-Task version, I was surprised to see the UI not update itself with calls made before the processing even started.

This video shows a two-part JAR signing operations.  The first part unsigns the JAR, removing any previous signatures.  The second part signs the JAR with the specified certificate.  As part of the signing, there are a lot of file operations such as unzipping and copying.

Non-Task Demo

The whole operation takes 6-7 seconds.  Notice in the video that the user doesn't get any feedback for about 5 seconds.  Then the user gets a "Not Responding" message and hourglass/circle icon for another 2 seconds.  Finally, the feedback -- in the form of a JavaFX Label, a ProgressIndicator, and a TextArea -- comes pouring in.  This is far too late; the user may attempt to kill the app.

video
What's interesting about this code is that there are some JavaFX operations that don't take effect until the end, despite the fact that they appear before any significant processing happens.  I put the ProgressIndicator to 10% at the start.  However, this never updates the UI until the end.

Here is the code for the method "sign".  unsignJAR() an signJAR() are the long-running operations.  Notice that status updates are sprinkled throughout the calls.  The IDE console shows me that the long-running operations are working in sequence and verifying that the program did execute the status updates.

@FXML
public void sign() {

UnsignCommand unsignCommand = unsignCommandProvider.get();
SignCommand signCommand = signCommandProvider.get();

lblStatus.setText("");
piSignProgress.setProgress(0.1d);

try {
    unsignCommand.unsignJAR(
     Paths.get(activeProfile.getSourceFileFileName()),
     Paths.get(activeProfile.getTargetFileFileName()),
     s ->
      Platform.runLater(() ->
txtConsole.appendText(s + System.getProperty("line.separator"))
      )
    );

    piSignProgress.setProgress(0.5d);

    signCommand.signJAR(
     Paths.get(activeProfile.getTargetFileFileName()),
     Paths.get(activeProfile.getJarsignerConfigKeystore()),
     activeProfile.getJarsignerConfigStorepass(),
     activeProfile.getJarsignerConfigAlias(),
     activeProfile.getJarsignerConfigKeypass(),
     s ->
      Platform.runLater(() ->
txtConsole.appendText(s + System.getProperty("line.separator"))
      )
    );

    lblStatus.setText("JAR signed successfully");
    piSignProgress.setProgress(1.0d);

} catch(CommandExecutionException exc) {

    logger.error("error unsigning and signing jar", exc);

    lblStatus.setText("Error signing JAR");
    piSignProgress.setProgress(1.0d);

    Alert alert = new Alert(Alert.AlertType.ERROR, exc.getMessage());
    alert.showAndWait();
}
}

With Task

This version of the sign() method uses a Task.  It keeps the UI lively and gives continuous feedback to the user.  I'm using the same long-running operations and making the same status update calls in the same order.  The only difference is the Task and adapting some of the calls to the Task class such as overridden methods an bound properties.

This video shows the improved version.  The operations still take the same amount of time, but you get a ProgressIndicator right away.  As the processing continues, the ProgressIndicator is updated, the status Label is updated, and logging messages scroll onto the TextArea.

video

The operation takes the same amount of time, but because the user is receiving visual feedback, they are far less inclined to force quit the app or describe it as lagging.


@FXML
public void sign() {

UnsignCommand unsignCommand = unsignCommandProvider.get();
SignCommand signCommand = signCommandProvider.get();

Task<Void> task = new Task<Void>() {

    @Override
    protected Void call() throws Exception {

 updateMessage("");
 updateProgress(0.1d, 1.0d);
 updateTitle("Unsigning JAR");

 unsignCommand.unsignJAR(
  Paths.get(activeProfile.getSourceFileFileName()),
  Paths.get(activeProfile.getTargetFileFileName()),
  s ->
   Platform.runLater(() ->
txtConsole.appendText(s + System.getProperty("line.separator"))
   )
 );

 if( isCancelled() ) {
     return null;
 }

 updateProgress(0.5d, 1.0d);
 updateTitle("Signing JAR");

 signCommand.signJAR(
  Paths.get(activeProfile.getTargetFileFileName()),
  Paths.get(activeProfile.getJarsignerConfigKeystore()),
  activeProfile.getJarsignerConfigStorepass(),
  activeProfile.getJarsignerConfigAlias(),
  activeProfile.getJarsignerConfigKeypass(),
  s ->
   Platform.runLater(() ->
txtConsole.appendText(s + System.getProperty("line.separator"))
   )
 );

 return null;
    }

    @Override
    protected void succeeded() {
 super.succeeded();

 updateProgress(1.0d, 1.0d);
 updateMessage("JAR signed successfully");

Platform.runLater( () -> {
  piSignProgress.progressProperty().unbind();
  lblStatus.textProperty().unbind();
});

    }

    @Override
    protected void failed() {
 super.failed();

 logger.error("error unsigning and signing jar", 
 exceptionProperty().getValue());

 updateProgress(1.0d, 1.0d);
 updateMessage("Error signing JAR");

 Platform.runLater(() -> {

     piSignProgress.progressProperty().unbind();
     lblStatus.textProperty().unbind();

     Alert alert = new Alert(Alert.AlertType.ERROR, 
     exceptionProperty().getValue().getMessage());
     alert.showAndWait();
 });
    }

    @Override
    protected void cancelled() {
 super.cancelled();

 if( logger.isWarnEnabled() ) {
     logger.warn("signing jar operation cancelled");
 }

 updateProgress(1.0d, 1.0d);
 updateMessage("JAR signing cancelled");

 Platform.runLater(() -> {

     piSignProgress.progressProperty().unbind();
     lblStatus.textProperty().unbind();

     Alert alert = new Alert(Alert.AlertType.INFORMATION, 
     "JAR signing cancelle");
     alert.showAndWait();
 });

    }
};

piSignProgress.progressProperty().bind(task.progressProperty());
lblStatus.textProperty().bind(task.messageProperty());

new Thread(task).start();
}

Notes on Task Implementation

Other Overrides

Rather than catching any exceptions in the call() method, I'm implementing a failed() method.  The call signature of call() has a throws which will allow my CommandExecutionException to be omitted from call().

I've also added a cancelled() implementation.  This would be called if the operation were halted in between the long-running unsignJAR() and signJAR() operations.  I return if the isCancelled() returns true.

Binding

I'm binding the Task's progress and message properties to my UI controls.  This allows me to make JavaFX thread-safe calls within the call() method.  I can't call lblStatus.setText() in my threaded-code, but i can call updateMessage() on the Task and have it call lblStatus.setText() in a thread-safe manner.

Notice that I'm also unbinding in the Task methods succeeded, failed, and cancelled.  Although the Task is built on-the-fly, piProgressIndicator and lblStatus will be around for the life of the app.  I clear these settings in other areas of the app, so I need to break the binding to the now unused Task or I'll get an error about updating bound property.

runLater()

For manipulating the UI outside of binding, I have to provide my own protection.  This comes in the form of a Platform.runLater() wrapper which makes my UI code run on the JavaFX thread.  Note that the whole thing isn't wrapped up in runLater().  I'd still like to preserve some parallelism with the file-oriented, back-end nature of the unsignJAR() an signJAR() methods, so I'll keep their implements (the file copies, jarsigner, etc) deliberately off the FX thread.

In cases where I do want the unsignJAR() and signJAR() operations to give feedback, I pass in an Observer.  This does have to be on the FX thread.

You would think that a call to JavaFX would immediately update the UI, but that's clearly not the case.  In my Swing programming, I might be inclined to force a refresh or paint operation in those circumstances.  You don't have that these calls in JavaFX, but once you master the use of the Task class, you'll find a suitable technique.


No comments:

Post a Comment