Carl Walker recently wrote an insightful post on TableView Binding in Kotlin<\/a> where he uses TornadoFX <\/a>to create a nice looking TableView with a couple of buttons that are disabled depending on the state of the currently selected item.<\/p>\n\n He also wrote the same program in plain Java<\/a> and the post illustrates how TornadoFX can greatly reduce boiler plate code.<\/p>\n Coming from a strong JavaFX background, Carl's approach is very reasonable, but I'd like to show you that we can improve upon it even more by leveraging some more TornadoFX features.<\/p>\n TornadoFX is all about reducing boiler plate, while increasing readability and maintainability.<\/p>\n First I'll post the complete application as Carl created it, then I'll walk through each element I feel we can improve upon and finally post the full source for the modified app.<\/p>\n Here is the initial application without imports:<\/p>\n We'll start with something simple and probably insignificant, but we want to attack every piece of boiler plate, so I'll mention it anyway.<\/p>\n In JavaFX, you're used to setting properties on objects as you create them. A typical example is the The box builders take spacing as a parameter, so you can write Next up is actually a feature we added to TornadoFX, but that also has a shorthand alternative. Configuring the constraints inside a Let's tackle the last minor issue before we move on to bigger fish. Setting preferred width and height can be done in a single statement:<\/p>\n I have a golden rule I always try to follow: Whenever possible, avoid references to other ui elements<\/em>. This reduces coupling, but more importantly it means that you will configure a single ui element in just one place. It's almost always less code as well. The original sample uses the When these objects are created inside the builders, they are assigned to these variables:<\/p>\n Later, in the The items are declared in one place, instantiated another and configured a third place in the code. That's three different places to look for how each of these elements are treated. We can actually change all this so everything is done in one place - inside the builder expression that created them. We can get rid of those The reason this is needed in the original sample, is that the binding expressions work on the selectedItem of the TableView. We want to avoid that alltogether, so before we can clean up these variables, let's create a ViewModel.<\/p>\n An The latter is used in the button bindings, so we need to expose that as a property we can bind to, and we need that binding to be the same even when the item changes.<\/p>\n The ItemViewModel can be defined like this:<\/p>\n The Since TornadoFX makes it so easy to create JavaFX properties, you should always do that when your domain objects are exposed to JavaFX Nodes. Now that we have our view model, we can inject it into the view:<\/p>\n Next, we can get rid of all the Inside the TableView builder we bind the selected state of the tableview to our view model:<\/p>\n Now whenever the selection changes, the item inside our view model is updated, and the The inventory button should be disabled when there is no selection in the table view. Now we can define everything in one place, and also leverage the It's much easier to reason about the intent of this code, as you can almost read it as an English expression, and it's defined in one place, not three.<\/p>\n We do the same for the tax button, but here something magical happens:<\/p>\n Whoa?! Remember how this used to look? All though it makes my skin crawl, I'll recite it for you:<\/p>\n It's almost hard to believe that these two pieces of code actually have the same effect. The first one "reads" something like "Selected item is empty or selected item is not taxable". This you can infer in a two second glance. I don't think you can say the same for the The real power here comes from binding the Below you'll find the modified code, as I feel it should be written using TornadoFX 1.5.9. I would probably make two other adjustments, but I wanted it to be as close to the original sample as possible. I would rename Here is the modified code:<\/p>\n TornadoFX has a lot of features to simplify your UI code. You most certainly don't need to use them all, just use whatever portion you feel comfortable with, but know that if you ever write boiler plate in a TornadoFX app, you're probably not following best practices or we're missing something in the framework \ud83d\ude42<\/p>\n As the syntax and features have progressed so much during 2016, we have a lot of dated samples out there. We'll try to clean up as many as possible over the coming weeks, but the TornadoFX Guide<\/a> is in pretty good shape already, and is one of the best resources to help you get started.<\/p>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"_links":{"self":[{"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/posts\/33"}],"collection":[{"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/comments?post=33"}],"version-history":[{"count":24,"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/posts\/33\/revisions"}],"predecessor-version":[{"id":66,"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/posts\/33\/revisions\/66"}],"wp:attachment":[{"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/media?parent=33"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/categories?post=33"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/edvin.town\/wp-json\/wp\/v2\/tags?post=33"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}data class Item(val sku : String, val descr : String, val price : Float, val taxable : Boolean)\n\nclass TableSelectView : View(\"TableSelectApp\") {\n\n private val items = FXCollections.observableArrayList(\n Item(\"KBD-0455892\", \"Mechanical Keyboard\", 100.0f, true),\n Item(\"145256\", \"Product Docs\", 0.0f, false),\n Item(\"OR-198975\", \"O-Ring (100)\", 10.0f, true)\n )\n\n var tblItems : TableView<Item> by singleAssign()\n var btnInventory : Button by singleAssign()\n var btnCalcTax : Button by singleAssign()\n\n override val root = vbox {\n tblItems = tableview(items) {\n\n column(\"SKU\", Item::sku)\n column(\"Item\", Item::descr)\n column(\"Price\", Item::price)\n column(\"Taxable\", Item::taxable)\n\n prefWidth = 667.0\n prefHeight = 376.0\n\n columnResizePolicy = CONSTRAINED_RESIZE_POLICY\n\n vboxConstraints {\n vGrow = Priority.ALWAYS\n }\n }\n hbox {\n btnInventory = button(\"Inventory\")\n btnCalcTax = button(\"Tax\")\n\n spacing = 8.0\n }\n\n padding = Insets(10.0)\n spacing = 10.0\n }\n\n init {\n\n btnInventory.disableProperty().bind( tblItems.selectionModel.selectedItemProperty().isNull )\n\n btnCalcTax.disableProperty().bind(\n tblItems.selectionModel.selectedItemProperty().isNull().or(\n Bindings.select<Boolean>(\n tblItems.selectionModel.selectedItemProperty(),\n \"taxable\"\n ).isEqualTo( false )\n )\n )\n }\n}\n\nclass TableSelectApp : App(TableSelectView::class)<\/code><\/pre>\n
Syntactic Sugar<\/h2>\n
spacing<\/code> property on
VBox<\/code> and
HBox<\/code> containers. The original sample does:<\/p>\n
vbox {\n spacing = 8.0\n}<\/code><\/pre>\n
vbox(8.0)<\/code> or
vbox(spacing = 8.0)<\/code> if you prefer. OK, that was low hanging fruit, but bear with me.<\/p>\n
VBox<\/code> can be done inside a
vboxConstraints<\/code> block, but when you only configure a single constraint you're better off just using this shorthand:<\/p>\n
vgrow = Priority.ALWAYS<\/code><\/pre>\n
setPrefSize(667.0, 376.0)<\/code><\/pre>\n
Builder Encapsulation<\/h2>\n
singleAssign<\/code> delegate that makes sure we only assign a value to the variable once. This is the original code:<\/p>\n
var tblItems : TableView<Item> by singleAssign()\nvar btnInventory : Button by singleAssign()\nvar btnCalcTax : Button by singleAssign()<\/code><\/pre>\n
tblItems = tableview(items)\nbtnInventory = button(\"Inventory\")\nbtnCalcTax = button(\"Tax\")<\/code><\/pre>\n
init<\/code> block of the class, these variables are configured further. The buttons gets their
disabledProperty<\/code> bound, and the
tblItems<\/code> is references from these bindings. This is the major issue with this code IMO.<\/p>\n
singleAssign<\/code> variable declarations, and make the binding expression much more concise in the process.<\/p>\n
ViewModel<\/h2>\n
ItemViewModel<\/code> can wrap an instance of your domain object and gives you properties you can bind against which will stay valid even when the item it represents is changed. We want the view model to solve two issues for us:<\/p>\n
\n
Item<\/code> is currently selected in the table<\/li>\n
taxable<\/code> state of that item<\/li>\n<\/ul>\n
class MyItemViewModel : ItemViewModel<Item>() {\n val taxable: BooleanProperty = bind { SimpleBooleanProperty(item?.taxable ?: false) }\n}<\/code><\/pre>\n
MyItemViewModel<\/code> can contain an item of type
Item<\/code> in our case, and it has a
BooleanProperty<\/code> called
taxable<\/code>. This property will always reflect the state of the selected item, or null if false. Now this might look a bit verbose. That's because our domain object
Item<\/code> doesn't contain JavaFX properties. That's another thing I would probably change if I wrote this app from scratch, but we'll keep it, so you see how to bind against POJO properties. If
Item<\/code> contained JavaFX properties it would be much simpler:<\/p>\n
val taxable = bind { item?.taxableProperty }<\/code><\/pre>\n
val mySelectedItem: MyItemViewModel by inject()<\/code><\/pre>\n
singleAssign<\/code> statements, the variable assignments (
tblItems =<\/code> etc) and that whole
init<\/code> block at the bottom. Instead we will define the bindings directly inside the builders.<\/p>\n
bindSelected(mySelectedItem)<\/code><\/pre>\n
taxable<\/code> property will reflect the state of the selected item. This gives us a chance to clean up the bindings.<\/p>\n
View Model Usage<\/h2>\n
empty<\/code> property you get for free with the
ItemViewModel<\/code>:<\/p>\n
button(\"Inventory\") {\n disableProperty().bind(mySelectedItem.empty)\n}<\/code><\/pre>\n
button(\"Tax\") {\n disableProperty().bind(mySelectedItem.empty.or(mySelectedItem.taxable.not()))\n}<\/code><\/pre>\n
btnCalcTax.disableProperty().bind(\n tblItems.selectionModel.selectedItemProperty().isNull().or(\n Bindings.select<Boolean>(\n tblItems.selectionModel.selectedItemProperty(),\n \"taxable\"\n ).isEqualTo( false )\n )\n)<\/code><\/pre>\n
Bindings.select<\/code> expression we had originally.<\/p>\n
ItemViewModel<\/code> to the TableView, and I'm sure you agree now that it was most definitely worth it.<\/p>\n
Tying it all together<\/h2>\n
Item<\/code> to
Product<\/code> or something similar to avoid the unfortunate name clash with
ItemViewModel<\/code>, which is a TornadoFX construct, but more importantly I would create the properties as real JavaFX properties, utilizing the property delegates of TornadoFX. That would clean up that swearing inside the ItemViewModel as I demonstrated.<\/p>\n
data class Item(val sku: String, val descr: String, val price: Float, val taxable: Boolean)\n\nclass MyItemViewModel : ItemViewModel<Item>() {\n val taxable: BooleanProperty = bind { SimpleBooleanProperty(item?.taxable ?: false) }\n}\n\nclass TableSelectView : View(\"TableSelectApp\") {\n\n private val items = FXCollections.observableArrayList(\n Item(\"KBD-0455892\", \"Mechanical Keyboard\", 100.0f, true),\n Item(\"145256\", \"Product Docs\", 0.0f, false),\n Item(\"OR-198975\", \"O-Ring (100)\", 10.0f, true)\n )\n\n val mySelectedItem = MyItemViewModel()\n\n override val root = vbox(10.0) {\n tableview(items) {\n column(\"SKU\", Item::sku)\n column(\"Item\", Item::descr)\n column(\"Price\", Item::price)\n column(\"Taxable\", Item::taxable)\n bindSelected(mySelectedItem)\n setPrefSize(667.0, 376.0)\n columnResizePolicy = CONSTRAINED_RESIZE_POLICY\n vgrow = Priority.ALWAYS\n }\n hbox(8.0) {\n button(\"Inventory\") {\n disableProperty().bind(mySelectedItem.empty)\n }\n button(\"Tax\") {\n disableProperty().bind(mySelectedItem.empty.or(mySelectedItem.taxable.not()))\n }\n }\n\n padding = Insets(10.0)\n }\n\n}<\/code><\/pre>\n
Conclusion<\/h2>\n