11. Editing Models and Validation

Editing Models and Validation

TornadoFX doesn't force any particular architectural pattern on you as a developer, and it works equally well with both MVC, MVP, and their derivatives.
To help with implementing these patterns TornadoFX provides a tool called ViewModel that helps cleanly separate your UI and business logic, giving you features like rollback/commit and dirty state checking. These patterns are hard or cumbersome to implement manually, so it is advised to leverage the ViewModel and ItemViewModel when it is needed.
Typically you will use the ItemViewModel when you are creating a facade in front of a single object, and a ViewModel for more complex situations.

A Typical Use Case

Let's say we have a certain domain type Person. We will allow its two properties to be nullable so that they can be entered later by the user.
import tornadofx.*
class Person(name: String? = null, title: String? = null) {
val nameProperty = SimpleStringProperty(this, "name", name)
var name by nameProperty
val titleProperty = SimpleStringProperty(this, "title", title)
var title by titleProperty
}
(Notice the import, you need to import at least tornadofx.getValue and tornadofx.setValue for the by delegate to work)
Consider a Master/Detail view where you have a TableView displaying a list of people, and a Form where the currently selected person's information can be edited. Before we get into the ViewModel, we will create a version of this View without using the ViewModel.
Figure 11.1
The code below is our first attempt to build this, and it has some problems that we will address.
import javafx.scene.control.TableView
import javafx.scene.control.TextField
import javafx.scene.layout.BorderPane
import tornadofx.*
class Person(name: String? = null, title: String? = null) {
val nameProperty = SimpleStringProperty(this, "name", name)
var name by nameProperty
val titleProperty = SimpleStringProperty(this, "title", title)
var title by titleProperty
}
class PersonEditor : View("Person Editor") {
override val root = BorderPane()
var nameField : TextField by singleAssign()
var titleField : TextField by singleAssign()
var personTable : TableView<Person> by singleAssign()
// Some fake data for our table
val persons = listOf(Person("John", "Manager"), Person("Jay", "Worker bee")).observable()
var prevSelection: Person? = null
init {
with(root) {
// TableView showing a list of people
center {
tableview(persons) {
personTable = this
column("Name", Person::nameProperty)
column("Title", Person::titleProperty)
// Edit the currently selected person
selectionModel.selectedItemProperty().onChange {
editPerson(it)
prevSelection = it
}
}
}
right {
form {
fieldset("Edit person") {
field("Name") {
textfield() {
nameField = this
}
}
field("Title") {
textfield() {
titleField = this
}
}
button("Save").action {
save()
}
}
}
}
}
}
private fun editPerson(person: Person?) {
if (person != null) {
prevSelection?.apply {
nameProperty.unbindBidirectional(nameField.textProperty())
titleProperty.unbindBidirectional(titleField.textProperty())
}
nameField.bind(person.nameProperty)
titleField.bind(person.titleProperty)
prevSelection = person
}
}
private fun save() {
// Extract the selected person from the tableView
val person = personTable.selectedItem!!
// A real application would persist the person here
println("Saving ${person.name} / ${person.title}")
}
}
We define a View consisting of a TableView in the center of a BorderPane and a Form on the right side. We define some properties for the form fields and the table itself so we can reference them later.
While we build the table, we attach a listener to the selected item so we can call the editPerson function when the table selection changes. The editPerson function binds the properties of the selected person to the text fields in the form.

Problems with our initial attempt

At first glance it might look OK, but when we dig deeper there are several issues.

Manual binding

Every time the selection in the table changes, we have to unbind/rebind the data for the form fields manually. Apart from the added code and logic, there is another huge problem with this: the data is updated for every change in the text fields, and the changes will even be reflected in the table. While this might look cool and is technically correct, it presents one big problem: what if the user does not want to save the changes? We have no way of rolling back. So to prevent this, we would have to skip the binding altogether and manually extract the values from the text fields, then create a new Person object on save. In fact, this is a pattern found in many applications and expected by most users. Implementing a "Reset" button for this form would mean managing variables with the initial values and again assigning those values manually to the text fields.

