Tornadofx tableview同步两个表

基本的新手问题:

我想同步/绑定两个表。
为了保持简单的例子,我使用了两个独立的表格视图。 这需要使用碎片和范围来完成,我认为这会使问题复杂化,因为我陷入了一个基本问题。
行为:点击表1的同步按钮,我希望表1选择的数据覆盖相应的表2数据 。 反之亦然

人模型:

class Person(firstName: String = "", lastName: String = "") { val firstNameProperty = SimpleStringProperty(firstName) var firstName by firstNameProperty val lastNameProperty = SimpleStringProperty(lastName) var lastName by lastNameProperty } class PersonModel : ItemViewModel<Person>() { val firstName = bind { item?.firstNameProperty } val lastName = bind { item?.lastNameProperty } } 

人员控制器(虚拟数据):

 class PersonController : Controller(){ val persons = FXCollections.observableArrayList<Person>() val newPersons = FXCollections.observableArrayList<Person>() init { persons += Person("Dead", "Stark") persons += Person("Tyrion", "Lannister") persons += Person("Arya", "Stark") persons += Person("Daenerys", "Targaryen") newPersons += Person("Ned", "Stark") newPersons += Person("Tyrion", "Janitor") newPersons += Person("Arya", "Stark") newPersons += Person("Taenerys", "Dargaryen") } } 

