TornadoFX runAsync with TaskStatus (screencast)

During yesterday's live hacking session it became apparent that TornadoFX was missing an important feature when executing long running tasks using the runAsync helper. In short, there was no way to access the underlying Task object, so you couldn't update the message and progress of your task. The solution was to manually implement the Task class and manage it the standard JavaFX way. We always take great care to not hide any parts of the JavaFX API, so this was relatively undramatic. The code in question looked like this:

val t = object : Task<Unit>() {
    override fun call() {
        updateMessage("Loading customers")
        updateProgress(0.4, 1.0)
        customerModel.loadCustomers()
    }

    override fun succeeded() {
        customers.set(customerModel.customers.observable())
    }
}

taskRunning.bind(t.runningProperty())
taskProgress.bind(t.progressProperty())
taskMessage.bind(t.messageProperty())

Thread(t).start()

The amount of boilerplate isn't staggering, and it's pretty easy to reason about what's going on, but not being able to use runAsync was painful, atleast for me. I've since changed the scope of runAsync so it has full access to the underlying Task object. I went one step further and introduced a TaskStatus helper to greatly reduce the boiler plate included to actually maintain and display the status of your task. The result can be seen in today's screencast:

The code you saw above the video was converted into this:

runAsync {
    updateMessage("Loading customers")
    updateProgress(0.4, 1.0)
    customerModel.loadCustomers()
} ui {
    customers.set(customerModel.customers.observable())
}

For completeness, the code for the whole application is provided below.

import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.ObservableList
import javafx.scene.Scene
import tornadofx.*
import javax.json.JsonObject

class Customer : JsonModel {
    val lastNameProperty = SimpleStringProperty()
    var lastName by lastNameProperty

    val firstNameProperty = SimpleStringProperty()
    var firstName by firstNameProperty

    val idProperty = SimpleIntegerProperty()
    var id by idProperty

    override fun toString() = "$firstName $lastName"

    override fun updateModel(json: JsonObject) = with(json) {
        id = getInt("id")
        firstName = getString("firstName")
        lastName = getString("lastName")
    }
}

class MainView : View("Customer App") {
    val status: TaskStatus by inject()

    override val root = vbox {
        splitpane {
            add(CustomerListFragment::class)
            add(CustomerDetailsFragment::class)
            paddingAll = 4
        }

        hbox(4.0) {
            progressbar(status.progress)
            label(status.message)
            visibleWhen { status.running }
            paddingAll = 4
        }
    }
}

class CustomerListFragment : Fragment() {
    val customerViewModel: CustomerViewModel by inject()

    override val root = vbox(4.0) {
        button("Refresh") {
            setOnAction { refresh() }
        }
        listview<Customer> {
            itemsProperty().bind(customerViewModel.customers)
            bindSelected(customerViewModel)
        }

        paddingAll = 10
    }

    fun refresh() {
        customerViewModel.refresh()
    }

}

class CustomerDetailsFragment : Fragment() {
    val customerViewModel: CustomerViewModel by inject()

    override val root = vbox {
        label("First Name")
        textfield(customerViewModel.selectedFirstName)
        label("Last Name")
        textfield(customerViewModel.selectedLastName)
        paddingAll = 10.0
    }
}

class CustomerViewModel : ItemViewModel<Customer>() {
    val customerModel: CustomerModel by inject()
    val customers = SimpleObjectProperty<ObservableList<Customer>>()
    val selectedFirstName = bind { item?.firstNameProperty }
    val selectedLastName = bind { item?.lastNameProperty }

    fun refresh() {
        runAsync {
            updateMessage("Loading customers")
            updateProgress(0.4, 1.0)
            customerModel.loadCustomers()
        } ui {
            customers.set(customerModel.customers.observable())
        }
    }
}

class CustomerModel : Controller() {
    val api: Rest by inject()
    val customers = mutableListOf<Customer>()

    fun loadCustomers() {
        customers.clear()
        customers.addAll(api.get("customers.json").list().toModel())
    }
}

class CustomerApp : App(MainView::class) {
    val api: Rest by inject()

    override fun createPrimaryScene(view: UIComponent) = Scene(view.root, 568.0, 320.0)

    init {
        api.baseURI = "https://www.bekwam.net/data"
    }
}

I hope you enjoyed this improvement for runAsync, I can't wait to share it with you in the upcoming TornadoFX 1.5.10 release!

Leave a Reply

Your email address will not be published. Required fields are marked *