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!