Tornadofx tableview同步两个表

时间:2016-12-23 10:35:45

标签: javafx kotlin tornadofx

基本新手问题:

我想同步/绑定两个表。
为了简化示例,我使用了两个单独的表视图。这需要使用片段和范围来完成,我认为这会使问题复杂化,因为我陷入了一个基本问题 行为:在单击表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)
                    }
                }
            }
        }
    }
}

Sync Two Tables

2 个答案:

答案 0 :(得分:2)

首先我们需要能够识别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。

答案 1 :(得分:0)

你有另一个选择是RxJavaFX / RxKotlinFX。我一直writing a companion guide for these libraries just like the TornadoFX one

当您必须处理复杂的事件流并保持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")
    }
}