首页 > 解决方案 > 为什么 runAsync 会导致 Java IllegalStateException?

问题描述

我一直在尝试学习如何将 TornadoFX 与 Kotlin 一起使用,并且通常对 JavaFX 的经验很少。我一直在学习这里的教程,并进入了演示如何使用 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$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

标签: user-interfaceasynchronouskotlintornadofx

解决方案


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

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

field {
  //here is some code
}

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

3. Kotlin Block Scopes - 就像上面提到的,块可以有能力改变它的接收者。但是,它不会阻止您引用自身之外的成员/函数。您只是碰巧将其命名为相同,并且参考的优先级导致了问题。您可以通过简单地将您的文本成员更改为不同的名称来解决此问题,或者:

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

推荐阅读