-1

I have the following code:

@FXML
private void test(){
    textField.setText("Pending...");
    boolean passed = doStuff();
    if(passed){
        textField.setText("OK");
    } else {
        textField.setText("Error");
    }
}

And what I tries to achieve is that while the doStuff() do his stuff in a textField in the GUI there should be written "Pending..." and as soon as it finish it should change to "OK" / "Error".

I want that the GUI is blocked while doStuff is running so the user has to wait and can't click something else.

But what happens is that as soon as I start test it does the doStuff() but only updates the textField with "OK"/"Error" but I never see "Pending...".

I have the feeling that I have somehow update the GUI, but I'm not sure how it should be done.

Update: What I tried is to move the doStuff in another Thread:

@FXML
private void test(){
    textField.setText("Pending...");
    Thread t = new Thread(){
        public void run(){
            boolean passed = doStuff();
            if(passed){
                textField.setText("OK");
            } else {
                textField.setText("Error");
            }
        }
    };
    t.start();
    t.join();
}

It would works if i would remove the t.join(); command, but then the UI wouldn't be blocked. So I'm at a loss right now.

Thanks

Boendal
  • 2,496
  • 1
  • 23
  • 36
  • Are you using a Task for doStuff or are you running everything in the same UI Thread? – Juan Feb 12 '19 at 14:06
  • I updated my post, yeah I tried it with and without but I'm not sure how I should do it right. – Boendal Feb 12 '19 at 14:23
  • Your use of `t.join()` completely negates the use of another `Thread`. Use a `javafx.concurrent.Task` (still run it on another `Thread`) and observe it for completion. Also, when accessing live GUI objects from another thread you must wrap that code in a `Platform.runLater` call. – Slaw Feb 12 '19 at 14:34
  • I don't see a difference if I use Thread or Task. Both way works but in both ways the UI isn't blocked. – Boendal Feb 12 '19 at 14:42
  • You use `Task` because it provides an API for communicating back to the _JavaFX Application Thread_. If you don't need that, or you just want something simple, you can use some other `Runnable`. However, in your current example you use `Thread.join()` which _blocks the calling thread until the target thread dies_ — you might as well be running the task on the calling thread. Also see [“implements Runnable” vs “extends Thread” in Java](https://stackoverflow.com/questions/541487/implements-runnable-vs-extends-thread-in-java). – Slaw Feb 12 '19 at 14:52
  • Okay yeah I see this. But I'm not sure how this helps me or how I use this. But my core problem is the following: While the doStuff is running the textfield should show the user that something is running (Textfield should show "Pending") & the GUI should be blocked. How can I achieve that? – Boendal Feb 12 '19 at 14:55
  • You should never block the UI thread. If you want to stop the user from interacting with your application while `doStuff` is running, then you have many other options - show a modal dialog, disable the root node, use some kind of flag that will stop user actions from executing, etc. – Guest 21 Feb 12 '19 at 14:56

1 Answers1

1

You must never run long running tasks on the JavaFX Application Thread. Doing so will prevent said thread from doing any GUI related things which results in a frozen UI. This makes your user(s) sad. However, your attempt at putting the long running task on a background task is flawed. You call Thread.join which will block the calling thread until the target thread dies; this is effectively the same thing as just running the task on the calling thread.

For a quick fix to your example, you could do the following:

@FXML
private void test(){
    textField.setText("Pending...");
    Thread t = new Thread(){
        @Override public void run(){
            boolean passed = doStuff();
            Platform.runLater(() -> {
                if(passed){
                    textField.setText("OK");
                } else {
                    textField.setText("Error");
                }
            });
        }
    };
    t.start();
}

That will create a thread, start it, and let it run in the background while letting the JavaFX Application Thread continue doing what it needs to. Inside the background thread you must update the TextField inside a Platform.runLater(Runnable) call. This is needed because you must never update a live scene graph from a thread other than the JavaFX Application Thread; doing so will lead to undefined behavior. Also, you should look into “implements Runnable” vs “extends Thread” in Java. It's better, or at least more idiomatic, to do:

Thread t = new Thread(() -> { /* background code */ });

You could also use a javafx.concurrent.Task which may make it easier to communicate back to the JavaFX Application Thread. One option would be:

@FXML
private void test(){
    textField.setText("Pending...");
    Task<Boolean> task = new Task<>() {
        @Override protected Boolean call() throws Exception {
            return doStuff();
        }
    };
    task.setOnSucceeded(event -> textField.setText(task.getValue() ? "Ok" : "Error"));
    new Thread(task).start();
}

You could also bind the TextField to the message property of the Task and call updateMessage("Pending...") inside the call method. You could even provide more detailed messages if and when possible.

That said, creating and starting Threads yourself is not ideal and you should look into thread pooling (using something like an ExecutorService). You might also want to look into javafx.concurrent.Service for "reusing" Tasks.


For more information about JavaFX concurrency see Concurrency in JavaFX and read the documentation of the classes in javafx.concurrent. For the basics of multi-threading in Java see Lesson: Concurrency from The Java™ Tutorials.

Slaw
  • 37,820
  • 8
  • 53
  • 80