bekwam courses

Cancelling a Long-Running Operation in TornadoFX

July 22, 2018

In TornadoFX, it's important to execute long-running operations off the JavaFX Thread. Such operations should be cancellable. This article presents an image fetcher that gives the user the ability to cancel the fetching.

This screenshot shows the application immediately after the Fetch Images Button is pressed. The Fetch Images Button is disabled and controls on a lower panel are displayed: a ProgressBar, a message Label, and a cancel Button. As the fetching proceeds, the message Label is updated with the relative URL being retrieved.

UI Showing Message Label with URL Being Loaded

App in Mid-Fetch

If the user presses the Cancel Button, the fetching stops on the next iteration. An Alert is displayed indicating that the operation was cancelled.

UI Showing Alert Indicating Cancel
App with Fetch Cancelled

This next screenshot shows a successful run. Several images were loaded in scrollable FlowPane.

UI Showing Several Images Loaded
Images Fetched

Code

The application is a single TornadoFX View. At the start of the class definition, there are several URLs in a List and a BASE_URL constant. The fetch operation iterates over these.

	
class CancelView : View("Cancel App") {

	val URL_BASE = "https://courses.bekwam.net/public_tutorials/images/"

	val imageList = listOf(
			"bkcourse_rc_charging.png",
			"bkcourse_rc_circuit.JPG",
			"bkcourse_rc_circuit_annotated.png",
			"bkcourse_rc_circuit_schem.png",
			"bkcourse_rc_charging.png",
			"bkcourse_greatgrid_fx.png",
			"bkcourse_greatgrid_spec.png",
			"bkcourse_greatgrid_swing.png",
			"bkcourse_flatwinapp_ea.png",
			"bkcourse_flatwinapp_final.png",
			"bkcourse_flatwinapp_initial_hier.png",
			"bkcourse_flatwinapp_mpu_gtk.png",
			"bkcourse_flatwinapp_mpu_mac.png",
			"bkcourse_flatwinapp_mpu_win.png",
			"bkcourse_flatwinapp_snagit.png",
			"bkcourse_kstudent_001_badcomment.png",
			"bkcourse_kstudent_001_mycomment.png",
			"bkcourse_kstudent_001_nocomment.png",
			"bkcourse_kstudent_001_println_docs.png",
			"bkcourse_kstudent_001_trykotlinlang.png",
			"bkcourse_kstudent_001_twoprintlns.png",
			"bkcourse_kstudent_001_twoprintlns_results.png"
	)
	
	

There is a field that is keeping track of the ImageView UI Controls which are filled with the bytes from the fetch operation. While you can add items directly to the FlowPane, it's a good programming practice to use binding and to have the fetch operation modify the children indirectly. This reduces coupling between UI controls, in this case a FlowPane and a Button action handler.

	
    val imageViews = mutableListOf<ImageView>().observable()

    val status : TaskStatus by inject()

    var runningTask : Task<MutableList<ByteArray>>? = null

status and runningTask keep track of the Task supporting the fetch. TaskStatus is a globally-available object that will be linked up to the Task created by runAsync{}. The fields of the TaskStatus object are bound to UI controls for reporting progress and messages and for hiding and disabling controls. runningTask is a handle -- possibly null -- to the running Task. It's used to execute a cancel() command on the Task.

The View's Fetch Button contains the code for executing the fetch. The Button is disabled when the status object indicates that a Task is running. The runAsync{} function is invoked when the Button is pressed. runAsync iterates through the list of URLs and retrieves the bytes of each images, storing them in a list imageBytesList. As each iteration proceeds, the progress and message are updated. Most important to this post, there is an isCancelled check prior to the start of a retrieval. This is a method of the Task class.


