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 niteesh    schedule 23.12.2016    source источник
comment
На всякий случай, вам просто нужен одинаковый выбор в обеих таблицах при нажатии кнопки синхронизации?   -  person Edvin Syse    schedule 23.12.2016
comment
да. Также будет обновлена ​​соответствующая модель.   -  person niteesh    schedule 23.12.2016
comment
В ПОРЯДКЕ. Краткое примечание: вы внедрили одну и ту же модель в оба представления и вызываете bindSelected в обоих. Обратите внимание, что вызов bindSelected должен вызываться один раз, а не при нажатии на действие. Просто убедитесь, что выбор таблицы обновит модель - не при вызове bindSelection, а при выборе. Не совсем уверен в вашем фактическом использовании, но позвольте мне немного его переписать.   -  person Edvin Syse    schedule 23.12.2016
comment
И последнее: вы действительно хотите, чтобы изменение происходило только при нажатии кнопки или оно должно происходить автоматически?   -  person Edvin Syse    schedule 23.12.2016
comment
Что ж, вариант использования: у меня есть две таблицы с одинаковым макетом, но с разными данными. Я хочу сравнить эти данные (строка за строкой) и выбрать правильную (нажав кнопку синхронизации. Примечание. Кнопка синхронизации синхронизирует соответствующую выбранную строку). Поскольку макет такой же, я создал один Person и PersonModel для сопоставления с данными.   -  person niteesh    schedule 23.12.2016
comment
Кроме того, у вас разные люди в двух таблицах, поэтому выбор никогда не будет совпадать в другой таблице. Вы хотите добавить выбранного человека к другому объекту синхронизации?   -  person Edvin Syse    schedule 23.12.2016
comment
Да, нажав кнопку "Синхронизировать", я хочу переместить выбранную строку в соответствующую строку другой таблицы.   -  person niteesh    schedule 23.12.2016
comment
ОК, и под соответствующей строкой вы подразумеваете тот же rowIndex, если в целевой строке меньше элементов, чем индекс строки, тогда просто добавьте как последний элемент?   -  person Edvin Syse    schedule 23.12.2016
comment
Да, та же строка Index. Для простоты размер данных одинаков на обоих   -  person niteesh    schedule 23.12.2016
comment
Хорошо, скоро ответ :)   -  person Edvin Syse    schedule 23.12.2016


Ответы (2)


Сначала нам нужно иметь возможность идентифицировать Person, поэтому включите equals / hashCode в объект Person:

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

Событие синхронизации уведомляется при возникновении события. Сначала он проверяет, есть ли в элементах для табличного представления этого человека, или добавляет его по правильному индексу, если нет. Настоящее приложение должно проверять, находится ли индекс в границах списка элементов.

Затем он проверяет, выбран ли этот человек уже, и если нет, он сделает выбор, а также запросит фокус на этой таблице. Проверка важна, так как исходная таблица также не запрашивает фокус и не выполняет (избыточный) выбор.

Как уже отмечалось, хорошей оптимизацией будет отправка списка элементов в качестве параметра, чтобы не дублировать код PersonList.

Также обратите внимание на использование нового синтаксиса построителя:

override val root = vbox {
}

Это намного удобнее, чем первое объявление корневого узла как VBox() и создание остальной части пользовательского интерфейса в блоке init.

Надеюсь, это то, что вы ищете :)

Важно: для этого решения требуется TornadoFX 1.5.9. Он будет выпущен сегодня :) Вы можете создать версию 1.5.9-SNAPSHOT, если хотите.

person Edvin Syse    schedule 23.12.2016
comment
Я не могу собрать 1.5.9-SNAPSHOT. Я пробовал это в моем gradle.build: dependencies {compile group: no.tornado, name: tornadofx, version: 1.5.9-SNAPSHOT, change: true} - person niteesh; 23.12.2016
comment
Его нет ни в одном публичном репо, поэтому вам нужно будет создать его самостоятельно, выполнив git clone https://github.com/edvin/tornadofx && mvn install. Я выпущу позже сегодня, если ты не хочешь с этим возиться :) - person Edvin Syse; 23.12.2016
comment
Здорово!! Ваше хорошо объясненное решение работает так, как ожидалось :) Мне просто нужно было сделать одну крошечную модификацию, когда мы подписываемся; т.е. добавление items.removeAt (event.index) для if (! items.contains (event.person)), чтобы гарантировать переопределяющее поведение. Благодаря тонну !! - person niteesh; 23.12.2016

Другой вариант - RxJavaFX / RxKotlinFX. Я писал сопутствующее руководство для этих библиотек, как и для TornadoFX.

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

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")
    }
}
person tmn    schedule 23.12.2016
comment
Отлично, только что проверил RxKotlinFx на github. Должен сказать, что это интригует, так как самая первая демонстрация похожа на то, что я ищу. Итак, я добавил необходимые зависимости и запустил приведенный выше код; Но привязки не работают. Когда я нажимаю кнопку «СИНХРОНИЗАЦИЯ», ничего не происходит. Нет ошибки. Что я пропустил? - person niteesh; 23.12.2016
comment
Да, я пропустил некоторые из ваших требований, и мы обсуждали это на канале Kotlin #tornadofx в Slack. Если мне представится возможность посмотреть ваше дело позже, я это сделаю. Но если вы хотите взглянуть на RxJavaFX / RxKotlinFX, вы можете воспользоваться бесплатным электронная книга, в которой подробно рассматривается RxJava с JavaFX / TornadoFX. Вы можете найти его полезным, если вы создаете сложные настольные приложения. - person tmn; 26.12.2016