Search code examples
javafxfxmlloaderitemsobservablelist

FXML Dynamically initialize ObservableList for ComboBox and TableView


I am trying to make a custom builder proposed in Dan Nicks's comment to this question.
The idea is to set combo's data before constructing it.
combo.fxml:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>

<ComboBox  fx:id="combo1" items="${itemLoader.items}"  prefWidth="150.0" 
   xmlns:fx="http://javafx.com/fxml/1">
</ComboBox>

The class that provides the data:

public class ComboLoader {

    public ObservableList<Item> items;

    public ComboLoader() {

        items = FXCollections.observableArrayList(createItems());
    }

    private List<Item> createItems() {
            return IntStream.rangeClosed(0, 5)
                    .mapToObj(i -> "Item "+i)
                    .map(Item::new)
                    .collect(Collectors.toList());
        }

    public ObservableList<Item> getItems(){

        return items;
    }

    public static class Item {

        private final StringProperty name = new SimpleStringProperty();

        public Item(String name) {
            this.name.set(name);
        }

        public final StringProperty nameProperty() {
            return name;
        }
     
    }
}

And the test:

public class ComboTest extends Application {

    @Override
    public void start(Stage primaryStage) throws IOException {

        primaryStage.setTitle("Populate combo from custom builder");

        Group group = new Group();

        GridPane grid = new GridPane();
        grid.setPadding(new Insets(25, 25, 25, 25));
        group.getChildren().add(grid);

        FXMLLoader loader = new FXMLLoader();
        ComboBox combo = loader.load(getClass().getResource("combo.fxml"));
        loader.getNamespace().put("itemLoader", new ComboLoader());
        grid.add(combo, 0, 0);

        Scene scene = new Scene(group, 450, 175);

         primaryStage.setScene(scene);
         primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

No errors produced, but combo is not populated.
What is missing ?


BTW: a similar solution for TableView works fine:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.cell.PropertyValueFactory?>

 <TableView items="${itemLoader.items}"   xmlns:fx="http://javafx.com/fxml/1">
     <columns>
         <TableColumn text="Item">
             <cellValueFactory><PropertyValueFactory property="name" /></cellValueFactory>
         </TableColumn>
     </columns>
 </TableView>

Solution

  • Starting from a nit-pick, I did some experiments on how to actually implement what I tried to outline in my comments to c0der's answer.

    The basic idea is to follow the same approach for the listCell as for the data, that is configure both content and appearance via namespace (my learn item of the day). The ingredients:

    • a generic custom listCell configurable with a function to convert an item to text
    • a generic "cellFactory factory" class for providing a cellFactory creating that cell

    The cell/factory:

    public class ListCellFactory<T> {
        
        private Function<T, String> textProvider;
    
        public ListCellFactory(Function<T, String> provider) {
            this.textProvider = provider;
        }
    
        public Callback<ListView<T>, ListCell<T>> getCellFactory() {
            return cc -> new CListCell<>(textProvider);
        }
        
        public ListCell<T> getButtonCell() {
            return getCellFactory().call(null);
        }
        
        public static class CListCell<T> extends ListCell<T> {
            
            private Function<T, String> converter;
    
            public CListCell(Function<T, String> converter) {
                this.converter = Objects.requireNonNull(converter, "converter must not be null");
            }
    
            @Override
            protected void updateItem(T item, boolean empty) {
                super.updateItem(item, empty);
                if (empty) {
                    setText(null);
                } else {
                    setText(converter.apply(item));
                }
            }
            
        }
    
    }
    

    The fxml to create and configure the combo:

    <?xml version="1.0" encoding="UTF-8"?>
    <?import javafx.scene.control.ComboBox?>
    
    <ComboBox  fx:id="combo1" items="${itemLoader.items}"  
        cellFactory="${cellFactoryProvider.cellFactory}" 
        buttonCell = "${cellFactoryProvider.buttonCell}"
        prefWidth="150.0" 
       xmlns:fx="http://javafx.com/fxml/1">
    </ComboBox>
    

    An example to use it:

    public class LocaleLoaderApp extends Application {
    
        private ComboBox<Locale> loadCombo(Object itemLoader, Function<Locale, String> extractor) throws IOException {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("comboloader.fxml"));
            loader.getNamespace().put("itemLoader", itemLoader);
            loader.getNamespace().put("cellFactoryProvider", new ListCellFactory<Locale>(extractor));
            ComboBox<Locale> combo = loader.load();
            return combo;
        }
        
        @Override
        public void start(Stage primaryStage) throws IOException {
    
            primaryStage.setTitle("Populate combo from custom builder");
    
            Group group = new Group();
            GridPane grid = new GridPane();
            grid.setPadding(new Insets(25, 25, 25, 25));
            group.getChildren().add(grid);
            LocaleProvider provider = new LocaleProvider();
            grid.add(loadCombo(provider, Locale::getDisplayName), 0, 0);
            grid.add(loadCombo(provider, Locale::getLanguage), 1, 0);
            Scene scene = new Scene(group, 450, 175);
    
            primaryStage.setScene(scene);
            primaryStage.show();
        }
        
        public static class LocaleProvider {
            ObservableList<Locale> locales = FXCollections.observableArrayList(Locale.getAvailableLocales());
            
            public ObservableList<Locale> getItems() {
                return locales;
            }
        }
        
    
        public static void main(String[] args) {
            launch(args);
        }
    }