3

I have followed this post

Binding hashmap with tableview (JavaFX)

and created a TableView that is populated by data from a HashMap.

The TableView receives its data from a HashMap called map by creating an ObservableList from map.entrySet() and handing that ObservableList off to the TableView's constructor. (Code is below)

However, although it's an ObservableList with SimpleStringPropertys, the TableView does not update when changes are made to the underlying HashMap.

Here is my code:

public class MapTableView extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        try {
            // sample data
            Map<String, String> map = new HashMap<>();
            map.put("one", "One");
            map.put("two", "Two");
            map.put("three", "Three");


            // use fully detailed type for Map.Entry<String, String> 
            TableColumn<Map.Entry<String, String>, String> column1 = new TableColumn<>("Key");
            column1.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Map.Entry<String, String>, String>, ObservableValue<String>>() {

                @Override
                public ObservableValue<String> call(TableColumn.CellDataFeatures<Map.Entry<String, String>, String> p) {
                    // this callback returns property for just one cell, you can't use a loop here
                    // for first column we use key
                    return new SimpleStringProperty(p.getValue().getKey());
                }
            });

            TableColumn<Map.Entry<String, String>, String> column2 = new TableColumn<>("Value");
            column2.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Map.Entry<String, String>, String>, ObservableValue<String>>() {

                @Override
                public ObservableValue<String> call(TableColumn.CellDataFeatures<Map.Entry<String, String>, String> p) {
                    // for second column we use value
                    return new SimpleStringProperty(p.getValue().getValue());
                }
            });

            ObservableList<Map.Entry<String, String>> items = FXCollections.observableArrayList(map.entrySet());
            final TableView<Map.Entry<String,String>> table = new TableView<>(items);

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

            Button changeButton = new Button("Change");
            changeButton.setOnAction((ActionEvent e) -> {
                map.put("two", "2");
                System.out.println(map);
            });
            VBox vBox = new VBox(8);
            vBox.getChildren().addAll(table, changeButton);

            Scene scene = new Scene(vBox, 400, 400);
            stage.setScene(scene);
            stage.show();
        }  catch(Exception e) {
            e.printStackTrace();
        }
    } 

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

}

This is exactly the code from Binding hashmap with tableview (JavaFX) except I have added the following button:

        Button changeButton = new Button("Change");
        changeButton.setOnAction((ActionEvent e) -> {
            map.put("two", "2");
            System.out.println(map);
        });

which I then add to a VBox with the TableView.

When I click the button, the TableView is not updated. However, I can verify that the underlying HashMap has indeed changed because of the System.out.println(map) output. Also when I click on a column header in the TableView to sort the data by one column, the new updated value appears after the table data is re-sorted.

How can I make the table update automatically when the underlying map is changed?

Thank you,

Mark

Community
  • 1
  • 1
skrilmps
  • 625
  • 2
  • 10
  • 29
  • I didn't look into this in detail, but perhaps you want to use an [ObservableMap](https://docs.oracle.com/javase/8/javafx/api/javafx/collections/FXCollections.html#observableMap-java.util.Map-). – jewelsea May 11 '16 at 20:03
  • Maybe this answer http://stackoverflow.com/a/21339428/2855515 – brian May 11 '16 at 20:31

2 Answers2

6

Use an ObservableMap with a listener that keeps the TableView items and the keys of the map the same and use the cellValueFactory with Bindings.valueAt, e.g.:

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

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

map.addListener((MapChangeListener.Change<? extends String, ? extends String> 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("one", "One");
map.put("two", "Two");
map.put("three", "Three");

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

TableColumn<String, String> column1 = new TableColumn<>("Key");
// display item value (= constant)
column1.setCellValueFactory(cd -> Bindings.createStringBinding(() -> cd.getValue()));

TableColumn<String, String> column2 = new TableColumn<>("Value");
column2.setCellValueFactory(cd -> Bindings.valueAt(map, cd.getValue()));

table.getColumns().setAll(column1, column2);
fabian
  • 80,457
  • 12
  • 86
  • 114
  • Thank you. Works beautifully! Interesting approach, making it a table of Strings and having a separate ObservableList for the keys. I wasn't aware of MapChangeListener and Bindings, either. Very helpful, thanks! – skrilmps May 11 '16 at 20:59
  • I'm trying to understand the Bindings inside the lambda expressions. Suppose I now want to have the same functionality for an `ObservableMap` where `Shade` is a custom class with a `StringProperty` member called `Description` (i.e., `getDescription()` returns a `String` and `descriptionProperty()` returns a `StringProperty`) that I would like to display in column2. How would I change the line `column2.setCellValueFactory(cd -> Bindings.valueAt(map, cd.getValue()));`? Thank you for any insight. – skrilmps May 11 '16 at 21:57
  • Unfortunately this kind of 2-level binding is not provided by the bindings API, so you have to programm it on your own by adding a listener to the appropriate property of the map value (and removing it from the property of the old value, when the value is changed to the different one). It's not *that* complicated, but the comments section is not the appropriate place to give a more detailed description. – fabian May 12 '16 at 01:47
1

This will update your tableView.

changeButton.setOnAction(e -> {
                map.put("two", "2");
                table.getColumns().setAll(column1, column2);
                System.out.println(map);
});
Hoek
  • 11
  • 2
  • Thanks. That does work. However, what if I add something to the table? For instance, if I change the line `map.put("two","2")` to `map.put("four","Four")`. In this case the table isn't updated to reflect the change. Why not? – skrilmps May 11 '16 at 20:30
  • Nevermind, I see why now. So I could change the line `table.getColumns().setAll(column1, column2)` to `table.setItems(FXCollections.observableArrayList(map.entrySet()));` But this seems inelegant because I am recreating an ArrayList and repopulating the table with it. Furthermore, doing so will "destroy" any sort order the user may have already established by clicking on a column header. Is there another way? – skrilmps May 11 '16 at 20:38
  • For versions >= 8u60 you should probably replace the `getColumns().getColumns().setAll(column1, column2)` with `refresh()` – fabian May 11 '16 at 20:57