Search code examples
asynchronouskotlintornadofx

Async Loading of a TreeView


Hey I am very new to tornadofx struggeling with async loading of data for the treeview. I am loading categories from a rest endpoint, which I want to show in there.

It seems like there's no direct data binding to the children.

when using 'bindChildren' I can provide the observable list, but I have to convert them into Node's. which then would make the populate block kind of obsolete.

What's the recommended way of doing this? I cannot find anything about this.

// Category
interface Category<T : Category<T>> {
  val id: String
  val name: String
  val subcategories: List<T>?
}


//default category:
class DefaultCategory(override val name: String) : Category<DefaultCategory> {
  override val id: String = "default"
  override val subcategories: List<DefaultCategory>? = null
}
//ViewModel
class CategoryViewModel : ViewModel() {
  val sourceProperty = SimpleListProperty<Category<*>>()
  fun loadData() {
    // load items for treeview into 'newItems'
    sourceProperty.value = newItems
  }
}

// TreeViewFactoryMethod
private fun createTreeView(
  listProperty: SimpleListProperty<Category<*>>
): TreeView<Category<*>> {
  return treeview {
    root = TreeItem(DefaultCategory("Categories"))
    isShowRoot = false
    root.isExpanded = true
    root.children.forEach { it.isExpanded = true }
    cellFormat { text = it.name }
    populate { parent ->
      when (parent) {
        root -> listProperty.value
        else -> parent.value.subcategories
      }
    }
  }
}

Assuming that on a button click I call viewmodel.loadData(), I would expect the TreeView to update as soon as there's some new data. (If I would've found a way to bind)


Solution

  • I've never had to use bindChildren for TornadoFX before and your use of async isn't very relevant to what I think is your primary problem. So, admittedly, this question kind of confused me at first but I'm guessing you're just wondering why the list isn't appearing in your TreeView? I've made a test example with changes to make it work.

    // Category
    interface Category<T : Category<T>> {
        val id: String
        val name: String
        val subcategories: List<T>?
    }
    
    //default category:
    class DefaultCategory(override val name: String) : Category<DefaultCategory> {
        override val id: String = "default"
        override val subcategories: List<DefaultCategory>? = null
    }
    //Just a dummy category
    class ChildCategory(override val name: String) : Category<ChildCategory> {
        override val id = name
        override val subcategories: List<ChildCategory>? = null
    }
    
    //ViewModel
    class CategoryViewModel : ViewModel() {
        //filled with dummy data
        val sourceProperty = SimpleListProperty<Category<*>>(listOf(
                ChildCategory("Categorya"),
                ChildCategory("Categoryb"),
                ChildCategory("Categoryc"),
                ChildCategory("Categoryd")
        ).asObservable())
    
        fun loadData() {
            sourceProperty.asyncItems {
                //items grabbed somehow
                listOf(
                        ChildCategory("Category1"),
                        ChildCategory("Category2"),
                        ChildCategory("Category3"),
                        ChildCategory("Category4")
                ).asObservable()
            }
        }
    }
    
    
    class TestView : View() {
        val model: CategoryViewModel by inject()
    
        override val root = vbox(10) {
            button("Refresh Items").action {
                model.loadData()
            }
            add(createTreeView(model.sourceProperty))
        }
    
        // TreeViewFactoryMethod
        private fun createTreeView(
                listProperty: SimpleListProperty<Category<*>>
        ): TreeView<Category<*>> {
            return treeview {
                root = TreeItem(DefaultCategory("Categories"))
                isShowRoot = false
                root.isExpanded = true
                root.children.forEach { it.isExpanded = true }
                cellFormat { text = it.name }
                populate { parent ->
                    when (parent) {
                        root -> listProperty
                        else -> parent.value.subcategories
                    }
                }
            }
        }
    }
    

    There are 2 important distinctions that are important.

    1. The more relevant distinction is that inside the populate block, root -> listProperty is used instead of root.listProperty.value. This will make your list appear. The reason is that a SimpleListProperty is not a list, it holds a list. So, yes, passing in a plain list is perfectly valid (like how you passed in the value of the list property). But now that means the tree view isn't listening to your property, just the list you passed in. With that in mind, I would be considerate over the categories' subcategory lists are implemented as well.

    2. Secondly, notice the use of asyncItems in the ViewModel. This will perform whatever task asynchronously, then set the items to list on success. You can even add fail or cancel blocks to it. I'd recommend using this, as long/intensive operations aren't supposed to be performed on the UI thread.