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?
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!