Search code examples
javamultithreadingjavafxconcurrencyjavafx-8

JavaFX Concurrency - Using a task, which runs in a thread, but hangs the UI


There are a lot of questions and answer around concurrency, and mine could be similar to others, but for me it's not a duplicate as for some reason I must be missing something and hope to get some advice...

My question is more one where I need a second pair of eyes to point out what I'm doing incorrectly to enable my code to run in a background thread, but also updated the GUI, without freezing it.

Initially, a PDF file is uploaded to the application, using a task in a thread.

This works fine.

A progress bar is displayed, which animates without issue:

uploadFile()

public void uploadFile(File fileToProcess) {

  fileBeingProcessed = fileToProcess;

  Task<Parent> uploadingFileTask = new Task<Parent>() {
    @Override
    public Parent call() {
      try {
        progressBarStackPane.setVisible(true);
        pdfPath = loadPDF(fileBeingProcessed.getAbsolutePath());
        createPDFViewer();
        openDocument();
      } catch (IOException ex) {
        java.util.logging.Logger.getLogger(MainSceneController.class.getName()).log(Level.SEVERE, null, ex);
      }
      return null;
    }
  };

  uploadingFileTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
    @Override
    public void handle(WorkerStateEvent event) {
      fileHasBeenUploaded = true;
      progressBarStackPane.setVisible(false);
      uploadFilePane.setVisible(false);
      tabPane.setVisible(true);

      /* This is where I am getting issue, more so in createThumbnailPanels() */

      setupThumbnailFlowPane();
      createThumbnailPanels();

      /****** ^^^^^^^^^^^^ ******/

    }
  });

  uploadingFileTask.setOnFailed(evt -> {
    uploadingFileTask.getException().printStackTrace(System.err);
    System.err.println(Arrays.toString(uploadingFileTask.getException().getSuppressed()));
  });

  Thread uploadingFileThread = new Thread(uploadingFileTask);
  uploadingFileThread.start();

}

Once the document has been uploaded, it is displayed in a tab which allows the user to view the document.

There is a secondary tab, which, after upload, is disabled, until the completion of another task called createThumbnailPanelsTask;

However, before this task is ran, the FlowPane for the Thumbnail Panels is created. This seems to work without issue, and doesn't appear to be the cause of the GUI hanging (this is clearly a loop in createThumbnailPanelsTask, but for clarity I will show setupThumbnailFlowPane()):

setupThumbnailFlowPane()

public void setupThumbnailFlowPane() {
  stage = model.getStage();
  root = model.getRoot();

  secondaryTabScrollPane.setFitToWidth(true);
  secondaryTabScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);

  /**
  This will be removed from here when refactored but for now it is here,
  I don't think this is anything to do with my issue
  **/

  Set<Node> nodes = secondaryTabScrollPane.lookupAll(".scroll-bar");
  for (final Node node : nodes) {
    if (node instanceof ScrollBar) {
      ScrollBar sb = (ScrollBar) node;
      if (sb.getOrientation() == Orientation.VERTICAL) {
        sb.setUnitIncrement(30.0);
      }
      if (sb.getOrientation() == Orientation.HORIZONTAL) {
        sb.setVisible(false);
      }
    }
  }

  secondaryTab = new FlowPane();
  secondaryTab.setId("secondaryTab");
  secondaryTab.setBackground(new Background(new BackgroundFill(Color.LIGHTSLATEGRAY, new CornerRadii(0), new Insets(0))));
  secondaryTab.prefWidthProperty().bind(stage.widthProperty());
  secondaryTab.prefHeightProperty().bind(stage.heightProperty());
  secondaryTab.setPrefWrapLength(stage.widthProperty().intValue() - 150);
  secondaryTab.setHgap(5);
  secondaryTab.setVgap(30);
  secondaryTab.setBorder(new Border(new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.NONE, CornerRadii.EMPTY, new BorderWidths(8, 10, 20, 10))));
  secondaryTab.setAlignment(Pos.CENTER);
}

Finally, createThumbnailPanels() is called, which is where I believe I am getting the problem.

What is suppose to happen is, after the document has uploaded, the upload file pane is hidden, revealing the Viewer Tab, and also the Secondary Tab.

The secondary tab is disabled at this point, and also has a loading image (a gif) on the left side of it.

The intended behaviour, is that the createThumbnailPanels() task will run in the background, and until it is complete, the tab will remain disabled, however, during this time, the gif image will be rotating, giving the impression there is some loading occurring.

Once the loading has completed, the gif is removed, and the tab is enabled, allowing the user to navigate to it, and see the generated thumbnail panels.

This all works, however, as mentioned, the task is hanging the GUI:

createThumbnailPanels()