Tight Coupling

Another issue is when it is time to save the edited person, the save function has to extract the selected item from the table again. For that to happen the save function has to know about the TableView. Alternatively it would have to know about the text fields like the editPerson function does, and manually extract the values to reconstruct a Person object.

Introducing ViewModel

The ViewModel is a mediator between the TableView and the Form. It acts as a middleman between the data in the text fields and the data in the actual Person object. As you will see, the code is much shorter and easier to reason about. Please note that this is not the recommended syntax, this merely serves as an explanation of the concepts.
class PersonEditor : View("Person Editor") {
override val root = BorderPane()
val persons = listOf(Person("John", "Manager"), Person("Jay", "Worker bee")).observable()
val model = PersonModel(Person())
init {
with(root) {
center {
tableview(persons) {
column("Name", Person::nameProperty)
column("Title", Person::titleProperty)
// Update the person inside the view model on selection change
model.rebindOnChange(this) { selectedPerson ->
item = selectedPerson ?: Person()
}
}
}
right {
form {
fieldset("Edit person") {
field("Name") {
textfield(model.name)
}
field("Title") {
textfield(model.title)
}
button("Save") {
enableWhen(model.dirty)
action {
save()
}
}
button("Reset").action {
model.rollback()
}
}
}
}
}
}
private fun save() {
// Flush changes from the text fields into the model
model.commit()
// The edited person is contained in the model
val person = model.item
// A real application would persist the person here
println("Saving ${person.name} / ${person.title}")
}
}
class PersonModel(person: Person) : ItemViewModel<Person>(person) {
val name = bind(Person::nameProperty)
val title = bind(Person::titleProperty)
}
This looks a lot better, but what exactly is going on here? We have introduced a subclass of ItemViewModel called PersonModel. The model holds a Person object and has properties for the name and title fields. We will discuss the model further after we have looked at the rest of the client code.
Note that we hold no reference to the TableView or the text fields. Apart from a lot less code, the first big change is the way we update the Person inside the model:
model.rebindOnChange(this) { selectedPerson ->
item = selectedPerson ?: Person()
}
The rebindOnChange() function takes the TableView as an argument and a function that will be called when the selection changes. This works with ListView,TreeView, TreeTableView, and any other ObservableValue as well. This function is called on the model and has the selectedPerson as its single argument. We assign the selected person to the item property of the model, or a new Person if the selection was empty/null. That way we ensure that there is always data for the model to present. Since we're calling this function from inside the TreeView builder, there is a function to simplify this even further: the 3 lines above can be replaced with simply bindSelected(model).
When we create the TextFields, we bind the model properties directly to it since most Node builders accept an ObservableValue to bind to.
field("Name") {
textfield(model.name)
}
Even when the selection changes, the model properties persist but the values for the properties are updated. We totally avoid the manual binding from our previous attempt.
Another big change in this version is that the data in the table does not update when we type into the text fields. This is because the model has exposed a copy of the properties from the person object and does not write back into the actual person object before we call model.commit(). This is exactly what we do in the save function. Once commit has been called, the data in the facade is flushed back into our person object and the table will now reflect our changes.

Rollback

Since the model holds a reference to the actual Person object, we can reset the text fields to reflect the actual data in our Person object. We could add a reset button like this:
button("Reset").action {
model.rollback()
}
When the button is pressed, any changes are discarded and the text fields show the actual Person object values again.

The PersonModel