  override val root = vbox {

        vbox {
            button("Fetch Images") {
                disableWhen { status.running }
                action {
                    runningTask = runAsync {
                        val imageBytesList = mutableListOf<ByteArray>()
                        for( (index,url) in imageList.withIndex() ) {
                            updateMessage("Loading $url...")
                            updateProgress( (index+1.0)/imageList.size, 1.0 )
                            if( !isCancelled ) {
                                imageBytesList.add(getBytes(URL_BASE + url))
                            }
                        }
                        imageBytesList
                    } ui {
                        addImages(it)
                    } fail {

                        val alert = Alert(Alert.AlertType.ERROR, it.message )
                        alert.headerText = "Error Loading Images"
                        alert.showAndWait()

                    } cancel {

                        val alert = Alert(Alert.AlertType.INFORMATION, "Operation Cancelled" )
                        alert.headerText = "Loading Images"
                        alert.showAndWait()

                    }
                }
            }
			

Note that there is no interrupt given when the operation is mid-retrieval. This means that you may have to rework code such that the iteration is available in the Task. For example, if your retrieval code is a one-liner that gets all of the data in a single call, isCancelled won't be of any use.

This UML Activity Diagram shows the iteration and logic of the runAsync{} alongside an unspecified "Process Other Events" Activity. The thick bars show concurrency. Start Task is divided into two parallel streams of execution. On the right is the Thread supporting runAsync{} and on the left is the FX Thread which continues to process the UI events. Another thick bar brings the two parallel streams together. This allows the independent Task to update the UI which always must occur on the FX Thread.

UML Activity Diagram
Two Threads of Execution

runAsync{} returns imageBytesList which is a list of ByteArrays. It's critical to leave out any FX code from the body of a runAsync{}. updateMessage(), updateProgress(), and updateTitle() are the only reliable ways to get feedback to the user. If I need more feedback during the operation, I use multiple Tasks (runAsync{} calls)).

There is a partial results technique for framing UI updates with runLater() inside of a runAsync{} presented in the JavaFX Task Javadoc. I've found this sometimes doesn't work. Once I coded a call() filled with runLaters().  Executing the Task deferred the runLaters() until the end of the operation. They all executed quickly at once at the end of the operation, making it in effect a success() call and no partial results were given.

ui, fail, and cancel are three extension functions tied to the outcome of the runAsync{} function. ui{} is called if the operation proceeds without an error. Within ui{}, "it" is the returned value, possible Void. In this case, ui{} updates the UI with the list of ByteArrays converted into ImageViews.

fail{} is called if the operation throws an exception. The "it" in fail{} is a Throwable. cancel{} is invoked if the user called the function "cancel" on the Task. fail{} and cancel{} display Alerts. cancel{} has only recently been added to the TornadoFX toolkit (PR# 758). A patch for earlier versions is included at the end of this article.

The second half of the View root is a FlowPane that binds its children to the previously-mentioned list of ImageViews. An HBox at the base of the screen is visible only when the Task is running. The HBox contains ProgressBar, Label, and Button controls. pressing the Cancel Button executes doCancel().


        scrollpane {
                flowpane {
                    children.bind( imageViews, { it } )

                    prefWidth = 667.0
                    prefHeight = 336.0

                    vgap = 10.0
                    hgap = 10.0
                }
                vgrow = Priority.ALWAYS
                padding = Insets(10.0)
            }

            vgrow = Priority.ALWAYS

            spacing = 10.0
            padding = Insets(10.0)
        }
        separator()
        hbox {
            visibleProperty().bind( status.running )
            progressbar(status.progress)
            button("Cancel") {
                action {
                    doCancel()
                }
            }
            label(status.message )

            alignment = Pos.CENTER_LEFT
            spacing = 4.0

            padding = Insets(0.0, 10.0, 10.0, 10.0)
        }

        spacing = 10.0

        prefWidth = 736.0
        prefHeight = 414.0
    }
	

doCancel() issues a cancel() command to the Task, if it is running.


private fun doCancel() {
        if( runningTask != null && runningTask!!.isRunning() ) {
            runningTask!!.cancel()
        }
    }

getBytes() retrieves the images as a list of ByteArrays. Because I'm running this retrieval off the JavaFX Thread, I'm forgoing the convenient constructors available in the Image and the ImageView classes. Manipulating these classes requires running on the JavaFX Thread. You may find that you have to segment your code like this if you're converting from a non-threaded implementation. That is, if you have an existing method that blends the retrieval with the UI update -- including creating objects like ImageView that aren't shown -- you'll need to factor out code that operates without JavaFX for the runAsync{} method.


private fun getBytes(url : String) : ByteArray {

        val BUF_SIZE = 100_000 // 100k
        val byteArray = ByteArray(BUF_SIZE)
        val baos = ByteArrayOutputStream()
        val byteStream = URL(url).openStream()

        try {
            var nbytes = byteStream.read(byteArray)
            baos.write(byteArray, 0, nbytes)

            while (nbytes > 0) {
                nbytes = byteStream.read(byteArray)
                if( nbytes > 0 ) {
                    baos.write(byteArray, 0, nbytes)
                }
            }

            return baos.toByteArray()

        } finally {
            byteStream.close()
            baos.close()
        }
    }
	

addImages() updates the UI from a list of ByteArrays. This is the action of the Task success() function. success() runs on the FX Thread, so this call is safe.


private fun addImages(imageBytesList : List<ByteArray>) {

        val images = imageBytesList.map {
            Image( ByteArrayInputStream(it), 200.0, 200.0, true, true )
        }

        imageViews.clear()
        imageViews.addAll( images.map { ImageView(it) })
    }
	

Patch

If you're running on a TornadoFX version earlier than 1.17, you'll need the following patch which you can include in your application files. Like the existing success() and fail() functions, this adds a handler to missing WorkStateEvent "CANCELLED".


// patch
infix fun <T> Task<T>.cancel(func: () -> Unit) = apply {
    fun attachCancelHandler() {
        if (state == Worker.State.CANCELLED) {
            func()
        } else {
            setOnCancelled {
                func()
            }
        }
    }

    if (Application.isEventThread())
        attachCancelHandler()
    else
        runLater { attachCancelHandler() }
}

This article presented a technique for cancelling a long-running operation with a digression into general Task coding. To use a cancel, your code must be structured to expose iteration to the runAsync{} function. There is no concept of an interrupt in Task, so if you're integrating a single line of code, you'll need to either break this up or look for an alternative cancel.

There is trivial App class and a trivial main function that are included in the source zip.

Resources

The source code presented in this video series is a Gradle project that can be imported into your IDE.

TaskDemos Source Zip (4Kb)
Headshot of Carl Walker

By Carl Walker

President and Principal Consultant of Bekwam, Inc