Data Controls

Any significant application works with data, and providing a means for users to view, manipulate, and modify data is not a trivial task for user interface development. Fortunately, TornadoFX streamlines many JavaFX data controls such as ListView, TableView, TreeView, and TreeTableView. These controls can be cumbersome to set up in a purely object-oriented way. But using builders through functional declarations, we can code all these controls in a much more streamlined way.

ListView

A ListView is similar to a ComboBox but it displays all items within a ScrollView and has the option of allowing multiple selections, as shown in Figure 5.1

listview<String> {
    items.add("Alpha")
    items.add("Beta")
    items.add("Gamma")
    items.add("Delta")
    items.add("Epsilon")
    selectionModel.selectionMode = SelectionMode.MULTIPLE
}

Figure 5.1

You can also provide it an ObservableList of items up front and omit the type declaration since it can be inferred. Using an ObservableList also has the benefit that changes to the list will automatically be reflected in the ListView.

val greekLetters = listOf("Alpha","Beta",
        "Gamma","Delta","Epsilon").observable()

listview(greekLetters) {
    selectionModel.selectionMode = SelectionMode.MULTIPLE
}

Like most data controls, keep in mind that by default, the ListView will call toString() to render the text for each item in your domain class.

Custom Cell formatting

Even though the default look of a ListView is rather boring (because it calls toString() and renders it as text) you can modify it so that every cell is a custom Node of your choosing. By calling cellCache(), TornadoFX provides a convenient way to override what kind of Node is returned for each item in your list (Figure 5.2).

class MyView: View() {

    val persons = listOf(
            Person("John Marlow", LocalDate.of(1982,11,2)),
            Person("Samantha James", LocalDate.of(1973,2,4))
    ).observable()

    override val root = listview(persons) {
        cellFormat {
            graphic = cache {
                form {
                    fieldset {
                        field("Name") {
                            label(it.name)
                        }
                        field("Birthday") {
                            label(it.birthday.toString())
                        }
                        label("${it.age} years old") {
                            alignment = Pos.CENTER_RIGHT
                            style {
                                fontSize = 22.px
                                fontWeight = FontWeight.BOLD
                            }
                        }
                    }
                }
            }
        }
    }
}

class Person(val name: String, val birthday: LocalDate) {
    val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

Figure 5.2 - A custom cell rendering for ListView

The cellFormat function lets you configure the text and/or graphic property of the cell whenever it comes into view on the screen.
The cells themselves are reused, but whenever the ListView asks the cell to update it's content, the cellFormat function is called. In our
example we only assign to graphic, but if you just want to change the string representation you should assign to text. It is completely
legal and normal to assign to both text and graphic. The values will automatically be cleared by the cellFormat function
when a certain list cell is not showing an active item.

Note that assigning new nodes to the graphic property every time the list cell is asked to update can be expensive. It might
be fine for many use cases, but for heavy node graphs, or node graphs where you utilize binding towards the ui components inside
the cell, you should cache resulting node so the node graph will only be created once per node. This is done using the cache
wrapper in the above example.

Assign If Null

If you have a reason for wanting to recreate the graphic property for a list cell, you can use the assignIfNull helper,
which will assign a value to any given property if the property doesn't already contain a value. This will make sure that
you avoid creating new nodes if updateItem is called on a cell that already has a graphic property assigned.

cellFormat {
    graphicProperty().assignIfNull {
        label("Hello")
    }
}

ListCellFragment

The ListCellFragment is a special fragment which can help you manage ListView cells. It extends Fragment, and
includes some extra ListView specific fields and helpers. You never instantiate these fragments manually, instead you
instruct the ListView to create them as needed. There is a one to one correlation between ListCells and ListCellFragment instances.
One ListCellFragment instance will over it's lifecycle be used to represent several different items.

To understand how this works, let's consider a manually implemented ListCell, essentially how you would do this in vanilla JavaFX.
The updateItem function will be called when the ListCell should represent a new item, no item or just an update to the same item. When you use a ListCellFragment,
you don't need to implement something akin to updateItem, but the itemProperty inside it will update to represent
the new item automatically. You can listen to changes to the itemProperty, or better yet, bind it directly to a ViewModel, so that
your UI can bind directly to the ViewModel and as such no longer need to care about changes to the underlying item anymore.

Let's recreate the form from the cellFormat example using a ListCellFragment. We need a ViewModel which we will
call PersonModel. Please see the Editing Models and Validation chapter for a full explanation of the ViewModel. For now,
just imagine that the ViewModel acts as a proxy for an underlying Person, and that the Person can be changed while the
observable values in the ViewModel remain the same. When we have created our PersonCellFragment, we need to configure
the ListView to use it:

listview(personlist) {
    cellFragment(PersonCellFragment::class)
}

Now comes the ListCellFragment itself.

class PersonListFragment : ListCellFragment<Person>() {
    val person = PersonModel().bindTo(this)

