JavaFX TableColumn с ObservableList в качестве значения

В моем TableView одним из значений TableColumn является ObservableList. Для упрощения он содержит только StringProperty значений. В моей фабрике значений ячеек для столбца я объединяю значения вместе, используя класс Bindings.

Результат, которого я хочу, должен быть StringExpression, который обновляется всякий раз, когда изменяется ObservableList.

Проблема, с которой я сталкиваюсь, заключается в том, что при изменении любого из значений списка сгенерированное StringExpression обновляется правильно, хотя, когда я добавляю или удаляю значения в списке, он не будет обновляться.

Моей первой мыслью было добавить к строке ListChangeListener, но это кажется нецелесообразным, поскольку в таблице могут быть десятки или сотни строк.

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);
    }
}

Я знаю, "почему" это не работает. StringExpression связано со значениями StringProperty списка, а не с самим списком. Поэтому всякий раз, когда я добавляю/удаляю значения, они не обновляются.

Однако, когда я удаляю значения (удаляя первый индекс), это заставляет StringExpression переоценивать себя (я полагаю?), и в этом случае появляются новые добавленные/удаленные значения. (Нажмите Удалить, а затем Изменить)

Простое добавление значений не поможет. (Нажмите «Добавить», затем «Изменить»)

Итак, как в обратном вызове фабрики значений ячеек я работаю со своим кодом, чтобы прослушивать ObservableList и возвращать ObservableValue, который может объединять список и отображать, когда происходят изменения, включая добавление/удаление?


person hotforfeature    schedule 12.11.2013    source источник


Ответы (1)


Что ж, я понял это, но я хотел бы узнать, практично ли мое решение.

Я еще недостаточно знаком с привязками и свойствами JavaFX, чтобы знать, подходит ли это решение или оно вызовет проблемы в будущем.

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;
        }
    });  

Версия TL;DR:

Я создал свой собственный StringExpression, используя Bindings.createStringBinding(), и добавил ObservableList, а также любые его значения, которые были объектами Observable, в качестве зависимостей к StringExpression.

Я полагаю, что это приведет к тому, что всякий раз, когда одно из значений списка изменится, StringExpression получит уведомление об обновлении. Дополнительно, когда сам список изменяется (добавляется/удаляется), StringExpression уведомляется об обновлении.

Дайте мне знать, если я ошибаюсь в приведенном выше утверждении!

person hotforfeature    schedule 12.11.2013