Can we have a long process in a thread updates a progress bar?

84 Views Asked by At

I’m facing a problem with thread and interface.

In my example I have a long process. Along calculations I must stop and ask the user informations. I cannot do that before. So I have to open a window and retrieve the user answer.

There are 2 ways that I know of  :

  1. create a new Thread.
  2. create a Runnable along with the use of Platform.RunLater.

The « copy multiple file » process will be a good example to explain what’s the problem. In this example we know we have to « launch » a long process : Copy every single file (one by one). The main interface has a ProgressBar. The goal is to update the ProgressBar on a regular basis. Then ; along the computation emerges a specific case which require the user attention.

If I used the Thread approach : The ProgressBar updates properly (using bound properties). I end up with the exception « not a JavaFx application » as soon as I try to open a new window (from this side process). This is normal and documented on this very website.

If I used the Runnable approach : the problem is about updates. The new window opens but the progress bar isn’t changed until a « refresh » occurs (see code example in the zip file linked below).

Any other suggestion I could find is not well documented or even explained properly (like service). So I'm stuck.

I’m a surprised not to be able to do that. I’m wondering if I’m doing something wrong or if I don’t use the right approach. Maybe this is JavaFx limitation. Any help greatly appreciated.

zip file

Thanks.

1

There are 1 best solutions below

0
Slaw On BEST ANSWER

Golden Rule of JavaFX: Any and all interactions with a live scene graph must occur on the JavaFX Application Thread. No exceptions.


In JavaFX, if you're going to run work on a background thread, and you need that work to report progress back to the user, then you should first consider using a javafx.concurrent.Task. It provides a nice API for publishing messages, progress, and a result on the JavaFX Application Thread. From there, you just need to figure out how to prompt the user for more information in the middle of the task executing on a background thread.

The simplest solution, at least in my opinion, is to use a CompletableFuture. You can configure it to execute a Supplier on the FX thread and have the background thread call join() to wait for a result. This only requires that you provide some sort of callback to your Task for the future to invoke. That callback could be anything. Some options include:

  • java.util.function.Supplier

  • java.util.function.Function

  • javafx.util.Callback (essentially equivalent to a Function)

  • ...

  • Or even your own interface/class. It doesn't have to be a functional interface, by the way.

Note this callback/listener idea can be applied to more than just prompting the user. For instance, if you've created a model/business class to perform the actual work, and you don't want this class to know anything about JavaFX, then you can adapt it to report messages/progress in its own way. The Task would then register listeners/callbacks as needed. That essentially makes the Task just a lightweight adapter allowing JavaFX to observe the progress and update the UI accordingly.

Proof of Concept

The code below launches a task that fakes long-running work. This task will prompt the user to ask if the task should continue upon half the work being completed. The task then waits for a response, and whether or not it will continue depends on said response.

The primary window is displaying a progress bar and message label to show such things still work as expected. When the task prompts the user, a modal alert will be displayed that will wait for the user to respond.

MockTask.java

Note I use a Callback<String, Boolean> for simplicity. But that interface is generic, and so it can be used with any types you want.

import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.util.Callback;

import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Supplier;

public class MockTask extends Task<Void> {

    private final Callback<String, Boolean> promptCallback;

    public MockTask(Callback<String, Boolean> promptCallback) {
        this.promptCallback = Objects.requireNonNull(promptCallback);
    }

    @Override
    protected Void call() throws Exception {
        int iterations = 10_000;

        updateProgress(0, iterations);
        for (int i = 0; i < iterations; i++) {
            updateMessage("Processing " + i + "...");
            Thread.sleep(1L);
            updateProgress(i + 1, iterations);

            if (i == iterations / 2) {
                boolean shouldContinue = promptUser("Should task continue?");
                if (!shouldContinue) {
                    throw new CancellationException();
                }
            }
        }

        return null;
    }

    private boolean promptUser(String prompt) {
        Supplier<Boolean> supplier = () -> promptCallback.call(prompt); // adapt Callback to Supplier
        Executor executor = Platform::runLater; // tells CompletableFuture to execute on FX thread

        // Calls the supplier on the FX thread and waits for a result
        return CompletableFuture.supplyAsync(supplier, executor).join();
    }
}

Main.java

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    private Stage primaryStage;

    @Override
    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;

        var progIndicator = new ProgressIndicator();
        var msgLabel = new Label();

        var root = new VBox(progIndicator, msgLabel);
        root.setAlignment(Pos.CENTER);
        root.setSpacing(10);

        primaryStage.setScene(new Scene(root, 500, 300));
        primaryStage.show();

        var task = new MockTask(this::handlePrompt);
        progIndicator.progressProperty().bind(task.progressProperty());
        msgLabel.textProperty().bind(task.messageProperty());

        var thread = new Thread(task, "task-thread");
        thread.setDaemon(true);
        thread.start();
    }

    private boolean handlePrompt(String prompt) {
        var alert = new Alert(Alert.AlertType.CONFIRMATION, prompt, ButtonType.YES, ButtonType.NO);
        alert.initOwner(primaryStage);
        alert.setHeaderText(null);
        return alert.showAndWait().map(bt -> bt == ButtonType.YES).orElse(false);
    }
}