JavaFX TableColumn with ObservableList as value

hotforfeature picture hotforfeature · Nov 12, 2013 · Viewed 7.3k times · Source

In my TableView, one of the TableColumn values is an ObservableList. To simplify things, it only contains StringProperty values. In my cell value factory for the column, I am concatenating the values together using the Bindings class.

The result I'm wanting should be a StringExpression that updates whenever the ObservableList changes.

The problem I'm having is that when any of the values of the list change, the generated StringExpression updates correctly, though when I add or remove values to the list, it will not update.

My first thought was to add a ListChangeListener to the row, but this seems impractical since there could be dozens or hundreds of rows to the table.

import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;

public class TestFx extends Application {

    @Override
    public void start(Stage primaryStage) {        
        //Note, I never really see this "row" value directly, they're loaded 
        //in from a database, and there can be dozens or hundreds of 
        //rows for the table, so I don't if adding a listener to each one
        //is the best idea.
        final ObservableList<StringProperty> row = FXCollections.observableArrayList();
        row.add(new SimpleStringProperty("test"));
        row.add(new SimpleStringProperty("one"));
        row.add(new SimpleStringProperty("two"));


        TableView<ObservableList<StringProperty>> table = new TableView();
        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        TableColumn<ObservableList<StringProperty>, String> concatColumns = new TableColumn<>("Concatentated Values");

        concatColumns.setCellValueFactory(new Callback<CellDataFeatures<ObservableList<StringProperty>, String>, ObservableValue<String>>() {
            @Override
            public ObservableValue<String> call(CellDataFeatures<ObservableList<StringProperty>, String> p) {
                ObservableList<StringProperty> row = p.getValue();

                List toConcat = new ArrayList();
                for (int i = 0; i < row.size(); i++) {
                    toConcat.add(row.get(i));
                    if (i+1 < row.size()) {
                        toConcat.add(", ");
                    }
                }
                return Bindings.concat(toConcat.toArray());
            }
        });        

        table.getColumns().add(concatColumns);
        table.getItems().add(row);


        BorderPane root = new BorderPane();        
        HBox buttons = new HBox();        

        Button add = new Button();
        add.setText("Add Item");
        add.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                row.add(new SimpleStringProperty("added"));
            }
        });
        Button remove = new Button();
        remove.setText("Remove Item");
        remove.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                if (row.size() > 0) {
                    row.remove(0);
                }
            }
        });
        Button change = new Button();
        change.setText("Change Item");
        change.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                if (row.size() > 0) {
                    row.get(0).set("Changed");
                }
            }
        });
        buttons.getChildren().addAll(add, remove, change);

        root.setCenter(table);
        root.setBottom(buttons);

        Scene scene = new Scene(root, 300, 250);        
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

I know "why" it isn't working. The StringExpression is linked to the StringProperty values of the list and not the list itself. Therefore, whenever I add/remove values, it doesn't update.

However, when I remove values (removing the first index), this causes the StringExpression to re-evaluate itself (I guess?), in which case the new added/removed values appear. (Press Remove and then Change)

Simply adding values won't do the trick. (Press Add then Change)

So how, in the cell value factory callback, do I work my code to listen to the ObservableList and return an ObservableValue that can concatenate the list and display when changes occur, including adding/removing?

Answer

hotforfeature picture hotforfeature · Nov 12, 2013

Well, I figured it out, but I'd like input on whether or not my solution is practical.

I'm not familiar enough yet with JavaFX bindings and properties to know whether or not this solution is fine or if it would cause problems down the road.

concatColumns.setCellValueFactory(new Callback<CellDataFeatures<ObservableList, String>, ObservableValue<String>>() {
        @Override
        public ObservableValue<String> call(CellDataFeatures<ObservableList, String> p) {
            final ObservableList row = p.getValue();

            List<Observable> dependencies = new ArrayList<>();
            for (Object value : row) {
                if (value instanceof Observable) {
                    dependencies.add((Observable)value);
                }
            }
            dependencies.add(row);

            StringExpression se = Bindings.createStringBinding(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < row.size(); i++) {
                        //Check for Property objects and append the value
                        if (row.get(i) instanceof Property) {
                            sb.append(((Property)row.get(i)).getValue());
                        }
                        else {
                            sb.append(row.get(i));
                        }

                        if (i+1 < row.size()) {
                            sb.append(", ");
                        }
                    }
                    return sb.toString();
                }
            }, dependencies.toArray(new Observable[dependencies.size()]));
            return se;
        }
    });  

The TL;DR version:

I created my own StringExpression using Bindings.createStringBinding() and I added the ObservableList as well as any of its values that were Observable objects as the dependencies to the StringExpression.

What I believe this will do is that whenever one of the list's values changes, the StringExpression is notified to update. Additional, when the list itself changes (add/remove), the StringExpression is notified to update.

Let me know if I'm wrong about the above statement!