Sebastian
Sebastian

Reputation: 1722

Bind dirty properties of different view-models

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:

enter image description here

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

Answers (1)

Edvin Syse
Edvin Syse

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

Related Questions