gronostaj
gronostaj

Reputation: 2282

Label text isn't updating even though UI thread seems to be responsive

I'm building an application with TornadoFX 1.7.11 using Kotlin 1.1.51 on JDK 8u121.

I'm trying to execute a long-running task in a separate thread and show progress in the UI using a progressbar and a label. Strangely, the label isn't updating. I thought that maybe I somehow ran the task on UI thread and it's stuck, but the progressbar works and UI is otherwise responsive (controls work etc.).

Screenshot with progressbar half-filled and console messages showing that message is updated. A red arrow is pointing to a place where label should be, but no text is visible there.

I've also tried to manually edit the label with ScenicView and it worked. I'm out of ideas, can you guys help?

Here are somewhat simplified code snippets:

MainView.kt
class MainView : View("") {
    private val controller: MainController by inject()

    override val root = borderpane {
        bottom(TasksView::class)
    }

    init {
        controller.reloadTranslations().completed.onChange {
            // do some lightweight UI stuff
        }
    }
}
MainController.kt
class MainController : Controller() {
    private val tasksController: TasksController by inject()

    fun reloadTranslations(): TaskStatus {
        val task = TaskStatus()
        tasksController.tasks.add(task)
        runAsync(task) {
            updateMessage(messages["loadingTranslations"])
            BibxCache.rebuild().subscribe {
                updateMessage(messages["loadingTranslations"] + " " + it.loaded)  // for debugging
                updateProgress(it.loaded.toLong(), it.total.toLong())
            }
        }
        return task
    }

    fun getTranslations() = BibxCache.values.toSortedSet()
}
TasksView.kt
class TasksView : View() {
    override val root = vbox()

    val controller: TasksController by inject()

    init {
        controller.tasks.onChange {
            root.clear()
            controller.tasks.map { TaskRow(it) }.forEach { root.add(it) }
        }
    }
}

class TaskRow(task: TaskStatus) : HBox() {
    init {
        val progressBar = ProgressBar(task.progress.get())
        progressBar.bind(task.progress)
        val label = Label(task.message.get())
        label.bind(task.message)
        task.message.onChange { println(it) }  // for debugging
        children.addAll(
                progressBar,
                Label(task.message.get())
        )
    }
}
TasksController.kt
class TasksController : Controller() {
    val tasks: ObservableList<TaskStatus> = FXCollections.observableArrayList()

    init {
        tasks.onChange { change ->
            change.next()
            change.addedSubList.forEach { added ->
                added.completed.onChange {
                    tasks.remove(added)
                }
            }
        }
    }
}

Upvotes: 1

Views: 396

Answers (1)

Edvin Syse
Edvin Syse

Reputation: 7297

Your code isn't runnable, but I created a minimal sample based on these concepts, using a more idiomatic approach.

class MainView : View("Tasks") {
    private val mainController: MainController by inject()

    override val root = borderpane {
        setPrefSize(600.0, 400.0)
        top {
            button("Start task").action {
                mainController.reloadTranslations()
            }
        }
        bottom(TasksView::class)
    }
}

class MainController : Controller() {
    private val tasksController: TasksController by inject()

    fun reloadTranslations(): TaskStatus {
        val task = TaskStatus()
        tasksController.tasks.add(task)
        runAsync(task) {
            updateMessage(messages["loadingTranslations"] + " $this...")
            Thread.sleep(Random().nextInt(2000).toLong())
            updateMessage(messages["loadingTranslations"] + " $this - half way")
            updateProgress(50.0, 100.0)
            Thread.sleep(Random().nextInt(2000).toLong())
        }
        return task
    }
}

class TasksView : View() {
    val controller: TasksController by inject()

    override val root = vbox {
        bindChildren(controller.tasks) { task ->
            hbox {
                progressbar(task.progress)
                label(task.message)
            }
        }
    }
}

class TasksController : Controller() {
    val tasks: ObservableList<TaskStatus> = FXCollections.observableArrayList()

    init {
        tasks.onChange { change ->
            change.next()
            change.addedSubList.forEach { added ->
                added.completed.onChangeOnce {
                    tasks.remove(added)
                }
            }
        }
    }
}

This can also be done with less fanfare, but I don't know the complexity or requirements of your app, so I changed it as little as possible :)

Upvotes: 3

Related Questions