人员列表视图:

 class PersonList : View() { val ctrl: PersonController by inject() val model : PersonModel by inject() var personTable : TableView<Person> by singleAssign() override val root = VBox() init { with(root) { tableview(ctrl.persons) { personTable = this column("First Name", Person::firstNameProperty) column("Last Name", Person::lastNameProperty) columnResizePolicy = SmartResize.POLICY } hbox { button("Sync") { setOnAction { personTable.bindSelected(model) //model.itemProperty.bind(personTable.selectionModel.selectedItemProperty()) } } } } } 

另一个人列表视图:

 class AnotherPersonList : View() { val model : PersonModel by inject() val ctrl: PersonController by inject() override val root = VBox() var newPersonTable : TableView<Person> by singleAssign() init { with(root) { tableview(ctrl.newPersons) { newPersonTable = this column("First Name", Person::firstNameProperty) column("Last Name", Person::lastNameProperty) columnResizePolicy = SmartResize.POLICY } hbox { button("Sync") { setOnAction { newPersonTable.bindSelected(model) } } } } } } 

同步两个表

首先,我们需要能够识别一个Person,所以在Person对象中包含equals / hashCode:

 class Person(firstName: String = "", lastName: String = "") { val firstNameProperty = SimpleStringProperty(firstName) var firstName by firstNameProperty val lastNameProperty = SimpleStringProperty(lastName) var lastName by lastNameProperty override fun equals(other: Any?): Boolean { if (this === other) return true if (other?.javaClass != javaClass) return false other as Person if (firstName != other.firstName) return false if (lastName != other.lastName) return false return true } override fun hashCode(): Int { var result = firstName.hashCode() result = 31 * result + lastName.hashCode() return result } } 

我们希望在点击“同步”按钮时触发一个事件,所以我们定义一个可以同时包含选定人员和行索引的事件:

 class SyncPersonEvent(val person: Person, val index: Int) : FXEvent() 

您不能注入相同的PersonModel实例,并在两个视图中使用bindSelected ,因为这将覆盖对方。 而且,只要选择发生变化, bindSelected就会作出反应,而不是在你调用bindSelected本身的时候,所以它不属于按钮处理器。 我们将为每个视图使用一个单独的模型,并绑定到选择。 那么当按钮处理程序运行时,我们可以很容易地知道选择了哪个人,并且我们不需要保留TableView的一个实例。 我们还将使用新的根构建器语法来清理所有内容。 这是PersonList视图:

 class PersonList : View() { val ctrl: PersonController by inject() val selectedPerson = PersonModel() override val root = vbox { tableview(ctrl.persons) { column("First Name", Person::firstNameProperty) column("Last Name", Person::lastNameProperty) columnResizePolicy = SmartResize.POLICY bindSelected(selectedPerson) subscribe<SyncPersonEvent> { event -> if (!items.contains(event.person)) { items.add(event.index, event.person) } if (selectedItem != event.person) { requestFocus() selectionModel.select(event.person) } } } hbox { button("Sync") { setOnAction { selectedPerson.item?.apply { fire(SyncPersonEvent(this, ctrl.persons.indexOf(this))) } } } } } } 

AnotherPersonList视图是相同的,除了ctrl.newPersons的引用,而不是两个地方的ctrl.persons 。 (您可以使用相同的片段并作为参数在列表中发送,因此您不需要复制所有这些代码)。

同步按钮现在触发我们的事件,只要在点击按钮时选择一个人:

 selectedPerson.item?.apply { fire(SyncPersonEvent(this, ctrl.persons.indexOf(this))) } 

在TableView内部,我们现在订阅SyncPersonEvent

 subscribe<SyncPersonEvent> { event -> if (!items.contains(event.person)) { items.add(event.index, event.person) } if (selectedItem != event.person) { requestFocus() selectionModel.select(event.person) } } 

事件触发时通知同步事件。 它首先检查tableview的项目是否包含这个人,如果没有,则将其添加到正确的索引处。 真正的应用程序应检查索引是否在项目列表的范围内。

然后检查这个人是否已经被选中,如果没有,那么将进行选择并且还要求重点到这个表中。 检查很重要,这样源表格不会请求焦点或执行(冗余)选择。

如上所述,一个好的优化是将项目列表作为参数发送,这样就不需要复制PersonList代码。

另请注意使用新的构建器语法:

 override val root = vbox { } 

这比首先将根节点声明为VBox()以及在init块中构建UI的其余部分时要简单得多。

希望这是你在找什么:)

重要提示:此解决方案需要TornadoFX 1.5.9。 它将在今天发布:)如果你喜欢,你可以在1.5.9-SNAPSHOT的同时进行构建。

另一个选项是RxJavaFX / RxKotlinFX。 我一直在为TornadoFX写这些库的配套指南 。

当您必须处理复杂的事件流并保持UI组件同步时,响应式编程对于这些情况才是有效的。

 package org.nield.demo.app import javafx.beans.property.SimpleStringProperty import javafx.collections.FXCollections import javafx.collections.ObservableList import rx.javafx.kt.actionEvents import rx.javafx.kt.addTo import rx.javafx.kt.onChangedObservable import rx.javafx.sources.CompositeObservable import rx.lang.kotlin.toObservable import tornadofx.* class MyApp: App(MainView::class) class MainView : View() { val personList: PersonList by inject() val anotherPersonList: AnotherPersonList by inject() override val root = hbox { this += personList this += anotherPersonList } } class PersonList : View() { val ctrl: PersonController by inject() override val root = vbox { val table = tableview(ctrl.persons) { column("First Name", Person::firstNameProperty) column("Last Name", Person::lastNameProperty) //broadcast selections selectionModel.selectedIndices.onChangedObservable() .addTo(ctrl.selectedLeft) columnResizePolicy = SmartResize.POLICY } button("SYNC").actionEvents() .flatMap { ctrl.selectedRight.toObservable() .take(1) .flatMap { it.toObservable() } }.subscribe { table.selectionModel.select(it) } } } class AnotherPersonList : View() { val ctrl: PersonController by inject() override val root = vbox { val table = tableview(ctrl.newPersons) { column("First Name", Person::firstNameProperty) column("Last Name", Person::lastNameProperty) //broadcast selections selectionModel.selectedIndices.onChangedObservable() .addTo(ctrl.selectedRight) columnResizePolicy = SmartResize.POLICY } button("SYNC").actionEvents() .flatMap { ctrl.selectedLeft.toObservable() .take(1) .flatMap { it.toObservable() } }.subscribe { table.selectionModel.select(it) } } } class Person(firstName: String = "", lastName: String = "") { val firstNameProperty = SimpleStringProperty(firstName) var firstName by firstNameProperty val lastNameProperty = SimpleStringProperty(lastName) var lastName by lastNameProperty } class PersonController : Controller(){ val selectedLeft = CompositeObservable<ObservableList<Int>> { it.replay(1).autoConnect().apply { subscribe() } } val selectedRight = CompositeObservable<ObservableList<Int>> { it.replay(1).autoConnect().apply { subscribe() } } val persons = FXCollections.observableArrayList<Person>() val newPersons = FXCollections.observableArrayList<Person>() init { persons += Person("Dead", "Stark") persons += Person("Tyrion", "Lannister") persons += Person("Arya", "Stark") persons += Person("Daenerys", "Targaryen") newPersons += Person("Ned", "Stark") newPersons += Person("Tyrion", "Janitor") newPersons += Person("Arya", "Stark") newPersons += Person("Taenerys", "Dargaryen") } }