JavaFX:两个不同类型的可观察列表之间的数据绑定

JavaFX: Data binding between two observable lists of different type

我有一个管理(银行)账户的应用程序。在我的数据模型中,我为账户定义了一个可观察列表:

private final ObservableList<Account> accounts = observableArrayList();

每个帐户都有一个现金流量列表,通过 属性(也是一个可观察列表)实现:

// in Account class:
private final SimpleListProperty<Cashflow> cashflows = new SimpleListProperty<>(observableArrayList());

在我的 UI 中,我有一个包含所有账户的 table,我正在使用现金流列表 属性 来显示每个账户的现金流量,效果很好.

帐户 table 还为 select 或未select 特定帐户提供复选框。在我的帐户 class 中也有一个 属性:

// in Account class:
private final SimpleBooleanProperty selected = new SimpleBooleanProperty();

现在我想向 UI 添加另一个 table,其中包含现金流 ,但仅适用于 selected 帐户,最好我想通过数据绑定来实现。

但我不知道如何实现。我很快打消了以某种方式直接使用账户 class 的现金流量 属性 的想法,因为我什至不知道从哪里开始。

所以我尝试的是为我的数据模型中的现金流定义一个单独的可观察列表:

private final ObservableList<Cashflow> cashflowsOfSelectedAccounts = observableArrayList();

我知道我可以为账户列表定义提取器,当发生变化时通知观察者。因此,例如,我可以将我的帐户列表扩展为:

private final ObservableList<Account> accounts = observableArrayList(
        account -> new Observable[]{
                account.selectedProperty(),
                account.cashflowsProperty().sizeProperty()});

这将触发对以下任何帐户列表中的侦听器的通知:

但现在我不知道如何将它与我的可观察现金流列表结合起来,因为我这里有两种不同的数据类型:账户和现金流。

我能想到的唯一解决方案是向帐户列表添加一个自定义侦听器以对上面列出的所有相关事件做出反应,并手动维护 cashflowsOfSelectedAccounts。

所以这是我的问题: 是否可以通过数据绑定或通过其他一些我不知道的方式将账户列表与 selected 账户的现金流列表同步,这比手动维护现金流列表更优雅帐户列表中的自定义侦听器?

谢谢!

就个人而言,除了 built-in 对 high-level 绑定 API 的支持的简单应用之外,我不会尝试使绑定​​过于复杂。一旦你添加了一些绑定,事情就已经足够复杂了。

选项 1

我建议你做的是:

  1. 创建所选帐户的过滤列表。

  2. 使用选定帐户的筛选列表作为第二个 table 的支持列表。

  3. 由于第二个table只是显示现金流数据,不是完整的账户数据,对于列数据,提供自定义值工厂来访问账户中的现金流数据。

  4. 使第二个 table 成为 TreeTableView 可能有意义,这样它就可以按帐户对现金流进行分组。

这可能是也可能不是您应用程序的有效方法。

备选方案 2

或者,也处理过滤后的账户列表,向过滤后的列表添加列表更改侦听器,当它更改时,更新单独的相关现金流列表的内容,您将其用作现金流的后备列表table.

处理您的用例。

An account is added or removed

只需在帐户列表中添加或删除即可。

A cashflow is added to or removed from an account

当关联的现金流发生变化以触发对现金流列表的更新时,可以触发帐户列表上的提取器和列表侦听器。

an account gets selected or unselected

查看链接过滤列表示例,它基于与过滤列表相结合的提取器。

  • Use a listener to get selected rows (mails) in tableView and add mails to my list of mails

选项 3

或者,您可以更改 UI。例如,为现金流数据和账户数据提供单独的编辑和提交页面,用户按下按钮以提交或放弃更改。提交更新支持数据库。然后,在提交之后,导航回原来的页面,该页面再次从源数据库读取新数据。这通常就是这些东西通常的工作方式,而不是一堆绑定。

