edvin / tornadofx

Lightweight JavaFX Framework for Kotlin
Apache License 2.0
3.67k stars 269 forks source link

How to react when a property change is committed? #1232

Closed SKeeneCode closed 4 years ago

SKeeneCode commented 4 years ago

I'm trying to create some application settings for my users. In my example I have a grid I want the user to be able to toggle visibility via a settings window. I have a checkbox binded to a boolean property in a view model:

val showGrid = bind { SimpleBooleanProperty(item?.drawGrid, "", config.boolean(CONFIG_SHOW_GRID) ?: false) }

There is a save button which is enabled when the model is dirty and it calls commit().

Elsewhere I'm trying to react to these changes. If I do:

grid.visibleWhen(settings.showGrid)

This runs as soon as the value is changed, regardless of whether the change has been committed to the underlying model or not.

How can I react to a change only when the model is committed?

SchweinchenFuntik commented 4 years ago
class ModelVView : ItemViewModel {
 override fun onCommit() {}
}

there is also a change state property (dirty), it will become false after a commit

SKeeneCode commented 4 years ago

Thanks @SchweinchenFuntik - I already use the above to save config settings, but it means if I want to react to the change outside the ItemViewModel, (such as in a view) I would need to do something like:

class ModelVView : ItemViewModel {
    val showGrid = bind(Model::booleanProperty)
    val showGridWasComitted = property(false)
    override fun onCommit() {
    showGridWasComitted.value = !showGridWasComitted
 }
}

and then bind to showGridWasComitted in the view. I really want to avoid the need to create another property for every single setting.

I got a little close with using the dirtyStateFor:

    grid().visibleWhen {
        BooleanBinding.booleanExpression(settings.showGrid)
                .and(settings.dirtyStateFor(PageSettingsViewModel::showGrid).not())
    }

But this doesn't quite behave as I would like.

Reading again into the docs, it looks like I need a way to bind to the backing property created by the ItemViewModel when bind is used, not the facade property.

From the docs:

The ViewModel keeps track of which actual property belongs to which facade, and when you call commit the values from the facade are flushed into the actual backing property. On the flip side, when you call rollback the exact opposite happens: The actual property value is flushed into the facade.

There seems to be a method to get the backing value, but not the backing property, is there a way to access it? I would love to avoid the solution above to double the size of my view model...

SchweinchenFuntik commented 4 years ago

Do you want to bind the show Grid property of the model and not the ViewModel? Or when the values of the model and ViewModel are different?

val model = NamedModel()
val bindValueDirty = booleanBinding(model.value, model.itemProperty) { value != model.item.value }

class Named() {
    val nameProperty = SimpleStringProperty()
    var name by nameProperty

    val valueProperty = SimpleIntegerProperty()
    var value by valueProperty

}

class NamedModel : ItemViewModel<Named>() {
    val name = bind(Named::nameProperty)
    val value = bind(Named::valueProperty)
}
SKeeneCode commented 4 years ago

I had actually forgot about binding to just the model and not the view model. I've had it drilled into my head that only the view model should communicate in anyway with the model and the view should bind only to the view model. Was I mistaken?

I am using the config example from the guide here. My view model declares its properties in a similar fashion:

val showGrid = bind { SimpleBooleanProperty(item?.drawGrid, "", config.boolean(CONFIG_SHOW_GRID) ?: false) }

The problem is item?.drawGrid is a plain Boolean meaning I can't bind directly to it. I need to declare the SimpleBooleanProperty in a place where I have access to config so I can set its initial value. I suppose I could pass it down as a parameter? I wonder if there is an easier way :/

SchweinchenFuntik commented 4 years ago

I` had actually forgot about binding to just the model and not the view model. I've had it drilled into my head that only the view model should communicate in anyway with the model and the view should bind only to the view model. Was I mistaken?

this is a public API - you can use it, of course you can make a property inside the ViewModel, but this error seems to me just the same, since you are making a special case public. But do not abuse such bindings

The problem is item?.drawGrid is a plain Boolean meaning I can't bind directly to it. I need to declare the SimpleBooleanProperty in a place where I have access to config so I can set its initial value. I suppose I could pass it down as a parameter? I wonder if there is an easier way :/

it doesn’t matter, you get attached to the itemProperty property and compare the values already, and it doesn’t matter whether they are stored in the JavaFX properties or kotlin properties

SchweinchenFuntik commented 4 years ago
class Named() {
    val nameProperty = SimpleStringProperty()
    var name by nameProperty

    val valueProperty = SimpleIntegerProperty()
    var value by valueProperty

    var showGrid: Boolean = false

}

class NamedModel : ItemViewModel<Named>() {
    val name = bind(Named::nameProperty)
    val value = bind(Named::valueProperty)
    val showGrid = bind(Named::showGrid)
}

class TestView() : View("My View") {

    val model = NamedModel()

    val bind = booleanBinding(model.value, model.itemProperty) { value != model.item.value }
    val bindShowGrid = booleanBinding(model.showGrid, model.itemProperty) { value != model.item.showGrid }

    override val root = vbox { }
}
SchweinchenFuntik commented 4 years ago

bindShowGrid - will be updated only when model.showGrid is updated or when item changes (property of model.itemProperty changes)

SKeeneCode commented 4 years ago

Nice explanation and example, thanks for once again taking time to help me out. I learn a little more with every question :)