MBrownG
MBrownG

Reputation: 53

Change ItemList in a ComboBox depending from another ComboBox choice

first I must apologize because english isn't my mother language, but I'll try to be clear in what I'm asking.

I have a set of rows in a tableview, every row has diferent comboboxs per columns. So, the interaction between combobox must be per row. If in the Combobox A1, I select Item 1, in the Combobox A2 the itemlist will be updated. My problem is that every combobox A2, B2, C2, etc. Is being updated according the choice in A1... same thing with B1,C1 combobox. I need to updated just the A2, according to A1. B2 according to B1, etc.

I set the comboboxes by cellfactory, because I have to save the data from behind in a serializable object.

Hope is clear.

Regards.

Upvotes: 2

Views: 1150

Answers (2)

CAD97
CAD97

Reputation: 1313

As @James_D's example no longer runs due to link rot, and I was dealing with this same issue, here's how I figured out to create this effect.

View the full test case here.

I extend the builtin ComboBoxTableCell<S, T> to expose necessary fields. The custom TableCell has a Supplier<S> tableValue = (S) this.getTableRow().getItem(); used to access the applicable Data object. Additionally, I reflectively retrieve and store a reference to the cell's ComboBox. Because it is lazily instantiated in the superclass, I also have to set it via reflection before I can get it. Finally, I have to initialize the ComboBox as well, as it would be in javafx.scene.control.cell.CellUtils.createComboBox, since I'm manually creating it. It is important to expose these, as:

In the column's CellFactory, we finish initializing the ComboBoxCell. We just need to create a new instance of our custom ComboBoxTableCell and then when the comboBox is shown for the first time (e.g. we can be sure that we have a Data object associated with the cell), we bind the ComboBox#itemsProperty to a Bindings.When returning the proper ObservableList for the case.

CellFactory:

    column1.setCellFactory(c -> {
        TransparentComboBoxTableCell<Data, Enum> tcbtc = new TransparentComboBoxTableCell<>();
        tcbtc.comboBox.setOnShown(e -> {
            if (!tcbtc.comboBox.itemsProperty().isBound()) tcbtc.comboBox.itemsProperty().bind(
                    Bindings.when(tcbtc.tableValue.get().base.isEqualTo(BASE.EVEN)).then(evens).otherwise(
                    Bindings.when(tcbtc.tableValue.get().base.isEqualTo(BASE.ODD)).then(odds).otherwise(
                    FXCollections.emptyObservableList()
                    ))
            );
        });
        return tcbtc;
    });

custom ComboBoxTableCell:

public static class TransparentComboBoxTableCell<S, T> extends ComboBoxTableCell<S, T> {
    public TransparentComboBoxTableCell() {
        this(FXCollections.observableArrayList());
    }
    public TransparentComboBoxTableCell(ObservableList<T> startingItems) {
        super(startingItems);
        try {
            Field f = ComboBoxTableCell.class.getDeclaredField("comboBox");
            f.setAccessible(true);
            f.set(this, new ComboBox<>());
            comboBox = (ComboBox<T>) f.get(this);
            // Setup out of javafx.scene.control.cell.CellUtils.createComboBox
            // comboBox.converterProperty().bind(converter);
            comboBox.setMaxWidth(Double.MAX_VALUE);
            comboBox.getSelectionModel().selectedItemProperty().addListener((ov, oldValue, newValue) -> {
                if (this.isEditing()) {
                    this.commitEdit((T) newValue);
                }
            });
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
            Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
            throw new Error("Error extracting 'comboBox' from ComboBoxTableCell", ex);
        }
        tableValue = () -> (S) this.getTableRow().getItem();
    }

    public final ComboBox<T> comboBox;
    public final Supplier<S> tableValue;
}

Upvotes: 0

James_D
James_D

Reputation: 209330

This is pretty much a pain...

From a TableCell, you can observe the TableRow via it's tableRowProperty().

From the TableRow, you can observe the item in the row, via the table row's itemProperty().

And of course, from the item in the row, you can observe any properties defined in your model class, and update a list of items in the combo box accordingly.

The painful part is that any of these value can, and will at some point change. So the things you need to observe keep changing, and you have to manage adding and removing listeners as this happens.

The Bindings.select method is supposed to help manage things like this, but as of JavaFX 8, it prints huge stack traces to the output as warnings whenever it encounters a null value, which it does frequently. So I recommend doing you own listener management until that is fixed. (For some reason, the JavaFX team doesn't seem to consider this a big deal, even though encountering null values in the path defined in a Bindings.select is explicitly supported in the API docs.)

Just to make it slightly more unpleasant, the getTableRow() method in TableCell<S,T> returns a TableRow, instead of the more obvious TableRow<S>. (There may be a reason for this I can't see, but, well...). So your code is additionally littered with casts.

I created an example that works: apologies for it being based on US geography, but I had much of the example already written. I really hope I'm missing something and that there are easier ways to do this: please feel free to suggest something if anyone has better ideas.

On last note: the EasyBind library may provide a simpler way to bind to the properties along an arbitrary path.

Upvotes: 2

Related Questions