January 28, 2018
This article demonstrates polling in a JavaFX UI using the TornadoFX framework. Polling is implemented using a scheduled background task. When the scheduled event comes due, a web site is contacted and the HTML contents displayed in a TextArea. The time at which the capture was made is also displayed.
Polling is needed to make sure that the UI stays lively while retrieving data. While it's conceptually easier to write an infinite loop that retrieves the data and makes Java FX Thread-safe runLater() calls, this can sometimes lead to undesirable behavior. The busy loop can be problematic from a responsiveness standpoint. Even if you aren't making call after call -- you have some sort of concurrency control -- this can slow down the app. Additionally, you may want the runLaters() to be called steadily as you process input. I've seen case where the runLaters() start to bunch up and get run all at once; this is allowed by design.
This is not an example of progress. If you want to show the state of a long-running operation, I've found updateMessage() and updateProgress() to be reliable. This pattern is focusing on the ongoing and infinite processing of data. Hence the returned transport object which you'll see in the ScheduledService and Task definitions.
The following video demonstrates the polling. The app is started and the user presses the Start Button. This creates starts a pre-created JavaFX ScheduledService set to tun at the specified poll interval of 2 seconds. ScheduledService creates a Java FX Task and related Thread. The Task uses the TornadoFX RESTful API to scrape a web page. When the Task is finished, the HTML contents and a timestamp are shown on the UI.
The program uses a Controller and a View. The View code contains a TextArea for the HTML contents and buttons for starting and stopping the polling. The update time is displayed in a Label in the lower-right corner of the screen.
class PollView : View("Poll App") {
val controller : PollController by inject()
override val root = vbox {
label(controller.url)
textarea(controller.currentData) {
vgrow = Priority.ALWAYS
}
hbox {
button("Start") {
disableProperty().bind( controller.stopped.not() )
setOnAction {
controller.start()
}
}
button("Stop") {
disableProperty().bind( controller.stopped )
setOnAction {
controller.stop()
}
}
button("Exit") {
setOnAction {
Platform.exit()
}
}
hbox {
label {
bind(
SimpleStringProperty("Last Updated: ")
.concat( controller.lastUpdated)
)
}
alignment = Pos.CENTER_RIGHT
hgrow = Priority.ALWAYS
}
spacing = 4.0
padding = Insets(4.0)
}
padding = Insets(10.0)
spacing = 10.0
}
}
The Button commands in the View, Start and Stop, delegate to the Controller. The View Label and TextArea controls bind to properties set in the Controller.
The View references a singleton Controller. The Controller contains several properties to support the UI as well as the SchedulerService object and a definition for the JavaFX Task that will execute the web page retrieval and update its properties. Through binding, the View will receive updates to these properties automatically.
const val POLL_INTERVAL = 2.0
data class DataResult(val data : String, val lastRetrieve : LocalDateTime)
class PollController : Controller() {
val api : Rest by inject()
val currentData = SimpleStringProperty()
val stopped = SimpleBooleanProperty(true)
val url = SimpleStringProperty(api.baseURI)
val lastUpdated = SimpleStringProperty("")
val scheduledService = object : ScheduledService<DataResult>() {
init {
period = Duration.seconds(POLL_INTERVAL)
}
override fun createTask() : Task<DataResult> = FetchDataTask()
}
fun start() {
scheduledService.restart()
stopped.value = false
}
fun stop() {
scheduledService.cancel()
stopped.value = true
}
inner class FetchDataTask : Task<DataResult>() {
override fun call(): DataResult {
val htmlBody = api.get("/") // fake "recv"
return DataResult(htmlBody.text()!!, LocalDateTime.now())
}
override fun succeeded() {
println("data updated: " + value.lastRetrieve)
lastUpdated.value = value.lastRetrieve.format(DateTimeFormatter.ISO_LOCAL_TIME)
this@PollController.currentData.value = value.data
}
override fun failed() {
println("failed retrieval")
exception.printStackTrace()
}
}
}
FetchDataTask is a JavaFX Task that does an HTTP GET on the website (https://www.bekwam.com) through the TornadoFX REST API. When the API GET returns, it fills a transport structure called DataResult. DataResult is the return value of the FetchDataTask. When the Task completes, the LocalDateTime captured in the call() method is returned as are the contents of the HTML document. A failure results in print statements.
DataResult is referenced again in the ScheduledService declaration. This declaration creates an object of ScheduledService which delegates its Task-building requirement to FetchDataTask. The period is set to a constant. The Start Button invokes the restart() method of the already-created ScheduledService (start() will only work once; you can't start() a stopped ScheduledService). The Stop Button stops the updates.
The App subclass and main can be found in the source .zip referenced below.
The source code presented in this article series is a Gradle project found in the zip file below.
To run the demo, create an Application configuration in your IDE that will run the MovingApp class. In IntellIJ,
By Carl Walker
President and Principal Consultant of Bekwam, Inc