Reputation: 1722
I have a tornadoFX application following the MVVM pattern with the model:
data class Person (
val name: String,
val cars: List<Car>
)
data class Car (
val brand: String,
val model: String
)
The application defines the following view:
There is a list-view that lists all persons. Besides the listView is a details-view with a text-field for the person´s name and a table-view for the person´s cars.
A double click on a car entry in the table opens a dialog, in which one can edit the car´s properties.
I want, that if I open the car-details and edit an entry, the changes will be reflected in the table-view. Since i can´t alter the Car-model (which is an immutable type) by adding fx-properties, i came up with the following view-model:
class PersonViewModel(): ItemViewModel<Person> {
val name = bind(Person::name)
val cars = bind { SimpleListProperty<CarViewModel>(item?.cars?.map{CarViewModel(it)}?.observable()) }
override fun onCommit {
// create new person based on ViewModel and store it
}
}
class CarViewModel(item: Car): ItemViewModel<Car> {
val brand = bind(Car::name)
val model = bind(Car::model)
init {
this.item = item
}
}
This way, if double-click on a car-entry in the table-view and open the car-detail-view, an update on the car will be directly reflected in the table-view.
My Problem here is, that I can´t find a way to bind the dirty properties of all my CarViewModels in the table to the PersonViewModel. So if I change a car, the PersonViewModel is not marked as dirty.
Is there a way to bind the dirty-properties of PersonViewModel and CarViewModel? (And also rebind them, if another person is selected).
Or is there even a better way to define my view-models?
Upvotes: 0
Views: 1138
Reputation: 7297
I've made a change to the framework to allow ViewModel bindings towards lists to observe ListChange events. This enables you to trigger the dirty state of a list property by altering the list somehow. Merely changing a property inside an item in the list will not trigger it, so in the following example I just get the index of the Car before committing, and reassigning the Car to the same index. This will trigger a ListChange event, which the framework now listens for.
The important action happens in the Car dialog save function:
button("Save").action {
val index = person.cars.indexOf(car.item)
car.commit {
person.cars[index] = car.item
close()
}
}
The index of the car is recorded before the values are committed (to make sure that equals/hashCode matches the same entry), then the newly committed item is inserted in the same index, thus triggering a change event on the list.
Here is a complete example, using mutable JavaFX properties, since they are the idiomatic JavaFX way. You can pretty easily adapt it to using immutable items, or use wrappers.
class Person(name: String, cars: List<Car>) {
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val carsProperty = SimpleListProperty<Car>(FXCollections.observableArrayList(cars))
var cars by carsProperty
}
class PersonModel : ItemViewModel<Person>() {
val name = bind(Person::nameProperty)
val cars: SimpleListProperty<Car> = bind(Person::carsProperty)
}
class Car(brand: String, model: String) {
val brandProperty = SimpleStringProperty(brand)
var brand by brandProperty
val modelProperty = SimpleStringProperty(model)
var model by modelProperty
}
class CarModel(car: Car? = null) : ItemViewModel<Car>(car) {
val brand = bind(Car::brandProperty)
val model = bind(Car::modelProperty)
}
class DataController : Controller() {
val people = FXCollections.observableArrayList<Person>()
init {
people.add(
Person("Person 1", listOf(Car("BMW", "M3"), Car("Ford", "Fiesta")))
)
}
}
class PersonMainView : View() {
val data: DataController by inject()
val selectedPerson: PersonModel by inject()
override val root = borderpane {
center {
tableview(data.people) {
column("Name", Person::nameProperty)
bindSelected(selectedPerson)
}
}
right(PersonEditor::class)
}
}
class PersonEditor : View() {
val person: PersonModel by inject()
val selectedCar : CarModel by inject()
override val root = form {
fieldset {
field("Name") {
textfield(person.name).required()
}
field("Cars") {
tableview(person.cars) {
column("Brand", Car::brandProperty)
column("Model", Car::modelProperty)
bindSelected(selectedCar)
onUserSelect(2) {
find<CarEditor>().openModal()
}
}
}
button("Save") {
enableWhen(person.dirty)
action {
person.commit()
}
}
}
}
}
class CarEditor : View() {
val car: CarModel by inject()
val person: PersonModel by inject()
override val root = form {
fieldset {
field("Brand") {
textfield(car.brand).required()
}
field("Model") {
textfield(car.model).required()
}
button("Save").action {
val index = person.cars.indexOf(car.item)
car.commit {
person.cars[index] = car.item
close()
}
}
}
}
}
The feature is available in TornadoFX 1.7.17-SNAPSHOT.
Upvotes: 6