【问题标题】:Tornadofx tableview sync two tablesTornadofx tableview 同步两个表
【发布时间】:2016-12-23 23:12:53
【问题描述】:

基本新手问题:

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

【问题讨论】:

  • 只是为了确定,当您点击同步按钮时,您只是想在两个表中选择相同的选项?
  • 是的。相应的模型也会得到更新
  • 好的。快速说明:您在两个视图中都注入了相同的模型,并在两者上调用 bindSelected。请注意,bindSelected 调用应该调用一次,而不是在您单击操作时调用。所做的只是确保表选择将更新模型 - 不是在您调用 bindSelection 时,而是在选择发生时。不完全确定您的实际用例,但让我尝试重写一下。
  • 最后一件事 - 您真的希望更改仅在您单击按钮时发生,还是应该自动发生?
  • 好吧,用例是,我有两个布局相同但数据不同的表。我想比较该数据(逐行)并选择正确的数据(通过单击同步按钮。注意同步按钮同步相应的选定行)。由于布局相同,我创建了一个 Person 和 PersonModel 来映射数据

标签: javafx kotlin tornadofx


【解决方案1】:

首先我们需要能够识别一个人,所以在 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.5.9-SNAPSHOT。我在我的 gradle.build: dependencies { compile group: "no.tornado", name: "tornadofx", version: "1.5.9-SNAPSHOT", changed: true } 中试过这个
  • 它不在任何公共仓库中,因此您需要通过 git clone https://github.com/edvin/tornadofx &amp;&amp; mvn install 自己构建它。如果您不想打扰,我将在今天晚些时候发布:)
  • 太棒了!您解释清楚的解决方案按预期工作:) 当我们订阅时,我只需要做一个微小的修改;即为 if (!items.contains(event.person)) 添加 items.removeAt(event.index) 以确保覆盖行为。非常感谢!
【解决方案2】:

您的另一个选择是 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")
    }
}

【讨论】:

  • 很好,刚刚检查了 github 上的 RxKotlinFx。有趣的是,我必须说第一个演示本身与我正在寻找的相似。所以我添加了所需的依赖项并运行了上面的代码;但是绑定不起作用。当我单击“同步”按钮时没有任何反应。没有错误。我错过了什么?
  • 是的,我错过了您的一些要求,我们在Kotlin #tornadofx channel in Slack 中讨论过。如果我以后有机会看你的案子,我会的。但是如果你想给 RxJavaFX/RxKotlinFX 看一看,你可以check out the free eBook 详细教授 RxJava 和 JavaFX/TornadoFX。如果您正在构建复杂的桌面应用程序,您可能会发现它很有用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-25
  • 2015-12-21
相关资源
最近更新 更多