necplsultra
necplsultra

Reputation: 49

TornadoFX how to add validation while editing TableView

Consider folowing example:

class Item(name: String, number: Int) {
    val nameProperty = SimpleStringProperty(name)
    var name by nameProperty

    val numberProperty by lazy { SimpleIntegerProperty(number) }
    var number by numberProperty
}

class MainView : View("Example") {
    val items = listOf(Item("One", 1), Item("Two", 2)).observable()

    override val root = vbox {
        tableview(items) {
            column("Name", Item::nameProperty).makeEditable()
            column("Number", Item::numberProperty).makeEditable(NumberStringConverter())
            enableCellEditing()
        }
    }
}

How can I add a validator while editing cells? Is the only way to do that is to add rowExpander with some textfield and try to validate a model there?

Upvotes: 1

Views: 1400

Answers (1)

Edvin Syse
Edvin Syse

Reputation: 7297

You can either implement your own cellfactory and return a cell that shows a textfield bound to a ViewModel when in edit mode and an label if not. Alternatively, if you're fine with always displaying a textfield, you can use cellFormat and bind the current item to an ItemModel so you can attach validation:

class ItemModel(item: Item) : ItemViewModel<Item>(item) {
    val name = bind(Item::nameProperty)
    val number = bind(Item::numberProperty)
}


class MainView : View("Example") {
    val items = listOf(Item("One", 1), Item("Two", 2)).observable()

    override val root = vbox {
        tableview(items) {
            column("Name", Item::nameProperty).makeEditable()
            column("Number", Item::numberProperty).cellFormat {
                val model = ItemModel(rowItem)
                graphic = textfield(model.number, NumberStringConverter()) {
                    validator {
                        if (model.number.value == 123) error("Invalid number") else null
                    }
                }
            }
        }
    }
}

It will look like this:

CellFormat Solution

While it works, it's sort of wasteful since the nodes are recreated frequently. I would recommend approach number one if performance is a concern, until we get cellFragment support for TableView like we have for ListView.

EDIT: I implemented cellFragment support, so it's possible to create a more robust solution which will show a label when not in edit mode and a validating textfield when you enter edit mode.

class ItemModel : ItemViewModel<Item>() {
    val name = bind(Item::nameProperty)
    val number = bind(Item::numberProperty)
}


class MainView : View("Example") {
    val items = listOf(Item("One", 1), Item("Two", 2)).observable()

    override val root = vbox {
        tableview(items) {
            column("Name", Item::nameProperty).makeEditable()
            column("Number", Item::numberProperty).cellFragment(NumberEditor::class)
        }
    }
}

class NumberEditor : TableCellFragment<Item, Number>() {
    // Bind our ItemModel to the rowItemProperty, which points to the current Item
    val model = ItemModel().bindToRowItem(this)

    override val root = stackpane {
        textfield(model.number, NumberStringConverter()) {
            removeWhen(editingProperty.not())
            validator {
                if (model.number.value == 123L) error("Invalid number") else null
            }
            // Call cell.commitEdit() only if validation passes
            action {
                if (model.commit()) {
                    cell?.commitEdit(model.number.value)
                }
            }
        }
        // Label is visible when not in edit mode, and always shows committed value (itemProperty)
        label(itemProperty) {
            removeWhen(editingProperty)
        }
    }

    // Make sure we rollback our model to avoid showing the last failed edit
    override fun startEdit() {
        model.rollback()
    }

}

This will be possible starting from TornadoFX 1.7.9.

Upvotes: 4

Related Questions