Home > OS >  Java how to close a stage from Service Object
Java how to close a stage from Service Object

Time:05-18

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?

CodePudding user response:

There are two main threading rules in JavaFX (similar to most other UI toolkits).

  1. You must not perform operations on the UI from a background thread. This includes creating, showing, or hiding windows.
  2. 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.
    }
}
  • Related