    override val root = form {
        fieldset {
            field("Name") {
                label(person.name)
            }
            field("Birthday") {
                label(person.birthday)
            }
            label(stringBinding(person.age) { "$value years old" }) {
                alignment = Pos.CENTER_RIGHT
                style {
                    fontSize = 22.px
                    fontWeight = FontWeight.BOLD
                }
            }
        }
    }
}

Because this Fragment will be reused to represent different list items, the easiest approach is to bind the ui elements to the ViewModel's properties.

The name and birthday properties are bound directly to the labels inside the fields. The age string in the last label needs to be constructed using a stringBinding
to make sure it updates when the item changes.

While this might seem like slightly more work than the cellFormat example, this approach makes it possible to leverage
everything the Fragment class has to offer. It also forces you to define the cell node graph outside of the builder hierarchy,
which improves refactoring possibilities and enables code reuse.

Additional helpers and editing support

The ListCellFragment also have some other helper properties. They include the cellProperty which will
update whenever the underlying cell changes and the editingProperty, which will tell you if this the underlying list cell
is in editing mode. There are also editing helper functions called startEdit, commitEdit, cancelEdit plus an onEdit
callback. The ListCellFragment makes it trivial to utilize the existing editing capabilites of the ListView. A complete example
can be seen in the TodoMVC demo application.

TableView

Probably one of the most significant builders in TornadoFX is the one for TableView. If you have worked with JavaFX, you might have experienced building a TableView in an object-oriented way. But TornadoFX provides a functional declaration construct pattern using extension functions that greatly simplifies the coding of a TableView.

Say you have a domain type, such as Person.

class Person(val id: Int, val name: String, val birthday: LocalDate) {
    val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

Take several instances of Person and put them in an ObservableList.

private val persons = listOf(
        Person(1,"Samantha Stuart",LocalDate.of(1981,12,4)),
        Person(2,"Tom Marks",LocalDate.of(2001,1,23)),
        Person(3,"Stuart Gills",LocalDate.of(1989,5,23)),
        Person(3,"Nicole Williams",LocalDate.of(1998,8,11))
).observable()

You can quickly declare a TableView with all of its columns using a functional construct, and specify the items property to an ObservableList<Person> (Figure 5.3).

tableview(persons) {
    column("ID",Person::id)
    column("Name", Person::name)
    column("Birthday", Person::birthday)
    column("Age",Person::age)
}

Figure 5.3

The column() functions are extension functions for TableView accepting a header name and a mapped property using reflection syntax. TornadoFX will then take each mapping to render a value for each cell in that given column.

If you want granular control over TableView column resize policies, see Appendix A2 for more information on SmartResize policies.

Using "Property" properties

If you follow the JavaFX Property conventions to set up your domain class, it will automatically support value editing.

You can create these Property objects the conventional way, or you can use TornadoFX's property delegates to automatically create these Property declarations as shown below.

class Person(id: Int, name: String, birthday: LocalDate) {
    var id by property(id)
    fun idProperty() = getProperty(Person::id)

    var name by property(name)
    fun nameProperty() = getProperty(Person::name)

    var birthday by property(birthday)
    fun birthdayProperty() = getProperty(Person::birthday)

    val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

You need to create xxxProperty() functions for each property to support JavaFX's naming convention when it uses reflection. This can easily be done by relaying their calls to getProperty() to retrieve the Property for a given field. See Appendix A1 for detailed information on how these property delegates work.

Now on the TableView, you can make it editable, map to the properties, and apply the appropriate cell-editing factories to make the values editable.

override val root = tableview(persons) {
    isEditable = true
    column("ID",Person::idProperty).useTextField(IntegerStringConverter())
    column("Name", Person::nameProperty).useTextField(DefaultStringConverter())
    column("Birthday", Person::birthdayProperty).useTextField(LocalDateStringConverter())
    column("Age",Person::age)
}

To allow editing and rendering, TornadoFX provides a few default cell factories you can invoke on a column easily through extension functions.

Extension Function Description
useTextField() Uses a standard TextField to edit values with a provided StringConverter
useComboBox() Edits a cell value via a ComboBox with a specified ObservableList<T> of applicable values
useChoiceBox() Accepts value changes to a cell with a ChoiceBox
useCheckBox() Renders an editable CheckBox for a Boolean value column
useProgressBar() Renders the cell as a ProgressBar for a Double value column
Property Syntax Alternatives

If you do not care about exposing the Property in a function (which is common in practial usage) you can express your class like this:

class Person(id: Int, name: String, birthday: LocalDate) {
    val idProperty = SimpleIntegerProperty(id)
    var id by idProperty

