Search code examples
user-interfaceasynchronouskotlintornadofx

Why is runAsync causing a Java IllegalStateException?


I've been trying to learn how to use TornadoFX with Kotlin and have very little experience with JavaFX in general. I've been following the tutorial here and I got to the part where it demonstrates how to use the runAsync block. The example in the tutorial is just a code fragment:

val textfield = textfield()
button("Update text") {
    action {
        runAsync {
            myController.loadText()
        } ui { loadedText ->
            textfield.text = loadedText
        }
    }
}

Well I decided to try to implement it with the following code:

class AsyncView : View(){
    val text = SimpleStringProperty();
    val llabel = SimpleStringProperty("No Commit");
    val controller: AsyncController by inject();

    override val root = form {
        fieldset {
            field("Current Input"){
                textfield(text);
            }
            label(llabel)
            button("Commit") {
                action {
                    runAsync {
                        controller.performWrite(text);
                        text = "";
                    } ui {
                        llabel.value = controller.getValue();
                    }
                }
            }
        }
    }
}

class AsyncController: Controller() {
    private var MyValue: String = "";
    fun performWrite(inputValue: String){
        MyValue = inputValue;
    }

    fun getValue(): String {
        return MyValue;
    }
}

But for some reason this throws a java IllegalStateException when I click on the button:

SEVERE: Uncaught error
java.lang.IllegalStateException: Not on FX application thread; currentThread = tornadofx-thread-1
    at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:236)

(full error below)

I've tried every search I could think of to try and get an answer to why this is happening. I tried catching the error in a try/catch block but nothing seems to be working. What's wrong here and how do I get an async button event to work? I'm using JDK8 with Kotlin.

Thank you in advance for your help!

Full Error:

Mar 04, 2020 1:39:37 PM tornadofx.DefaultErrorHandler uncaughtException
SEVERE: Uncaught error
java.lang.IllegalStateException: Not on FX application thread; currentThread = tornadofx-thread-1
    at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:236)
    at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
    at javafx.scene.Parent$2.onProposedChange(Parent.java:367)
    at com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:113)
    at com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:108)
    at com.sun.javafx.scene.control.skin.LabeledSkinBase.updateChildren(LabeledSkinBase.java:575)
    at com.sun.javafx.scene.control.skin.LabeledSkinBase.handleControlPropertyChanged(LabeledSkinBase.java:204)
    at com.sun.javafx.scene.control.skin.ButtonSkin.handleControlPropertyChanged(ButtonSkin.java:71)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase.lambda$registerChangeListener$61(BehaviorSkinBase.java:197)
    at com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler$1.changed(MultiplePropertyChangeListenerHandler.java:55)
    at javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:89)
    at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:182)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.beans.property.StringPropertyBase.fireValueChangedEvent(StringPropertyBase.java:103)
    at javafx.beans.property.StringPropertyBase.markInvalid(StringPropertyBase.java:110)
    at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:144)
    at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:49)
    at javafx.beans.property.StringProperty.setValue(StringProperty.java:65)
    at javafx.scene.control.Labeled.setText(Labeled.java:145)
    at AsyncView$root$1$1$2$1$1.invoke(AsyncView.kt:21)
    at AsyncView$root$1$1$2$1$1.invoke(AsyncView.kt:6)
    at tornadofx.FXTask.call(Async.kt:457)
    at javafx.concurrent.Task$TaskCallable.call(Task.java:1423)
    at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
    at java.util.concurrent.FutureTask.run(FutureTask.java)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

Disconnected from the target VM, address: '127.0.0.1:55193', transport: 'socket'

Process finished with exit code 0

Solution

  • The Answer:
    There are 3 things you need to understand:
    1. Kotlin Simplifies Java Get/Set - This one is simple. Kotlin does this wonderful thing where if it recognizes that it's using a Java class and that a member within that class has a setter/getter functions, it will let you access those members as if it was a direct reference. So for example button.setText("some text"); now becomes button.text = "some text".

    2. TornadoFX builder functions - Let's say you hover over form in your IDE. You'll notice that the function includes op: Form.() → Unit in its parameters. This means that when you attach brackets to form, like this:

    field {
      //here is some code
    }
    

    then anything inside those brackets will change the receiver to the created Form instead of AsyncView. So when you mentioned text in textfield(text), you ended up referencing the text belonging to the Field object. When you wrote controller.performWrite(text) you ended up referencing the text belonging to the Button object, and so on.

    3. Kotlin Block Scopes - Like mentioned above, blocks can have the ability to change it's receiver. However, it doesn't block you from referencing members/functions outside of itself. You just happened to name it the same and the priority of reference caused the issue. You can fix this by simply changing your text member to a different name, or:

    override val root = form {
            fieldset {
                field("Current Input") {
                    textfield(this@AsyncView.text)
                }
                label(llabel)
                button("Commit") {
                    action {
                        runAsync {
                            controller.performWrite(this@AsyncView.text.value)
                            this@AsyncView.text.value = ""
                        } ui {
                            llabel.value = controller.getValue()
                        }
                    }
                }
            }
        }
    

    Use explicit tagging for this.

    The Improvement:
    PLEASE look at all of my comments. This still doesn't cover all you can do with the power of TornadoFX and Kotlin but it's a start. Also: remove all those semicolons!!!

    class AsyncView : View() {
        val controller: AsyncController by inject()
    
        val inputProperty = SimpleStringProperty() //Name is descriptive and appropriate to its role
        var input by inputProperty //TornadoFX-unique way to get/set property values
    
        val valueLabelTextProperty = SimpleStringProperty("No Commit")  //Name is descriptive and appropriate to its role
        var valueLabelText by valueLabelTextProperty
    
        override val root = form {
            fieldset {
                field("Current Input") {
                    textfield(inputProperty)
                }
                label(valueLabelTextProperty)
                button("Commit") {
                    action {
                        runAsync {
                            controller.performWrite(input)
                            input = ""
                            controller.myValue //The last line's value gets passed to success block. Leave as little work to UI as possible
                        } success { value -> // ui is only included for backwards compatibility. success is replacement.
                            valueLabelText = value
                        }
                    }
                }
            }
        }
    }
    
    class AsyncController : Controller() {
        var myValue: String = "" //Naming should be camel-cased
            private set //No need for old-school Java getters/setters. Simply private the set. Look into Kotlin get/set for more info
    
        //If you do not plan to do more than change `myValue` in the future with this method,
        //delete it and remove private set from `myValue`. You can use custom Kotlin getters/setters instead.
        fun performWrite(inputValue: String) {
            myValue = inputValue
        }
    }