javafxbackground-taskui-threadtreetableview

JavaFX avoiding locking UI during long running operations with mixed UI and not UI elements


In JavaFX in my controller after clicking on Play button starting method 'createTTV()' creating nested TreeTableView(This treetableview will be even larger, but I don't want to paste too much unnecessary code). The method has many UI/not UI elements which are impossible to split for UI/not UI. Is there a way for running such method in background and when everything ready updating UI with not locking whole UI? My code below works, but the UI obviously doesn't respond until the createTTV() method finishes running.

public void buildUi() {
        vBoxForProgressIndicator.setVisible(false);
        fuses_ttv.setRoot(root_FRCs);

        btnPlay.setOnAction(event ->{
            clear_hook();
            vBoxForTableView.setVisible(false);
            vBoxForProgressIndicator.setVisible(true);

            new Thread(()->{
                createTTV();
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        vBoxForProgressIndicator.setVisible(false);
                        vBoxForTableView.setVisible(true);
                    }
                });
            }).start();
        });
}
private void createTTV(){
tdoStructure = model.getDataStructure();
            allFRCs = tdoStructure.getAllFRCsAndCandidates();
            allWires = tdoStructure.getAllWires();
            allParts = tdoStructure.collectAllParts();
            allFusesAndRelays = tdoStructure.getAllFusesAndRelais();

Platform.runLater(()->{
            root_FRCs.getChildren().clear();
            partDescription_ttc.setCellValueFactory(new TreeItemPropertyValueFactory<>("description"));
            partIdentification_ttc.setCellValueFactory(new TreeItemPropertyValueFactory<>("identification"));
            partNo_ttc.setCellValueFactory(new TreeItemPropertyValueFactory<>("partNumber"));
            slot_ttc.setCellValueFactory(new TreeItemPropertyValueFactory<>("slot"));
            pin_ttc.setCellValueFactory(new TreeItemPropertyValueFactory<>("pin"));
            moduleFamilyName_ttc.setCellValueFactory(new TreeItemPropertyValueFactory<>("module"));
            partType_ttc.setCellValueFactory(new TreeItemPropertyValueFactory<>("type"));
            variants_ttc.setCellValueFactory(new TreeItemPropertyValueFactory<>("variants"));
if (frcElement != null) {
                        TreeItem frcTreeItem = new TreeItem<>(new FuseValidationTreeObject(frcElement));
                        if (!frcElement.getFrcSlots().isEmpty()) {
                            for (Map.Entry<String, ArrayList<String>> entrySlot : frcElement.getFrcSlots().entrySet()) {
                                String slot = entrySlot.getKey();
                                TreeItem slotItem = new TreeItem<>(new FuseValidationTreeObject(slot));
                                slotItem.setExpanded(true);
                                frcTreeItem.getChildren().add(slotItem);
                                for (Part fuseOrRelay : allFusesAndRelays) {
                                    LinkedList<String> fuseOrRelayVariants = AppUtils.getAllVariantsForPart(fuseOrRelay);
                                    if (fuseOrRelay.getReferencePartShortName().equals(frcElement.getShortName()) && AppUtils.getSlotIdFromFuseOrRelayName(fuseOrRelay.getShortName()).equals(slot)) {
                                        TreeItem fuseRelayItem = new TreeItem(new FuseValidationTreeObject(fuseOrRelay));
                                        boolean[] fuseOrRelayHasNoWires = {true};
                                        //assign wires to fuse or relay
                                        allWires.forEach(wire -> {
                                            LinkedList<String> wireVariants = AppUtils.getAllVariantsForPart(wire);
                                            if (fuseOrRelay.getReferencePartShortName().equals(wire.getWireToShortName())
                                                    && entrySlot.getValue().contains(wire.getWireToPin()) && fuseOrRelay.getMemberModuleFamily().getDescription().equals(wire.getMemberModuleFamily().getDescription())) {
                                                if (AppUtils.containsWireVariantsAnyOfFuseVariant(wireVariants, fuseOrRelayVariants)) {
                                                    TreeItem wireToItem = new TreeItem<>(new FuseValidationTreeObject(wire));
                                                    wireToItem.setExpanded(true);
                                                    fuseRelayItem.getChildren().add(wireToItem);
                                                    fuseOrRelayHasNoWires[0] = false;

                                                    if (!fuseOrRelayVariants.containsAll(wireVariants)) {
                                                        if (!wiresWithWrongVariants.contains(wire.getWireNumber())) {
                                                            wiresWithWrongVariants.add(wire.getWireNumber());
                                                        }
                                                    }
                                                }
                                            }
                                            if (fuseOrRelay.getReferencePartShortName().equals(wire.getWireFromShortName())
                                                    && entrySlot.getValue().contains(wire.getWireFromPin()) && fuseOrRelay.getMemberModuleFamily().getDescription().equals(wire.getMemberModuleFamily().getDescription())) {
                                                if (AppUtils.containsWireVariantsAnyOfFuseVariant(wireVariants, fuseOrRelayVariants)) {
                                                    TreeItem wireFromItem = new TreeItem<>(new FuseValidationTreeObject(wire));
                                                    wireFromItem.setExpanded(true);
                                                    fuseRelayItem.getChildren().add(wireFromItem);
                                                    fuseOrRelayHasNoWires[0] = false;

                                                    if (!fuseOrRelayVariants.containsAll(wireVariants)) {
                                                        if (!wiresWithWrongVariants.contains(wire.getWireNumber())) {
                                                            wiresWithWrongVariants.add(wire.getWireNumber());
                                                        }
                                                    }
                                                }
                                            }
                                        });
                                        fuseRelayItem.setExpanded(true);
                                        if (fuseOrRelayHasNoWires[0]) {
                                            emptyFusesOrRelays.add(fuseOrRelay.getShortName());
                                        }
                                        slotItem.getChildren().add(fuseRelayItem);
                                    }
                                }
                                if (slotItem.isLeaf()) {
                                    //insert empty slot into emptySlots Map
                                    if (emptySlots.containsKey(frcElement.getShortName())) {
                                        emptySlots.get(frcElement.getShortName()).add(0, slot);
                                    } else {
                                        emptySlots.put(frcElement.getShortName(), new ArrayList<>(Arrays.asList(slot)));
                                    }
                                    //some more conditions
                                    allWires.forEach(wire -> {
                                        //if there is no fuse in the slot but there is still wire to fuse in this slot
                                        if (wire.getWireToShortName().equals(frcElement.getShortName()) && entrySlot.getValue().contains(wire.getWireToPin())) {
                                            wiresWithNoFuseOrRelay.add(wire);
                                            TreeItem emptyWireTo = new TreeItem<>(new FuseValidationTreeObject(wire));
                                            slotItem.getChildren().add(emptyWireTo);
                                        }
                                        if (wire.getWireFromShortName().equals(frcElement.getShortName()) && entrySlot.getValue().contains(wire.getWireFromPin())) {
                                            wiresWithNoFuseOrRelay.add(wire);
                                            TreeItem emptyWireFrom = new TreeItem<>(new FuseValidationTreeObject(wire));
                                            slotItem.getChildren().add(emptyWireFrom);
                                        }
                                    });
                                }
                            }
                        }
                        frcTreeItem.setExpanded(true);
                        root_FRCs.getChildren().add(frcTreeItem);
                    }
}
}

Solution

  • UI elements can be created on another thread if they aren't attached to an active scene.

    For most JavaFX controls (there are a few exceptions such as WebView), the rule to create or modify them only on the JavaFX thread only applies to modifications to a live scene graph that is attached to a Stage which is showing, or for which a snapshot is being made. The controls which can only be created on the JavaFX thread are listed as such in their documentation.

    For example, it is perfectly fine to load an FXML file asynchronously on another thread (e.g. in a Task) and attach it to the scene graph on the JavaFX application thread later.

    You can use runLater to add a created TreeTableView to an active scene.

    Quoting from the Task documentation, section: "A Task Which Modifies the Scene Graph".

    Generally, Tasks should not interact directly with the UI. Doing so creates a tight coupling between a specific Task implementation and a specific part of your UI. However, when you do want to create such a coupling, you must ensure that you use Platform.runLater so that any modifications of the scene graph occur on the FX Application Thread.

    In the example provided, UI elements are created on the task thread, and Platform.runLater() is only used to add the created element to the scene graph.

    final Group group = new Group();
    Task<Void> task = new Task<Void>() {
        @Override protected Void call() throws Exception {
            for (int i=0; i<100; i++) {
                if (isCancelled()) break;
                final Rectangle r = new Rectangle(10, 10);
                r.setX(10 * i);
                Platform.runLater(new Runnable() {
                    @Override public void run() {
                        group.getChildren().add(r);
                    }
                });
            }
            return null;
        }
    };
    

    Alternately, you can create the TreeTableView in a task and add it to the scene onSucceeded

    So you could write this:

    Task<TreeTableView> task = new Task<TreeTableView>() {
        @Override protected TreeTableView call() {
            return createTreeTableView();
        }
    
        private TreeTableView createTreeTableView() {
            // Create and return a new TreeTableView with all new TreeItems initialized.
        }
    };
    
    task.setOnSucceeded(e ->
            myParentNode.getChildren().add(
                    task.getValue()
            )
    );
    

    Or, you can create only the TreeItems in your Task

    If you are still uncomfortable with creating UI elements in another thread, then you can follow James_D's advice from comments:

    The TreeItem instances are not UI components; they only contain data. You can create all the TreeItem instances on the background thread. Then just create the tree table, columns, configure them, and set the root of the tree table on the FX Application Thread. The latter operations are not excessively time-consuming.