Search code examples
kotlintornadofx

Can model in MVC rollback to last valid state?


I am following TornadoFX Guide from here, trying to run the sample wizard: Wizard

I am having the following trouble, see item 3):

1) When I press Cancel the values are rolled back to empty values on next open

2) When I press Finish the values are still in the wizard on next open

3) When I now press Cancel changes are not rolled back to 2) but to 1), i. e. empty fields

How do I get different Cancel behaviour: rolling back the CustomerModel to the last valid state?

This is my updated CustomerWizard.kt:

package com.example.demo.view

import com.example.demo.app.Customer
import com.example.demo.app.CustomerModel
import tornadofx.*
class CustomerWizard : Wizard("Create customer", "Provide customer information") {
    val customer: CustomerModel by inject()

    override val canGoNext = currentPageComplete
    override val canFinish = allPagesComplete

    override fun onCancel() {
        super.onCancel()
        customer.rollback()

    }

    override fun onSave() {
        super.onSave()
        customer.commit()

        println("customer.name=" + customer.name)
        println("customer.type=" + customer.type)
        println("customer.zip=" + customer.zip)
        println("customer.city=" + customer.city)
    }

    init {
        graphic = resources.imageview("/graphics/customer.png")
        add(BasicData::class)
        add(AddressInput::class)
    }
}

class BasicData : View("Basic Data") {
    val customer: CustomerModel by inject()

    override val complete = customer.valid(customer.name)

    override val root = form {
        fieldset(title) {
            field("Type") {
                combobox(customer.type, Customer.Type.values().toList())
            }
            field("Name") {
                textfield(customer.name).required()
            }
        }
    }
}

class AddressInput : View("Address") {
    val customer: CustomerModel by inject()

    override val complete = customer.valid(customer.zip, customer.city)

    override val root = form {
        fieldset(title) {
            field("Zip/City") {
                textfield(customer.zip) {
                    prefColumnCount = 5
                    required()
                }
                textfield(customer.city).required()
            }
        }
    }
}

This is my CustomerModel.kt:

package com.example.demo.app

import tornadofx.*

class CustomerModel(customer: Customer? = null) : ItemViewModel<Customer>(customer) {
    val name = bind(Customer::nameProperty, autocommit = true)
    val zip  = bind(Customer::zipProperty, autocommit = true)
    val city = bind(Customer::cityProperty, autocommit = true)
    val type = bind(Customer::typeProperty, autocommit = true)
}

This is my MainView.kt:

package com.example.demo.view

import com.example.demo.app.Customer
import com.example.demo.app.CustomerModel
import com.example.demo.app.Styles
import javafx.geometry.Pos
import javafx.scene.layout.Priority
import javafx.scene.paint.Color
import tornadofx.*

class MainView : View("Hello TornadoFX") {

    private val myCustomer: Customer? = Customer("test", 12345, "", Customer.Type.Private)
    override val root = drawer {
            item("Generate & sign", expanded = true) {
                button("Add Customer").action {
                    find<CustomerWizard>(Scope(CustomerModel(myCustomer))).openModal()
                }
            }
            item("Verify") {
                borderpane {
                    top = label("TOP") {
                        useMaxWidth = true
                        alignment = Pos.CENTER
                        style {
                            backgroundColor += Color.RED
                        }
                    }

                    bottom = label("BOTTOM") {
                        useMaxWidth = true
                        alignment = Pos.CENTER
                        style {
                            backgroundColor += Color.BLUE
                        }
                    }

                    left = label("LEFT") {
                        useMaxWidth = true
                        useMaxHeight = true
                        style {
                            backgroundColor += Color.GREEN
                        }
                    }

                    right = label("RIGHT") {
                        useMaxWidth = true
                        useMaxHeight = true
                        style {
                            backgroundColor += Color.PURPLE
                        }
                    }

                    center = label("CENTER") {
                        useMaxWidth = true
                        useMaxHeight = true
                        alignment = Pos.CENTER

                        style {
                            backgroundColor += Color.YELLOW
                        }
                    }
                }
            }
            item("Sign next") {
                borderpane {
                    top = label("TOP") {
                        useMaxWidth = true
                        alignment = Pos.CENTER
                        style {
                            backgroundColor += Color.RED
                        }
                    }

                    bottom = label("BOTTOM") {
                        useMaxWidth = true
                        alignment = Pos.CENTER
                        style {
                            backgroundColor += Color.BLUE
                        }
                    }

                    left = label("LEFT") {
                        useMaxWidth = true
                        useMaxHeight = true
                        style {
                            backgroundColor += Color.GREEN
                        }
                    }

                    right = label("RIGHT") {
                        useMaxWidth = true
                        useMaxHeight = true
                        style {
                            backgroundColor += Color.PURPLE
                        }
                    }

                    center = label("CENTER") {
                        useMaxWidth = true
                        useMaxHeight = true
                        alignment = Pos.CENTER

                        style {
                            backgroundColor += Color.YELLOW
                        }
                    }
                }
            }
        }

        //class Link(val name: String, val uri: String)
        //class Person(val name: String, val nick: String)

        // Sample data variables left out (iPhoneUserAgent, TornadoFXScreencastsURI, people and links)
    }

Solution

  • When you call rollback() on an ItemViewModel, it will roll back to the data found in the underlying item, or blank values if you never backed your ItemViewModel with an item.

    In your case, that means you need to assign a Customer to the item property of your CustomerModel. After you have done that, you can call rollback() and the CustomerModel will show the state from the Customer it is backing.

    If you get the same state when you reopen the Wizard, that means that you have reopened the exact same Wizard instance. A Wizard extends View, which makes it a singleton within it's scope, so if you just call find() to locate the Wizard without specifying a scope, the second attempt will get you the same instance as the first.

    You didn't post your Wizard init code, but generally you should create a new scope for the Wizard if you want to avoid this. If you want the wizard to edit a specific customer instance, you should do:

    val model = CustomermerModel()
    model.item = myCustomer
    find<CustomerWizard>(Scope(model).openModal()
    

    It is for this reason normal to let the view model accept an instance in the constructor, which you pass on to the ItemViewModel constructor, so that the item is assigned for you automatically:

    class CustomerModel(customer: Customer? = null) : ItemViewModel<Customer>(customer)
    

    Not that I made sure to allow a no-args constructor for the CustomerModel, to support inject in cases where there is no CustomerModel in scope already. In that case a new CustomerModel (not supporting any customer item) will be created, so this situation requires a no-args constructor.

    With that in place, the wizard init code becomes:

    find<CustomerWizard>(Scope(CustomerModel(myCustomer)).openModal()
    

    Hope this helps.