    val nameProperty = SimpleStringProperty(name)
    var name by nameProperty

    val birthdayProperty = SimpleObjectProperty(birthday)
    var birthday by birthdayProperty

    val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

This alternative pattern exposes the Property as a field member instead of a function. If you like the above syntax but want to keep the function, you can make the property private and add the function like this:

private val nameProperty = SimpleStringProperty(name)
fun nameProperty() = nameProperty
var name by nameProperty

Choosing from these patterns are all a matter of taste, and you can use whatever version meets your needs or preferences best.

You can also convert plain properties to JavaFX properties using the TornadoFX Plugin. Refer to Chapter 13 to learn how to do this.

Using cellFormat()

There are other extension functions applied to TableView that can assist the flow of declaring a TableView. For instance, you can call a cellFormat() function on a given column to apply formatting rules, such as highlighting "Age" values less than 18 (Figure 5.4).

tableview(persons) {
    column("ID", Person::id)
    column("Name", Person::name)
    column("Birthday", Person::birthday)
    column("Age", Person::age).cellFormat {
        text = it.toString()
        style {
            if (it < 18) {
                backgroundColor += c("#8b0000")
                textFill = Color.WHITE
            } else {
                backgroundColor += Color.WHITE
                textFill = Color.BLACK            
            }
        }
     }
}

Figure 5.4

Declaring Column Values Functionally

If you need to map a column's value to a non-property (such as a function), you can use a non-reflection means to extract the values for that column.

Say you have a WeeklyReport type that has a getTotal() function accepting a DayOfWeek argument (an enum of Monday, Tuesday... Sunday).

abstract class WeeklyReport(val startDate: LocalDate) {
    abstract fun getTotal(dayOfWeek: DayOfWeek): BigDecimal
}

Let's say you wanted to create a column for each DayOfWeek. You cannot map to properties, but you can map each WeeklyReport item explicitly to extract each value for that DayOfWeek.

tableview<WeeklyReport> {
    for (dayOfWeek in DayOfWeek.values()) {
        column<WeeklyReport, BigDecimal>(dayOfWeek.toString()) {
            ReadOnlyObjectWrapper(it.value.getTotal(dayOfWeek))
        }
    }
}

This more closely resembles the traditional setCellValueFactory() for the JavaFX TableColumn.

Row Expanders

Later we will learn about the TreeTableView which has a notion of "parent" and "child" rows, but the constraint with this control is the parent and child must have the same columns. Fortunately, TornadoFX comes with an awesome utility to not only reveal a "child table" for a given row, but any kind of Node control.

Say we have two domain types: Region and Branch. A Region is a geographical zone, and it contains one or more Branch items which are specific business operation locations (warehouses, distribution centers, etc). Here is a declaration of these types and some given instances.

class Region(val id: Int, val name: String, val country: String, val branches: ObservableList<Branch>)

class Branch(val id: Int, val facilityCode: String, val city: String, val stateProvince: String)

val regions = listOf(
        Region(1,"Pacific Northwest", "USA",listOf(
                Branch(1,"D","Seattle","WA"),
                Branch(2,"W","Portland","OR")
        ).observable()),
        Region(2,"Alberta", "Canada",listOf(
                Branch(3,"W","Calgary","AB")
        ).observable()),
        Region(3,"Midwest", "USA", listOf(
                Branch(4,"D","Chicago","IL"),
                Branch(5,"D","Frankfort","KY"),
                Branch(6, "W","Indianapolis", "IN")
        ).observable())
).observable()

We can create a TableView where each row has a rowExpander() function defined, and there we can arbitrarily create any Node control built off that particular row's item. In this case, we can nest another TableView for a given Region to show all the Branch items belonging to it. It will have a "+" button column to expand and show this expanded control (Figure 5.5).

Figure 5.5

There are a few configurability options, like "expand on double-click" behaviors and accessing the expanderColumn (the column with the "+" button) to drive a padding (Figure 5.6).

override val root = tableview(regions) {
        column("ID",Region::id)
        column("Name", Region::name)
        column("Country", Region::country)
        rowExpander(expandOnDoubleClick = true) {
            paddingLeft = expanderColumn.width
            tableview(it.branches) {
                column("ID",Branch::id)
                column("Facility Code",Branch::facilityCode)
                column("City",Branch::city)
                column("State/Province",Branch::stateProvince)
            }
        }
    }

Figure 5.6

The rowExpander() function does not have to return a TableView but any kind of Node, including Forms and other simple or complex controls.

Accessing the expander column

You might want to manipulate or call functions on the actual expander column. If you activate expand on double click, you might not want to show the expander column in the table at all. First we need a reference to the expander:

val expander = rowExpander(true) { ... }

If you want to hide the expander column, just call expander.isVisible = false. You can also programmatically toggle the expanded state of any column by calling expander.toggleExpanded(rowIndex).

TreeView

The TreeView contains elements where each element may contain child elements. Typically arrows allow you to expand a parent element to see its children. For instance, we can nest employees under department names

Traditionally in JavaFX, populating these elements is rather cumbersome and verbose. Fortunately TornadoFX makes it relatively simple.

Say you have a simple type Person and an ObservableList containing several instances.

data class Person(val name: String, val department: String)

val persons = listOf(
        Person("Mary Hanes","Marketing"),
        Person("Steve Folley","Customer Service"),
        Person("John Ramsy","IT Help Desk"),
        Person("Erlick Foyes","Customer Service"),
        Person("Erin James","Marketing"),
        Person("Jacob Mays","IT Help Desk"),
        Person("Larry Cable","Customer Service")
        )

Creating a TreeView with the treeview() builder can be done functionally Figure 5.7).

// Create Person objects for the departments
// with the department name as Person.name

val departments = persons
    .map { it.department }
    .distinct().map { Person(it, "") }

treeview<Person> {
    // Create root item
    root = TreeItem(Person("Departments", ""))

    // Make sure the text in each TreeItem is the name of the Person
    cellFormat { text = it.name }

    // Generate items. Children of the root item will contain departments
    populate { parent ->
        if (parent == root) departments else persons.filter { it.department == parent.value.name }
    }
}

Figure 5.7

Let's break this down:

val departments = persons
    .map { it.department }
    .distinct().map { Person(it, "") }

First we gather a distinct list of all the departments derived from the persons list. But then we put each department String in a Person object since the TreeView only accepts Person elements. While this is not very intuitive, this is the constraint and design of TreeView. We must make each department a Person for it to be accepted.

treeview<Person> {
    // Create root item
    root = TreeItem(Person("Departments", ""))

Next we specify the highest root for the TreeView that all departments will be nested under, and we give it a placeholder Person called "Departments".

    cellFormat { text = it.name }

Then we specify the cellFormat() to render the name of each Person (including departments) on each cell.

   populate { parent ->
        if (parent == root) departments else persons.filter { it.department == parent.value.name }
    }

Finally, we call the populate() function and provide a block instructing how to provide children to each parent. If the parent is indeed the root, then we return the departments. Otherwise the parent is a department and we provide a list of Person objects belonging to that department.

Data driven TreeView

If the child list you return from populate is an ObservableList, any changes to that list will automatically be reflected in the TreeView. The populate function will be called for any new children that appears, and
removed items will result in removed TreeItems as well.

TreeView with Differing Types

It is not necessarily intuitive to make every entity in the previous example a Person. We made each department a Person as well as the root "Departments". For a more complex TreeView<T> where T is unknown and can be any number of types, it is better to leverage star projection for type T.

Using star projection, you can safely populate multiple types nested into the TreeView.

For instance, you can create a Department type and leverage cellFormat() to utilize type-checking for rendering. Then you can use a populate() function that will iterate over each element, and you specify the children for each element (if any).

data class Department(val name: String)

// Create Department objects for the departments by getting distinct values from Person.department
val departments = persons.map { it.department }.distinct().map { Department(it) }

// Type safe way of extracting the correct TreeItem text
cellFormat {
    text = when (it) {
        is String -> it
        is Department -> it.name
        is Person -> it.name
        else -> throw IllegalArgumentException("Invalid value type")
    }
}

// Generate items. Children of the root item will contain departments, children of departments are filtered
populate { parent ->
    val value = parent.value
    if (parent == root) departments
    else if (value is Department) persons.filter { it.department == value.name }
    else null
}

TreeTableView

The TreeTableView operates and functions similarly to a TreeView, but it has multiple columns since it is a table. Please note that the columns in a TreeTableView are the same for each parent and child element. If you want the columns to be different between parent and child, use a TableView with a rowExpander() as covered earlier in this chapter.

Say you have a Person class that optionally has an employees parameter, which defaults to an empty List<Person> if nobody reports to that Person.

class Person(val name: String,
  val department: String,
  val email: String,
  val employees: List<Person> = emptyList())

Then you have an ObservableList<Person> holding instances of this class.

val persons = listOf(
        Person("Mary Hanes", "IT Administration", "mary.hanes@contoso.com", listOf(
            Person("Jacob Mays", "IT Help Desk", "jacob.mays@contoso.com"),
            Person("John Ramsy", "IT Help Desk", "john.ramsy@contoso.com"))),
        Person("Erin James", "Human Resources", "erin.james@contoso.com", listOf(
            Person("Erlick Foyes", "Customer Service", "erlick.foyes@contoso.com"),
            Person("Steve Folley", "Customer Service", "steve.folley@contoso.com"),
            Person("Larry Cable", "Customer Service", "larry.cable@contoso.com")))
).observable()

You can create a TreeTableView by merging the components needed for a TableView and TreeView together. You will need to call the populate() function as well as set the root TreeItem.

val treeTableView = TreeTableView<Person>().apply {
    column("Name", Person::nameProperty)
    column("Department", Person::departmentProperty)
    column("Email", Person::emailProperty)

    /// Create the root item that holds all top level employees
    root = TreeItem(Person("Employees by leader", "", "", persons))

    // Always return employees under the current person
    populate { it.value.employees }

    // Expand the two first levels
    root.isExpanded = true
    root.children.forEach { it.isExpanded = true }

    // Resize to display all elements on the first two levels
    resizeColumnsToFitContent()
}

It is also possible to work with more of an ad hoc backing store like a Map. That would look something like this:

val tableData = mapOf(
    "Fruit" to arrayOf("apple", "pear", "Banana"),
    "Veggies" to arrayOf("beans", "cauliflower", "cale"),
    "Meat" to arrayOf("poultry", "pork", "beef")
)

treetableview<String>(TreeItem("Items")) {
    column<String, String>("Type", { it.value.valueProperty() })
    populate {
        if (it.value == "Items") tableData.keys
        else tableData[it.value]?.asList()
    }
}

DataGrid

A DataGrid is similar to the GridPane in that it displays items in a flexible grid of rows and columns, but the similarities ends there. While the GridPane requires you to add Nodes to the children list, the DataGrid is data driven in the same way as TableView and ListView. You supply it with a list of items and tell it how to convert those children to a graphical representation.

It supports selection of either a single item or multiple items at a time so it can be used as for example the display of an image viewer or other components where you want a visual representation of the underlying data. Usage wise it is close to a ListView, but you can create an arbitrary scene graph inside each cell so it is easy to visualize multiple properties for each item.

val kittens = listOf("http://i.imgur.com/DuFZ6PQb.jpg", "http://i.imgur.com/o2QoeNnb.jpg") // more items here

datagrid(kittens) {
    cellCache {
         imageview(it)
    }
}

Figure 5.8

The cellCache function receives each item in the list, and since we used a list of Strings in our example, we simply pass that string to the imageview() builder to create an ImageView inside each table cell. It is important to call the cellCache function instead of the cellFormat function to avoid recreating the images every time the DataGrid redraws. It will reuse the items.

Let's create a scene graph that is a little bit more involved, and also change the default size of each cell:

val numbers = (1..10).toList()

datagrid(numbers) {
    cellHeight = 75.0
    cellWidth = 75.0

    multiSelect = true

    cellCache {
        stackpane {
            circle(radius = 25.0) {
                fill = Color.FORESTGREEN
            }
            label(it.toString())
        }
    }
}

Figure 5.9

The grid is supplied with a list of numbers this time. We start by specifying a cell height and width of 75 pixels, half of the default size. We also configure multi select to be able to select more than a single element. This is a shortcut of writing selectionModel.selectionMode = SelectionMode.MULTIPLE via an extension property. We create a StackPane that stacks a Label on top of a Circle.

You might wonder why the label got so big and bold by default. This is coming from the default stylesheet. The stylesheet is a good starting point for further customization. All properties of the data grid can be configured in code as well as in CSS, and the stylesheet lists all possible style properties.

The number list showcased multiple selection. When a cell is selected, it receives the CSS pseudo class of selected. By default it will behave mostly like a ListView row with regards to selection styles. You can access the selectionModel of the data grid to listen for selection changes, see what items are selected etc.

Summary

Functional constructs work well with data controls like TableView, TreeView, and others we have seen in this chapter. Using the builder patterns, you can quickly and functionally declare how data is displayed.

In Chapter 7, we will embed controls in layouts to create more complex UI's easily.

results matching ""

    No results matching ""