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

Контроллер Person (фиктивные данные):

 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, поэтому включите 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) } } 

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

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

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

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

 override val root = vbox { } 

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

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

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

Другой вариант, который у вас есть, – 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") } }