7

I am trying to provide feedback in a JavaFX 8 application when a user chooses a menu item that launches a blocking process in another thread. In my real application it's a file download, but I have created a test case using minimal code by way of example:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.MenuButton;
import javafx.scene.control.ToolBar;
import javafx.scene.control.MenuItem;
import javafx.stage.Stage;

public class BlockingThreadTestCase extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        MenuItem menuItem = new MenuItem("Start");
        MenuButton menuButton = new MenuButton();
        menuButton.setText("Async Process");
        menuButton.getItems().addAll(menuItem);

        menuItem.setOnAction(event -> {
            menuButton.setText("Running...");

            Platform.runLater(() -> {
                try {
                    // Simulate a blocking process
                    Thread.sleep(5000);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                menuButton.setText(menuButton.getText() + "Done!");
            });
        });

        final ToolBar toolbar = new ToolBar(menuButton);
        final Scene scene = new Scene(toolbar);
        primaryStage.setScene(scene);
        primaryStage.setWidth(150);
        primaryStage.show();
    }
}

Here's how it's supposed to work: When you select the "Start" menu item, the main menu text should immediately change to "Running...", and then it should append "Done!" after the 5-second sleep that simulates my file download.

What is actually happening is both text updates are firing after the blocking process is done, even though I'm using Platform.runLater(). What am I doing wrong?

4
  • 2
    Platform.runLater will execute the Runnable block ON the Event Dispatching Thread, which means it will block your UI. This isn't how you want to delayed callbacks, I'm not familiar with JavaFX, but you want some kind of Timer which can be executed (with a delay of 5seconds) and which when triggered, can update the state of you menu button within the the context of the EDT Commented Oct 13, 2015 at 21:03
  • 1
    Use a Task running on a new thread that you create. Commented Oct 13, 2015 at 21:06
  • I have tried creating my own thread, but then I get an error when I try to update the menu text because JavaFX UI changes can only be made from the main UI thread. Commented Oct 13, 2015 at 21:12
  • Please read the Task javadoc. Specifically the section titled "A Task Which Modifies The Scene Graph" and note how Platform.runLater is used in combination with a Task. (Also read the linked Platform.runLater documentation). Commented Oct 13, 2015 at 21:19

1 Answer 1

10

The easiest way to do this is by using a Task. Platform.runLater is only needed if you need to update the UI from a different thread and therefore is not necessary in your case. If you would like to track progress of the background task while it is running, you may use updateMessage and updateProgress methods in the task to safely pass messages to the UI thread without worrying about the scheduling via EDT. You may find more information on this here https://docs.oracle.com/javase/8/javafx/interoperability-tutorial/concurrency.htm .

See the minimal working example below.

import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToolBar;
import javafx.stage.Stage;


public class BlockingThreadTestCase extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        MenuItem menuItem = new MenuItem("Start");
        MenuButton menuButton = new MenuButton();
        menuButton.setText("Async Process");
        menuButton.getItems().addAll(menuItem);

        menuItem.setOnAction(event -> {
            menuButton.setText("Running...");

            Task task = new Task<Void>() {
                @Override
                public Void call() {
                    //SIMULATE A FILE DOWNLOAD
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return null;
                }
            };
            task.setOnSucceeded(taskFinishEvent -> menuButton.setText(menuButton.getText() + "Done!"));
            new Thread(task).start();
        });

        final ToolBar toolbar = new ToolBar(menuButton);
        final Scene scene = new Scene(toolbar);
        primaryStage.setScene(scene);
        primaryStage.setWidth(150);
        primaryStage.show();
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

THANK YOU! I have been through several iterations, and I did try a Task, but I was missing the setOnSucceeded() part. That was my missing piece.
In a long-living application it would be better to use Executors instead of explicit Thread instantiation.
To implement ursa's suggestion you could use a Service instead of a Task. A Service has some built-in facilities for thread management via executors. You can use Executors with Tasks too, though the Task API does not provide any assisting API for this. Usage of a Service is marginally more complicated than a Task.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.