class PersonModel(person: Person) : ItemViewModel<Person>(person) {
val name = bind(Person::nameProperty)
val title = bind(Person::titleProperty)
}
We never explained how the PersonModel works yet. It can hold a Person object, and it has defined two strange-looking properties called name and title via the bind delegate. Yeah it looks weird, but there is a very good reason for it. The Person::nameProperty parameter for the bind function is a KProperty (in other words, it is used to get the actual Person.nameProperty from any Person instance). This way, whenever the Person object is changed (using rebindOnChange() or model.item = ..., the name and title properties can be changed accordingly to reflect the properties of the new Person object.
When we bind a text field to the name property of the model, only the copy is updated when you type into the text field. 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.
The reason the actual property is wrapped in a function is that this makes it possible to change the person variable and then extract the property from that new person. You can read more about this below (rebinding).

Dirty Checking

The model has a Property called dirty. This is a BooleanBinding which you can observe to enable or disable certain features. For example, we could easily disable the save button until there are actual changes. The updated save button would look like this:
button("Save") {
enableWhen(model.dirty)
action {
save()
}
}
There is also a plain val called isDirty which returns a Boolean representing the dirty state for the entire model.
One thing to note is that if the backing object is being modified while the ViewModel is also modified via the UI, all uncommitted changes in the ViewModel are being overridden by the changes in the backing object. That means the data in the ViewModel might get lost if external modification of the backing object takes place.
val person = Person("John", "Manager")
val model = PersonModel(person)
model.name.value = "Johnny" //modify the ViewModel
person.name = "Johan" //modify the underlying object
println(" Person = ${person.name}, ${person.title}") //output: Person = Johan, Manager
println("Is dirty = ${model.isDirty}") //output: Is dirty = false
println(" Model = ${model.name.value}, ${model.title.value}") //output: Model = Johan, Manager
As can be seen above the changes in the ViewModel got overridden when the underlying object was modified. And the ViewModel was not flagged as dirty.

Dirty Properties

You can check if a specific property is dirty, meaning that it has been changed compared to the backing source object value.
val nameWasChanged = model.isDirty(model.name)
There is also an extension property version that accomplishes the same task:
val nameWasChanged = model.name.isDirty
The shorthand version is an extension val on Property<T> but it will only work for properties that are bound inside a ViewModel. You will find model.isNotDirty properties as well.
If you need to dynamically react based on the dirty state of a specific property in the ViewModel, you can get a hold of a BooleanBinding representing the dirty state of that field like this:
val nameDirtyProperty = model.dirtyStateFor(PersonModel::name)

Extracting the Source Object Value

To retrieve the backing object value for a property you can call model.backingValue(property).
val value = model.backingValue(property)

Specific Property Subtypes (IntegerProperty, BooleanProperty)

If you bind, for example, an IntegerProperty, the type of the facade property will look like Property<Int> but it is in fact an IntegerProperty under the hood. If you need to access the special functions provided by IntegerProperty, you will have to cast the bind result:
val age = bind(Person::ageProperty) as IntegerProperty
Similarly, you can expose a read only property by specifying a read only type:
val age = bind(Person::ageProperty) as ReadOnlyIntegerProperty
The reason for this is an unfortunate shortcoming on the type system that prevents the compiler from differentiating between overloaded bind functions for these specific types, so the single bind function inside ViewModel inspects the property type and returns the best match, but unfortunately the return type signature has to be Property<T> for now.

Rebinding

As you saw in the TableView example above, it is possible to change the domain object that is wrapped by the ItemViewModel. This test case sheds some more light on that:
@Test fun swap_source_object() {
val person1 = Person("Person 1")
val person2 = Person("Person 2")
val model = PersonModel(person1)
assertEquals(model.name, "Person 1")
model.item = person2
assertEquals(model.name, "Person 2")
}
The test creates two Person objects and an ItemViewModel. The model is initialised with the first person object. It then checks that model.name corresponds to the name in person1. Now something weird happens:
model.item = person2
The ItemViewProperty has an itemProperty which holds the current item (accessible via the item Kotlin property, just like our Person's nameProperty can be accessed using name). Whenever this property changes, the ItemViewModel updates all of its properties (such as name and age in our case) to reflect the new Person object.

Rebind Listener

Our TableView example called the rebindOnChange() function and passed in a TableView as the first argument. This made sure that rebind was called whenever the selection of the TableView changed. This is actually just a shortcut to another function with the same name that takes an observable and calls rebind whenever that observable changes. If you call this function, you do not need to call rebind manually as long as you have an observable that represent the state change that should cause the model to rebind.
As you saw, TableView has a shorthand support for the selectionModel.selectedItemProperty. If not for this shorthand function call, you would have to write it like this:
model.rebindOnChange(table.selectionModel.selectedItemProperty()) {
item = it ?: Person()
}
The above example is included to clarify how the rebindOnChange() function works under the hood. For real use cases involving a TableView, you should opt for the bindSelected(model) function that is available when you combine TableView and ItemViewModel.

The low-level ViewModel

The ItemViewModel is an extension to ViewModel, where all properties correspond to a given object's properties (a Person object in our case). It also provides a simple way to rebind all of its properties at the same time by changing the item property to a new value.
A ViewModel allows you to bind to any property you would like (not necessarily encapsulated in a special class such as Person) and apply the same functionality to it: obtain a new property that can be edited by the user, then commited or rolled back to the original as needed. Instead of using bind(Person::name), you can use a lambda: bind { obtainProperty() }, where the lambda would return different properties depending on the situation, in a similar manner our Person object changes in the ItemViewModel. It should be noted, though, that the lambda will not be re-evaluated until the rollback() function is called (Note: in the case of the ItemViewModel, this function is called automatically after changing the value of the item property). Specifically, should the properties be changed, the function to do so is rebind { }. Under the hood, all this function does is to execute the lambda and then call rollback().
A very simple implementation of a ViewModel:
class MyViewModel(initialProperty : Property<String>) : ViewModel() {
var myProperty : Property<String> = initialProperty
val propertyFacade = bind { myProperty }
}
If we would like to update the property, we can do it like so:
var model = MyViewModel(someInitialProperty)
// more code...
model.rebind {
myProperty = someOtherProperty
}
Basically, instead of having to rely on a Person class to bind to its properties, we can bind to any property. Now you may wonder if there are any cases would be for a low-level implementation ViewModel when you could simply use the streamlined ItemViewModel. The answer is that while you would typically extend ItemViewModel more than 90% of the time, there are some use cases where it does not make sense. Since ViewModels can be injected and used to keep navigational state and overall UI state, you might use it for situations where you do not have a single domain object - you could have multiple domain objects or just a collection of loose properties. In this use case the ItemViewModel does not make any sense, and you might implement the ViewModel directly. For common cases though, ItemViewModel is your best friend.
There is one potential issue with this approach. If we want to display multiple "pairs" of lists and forms, perhaps in different windows, we need a way to separate and bind the model belonging to a specific pair of list and form. There are many ways to deal with that, but one tool very well suited for this is the scopes. Check out the scope documentation for more information about this approach.

OnCommit callback

Sometimes it's desirable to do a specific action after the model was successfully committed. The ViewModel offers two callbacks, onCommit and onCommit(commits: List<Commit>), for that.
The first function onCommit, has no parameters and will be called after a successful commit, right before the optional successFn is invoked (see: commit).
The second function will be called in the same order and with the addition of passing a list of committed properties along. Each Commit in the list, consists of the original ObservableValue, the oldValue and the newValue and a property changed, to signal if the oldValue is different then the newValue.
Let's look at an example of how we can retrieve only the changed objects and print them to stdout.
To find out which object changed we defined a little extension function, which will find the given property and if it was changed will return the old and new value or null if there was no change.
class PersonModel : ItemViewModel<Person>() {
val firstname = bind(Person::firstName)
val lastName = bind(Person::lastName)
override fun onCommit(commits: List<Commit>) {
// The println will only be called if findChanged is not null
commits.findChanged(firstName)?.let { println("First-Name changed from ${it.second} to ${it.first}")}
commits.findChanged(lastName)?.let { println("Last-Name changed from ${it.second} to ${it.first}")}
}
private fun <T> List<Commit>.findChanged(ref: Property<T>): Pair<T, T>? {
val commit = find { it.property == ref && it.changed}
return commit?.let { (it.newValue as T) to (it.oldValue as T) }
}
}

Injectable Models

Most commonly you will not have both the TableView and the editor in the same View. We would then need to access the ViewModel from at least two different views, one for the TableView and one for the form. Luckily, the ViewModel is injectable, so we can rewrite our editor example and split the two views:
class PersonList : View("Person List") {
val persons = listOf(Person("John", "Manager"), Person("Jay", "Worker bee")).observable()
val model : PersonModel by inject()
override val root = tableview(persons) {
title = "Person"
column("Name", Person::nameProperty)
column("Title", Person::titleProperty)
bindSelected(model)
}
}
The person TableView now becomes a lot cleaner and easier to reason with. However, in a real application, the list of persons would likely come from a controller or a remote call. The model is simply injected into the View, and we will do the same for the editor:
class PersonEditor : View("Person Editor") {
val model : PersonModel by inject()
override val root = form {
fieldset("Edit person") {
field("Name") {
textfield(model.name)
}
field("Title") {
textfield(model.title)
}
button("Save") {
enableWhen(model.dirty)
action {
save()
}
}
button("Reset").action {
model.rollback()
}
}
}
private fun save() {
model.commit()
println("Saving ${model.item.name} / ${model.item.title}")
}
}
The injected instance of the model will be exactly the same one in both views. Again, in a real application the save call would probably be offloaded to a controller asynchronously.

When to Use ViewModel vs ItemViewModel

This chapter has progressed from the low-level implementation ViewModel into a streamlined ItemViewModel. You might wonder if there are any use cases for inheriting from ViewModel instead of ItemViewModel at all. The answer is that while you would typically extend ItemViewModel more than 90% of the time, there are some use cases where it does not make sense. Since ViewModels can be injected and used to keep navigational state and overall UI state, you might use it for situations where you do not have a single domain object - you could have multiple domain objects or just a collection of loose properties. In this use case the ItemViewModel does not make any sense, and you might implement the ViewModel directly. For common cases though, ItemViewModel is your best friend.
There is one potential issue with this approach. If we want to display multiple "pairs" of lists and forms, perhaps in different windows, we need a way to separate and bind the model belonging to a specific pair of list and form. There are many ways to deal with that, but one tool very well suited for this is the scopes. Check out the scope documentation for more information about this approach.

Validation

Almost every application needs to check that the input supplied by the user conforms to a set of rules or are otherwise acceptable. TornadoFX sports an extensible validation and decoration framework.
We will first look at validation as a standalone feature before we integrate it with the ViewModel.

Under the Hood

The following explanation is a bit verbose and does not reflect the way you would write validation code in your application. This section will provide you with a solid understanding of how validation works and how the individual pieces fit together.

Validator

A Validator knows how to inspect user input of a specified type and will return a ValidationMessage with a ValidationSeverity describing how the input compares to the expected input for a specific control. If a Validator deems that there is nothing to report for an input value, it returns null. A text message can optionally accompany the ValidationMessage, and would normally be displayed by the Decorator configured in the ValidationContext. We will cover more on decorators later.
The following severity levels are supported:
  • Error - Input was not accepted
  • Warning - Input is not ideal, but accepted
  • Success - Input is accepted
  • Info - Input is accepted
There are multiple severity levels representing successful input to easier provide the contextually correct feedback in most cases. For example, you might want to give an informational message for a field no matter the input value, or specifically mark fields with a green checkbox when they are entered. The only severity that will result in an invalid status is the Error level.

ValidationTrigger

By default, validation will happen when the input value changes. The input value is always an ObservableValue<T>, and the default trigger simply listens for changes. You can however choose to validate when the input field looses focus, or when a save button is clicked for instance. The following ValidationTriggers can be configured for each validator:
  • OnChange - Validate when input value changes, optionally after a given delay in milliseconds
  • OnBlur - Validate when the input field looses focus
  • Never - Only validate when ValidationContext.validate() is called

ValidationContext

Normally you would validate user input from multiple controls or input fields at once. You can gather these validators in a ValidationContext so you can check if all validators are valid, or ask the validation context to perform validation for all fields at any given time. The context also controls what kind of decorator will be used to convey the validation message for each field. See the Ad Hoc validation example below.

Decorator

The decorationProvider of a ValidationContext is in charge of providing feedback when a ValidationMessage is associated with an input. By default this is an instance of SimpleMessageDecorator which will mark the input field with a colored triangle in the topper left corner and display a popup with the message while the input has focus.
Figure 11.2 The default decorator showing a required field validation message
If you don't like the look of the default decorator you can easily create your own by implementing the Decorator interface:
interface Decorator {
fun decorate(node: Node)
fun undecorate(node: Node)
}
You can assign your decorator to a given ValidationContext like this:
context.decorationProvider = MyDecorator()
Tip: You can create a decorator that applies CSS style classes to your inputs instead of overlaying other nodes to provide feedback.

Ad Hoc Validation

While you will probably never do this in a real application, it is possible to set up a ValidationContext and apply validators to it manually. The following example is actually taken from the internal tests of the framework. It illustrates the concept, but is not a practical pattern in an application.
// Create a validation context
val context = ValidationContext()
// Create a TextField we can attach validation to
val input = TextField()
// Define a validator that accepts input longer than 5 chars
val validator = context.addValidator(input, input.textProperty()) {
if (it!!.length < 5) error("Too short") else null
}
// Simulate user input
input.text = "abc"
// Validation should fail
assertFalse(validator.validate())
// Extract the validation result
val result = validator.result
// The severity should be error
assertTrue(result is ValidationMessage && result.severity == ValidationSeverity.Error)
// Confirm valid input passes validation
input.text = "longvalue"
assertTrue(validator.validate())
assertNull(validator.result)
Pay special attention to the last parameter of the addValidator call. It is the actual validation logic. The function receives the current input for the property that it validates and must return null if there are no messages, or an instance of ValidationMessage if something is noteworthy about the input. A message with severity Error will cause the validation to fail. As you can see, you don't need to instantiate a ValidationMessage yourself, simply use one of the functions error, warning, success or info instead.

Validation with ViewModel

Every ViewModel contains a ValidationContext, so you don't need to instantiate one yourself. The Validation framework integrates with the type safe builders as well, and even provides some built in validators, like the required validator. Going back to our person editor, we can make the input fields required with this simple change:
field("Name") {
textfield(model.name).required()
}
That's all there is to it. The required validator optionally takes a message that will be presented to the user if the validation fails. The default text is "This field is required".
Instead of using the built in required validator we can express the same thing manually:
field("Name") {
textfield(model.name).validator {
if (it.isNullOrBlank()) error("The name field is required") else null
}
}
If you want to further customize the textfield, you might want to add another set of curly braces:
field("Name") {
textfield(model.name) {
// Manipulate the text field here
validator {
if (it.isNullOrBlank()) error("The name field is required") else null
}
}
}

Binding buttons to validation state

You might want to only enable certain buttons in your forms when the input is valid. The model.valid property can be used for this purpose. Since the default validation trigger is OnChange, the valid state would only be accurate when you first try to commit the model. However, if you want to bind a button to the valid state of the model you can call model.validate(decorateErrors = false) to force all validators to report their results without actually showing any validation errors to the user.
field("username") {
textfield(username).required()
}
field("password") {
passwordfield(password).required()
}
buttonbar {
button("Login", ButtonBar.ButtonData.OK_DONE) {
enableWhen(model.valid)
action {
model.commit {
doLogin()
}
}
}
}
// Force validators to update the `model.valid` property
model.validate(decorateErrors = false)
Notice how the login button's enabled state is bound to the enabled state of the model via enableWhen { model.valid } call. After all the fields and validators are configured, the model.validate(decorateErrors = false) make sure the valid state of the model is updated without triggering error decorations on the fields that fail validation. The decorators will kick in on value change by default, unless you override the trigger parameter to validator. The required() built in validator also accepts this parameter. For example, to only run the validator when the input field looses focus you can call textfield(username).required(ValidationTrigger.OnBlur).

Validation in dialogs

The dialog builder creates a window with a form and a fieldset and let's you start adding fields to it. Some times you don't have a ViewModel for such cases, but you might still want to use the features it provides. For such situations you can instantiate a ViewModel inline and hook up one or more properties to it. Here is an example dialog that requires the user to enter some input in a textarea:
dialog("Add note") {
val model = ViewModel()
val note = model.bind { SimpleStringProperty() }
field("Note") {
textarea(note) {
required()
whenDocked { requestFocus() }
}
}
buttonbar {
button("Save note").action {
model.commit { doSave() }
}
}
}
Figure 11.3 A dialog with a inline ViewModel context
Notice how the note property is connected to the context by specifying it's bean parameter. This is crucial for making the field validation available.

Partial commit

It's also possible to do a partial commit by supplying a list of fields you want to commit in order to avoid committing everything. This can be convenient in situations where you edit the same ViewModel instance from different Views, for example in a Wizard. See the Wizard chapter for more information about partial commit, and the corresponding partial validation features.

TableViewEditModel

If you are pressed for screen real estate and do not have space for a master/detail setup with a TableView, an effective option is to edit the TableView directly. By enabling a few streamlined features in TornadoFX, you can not only enable easy cell editing but also enable dirty state tracking, committing, and rollback. By calling enableCellEditing() and enableDirtyTracking(), as well as accessing the tableViewEditModel property of a TableView, you can easily enable this functionality.
When you edit a cell, a blue flag will indicate its dirty state. Calling rollback() will revert dirty cells to their original values, whereas commit() will set the current values as the new baseline (and remove all dirty state history).
import tornadofx.*
class MyApp: App(MyView::class)
class MyView : View("My View") {
val controller: CustomerController by inject()
var tableViewEditModel: TableViewEditModel<Customer> by singleAssign()
override val root = borderpane {
top = buttonbar {
button("COMMIT").setOnAction {
tableViewEditModel.commit()
}
button("ROLLBACK").setOnAction {
tableViewEditModel.rollback()
}
}
center = tableview<Customer> {
items = controller.customers
isEditable = true
column("ID",Customer::idProperty)
column("FIRST NAME", Customer::firstNameProperty).makeEditable()
column("LAST NAME", Customer::lastNameProperty).makeEditable()
enableCellEditing() //enables easier cell navigation/editing
enableDirtyTracking() //flags cells that are dirty
tableViewEditModel = editModel
}
}
}
class CustomerController : Controller() {
val customers = listOf(
Customer(1, "Marley", "John"),
Customer(2, "Schmidt", "Ally"),
Customer(3, "Johnson", "Eric")
).observable()
}
class Customer(id: Int, lastName: String, firstName: String) {
val lastNameProperty = SimpleStringProperty(this, "lastName", lastName)
var lastName by lastNameProperty
val firstNameProperty = SimpleStringProperty(this, "firstName", firstName)
var firstName by firstNameProperty
val idProperty = SimpleIntegerProperty(this, "id", id)
var id by idProperty
}
Figure 11.4 A TableView with dirty state tracking, with rollback() and commit() functionality.
Note also that there are many other helpful properties and functions on the TableViewEditModel. The items property is an ObservableMap<S, TableColumnDirtyState<S>> mapping the dirty state of each record item S. If you want to filter out and commit only dirty records so you can persist them somewhere, you can have your "Commit" Button perform this action instead.
button("COMMIT").action {
tableViewEditModel.items.asSequence()
.filter { it.value.isDirty }
.forEach {
println("Committing ${it.key}")
it.value.commit()
}
}
There are also commitSelected() and rollbackSelected() to only commit or rollback the selected records in the TableView.
Copy link
On this page
Editing Models and Validation
A Typical Use Case
Problems with our initial attempt
Introducing ViewModel
Rollback
The PersonModel
Dirty Checking
Dirty Properties
Extracting the Source Object Value
Specific Property Subtypes (IntegerProperty, BooleanProperty)
Rebinding
The low-level ViewModel
Injectable Models
When to Use ViewModel vs ItemViewModel
Validation
Under the Hood
Decorator
Ad Hoc Validation
Validation with ViewModel
Binding buttons to validation state
Validation in dialogs
Partial commit
TableViewEditModel