skrilmps
skrilmps

Reputation: 695

JavaFX: Populate TableView with an ObservableMap that has a custom class for its values

I have the following ObservableMap that I would like to display in a TableView:

private ObservableMap<String, Shape> myShapes = FXCollections.observableHashMap();

Where Shape is defined as follows:

public class Shape {

private StringProperty areaFormula = new SimpleStringProperty();
private IntegerProperty numSides = new SimpleIntegerProperty();

public Shape(String areaFormula, int numSides)
{
    this.areaFormula.set(areaFormula);
    this.numSides.set(numSides);
}

public String getAreaFormula() { return areaFormula.get(); }

public void setAreaFormula(String areaFormula) { this.areaFormula.set(areaFormula); }

public StringProperty areaFormulaProperty() { return this.areaFormula; }

public int getNumSides() { return numSides.get(); }

public void setNumSides(int sides) { this.numSides.set(sides); }

public IntegerProperty numSidesProperty() { return this.numSides; }
}

I would like the first column of the TableView to be the Map Key (call it key), the second column to be myShapes.get(key).getAreaFormula(), and the third column to be myShapes.get(key).getnumSides(). And I would like the TableView to automatically update when the Map is changed.

It would also be really nice to make the second and third columns of the TableView editable by the user, if possible, with those edits being updated in the Map.

I have already asked stackoverflow about populating a TableView with an ObservableMap such that it will update on changes here: Populating a TableView with a HashMap that will update when HashMap changes . However as you can see in the discussion there, it does not handle a custom class like I have here.

Here is my solution so far:

        ObservableMap<String, Shape> map = FXCollections.observableHashMap();

        ObservableList<String> keys = FXCollections.observableArrayList();

        map.addListener((MapChangeListener.Change<? extends String, ? extends Shape> change) -> {
            boolean removed = change.wasRemoved();
            if (removed != change.wasAdded()) {
                // no put for existing key
                if (removed) {
                    keys.remove(change.getKey());
                } else {
                    keys.add(change.getKey());
                }
            }
        });

        map.put("square", new Shape("L^2", 4));
        map.put("rectangle", new Shape("LW", 4));
        map.put("triangle", new Shape("0.5HB", 3));

        final TableView<String> table = new TableView<>(keys);

        TableColumn<String, String> column1 = new TableColumn<>("Key");

        column1.setCellValueFactory(cd -> Bindings.createStringBinding(() -> cd.getValue()));

        TableColumn<String, String> column2 = new TableColumn<>("Value");

        column2.setCellValueFactory( ... );

        table.getColumns().setAll(column1, column2);

The place where I'm stuck is the second-to-last line column2.setCellValueFactory( ... ). I would like column2 to display the Shape's getAreaFormula() SimpleStringProperty but I don't know how to set up the CellValueFactory to do so.

Thanks for your help.

Upvotes: 1

Views: 2021

Answers (1)

fabian
fabian

Reputation: 82461

It's easiest, if you create a list of immutable key-value pairs instead of simply a list of keys. This drastically reduces the complexity of the listeners you need to use, since the MapChangeListener will always replace an item, if a value is updated and you don't need to add listeners to the Map in the cellValueFactorys:

Key Value pair class

public final class MapEntry<K, V> {

    private final K key;
    private final V value;

    public MapEntry(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public boolean equals(Object obj) {
        // check equality only based on keys
        if (obj instanceof MapEntry) {
            MapEntry<?, ?> other = (MapEntry<?, ?>) obj;
            return Objects.equals(key, other.key);
        } else {
            return false;
        }
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

}

Table creation

ObservableMap<String, Shape> map = FXCollections.observableHashMap();

ObservableList<MapEntry<String, Shape>> entries = FXCollections.observableArrayList();

map.addListener((MapChangeListener.Change<? extends String, ? extends Shape> change) -> {
    boolean removed = change.wasRemoved();
    if (removed != change.wasAdded()) {
        if (removed) {
            // no put for existing key
            // remove pair completely
            entries.remove(new MapEntry<>(change.getKey(), (Shape) null));
        } else {
            // add new entry
            entries.add(new MapEntry<>(change.getKey(), change.getValueAdded()));
        }
    } else {
        // replace existing entry
        MapEntry<String, Shape> entry = new MapEntry<>(change.getKey(), change.getValueAdded());

        int index = entries.indexOf(entry);
        entries.set(index, entry);
    }
});

map.put("one", new Shape("a", 1));
map.put("two", new Shape("b", 2));
map.put("three", new Shape("c", 3));

final TableView<MapEntry<String, Shape>> table = new TableView<>(entries);
TableColumn<MapEntry<String, Shape>, String> column1 = new TableColumn<>("Key");

// display item value (= constant)
column1.setCellValueFactory(cd -> Bindings.createStringBinding(() -> cd.getValue().getKey()));

TableColumn<MapEntry<String, Shape>, String> column2 = new TableColumn<>("formula");
column2.setCellValueFactory(cd -> cd.getValue().getValue().areaFormulaProperty());

TableColumn<MapEntry<String, Shape>, Number> column3 = new TableColumn<>("sides");
column3.setCellValueFactory(cd -> cd.getValue().getValue().numSidesProperty());

table.getColumns().setAll(column1, column2, column3);

Upvotes: 2

Related Questions