Issue
I am in the situation where I start a new view but in its controller i make a request to server to get some objects. This request takes some time so i want to implement a little loading view during this waiting time. So, in my controller i implemented a Service Object to make this request in another thread :
public void initialize(URL url, ResourceBundle resourceBundle) {
try {
Parent root = FXMLLoader.load(getClass().getResource("/MiniPages/LoadingPage.fxml"));
Scene scene = new Scene(root);
Stage primaryStage = new Stage();
primaryStage.initStyle(StageStyle.UNDECORATED);
Service<ProjectModel> service = new Service<ProjectModel>() {
@Override
protected Task<ProjectModel> createTask() {
return new Task<ProjectModel>() {
@Override
protected ProjectModel call() throws Exception {
SenderText data = new SenderText();
int id = Integer.parseInt(data.getData());
Client client = Client.getInstance();
JSONObject tosend = new JSONObject();
tosend.put("Type", "Get Project");
tosend.put("IDproject", id);
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LocalDate.class, new LocalDateSerializer());
gsonBuilder.registerTypeAdapter(LocalDate.class, new LocalDateDeserializer());
Gson gson = gsonBuilder.setPrettyPrinting().create();
client.sendText(tosend.toString());
String response = client.receiveText();
ProjectModel project = gson.fromJson(response, ProjectModel.class);
projectLocal = project;
System.out.println(gson.toJson(projectLocal));
primaryStage.close();
primaryStage.hide();
return project;
}
};
}
};
service.start();
primaryStage.setScene(scene);
primaryStage.initModality(Modality.APPLICATION_MODAL);
while (service.getValue() == null) {
primaryStage.showAndWait();
}
primaryStage.close();
System.out.println("Finish");
primaryStage.close();
} catch (Exception e) {
e.printStackTrace();
}
}
I tested and the object is set on call() method. But the loading view does not close. I also tried to call primaryStage.show()
instead of showandWait()
but it did not work. Any idea how to solve it?
Solution
There are two main threading rules in JavaFX (similar to most other UI toolkits).
- You must not perform operations on the UI from a background thread. This includes creating, showing, or hiding windows.
- You must not block the UI thread (the "FX Application Thread").
You violate the first rule by calling primaryStage.close()
in the call()
method.
You violate the second rule with your busy while
loop. (Worse; I think the service's value
property is only updated on the FX Application Thread. So since you're blocking that thread, the value
property can't be updated and the while
loop never exits.)
To perform a UI action when the background Task
completes, you can use the onSucceeded
handler. This handler is invoked on the FX Application Thread, after the background task completes successfully.
public void initialize(URL url, ResourceBundle resourceBundle) {
try {
Parent root = FXMLLoader.load(getClass().getResource("/MiniPages/LoadingPage.fxml"));
Scene scene = new Scene(root);
Stage primaryStage = new Stage();
primaryStage.initStyle(StageStyle.UNDECORATED);
Service<ProjectModel> service = new Service<ProjectModel>() {
@Override
protected Task<ProjectModel> createTask() {
return new Task<ProjectModel>() {
@Override
protected ProjectModel call() throws Exception {
SenderText data = new SenderText();
int id = Integer.parseInt(data.getData());
Client client = Client.getInstance();
JSONObject tosend = new JSONObject();
tosend.put("Type", "Get Project");
tosend.put("IDproject", id);
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LocalDate.class, new LocalDateSerializer());
gsonBuilder.registerTypeAdapter(LocalDate.class, new LocalDateDeserializer());
Gson gson = gsonBuilder.setPrettyPrinting().create();
client.sendText(tosend.toString());
String response = client.receiveText();
ProjectModel project = gson.fromJson(response, ProjectModel.class);
return project;
}
};
}
};
service.setOnSucceeded(event -> {
System.out.println("Finish");
// I think; don't know what projectLocal is, or
// which threads it should be accessed from:
projectLocal = project ;
primaryStage.hide();
});
service.start();
primaryStage.setScene(scene);
primaryStage.showAndWait();
} catch (Exception e) {
e.printStackTrace();
}
}
As an aside, you probably don't need a Service
here. Service
is really designed for repeated use, and you are only using it once. Simply creating a Task
and running it on a background thread should be enough:
public void initialize(URL url, ResourceBundle resourceBundle) {
try {
Parent root = FXMLLoader.load(getClass().getResource("/MiniPages/LoadingPage.fxml"));
Scene scene = new Scene(root);
Stage primaryStage = new Stage();
primaryStage.initStyle(StageStyle.UNDECORATED);
Task<ProjectModel> task = new Task<ProjectModel>() {
@Override
protected ProjectModel call() throws Exception {
SenderText data = new SenderText();
int id = Integer.parseInt(data.getData());
Client client = Client.getInstance();
JSONObject tosend = new JSONObject();
tosend.put("Type", "Get Project");
tosend.put("IDproject", id);
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LocalDate.class, new LocalDateSerializer());
gsonBuilder.registerTypeAdapter(LocalDate.class, new LocalDateDeserializer());
Gson gson = gsonBuilder.setPrettyPrinting().create();
client.sendText(tosend.toString());
String response = client.receiveText();
ProjectModel project = gson.fromJson(response, ProjectModel.class);
return project;
}
};
task.setOnSucceeded(event -> {
System.out.println("Finish");
// I think; don't know what projectLocal is, or
// which threads it should be accessed from:
projectLocal = project ;
primaryStage.hide();
});
new Thread(task).start();
primaryStage.setScene(scene);
primaryStage.showAndWait();
} catch (Exception e) {
e.printStackTrace();
}
}
The structure of your code looks a little weird here. It looks like you are needing to create a model for your application at startup, and that startup takes some time, so you are showing a "splash screen" of some kind while it loads. The weird part here is that you are doing this in the initialize()
method of a FXML controller. This should really be done in your Application
class as part of the lifecycle method start()
. The structure should look something like this:
public class App extends Application {
public static void main(String[] args) {
Application.launch();
}
@Override
public void start(Stage primaryStage) throws Exception {
Task<ProjectModel> task = new Task<>() {
@Override
public ProjectModel call() throws Exception {
return loadModel();
}
};
Stage loadingStage = showLoadingStage();
task.setOnSucceeded(e -> {
loadingStage.hide();
showMainStage(primaryStage, task.getValue());
});
new Thread(task).start();
}
private ProjectModel loadModel() throws Exception {
// load model and return it....
}
private Stage showLoadingStage() throws Exception {
Scene scene = new Scene(getClass().getResource("LoadingPage.fxml"));
Stage loadingStage = new Stage();
loadingStage.initStyle(StageStyle.UNDECORATED);
loadingStage.setScene(scene);
loadingStage.show();
return loadingStage ;
}
private void showMainStage(Stage stage, ProjectModel model) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml"));
Parent root = loader.load();
MainController controller = loader.getController();
controller.setModel(model);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
}
with the MainController
being a regular FXML controller with a setModel(...)
method:
public class MainController {
private ProjectModel model ;
// @FXML-annotated fields
@FXML
private void initialize() {
// initialization work that does not require model
}
public void setModel(ProjectModel model) {
this.model = model ;
// initialize any UI that depends on the model, etc.
}
}
Answered By - James_D
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)