javajavafxobservablelist

JavaFX using ObversableList.getItems().addAll() cannot be used multiple times


I'm trying to populate multiple MenuButton's in javaFX with an arrayList of checkMenuItems. To add the check menu items im doing:

myMenuButton.getItems().addAll(ChecKMenuItemList);

this is what my code looks like

class Scratch {


    private void updateClassList(ArrayList<Class> classArrayList) {
        ArrayList<String> classCodeList = new ArrayList<>();
        ArrayList<CheckMenuItem> checkMenuItemList = new ArrayList<>();
        ArrayList<CheckMenuItem> checkMenuItemList2 = new ArrayList<>();
        ArrayList<String> classNameList = new ArrayList<>();

        //Create Arrays of class elements
        for(Class aClass : classArrayList){
            checkMenuItemList.add(new CheckMenuItem(aClass.getClassCode()));
        }

        //Clear Class Lists
        addStudentsToClassClassListView.getItems().clear();
        assignClassesToTeachersClassListView.getItems().clear();

        //Populate dropdown lists

        addStudentSelectClassesMenuButton.getItems().setAll(checkMenuItemList);
        addTeacherSelectClasses.getItems().setAll(checkMenuItemList);
    }
}

This function is called from another function after the user inputs a json file that is parsed for data.

The problem im running into is when i try to use .getItems().addAll() it only works once, in my code if you comment one of the two lines the other one will work and vice versa, its strange since they work on their own but not together

Since both of them work on their own I'm not sure what the issue would be thats causing it not too update. There is no error or exception simply nothing happens. After both of the lines executes and before the function completes while debugging it says the both menubuttons have 6 items but when you click on the menu button nothing happens


Solution

  • The issue is NOT:

    ObservableList.getItems().addAll() cannot be used multiple times

    it definitely can be used multiple times.

    ObservableList<Integer> list = FXCollections.observableArrayList(1, 2, 3);
    list.addAll(4, 5, 6);
    list.addAll(7, 8, 9);
    
    System.out.println(list);
    

    Will output as expected:

    [1, 2, 3, 4, 5, 6, 7, 8, 9]
    

    However, you need to use APIs correctly in context.

    Items in the scene graph can only be in one position at a time. A CheckMenuItem is not a node, but it is probably backed by nodes and thus acts like a node, so I wouldn't add a single instance to more than one menu at a time.

    Instead, create another CheckMenuItem instance, with the same data, and add that. Bidirectional binding can be used to ensure that if one menu item is checked, the other menu item's state is updated to reflect that, and vice versa.

    See the scene javadoc:

    A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in the children list of a Parent or as the clip of a Node. See the Node class for more details on these restrictions.

    Also, the node javadoc:

    If a program adds a child node to a Parent (including Group, Region, etc) and that node is already a child of a different Parent or the root of a Scene, the node is automatically (and silently) removed from its former parent.

    It would appear that CheckMenuItem acts the same way. It would probably be better if the Menu documentation stated that items can only appear in one menu at a time.

    Examples to demonstrate failure and fixes

    In this example, two menus are created and the same items are added to both menus. On execution, only one of the menus (the last one to which the items were added) will contain the added items.

    The execution warns you, in the system error console, that there is a problem.

    Dec 13, 2022 4:00:27 PM javafx.scene.control.Menu$6 onChanged
    WARNING: Adding MenuItem Check 1 that has already been added to Menu 1
    Dec 13, 2022 4:00:27 PM javafx.scene.control.Menu$6 onChanged
    WARNING: Adding MenuItem Check 2 that has already been added to Menu 1
    

    Broken code

    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.stage.Stage;
    
    public class MenuItemApp extends Application {
        @Override
        public void start(Stage stage) throws Exception {
            MenuItem[] menuItems = createCheckMenuItems();
    
            Menu menu1 = new Menu("Menu 1");
            menu1.getItems().addAll(menuItems);
    
            Menu menu2 = new Menu("Menu 2");
            menu2.getItems().addAll(menuItems);
    
            MenuBar menuBar = new MenuBar(menu1, menu2);
    
            Scene scene = new Scene(menuBar);
            stage.setScene(scene);
            stage.show();
        }
    
        private MenuItem[] createCheckMenuItems() {
            return new MenuItem[] {
                new CheckMenuItem("Check 1"),
                new CheckMenuItem("Check 2")
            };
        }
    
        public static void main(String[] args) {
            Application.launch();
        }
    }
    

    We can fix this by just creating new check menu items for each menu.

    Menu menu1 = new Menu("Menu 1");
    menu1.getItems().addAll(createCheckMenuItems());
    
    Menu menu2 = new Menu("Menu 2");
    menu2.getItems().addAll(createCheckMenuItems());
    

    But now the check menu items aren't in sync, if you change one, the other one doesn't automatically change. If you also want that behavior, you can use an MVC approach with a shared binding.

    Fixed code with bidirectional binding to model properties

    import javafx.application.Application;
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.stage.Stage;
    
    public class MenuItemApp extends Application {
        class Model {
            private final BooleanProperty boolean1 = new SimpleBooleanProperty();
            private final BooleanProperty boolean2 = new SimpleBooleanProperty();
    
            public BooleanProperty boolean1Property() {
                return boolean1;
            }
    
            public BooleanProperty boolean2Property() {
                return boolean2;
            }
        }
    
        @Override
        public void start(Stage stage) throws Exception {
            Model model = new Model();
    
            Menu menu1 = new Menu("Menu 1");
            menu1.getItems().addAll(createCheckMenuItems(model));
    
            Menu menu2 = new Menu("Menu 2");
            menu2.getItems().addAll(createCheckMenuItems(model));
    
            MenuBar menuBar = new MenuBar(menu1, menu2);
    
            Scene scene = new Scene(menuBar);
            stage.setScene(scene);
            stage.show();
        }
    
        private MenuItem[] createCheckMenuItems(Model model) {
            return new MenuItem[] {
                createCheckMenuItem(1, model.boolean1Property()),
                createCheckMenuItem(2, model.boolean2Property()),
            };
        }
    
        private CheckMenuItem createCheckMenuItem(int n, BooleanProperty modelProperty) {
            CheckMenuItem checkMenuItem = new CheckMenuItem("Check " + n);
            checkMenuItem.selectedProperty().bindBidirectional(modelProperty);
    
            return checkMenuItem;
        }
    
        public static void main(String[] args) {
            Application.launch();
        }
    }