Philayyy Philayyy - 2 months ago 34
Java Question

How to thread a background task whilst changing the cursor to WAIT in javaFX?

I have an application which upon login accesses a database through an interface class. The login process causes the application to not respond for a period while it hits the database and therefore I have been looking into threads and the waiting cursor to allow this to run smoothly. I have attempted to use threading via many of the examples on the web and stack overflow but my method doesn't seem to be working, I receive the java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-4 exception, and am not sure how to proceed from here. What I am attempting to do is to change the cursor to WAIT mode whilst this background thread is running the loginLoadEverything() method (although I haven't included the code within it as it is too long). Here is my controller class:

package main.java.gui;

import javafx.application.Platform;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Cursor;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;
import main.java.databaseInterface.BackendInterface;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.concurrent.CountDownLatch;

public class LoginController implements Initializable {


private BackendInterface backendInterface;
private DashboardController dashboardController;
private StudentsController studentsController;
private ConsultationController consultationController;
private CreateStudentController createStudentController;
private CreateConsultationController createConsultationController;

@FXML
TextField username;

@FXML
PasswordField password;

@FXML
Button loginButton;

@FXML
Label loginLabel;

@FXML
public void loginButtonPress(ActionEvent event) {

Service<Void> service = new Service<Void>() {
@Override
protected Task<Void> createTask() {
return new Task<Void>() {
@Override
protected Void call() throws Exception {

loginLoadEverything();

final CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(new Runnable() {
@Override
public void run() {
try {

Scene s1 = loginLabel.getScene();
s1.setCursor(Cursor.WAIT);

} finally {
latch.countDown();
}
}
});
latch.await();
return null;
}
};
}
};
service.start();

}

public void loginLoadEverything() {

//chance to true when complete
if (username.getText().isEmpty() == false || password.getText().isEmpty() == false) {

loginLabel.setText("Please enter data in the fields below");

} else {

username.setText("-----");
password.setText("-----");
//initialises backend interface with username and password
backendInterface = new BackendInterface(username.getText(), password.getText().toCharArray());

// Open a connection to the database

if (backendInterface.openConnection()) {

//return and print response
System.out.println(backendInterface.getConnectionResponse());

//directs the user to the dashboard after successful login
try {
if (backendInterface.getAllStudents() &&
backendInterface.getAllConsultations() &&
backendInterface.getCourses() &&
backendInterface.getConsultationCategories() &&
backendInterface.getConsultationPriorities()) {


FXMLLoader loader1 = new FXMLLoader();
loader1.setLocation(getClass().getResource("/main/res/dashboard.fxml"));
loader1.load();
Parent p = loader1.getRoot();
Stage stage = new Stage();
stage.setScene(new Scene(p));
stage.show();

//set instances to the dashboard controller
dashboardController = loader1.getController();
dashboardController.setBackendInterface(backendInterface); //pass backendInterface object to controller
dashboardController.setDashboardController(loader1.getController()); //pass dashboard as reference

//load images
Image logoutImage = new Image(getClass().getResourceAsStream("images/logout.png"));
Image userImage = new Image(getClass().getResourceAsStream("images/users.png"));
Image calendarImage = new Image(getClass().getResourceAsStream("images/calendar.png"));
Image leftArrowImage = new Image(getClass().getResourceAsStream("images/leftArrow.png"));
Image notepadImage = new Image(getClass().getResourceAsStream("images/notepad.png"));

//set images
dashboardController.studentLabel.setGraphic(new ImageView(userImage));
dashboardController.logoutLabel.setGraphic(new ImageView(logoutImage));
dashboardController.consultationLabel.setGraphic(new ImageView(notepadImage));

} else {
system.out.println(backendInterface.getExceptionMessage);
}


@Override
public void initialize(URL location, ResourceBundle resources) {



}

Answer

You probably don't need a Service here: you just need a Task.

The call() method is the method executed on the background thread. It should do the work that takes a long time to execute (i.e. connecting to the database and getting the data from it) and it must not do any UI work, as changes to the UI must be made on the FX Application Thread. The reason you are getting the exception is that you are creating and showing a Stage from the background thread.

So the basic idea is to have the task get the data from the database and return it; then use the onSucceeded handler for the task to display the UI, using the results of the task. (The onSucceeded handler is executed on the FX Application Thread, allowing you to safely modify the UI here.)

I don't know exactly how your classes are implemented, etc, but something along the following lines might work. The important thing is that you don't do anything in the background thread that interacts with the UI.

@FXML
public void loginButtonPress(ActionEvent event) {

    if (( ! username.getText().isEmpty()) || (! password.getText().isEmpty()) ) {

        loginLabel.setText("Please enter data in the fields below");

    } else {

        // I assume you want these values before you set them to "-----", no???
        final String uName = username.getText();
        final char[] pw = password.getText().toCharArray();

        username.setText("-----");
        password.setText("-----");

        // create task for retrieving data:

        Task<BackendInterface> loadDataTask = new Task<BackendInterface>() {

            @Override
            public BackendInterface call() throws Exception {

                BackendInterface backendInterface = new BackendInterface(uName, pw);
                if (backendInterface.openConnection()) {

                    if (backendInterface.getAllStudents() &&
                        backendInterface.getAllConsultations() &&
                        backendInterface.getCourses() &&
                        backendInterface.getConsultationCategories() &&
                        backendInterface.getConsultationPriorities()) {

                        return backendInterface ;
                    }
                 }

                 // maybe throw an exception here, depending on your requirements...
                 return null ;
            }

        };

        // show UI on task completion:

    loadDataTask.setOnSucceeded(e -> {

        BackendInterface backendInterface = loadDataTask.getValue();

        if (backendInterface == null) {
            // something went wrong... bail, or probably show error message...
            return ;
        }

        FXMLLoader loader1 = new FXMLLoader();
        loader1.setLocation(getClass().getResource("/main/res/dashboard.fxml"));
        Parent p = loader1.load();
        DashboardController controller = loader.getController();
        controller.setBackendInterface(backendInterface);

        Stage stage = new Stage();
        stage.setScene(new Scene(p));
        stage.show();

        // etc etc with your Images, etc (not sure why this isn't done in DashboardController though...)

        // set cursor back to default:
        loginLabel.getScene().setCursor(Cursor.DEFAULT);
    });

    loadDataTask.setOnFailed(e -> {
        // show error message or otherwise handle database exception here
    });

    // set cursor to WAIT:
    loginLabel.getScene().setCursor(Cursor.WAIT);

    // and run task in a background thread:
    Thread t = new Thread(loadDataTask);
    t.start();

}