TornadoFX Advanced TableView Editing

Yesterday I started working on an editable orderline grid for the TornadoFX version of our CRM system, which we are currently porting from HTML5. I ran across some issues with the default JavaFX TableView control, and I wanted to share these issues and their solution with you.

The experience spawned several new features, which will be in the TornadoFX 1.5.10 release scheduled for release this week.

The most interesting feature displayed is the type safe way to construct select bindings, or deeply nested, editable properties. This has a number of handy use cases, some of which is showcased in this screencast.

This example shows you how to create a writeable property for the Orderline -> Product -> Id property in the example data model:

class Orderline {
    val productProperty = SimpleObjectProperty<Product>()
    val productIdProperty = productProperty.select(Product::idProperty)
} 

You can use the productIdProperty as a completely normal Property<Int>. These type safe select bindings even let you swap out any property in the select chain and the resulting property will still work and do the right thing.

The code for the OrderlineEditor as shown in the video looks like this:

class OrderlinesEditor : Fragment() {
    val order: OrderModel by inject()
    val orderline: OrderlineModel by inject()

    val orderController: OrderController by inject()

    override val root = tableview(order.orderlines.value) {
        column("Product", Orderline::productId).fixedWidth(70)
        column("Text", Orderline::textProperty).remainingWidth().makeEditable()
        column("Price", Orderline::priceProperty).fixedWidth(70).makeEditable(AmountConverter).addClass(right)
        column("Price Inc", Orderline::priceIncProperty).fixedWidth(70).makeEditable(AmountConverter).addClass(right)
        column("Qty", Orderline::qtyProperty).fixedWidth(50).makeEditable(LocaleAwareBigDecimalStringConverter()).addClass(center)
        column("Vat", Orderline::vatFactorProperty).fixedWidth(50).makeEditable(LocaleAwareBigDecimalStringConverter()).addClass(right)
        column("Sum", Orderline::sum).converter(AmountConverter).fixedWidth(70).addClass(right, bold)
        column("Sum Inc", Orderline::sumInc).converter(AmountConverter).addClass(right, bold)

        enableCellEditing()
        regainFocusAfterEdit()
        bindSelected(orderline)

        onEditCommit {
            orderController.updateOrderline(it)
        }

        columnResizePolicy = SmartResize.POLICY
    }

}

You might wonder why the AmountConverter is not instantiated like the others? I just call converter(AmountConverter) instead of converter(AmountConverter() as you would expect. This converter has no shared state, so I didn't see a reason to create multiple versions of it. Therefor I elected to make it an object:

object AmountConverter : StringConverter<Amount>() {
    override fun toString(amount: Amount?) = amount?.toNorwegianString()
    override fun fromString(string: String?) = if (string == null) null else Amount.of(string)
}