为什么 runAsync 导致 Java IllegalStateException?

Why is runAsync causing a Java IllegalStateException?

我一直在尝试学习如何将 TornadoFX 与 Kotlin 一起使用,但总体上对 JavaFX 的使用经验很少。 我一直在学习教程 here,并且我到达了演示如何使用 runAsync 块的部分。 教程中的例子只是一个代码片段:

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

好吧,我决定尝试使用以下代码来实现它:

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;
    }
}

但出于某种原因,当我单击按钮时,这会引发 java IllegalStateException:

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)

(下面是完整的错误)

我已经尝试了所有我能想到的搜索来尝试找到发生这种情况的原因的答案。我尝试在 try/catch 块中捕获错误,但似乎没有任何效果。这里有什么问题以及如何让异步按钮事件起作用? 我正在使用 JDK8 和 Kotlin。

预先感谢您的帮助!

完整错误:

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.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(BehaviorSkinBase.java:197)
    at com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler.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.invoke(AsyncView.kt:21)
    at AsyncView$root.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

答案:
您需要了解 3 件事:
1. Kotlin 简化 Java Get/Set - 这个很简单。 Kotlin 做了一件很棒的事情,如果它认识到它正在使用 Java class 并且 class 中的成员具有 setter/getter 函数,它将允许您访问这些成员就好像它是直接引用一样。例如 button.setText("some text"); 现在变成 button.text = "some text".

2. TornadoFX 生成器函数 - 假设您将鼠标悬停在 IDE 中的 form 上。您会注意到该函数的参数中包含 op: Form.() → Unit。这意味着当您将括号附加到 form 时,如下所示:

field {
  //here is some code
}

那么这些括号内的任何内容都会将接收器更改为创建的 Form 而不是 AsyncView。因此,当您在 textfield(text) 中提到 text 时,您最终引用了属于 Field 对象的文本。当您编写 controller.performWrite(text) 时,您最终引用了属于 Button 对象的文本,依此类推。

3. Kotlin Block Scopes - 如上所述,块可以更改其接收者。但是,它不会阻止您在自身之外引用 members/functions。您只是碰巧将其命名为相同的名称,并且引用的优先级导致了问题。您可以通过简单地将文本成员更改为不同的名称来解决此问题,或者:

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()
                    }
                }
            }
        }
    }

this 使用显式标记。

改进:
请看看我所有的评论。这仍然没有涵盖您可以使用 TornadoFX 和 Kotlin 的强大功能做的所有事情,但这只是一个开始。另外:删除所有那些分号!!!

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
    }
}