Modal JaxaFX Progress Indicator running in Background

There are times when one just wishes to magically pop-up a modal progress indicator while doing some hard work in the application’s background. The “modality” prevents the user from clicking around triggering all kinds of things, and the nice animation animates the spectator to stay looking at the screen trying to guess various parameters of the operation running behind.

Doing this in JavaFX is a bit more complicated. We have to juggle with Threads and the UI thread cannot be blocked, neither by the animation mathematics nor by the working classes. Moreover, processing work must be executed in a different thread than the UI thread, otherwise the UI freezes. Still, it would be very nice to have the application waiting for the operation to conclude while remaining responsive.

The following small class does just that. It creates a model dialog box which makes the user wait, a thread to animate the progress bar/indicator (if needed) and a worker thread that does the heavy lifting. The processing code is passed as lambda expression, but this can be replaced by something different easily. It is however important to understand that the worker thread CAN NOT access the UI elements in this example. (I’ll present a different approach that addresses this issue in another post). The animation thread on the other hand uses a special facility that is just made to communicate updates between animation thread and UI thread.

Another issue is the notification after the task finished. Since “exec()” starts threads, it returns immediately. The pane is modal, so the UI cannot be used, still we do not know when the task finishes and how to get the result. In order to solve the problem, we use a trick: we create an ObservableList to which we add the result “as notification”. A listener gets called when an element is added, but this happens in the UI thread after the panel disappeared. By this, we can show a error message alert box, if needed.

Note: To verify that the listener is indeed executed in the UI thread and not the worker thread, we just place some debug output of “Thread.currentThread().getName()” at various strategic places. The output indicates that the dialog is created in thread “JavaFX Application Thread” (the GUI thread), the task is executed in thread “Thread-7” and the notification is again executed in thread “JavaFX Application Thread“. Notifications on JavaFX Observables are consumed asynchroneously. All is good :) (Update: Real pro’s would use Platform.isFxApplicationThread() of course :)

Invoking the progress indicator is a peace of cake:


private WorkIndicatorDialog wd=null;

@FXML
void onOpenProject(ActionEvent event) {

    wd = new WorkIndicatorDialog(tralalaControl.getScene().getWindow(), "Loading Project Files...");

    wd.addTaskEndNotification(result -> {
       System.out.println(result);
       wd=null; // don't keep the object, cleanup
    });

    wd.exec("123", inputParam -> {
       // Thinks to do...
       // NO ACCESS TO UI ELEMENTS!
       for (int i = 0; i < 20; i++) {
           System.out.println("Loading data... '123' =->"+inputParam);
           try {
              Thread.sleep(1000);
           }
           catch (InterruptedException e) {
              e.printStackTrace();
           }
       }
       return new Integer(1);
    });
}

The results is simple, but efficient:

Finally, this is the code. You can adapt the modal dialog box as the progress bar. In case, the progress bar should show a real progress, the UI animation thread just does that.

package be.imifos.presentation.common;

import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;

import java.util.function.ToIntFunction;

/**
 * Public domain. Use as you like. No warranties.
 * P = Input parameter type. Given to the closure as parameter. Return type is always Integer.
 * (cc) @imifos
 */
public class WorkIndicatorDialog<P> {

    private Task animationWorker;
    private Task<Integer> taskWorker;

    private final ProgressIndicator progressIndicator = new ProgressIndicator(ProgressIndicator.INDETERMINATE_PROGRESS);
    private final Stage dialog = new Stage(StageStyle.UNDECORATED);
    private final Label label = new Label();
    private final Group root = new Group();
    private final Scene scene = new Scene(root, 330, 120, Color.WHITE);
    private final BorderPane mainPane = new BorderPane();
    private final VBox vbox = new VBox();

    /** Placing a listener on this list allows to get notified BY the result when the task has finished. */
    public ObservableList<Integer> resultNotificationList=FXCollections.observableArrayList();

    public Integer resultValue;

    /**
     *
     */
    public WorkIndicatorDialog(Window owner, String label) {
        dialog.initModality(Modality.WINDOW_MODAL);
        dialog.initOwner(owner);
        dialog.setResizable(false);
        this.label.setText(label);
    }

    /**
     * 
     */
    public void addTaskEndNotification(Consumer<Integer> c) {
        resultNotificationList.addListener((ListChangeListener<? super Integer>) n -> {
            resultNotificationList.clear();
            c.accept(resultValue);
        });
    }

    /**
     *
     */
    public void exec(P parameter, ToIntFunction func) {
        setupDialog();
        setupAnimationThread();
        setupWorkerThread(parameter, func);
    }

    /**
     *
     */
    private void setupDialog() {
        root.getChildren().add(mainPane);
        vbox.setSpacing(5);
        vbox.setAlignment(Pos.CENTER);
        vbox.setMinSize(330, 120);
        vbox.getChildren().addAll(label,progressIndicator);
        mainPane.setTop(vbox);
        dialog.setScene(scene);

        dialog.setOnHiding(event -> { /* Gets notified when task ended, but BEFORE 
                     result value is attributed. Using the observable list above is 
                     recommended. */ });
        
        dialog.show();
    }

    /**
     *
     */
    private void setupAnimationThread() {

        animationWorker = new Task() {
            @Override
            protected Object call() throws Exception {
                /*
                This is activated when we have a "progressing" indicator.
                This thread is used to advance progress every XXX milliseconds.
                In case of an INDETERMINATE_PROGRESS indicator, it's not of use.
                for (int i=1;i<=100;i++) {
                    Thread.sleep(500);
                    updateMessage();
                    updateProgress(i,100);
                }
                */
                return true;
            }
        };

        progressIndicator.setProgress(0);
        progressIndicator.progressProperty().unbind();
        progressIndicator.progressProperty().bind(animationWorker.progressProperty());

        animationWorker.messageProperty().addListener((observable, oldValue, newValue) -> {
            // Do something when the animation value ticker has changed
        });

        new Thread(animationWorker).start();
    }

    /**
     *
     */
    private void setupWorkerThread(P parameter, ToIntFunction<P> func) {

        taskWorker = new Task<Integer>() {
            @Override
            public Integer call() {
                return func.applyAsInt(parameter);
            }
        };

        EventHandler<WorkerStateEvent> eh = event -> {
            animationWorker.cancel(true);
            progressIndicator.progressProperty().unbind();
            dialog.close();
            try {
                resultValue = taskWorker.get();
                resultNotificationList.add(resultValue);   
            } 
            catch (Exception e) {
                throw new RuntimeException(e);
            }
        };

        taskWorker.setOnSucceeded(eh);
        taskWorker.setOnFailed(eh);

        new Thread(taskWorker).start();
    }

    /**
     * For those that like beans :)
     */
    public Integer getResultValue() {
        return resultValue;
    }

}

Leave a Reply

Your email address will not be published. Required fields are marked *