c0der
c0der

Reputation: 18812

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>

Upvotes: 0

Views: 3045

Answers (2)

c0der
c0der

Reputation: 18812

Edited following comments by kleopatra:
Loading combo.fxml given in the question with Strings can be done using the following loader:

//load observable list with strings
public class ComboStringLoader {

    private final ObservableList<String> items;

    public ComboStringLoader() {
        items = FXCollections.observableArrayList(createStrings());
    }

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

    //name of this method corresponds to itemLoader.items in xml.
    //if xml name was itemLoader.a this method should have been getA(). 
    public ObservableList<String> getItems(){
        return items;
    }
}

Loading combo with Item instances in a similar fashion simply means that Item#toString for the text in the combo:

//load observable list with Item#toString
public class ComboObjectLoader1 {

    public ObservableList<Item> items;

    public ComboObjectLoader1() {
        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;
    }
}

Where Item is defined as:

class Item {

    private final StringProperty name = new SimpleStringProperty();

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

    public final StringProperty nameProperty() {
        return name;
    }

    @Override
    public String toString() {
        return name.getValue();
    }
}

A better approach is to load combo with custom ListCell<item>:

//load observable list with custom ListCell
public class ComboObjectLoader2 {

    private final ObservableList<ItemListCell> items;

    public ComboObjectLoader2() {
        items =FXCollections.observableArrayList (createCells());
    }

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

    public ObservableList<ItemListCell> getItems(){
        return items;
    }
}

class ItemListCell extends ListCell<Item> {

    private final Label text;

    public ItemListCell(Item item) {
        text = new Label(item.nameProperty().get());
        setGraphic(new Pane(text));
    }

    @Override
    public void updateItem(Item item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            text.setText(item.nameProperty().get());
        }
    }
}

The last but not least alternative is to set a custom ListCell<Item> as a cell factory for the combo.
This can be done by adding a controller to the fxml file:
combo2.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" 
      xmlns="http://javafx.com/javafx/10.0.1" fx:controller="test.ComboObjectLoaderAndController">
</ComboBox>

Where ComboObjectLoaderAndController is both a loader and a controller:

//loads observable list with Items and serves as controller to set cell factory
public class ComboObjectLoaderAndController {

    public ObservableList<Item> items;
    @FXML ComboBox<Item> combo1;

    public ComboObjectLoaderAndController() {
        items = FXCollections.observableArrayList(createItems());
    }

    @FXML
    public void initialize() {
        combo1.setCellFactory(l->new ItemListCell());
    }

    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;
    }

    class ItemListCell extends  ListCell<Item>{

        @Override
        public void updateItem(Item item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                setText(item.nameProperty().get());
            }
        }
    }
}

Edit:
following kleopatra's answer I added a generic custom ListCell

public class ObjectListCell<T> extends ListCell<T> {

    Function<T,String> textSupplier;

    public  ObjectListCell(Function<T,String> textSupplier) {
        this.textSupplier = textSupplier;
    }

    public Callback<ListView<T>, ListCell<T>> getFactory() {
        return cc -> new ObjectListCell<>(textSupplier);
    }

    public ListCell<T> getButtonCell() {
        return getFactory().call(null);
    }

    @Override
    public void updateItem(T t, boolean empty) {
        super.updateItem(t, empty);
        if (t== null || empty) {
            setText(null);
            setGraphic(null);
        } else {
            setText(textSupplier.apply(t));
        }
    }
}

The factory is set in the fxml file:
combo3.fxml:

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

<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}"  cellFactory="${cellFactoryProvider.factory}" 
   buttonCell = "${cellFactoryProvider.buttonCell}"
   prefWidth="150.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/10.0.1">
</ComboBox>

A test class:

public class ComboTest extends Application {

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

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

        //Combo of Strings
        FXMLLoader loader = new FXMLLoader(getClass().getResource("combo.fxml"));
        loader.getNamespace().put("itemLoader", new ComboStringLoader());
        ComboBox<String>stringCombo = loader.load();

        //Combo of Item
        loader = new FXMLLoader(getClass().getResource("combo.fxml"));
        loader.getNamespace().put("itemLoader", new ComboObjectLoader1());
        ComboBox<Item>objectsCombo1 = loader.load();

        //Combo of custom ListCell
        loader = new FXMLLoader(getClass().getResource("combo.fxml"));
        loader.getNamespace().put("itemLoader", new ComboObjectLoader2());
        ComboBox<ItemListCell>objectsCombo2 = loader.load();

        //Combo of Item with custom ListCell factory
        loader = new FXMLLoader(getClass().getResource("combo2.fxml"));
        loader.getNamespace().put("itemLoader", new ComboObjectLoaderAndController());
        ComboBox<Item>objectsCombo3 = loader.load();

        //Combo of Item with custom ListCell factory. Factory is set in FXML
        loader = new FXMLLoader(getClass().getResource("combo3.fxml"));
        loader.getNamespace().put("itemLoader", new ComboObjectLoader1());
        loader.getNamespace().put("cellFactoryProvider", new ObjectListCell<Item>(t -> t.nameProperty().get()));
        ComboBox<Item>objectsCombo4 = loader.load();

        HBox pane = new HBox(25, stringCombo, objectsCombo1,objectsCombo2, objectsCombo3, objectsCombo4);
        pane.setPadding(new Insets(25, 25, 25, 25));

        Scene scene = new Scene(pane, 550, 175);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Upvotes: 0

kleopatra
kleopatra

Reputation: 51535

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);
    }
}

Upvotes: 1

Related Questions