我知道这些选项中的 none 是您要问的,其中一些选项可能会起作用,您试图通过不同的绑定类型避免这些选项,但是,这些是我想出的想法.

例子

FWIW,这是替代方案 2 的示例,它依赖于帐户列表、过滤帐户列表、单独的现金流列表以及提取器和侦听器来保持内容同步。

它不会完全是你想要的,但也许你可以调整它或从中学到一些东西。

我会注意到现金流量列表不会将给定的现金流量与给定的帐户联系起来,因此,如果您想这样做,您可能需要添加额外的功能来支持该关联的视觉反馈。

初始状态:

Select只有一个账号:

删除账户并更改给定账户的现金流数据:

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class FlowApp extends Application {

    private final ObservableList<Account> accounts = FXCollections.observableArrayList(
            account -> new Observable[] { account.selectedProperty(), account.getCashflows() }
    );

    private final ObservableList<Account> cashflowAccounts = new FilteredList<>(
            accounts,
            account -> account.selectedProperty().get()
    );

    private final ObservableList<Cashflow> cashflows = FXCollections.observableArrayList(
            cashflow -> new Observable[] { cashflow.amountProperty() }
    );

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

    @Override
    public void start(Stage stage) {
        cashflowAccounts.addListener((ListChangeListener<Account>) c -> updateCashflows());
        initDataStructures();

        final TableView<Account> accountSelectionTableView =
                createAccountSelectionTableView();

        final TableView<Cashflow> cashflowView =
                createCashflowView();

        final Button change = new Button("Change");
        change.setOnAction(e -> changeData(change));

        final Button reset = new Button("Reset");
        reset.setOnAction(e -> { initDataStructures(); change.setDisable(false); });

        final VBox vbox = new VBox(
                10,
                new TitledPane("Accounts", accountSelectionTableView),
                new TitledPane("Cashflows", cashflowView),
                new HBox(10, change, reset)
        );
        vbox.setPadding(new Insets(10));

        stage.setScene(new Scene(vbox));
        stage.show();
    }

    private void changeData(Button change) {
        accounts.get(accounts.size() - 1);

        // Paul dies.
        accounts.removeIf(
                account -> account.firstNameProperty().get()
                        .equals("Paul")
        );

        // Albert.
        Account albert = accounts.stream()
                .filter(
                        account -> account.firstNameProperty().get().equals(
                                "Albert"
                        )
                ).findFirst().orElse(null);

        if (albert == null) {
            return;
        }

        // Albert stops receiving alimony.
        albert.getCashflows().removeIf(
                c -> c.sourceProperty().get().equals(
                                CashflowSource.ALIMONY
                        )
                );

        // Albert's rent increases.
        Cashflow albertsRent = albert.getCashflows().stream()
                .filter(
                        cashflow -> cashflow.sourceProperty().get().equals(
                                CashflowSource.RENT
                        )
                ).findFirst().orElse(null);

        if (albertsRent == null) {
            return;
        }

        albertsRent.amountProperty().set(
                albertsRent.amountProperty().get() + 5
        );

        // only allow one change.
        change.setDisable(true);
    }

    private void initDataStructures() {
        accounts.setAll(
                new Account("Ralph", "Alpher", true, "ralph.alpher@example.com",
                        new Cashflow(CashflowSource.RENT, 10),
                        new Cashflow(CashflowSource.ALIMONY, 5)
                ),
                new Account("Hans", "Bethe", false, "hans.bethe@example.com"),
                new Account("George", "Gammow", true, "george.gammow@example.com",
                        new Cashflow(CashflowSource.SALARY, 3)
                ),
                new Account("Paul", "Dirac", false, "paul.dirac@example.com",
                        new Cashflow(CashflowSource.RENT, 17),
                        new Cashflow(CashflowSource.SALARY, 4)
                ),
                new Account("Albert", "Einstein", true, "albert.einstein@example.com",
                        new Cashflow(CashflowSource.RENT, 2),
                        new Cashflow(CashflowSource.ALIMONY, 1),
                        new Cashflow(CashflowSource.DIVIDENDS, 8)
                )
        );
    }

    private void updateCashflows() {
        cashflows.setAll(
                cashflowAccounts.stream()
                        .flatMap(a ->
                                a.getCashflows().stream()
                        ).toList()
        );
    }

    private TableView<Account> createAccountSelectionTableView() {
        final TableView<Account> selectionTableView = new TableView<>(accounts);
        selectionTableView.setPrefSize(540, 180);

        TableColumn<Account, String> firstName = new TableColumn<>("First Name");
        firstName.setCellValueFactory(cd -> cd.getValue().firstNameProperty());
        selectionTableView.getColumns().add(firstName);

        TableColumn<Account, String> lastName = new TableColumn<>("Last Name");
        lastName.setCellValueFactory(cd -> cd.getValue().lastNameProperty());
        selectionTableView.getColumns().add(lastName);

        TableColumn<Account, Boolean> selected = new TableColumn<>("Selected");
        selected.setCellValueFactory(cd -> cd.getValue().selectedProperty());
        selected.setCellFactory(CheckBoxTableCell.forTableColumn(selected));
        selectionTableView.getColumns().add(selected);

        TableColumn<Account, String> email = new TableColumn<>("Email");
        email.setCellValueFactory(cd -> cd.getValue().emailProperty());
        selectionTableView.getColumns().add(email);

        TableColumn<Account, Integer> numCashflows = new TableColumn<>("Num Cashflows");
        numCashflows.setCellValueFactory(cd -> Bindings.size(cd.getValue().getCashflows()).asObject());
        numCashflows.setStyle("-fx-alignment: baseline-right;");
        selectionTableView.getColumns().add(numCashflows);

        selectionTableView.setEditable(true);
        return selectionTableView;
    }

    private TableView<Cashflow> createCashflowView() {
        TableView<Cashflow> cashflowView = new TableView<>();

        TableColumn<Cashflow, CashflowSource> source = new TableColumn<>("Source");
        source.setCellValueFactory(cd -> cd.getValue().sourceProperty());
        cashflowView.getColumns().add(source);

        TableColumn<Cashflow, Integer> amount = new TableColumn<>("Amount");
        amount.setCellValueFactory(cd -> cd.getValue().amountProperty().asObject());
        amount.setStyle("-fx-alignment: baseline-right;");
        cashflowView.getColumns().add(amount);

        cashflowView.setItems(cashflows);
        cashflowView.setPrefHeight(160);

        return cashflowView;
    }

    private static class Account {
        private final StringProperty firstName;
        private final StringProperty lastName;
        private final BooleanProperty selected;
        private final StringProperty email;
        private final ObservableList<Cashflow> cashflows;

        private Account(String fName, String lName, boolean selected, String email, Cashflow... cashflows) {
            this.firstName = new SimpleStringProperty(fName);
            this.lastName = new SimpleStringProperty(lName);
            this.selected = new SimpleBooleanProperty(selected);
            this.email = new SimpleStringProperty(email);
            this.cashflows = FXCollections.observableArrayList(cashflows);
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public StringProperty lastNameProperty() {
            return lastName;
        }

        public BooleanProperty selectedProperty() {
            return selected;
        }

        public StringProperty emailProperty() {
            return email;
        }

        public ObservableList<Cashflow> getCashflows() {
            return cashflows;
        }
    }

    class Cashflow {
        private final ObjectProperty<CashflowSource> source;
        private final IntegerProperty amount;

        public Cashflow(CashflowSource source, int amount) {
            this.source = new SimpleObjectProperty<>(source);
            this.amount = new SimpleIntegerProperty(amount);
        }

        public ObjectProperty<CashflowSource> sourceProperty() {
            return source;
        }

        public IntegerProperty amountProperty() {
            return amount;
        }
    }

    enum CashflowSource {
        RENT, SALARY, DIVIDENDS, ALIMONY
    }
}