public void createThumbnailPanels() {
  Task<Void> createThumbnailPanelsTask = new Task<Void>() {
    @Override
    public Void call() {
      if (model.getIcePdfDoc() != null) {
        numberOfPagesInDocument = model.getIcePdfDoc().getNumberOfPages();
        for (int thumbIndex = 0; thumbIndex < numberOfPagesInDocument; thumbIndex++) {
          ThumbnailPanel tb = new ThumbnailPanel(thumbIndex, main, model);
          Thumbnail tn = new Thumbnail(tb);
          model.setThumbnailAt(tn, thumbIndex);
          eventHandlers.setMouseEventsForThumbnails(tb);

          /*
          I have added this in as I am under the impression that a task runs in a background thread,
          and then to update the GUI, I need to call this:
          */

          Platform.runLater(() -> {
            secondaryTab.getChildren().add(tb);
          });

        }

      }

      return null;
    }
  };

  createThumbnailPanelsTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
    @Override
    public void handle(WorkerStateEvent event) {

      /*
      Further GUI modification run in setOnSucceeded so it runs on main GUI thread(?)
      */
      secondaryTabScrollPane.setContent(secondaryTab);
      secondaryTab.setDisable(false);
      secondaryTab.setGraphic(null);

    }
  });

  createThumbnailPanelsTask.setOnFailed(evt -> {
    createThumbnailPanelsTask.getException().printStackTrace(System.err);
    System.err.println(Arrays.toString(createThumbnailPanelsTask.getException().getSuppressed()));
  });

  Thread createThumbnailPanelsThread = new Thread(createThumbnailPanelsTask);
  createThumbnailPanelsThread.start();
}

Everything, bar the GUI hanging while it creates the panels, works fine.

Once they've been created, the GUI can be controlled again, the loading gif has been removed, the tab is enabled and the user can navigate to it and view the panels.

Clearly, there is something I am missing about concurrency here.

As mentioned, I was under the impression that a Task runs in a background thread, so I'm a little confused by why it doesn't appear to be doing this. Again, clearly something I am missing.

I have read, and read, and read about concurrency, but just can't seem to work out where in my approach I have gone wrong. I am tempted to try using a Service, however, I feel that I am just over complicating things by considering that, and that there is clearly a simply way to do what I want to achieve.

Any help will be greatly appreciated... a push in the right direction, or some clarification on where I have gone wrong in my understanding.

Thanks in advance, no doubt it's something obvious that once sorted will help me avoid this issue in future!

UPDATED CODE

createThumbnailPanels()

 public void createThumbnailPanels() {
        Task<Void> createThumbnailPanelsTask = new Task<Void>() {
            //TODO: Need to check that it's a PDF
            @Override
            public Void call() {
                if (model.getIcePdfDoc() != null) {
                    numberOfPagesInDocument = model.getIcePdfDoc().getNumberOfPages();
                    for (int thumbIndex= 0; thumbIndex< numberOfPagesInDocument; thumbIndex++) {
                        ThumbnailPanel tb = new ThumbnailPanel(thumbIndex, main, model);
                        Thumbnail tn = new Thumbnail(tb);                        
                        eventHandlers.setMouseEventsForThumbnails(tb);       
                        model.setThumbnailAt(tn, thumbIndex);
                        model.setThumbnailPanels(tb);
                    }
                    setThumbnailPanelsToScrollPane();
                }
                return null;
            }
        };

        createThumbnailPanelsTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
            @Override
            public void handle(WorkerStateEvent event) {
//              setThumbnailPanelsToScrollPane();
            }
        });

        createThumbnailPanelsTask.setOnFailed(evt -> {

createThumbnailPanelsTask.getException().printStackTrace(System.err);
            System.err.println(Arrays.toString(createThumbnailPanelsTask.getException().getSuppressed()));
        });

        Thread createThumbnailPanelsThread = new Thread(createThumbnailPanelsTask);
        createThumbnailPanelsThread.start();
    }

setThumbnailPanelsToScrollPane()

 public void setThumbnailPanelsToScrollPane() {
         Task<Void> setThumbnailPanelsToScrollPaneTask = new Task<Void>() {
            //TODO: Need to check that it's a PDF
            @Override
            public Void call() {
                 Platform.runLater(() -> {                     
                    secondaryTab.getChildren().addAll(model.getThumbnailPanels());
                    secondaryTabScrollPane.setContent(main.informationExtractionPanel);
                    secondaryTab.setDisable(false);
                    secondaryTab.setGraphic(null);
                  });   

                return null;
            }
        };


        setThumbnailPanelsToScrollPaneTask.setOnFailed(evt -> {
                setThumbnailPanelsToScrollPaneTask.getException().printStackTrace(System.err);
            System.err.println(Arrays.toString(setThumbnailPanelsToScrollPaneTask.getException().getSuppressed()));
        });

        Thread setThumbnailPanelsToScrollPaneThread = new Thread(setThumbnailPanelsToScrollPaneTask);
        setThumbnailPanelsToScrollPaneThread.start();
    }

FYI: If I call setThumbnailPanelsToScrollPane(); in the setOnSucceeded, it doesn't appear to work.


Solution

  • getChildren().add is running on the JavaFX GUI thread(thats what Platform.runLater does), but its only required to run it in a Platform.runLater if the parent that you add children to is connected to the root of the shown gui, that means you should be able to add children to a parent that is not connected to any root, and add the whole parent to the root at the end of the children addition process, if you're doing Platform.runLater in any asynchronous code it will run on the gui thread in your case it is in your asynchronous for loop adding ThumbnailPanels and if the number of them is large the gui will hang.