5

I was looking at this question JavaFX show dialogue after thread task is completed, but my question is kind of the opposite. What is the best way to thread off after a filechooser or alert where you need some data back from the user?

Here's what I have now:

Platform.runLater(()->{
    File file = fileChooser.showOpenDialog(root.getScene().getWindow());
    if(file == null) {
        return;
    }
    executorService.execute(()->{
        //more code here which uses file
    });
});

where executorService is an ExecutorService that was made earlier. I suppose I could just as easily use a Task or a Thread or anything else, but how it's threaded off doesn't matter, just that it's something that takes a while that I don't want to have happen on the Application thread because it would lock up the UI.

I know this isn't an mvce, but I hope it demonstrates the problem I'm having with threads inside Platform.runLater calls.

Here's an extreme example of how convoluted this kind of thing gets

@FXML
public void copyFiles(ActionEvent event){
    //this method is on the application thread because a button or something started it
    // so we thread off here
    executorService.execute(()->{
        // do some stuff
        // ...
        // get location to copy to from user
        // must happen on the application thread!
        Platform.runLater(()->{
            File file = fileChooser.showOpenDialog(root.getScene().getWindow());
            if(file == null) {
                return;
            }
            executorService.execute(()->{
                // more code here which uses file
                // ...
                // oh wait, some files have the same names! 
                // we need a user's confirmation before proceeding
                Platform.runLater(()->{
                    Alert alert = new Alert(AlertType.CONFIRMATION, "Do you want to overwrite files with the same names?", ButtonType.OK, ButtonType.CANCEL);
                    Optional<ButtonType> choice = alert.showAndWait();
                    if(choice.isPresent && choice.get == ButtonType.OK){
                        // do something, but not on the application thread
                        executorService.execute(()->{
                            // do the last of the copying
                            // ...
                        });
                    }
                });
            });
        });
    });
}
MMAdams
  • 1,508
  • 15
  • 29

2 Answers2

5

If you need to do something on the UI thread that returns a result, create a FutureTask, submit it the UI thread, and then on the background thread wait for it to complete. This allows you to "flatten" the code.

You can also abstract Platform.runLater(...) as an Executor (after all, it is just something that executes Runnables), which can make it (perhaps) slightly cleaner.

By dividing up into smaller methods (and generally just using other standard programming techniques), you can make the code pretty clean.

Here's the basic idea (you'll need to add exception handling, or create a Callable (which can throw an exception) instead of a Runnable):

@FXML
public void copyFiles(ActionEvent event){

    Executor uiExec = Platform::runLater ;

    //this method is on the application thread because a button or something started it
    // so we thread off here

    Callable<Void> backgroundTask = () -> {
        doFirstTimeConsumingThing();

        FutureTask<File> getUserFile = new FutureTask<>(this::getUserFile) ;
        uiExec.execute(getUserFile);
        File file = getUserFile.get();
        if (file == null) return null ;

        doAnotherTimeConsumingThing(file);
        FutureTask<Boolean> getUserConfirmation = new FutureTask<>(this::showConfirmation);
        uiExec.execute(getUserConfirmation);
        if (! getUserConfirmation.get()) return null ;

        doMoreTimeConsumingStuff();

        // etc...

        return null ;
    };
    executorService.execute(backgroundTask);
}

private File getUserFile() {
    return fileChooser.showOpenDialog(root.getScene().getWindow());
}

private Boolean getUserConfirmation() {
    Alert alert = new Alert(AlertType.CONFIRMATION, "Do you want to overwrite files with the same names?", ButtonType.OK, ButtonType.CANCEL);
    return alert.showAndWait()
        .filter(ButtonType.OK::equals)
        .isPresent();
}

private void doFirstTimeConsumingThing() {
    // ...
}

private void doAnotherTimeConsumingThing(File file) {
    // ....
}

private void doMoreTimeConsumingStuff() {
    // ...
}
James_D
  • 201,275
  • 16
  • 291
  • 322
4

It seems your issue is needing information in the middle of a background task that can only be retrieved while on the JavaFX Application thread. The answer given by James_D works perfectly for this using FutureTask. I'd like to offer an alternative: CompletableFuture (added in Java 8).

public void copyFiles(ActionEvent event) {

    executorService.execute(() -> {

        // This uses CompletableFuture.supplyAsync(Supplier, Executor)

        // need file from user
        File file = CompletableFuture.supplyAsync(() -> {
            // show FileChooser dialog and return result
        }, Platform::runLater).join(); // runs on FX thread and waits for result

        if (file == null) {
            return;
        }

        // do some stuff

        // ask for confirmation
        boolean confirmed = CompletableFuture.supplyAsync(() -> {
            // show alert and return result
        }, Platform::runLater).join(); // again, runs on FX thread and waits for result

        if (confirmed) {
            // do more stuff
        }

    });
}

Both FutureTask and CompletableFuture will work for you. I prefer CompletableFuture because it it provides more options (if needed) and the join() method doesn't throw checked exceptions like get() does. However, CompletableFuture is a Future (just like FutureTask) and so you can still use get() with a CompletableFuture.

Slaw
  • 37,820
  • 8
  • 53
  • 80