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)
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.
Related
I have a spinner with an array adapter. The spinner is populated inside a fragment onCreateView().
spinner.setSelection(0)
spinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
}
override fun onNothingSelected(var1: AdapterView<*>?) {
}
}
Whenever I get back to the fragment and the spinner is created, the last selected item is selected when onItemSelected() is called automatically and ignoring the spinner.setSelection(0) call.
I have put many logs to see what is going, but I cannot understand why the lately selected item is the one being selected by default and not the one at position 0.
I solved the issue by setting a click listener on the drop down view and basically do the same stuff I was doing with the OnItemSelectedListener.
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = SpinnerItemChartDropdownBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val item = getItem(position)
val root = binding.root
bindDropdown(root, item)
binding.setClickListener {
listener.onChartRangeSelected(item)
}
return root
}
One important stuff. You need to do something like this, to dismiss the drop down view after an item has been selected:
fun hideSpinnerDropDown(spinner: Spinner) {
try {
val method: Method = Spinner::class.java.getDeclaredMethod("onDetachedFromWindow")
method.isAccessible = true
method.invoke(spinner)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
I'm trying to build an app (macOS, but would be the same for iOS) that creates a number of grids, the outcome of which is to be shown in a second screen. For this, I'm sharing data across these screens, and I'm running into an issue here, I hope someone can help or point me in the right direction. I'll share a simplified version of the code below (working in Xcode 14.0.1)
The code creates a dictionary that can be shown in a grid, on which calculations can be done. The idea is then to add this grid, with some descriptive variables, into another dictionary
The building blocks of the grid are cells
Import Foundation
struct Cell: Comparable, Equatable, Identifiable, Hashable {
static func == (lhs: Cell, rhs: Cell) -> Bool {
lhs.randomVarOne == rhs.randomVarOne
}
var randomVarOne: Double
var randomVarTwo: Bool
// other vars omitted here
var id: Int { randomVarOne }
static func < (lhs: Cell, rhs: Cell) -> Bool {
return lhs.randomVarOne < rhs.randomVarOne
}
}
this is also where there are a bunch of funcs to calculate next neighbor cells in the grid etc
then the grid is defined in a class:
class Info: ObservableObject, Hashable {
static func == (lhs: Info, rhs: Info) -> Bool {
lhs.grid == rhs.grid
}
func hash(into hasher: inout Hasher) {
hasher.combine(grid)
}
#Published var grid = [Cell]()
var arrayTotal = 900
#Published var toBeUsedForTheGridCalculations: Double = 0.0
var toBeUsedToSetTheVarAbove: Double = 0.0
var rowTotalDouble: Double {sqrt(Double(arrayTotal)) }
var rowTotal: Int {
Int(rowTotalDouble) != 0 ? Int(rowTotalDouble) : 10 }
The class includes a func to create and populate the grid with Cells and add these Cells to the grid var. It also includes the formulas to do the calculations on the grid using a user input. The class did not seem to need an initializer.
This is the Scenario struct:
struct Scenario: Comparable, Equatable, Identifiable, Hashable {
static func == (lhs: Scenario, rhs: Scenario) -> Bool {
lhs.scenarioNumber == rhs.scenarioNumber
}
func hash(into hasher: inout Hasher) {
hasher.combine(scenarioNumber)
}
var scenarioNumber: Int
var date: Date
var thisIsOneSnapshot = [Info]()
var id: Int { scenarioNumber }
static func < (lhs: Scenario, rhs: Scenario) -> Bool {
return lhs.scenarioNumber < rhs.scenarioNumber
}
}
added hashable since it uses the Info class as an input.
Then there is the class showing the output overview
class OutputOverview: ObservableObject {
#Published var snapshot = [Scenario]()
// the class includes a formula of how to add the collection of cells (grid) and the additional variables to the snapshot dictionary. Again no initializer was necessary.
Now to go to the ContentView.
struct ContentView: View {
#Environment(\.openURL) var openURL
var scenarioNumberInput: Int = 0
var timeStampAssigned: Date = Date.now
#ObservedObject private var currentGrid: Info = Info()
#ObservedObject private var scenarios: Combinations = Combinations()
var usedForTheCalculations: Double = 0.0
var rows =
[
GridItem(.flexible()),
// whole list of GridItems, I do not know how to calculate these:
// var rows = Array(repeating: GridItem(.flexible()), count: currentGrid.rowTotal)
//gives error "Cannot use instance member 'currentGrid' within property initializer;
// property iunitializers run before 'self' is available
]
var body: some View {
GeometryReader { geometry in
VStack {
ScrollView {
LazyHGrid(rows: rows, spacing: 0) {
ForEach(0..<currentGrid.grid.count, id :\.self) { w in
let temp = currentGrid.grid[w].varThatAffectsFontColor
let temp2 = currentGrid.grid[w].varThatAffectsBackground
Text("\(currentGrid.grid[w].randomVarOne, specifier: "%.2f")")
.frame(width: 25, height: 25)
.border(.black)
.font(.system(size: 7))
.foregroundColor(Color(wordName: temp))
.background(Color(wordName: temp2))
}
}
.padding(.top)
}
VStack{
HStack {
Button("Start") {
}
// then some buttons to do the calculations
Button("Add to collection"){
scenarios.addScenario(numbering: scenarioNumberInput, timeStamp:
Date.now, collection: currentGrid.grid)
} // this should add the newly recalculated grid to the dictionary
Button("Go to Results") {
guard let url = URL(string: "myapp://scenario") else { return }
openURL(url)
} // to go to the screen showing the scenarios
Then the second View, the ScenarioView:
struct ScenarioView: View {
#State var selectedScenario = 1
#ObservedObject private var scenarios: OutputOverview
var pickerNumbers = [ 1, 2, 3, 4 , 5]
// this is to be linked to the number of scenarios completed,this code is not done yet.
var rows =
[
GridItem(.flexible()),
GridItem(.flexible()),
// similar list of GridItems here....
var body: some View {
Form {
Section {
Picker("Select a scenario", selection: $selectedScenario) {
ForEach(pickerNumbers, id: \.self) {
Text("\($0)")
}
}
}
Section {
ScrollView {
if let idx = scenarios.snapshot.firstIndex(where:
{$0.scenarioNumber == selectedScenario}) {
LazyHGrid(rows: rows, spacing: 0) {
ForEach(0..<scenarios.snapshot[idx].thisIsOneSnapshot.count,
id :\.self) { w in
let temp =
scenarios.snapshot[idx].thisIsOneSnapshot[w].varThatAffectsFontColor
let temp2 =
scenarios.snapshot[idx].thisIsOneSnapshot[w].varThatAffectsBackground
Text("\(scenarios.snapshot[idx].thisIsOneSnapshot[w].randomVarOne, specifier: "%.2f")")
.frame(width: 25, height: 25)
.border(.black)
.font(.system(size: 7))
.foregroundColor(Color(wordName: temp))
.background(Color(wordName: temp2))
}
}
}
}
}
}
}
}
Now while the above does not (for the moment..) give me error messages, I am not able to run the PreviewProvider in the second View. The main problem is in #main:
import SwiftUI
#main
struct ThisIsTheNameOfMyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.handlesExternalEvents(matching: ["main"])
WindowGroup("Scenarios") {
ScenarioView()
// error messages here: 'ScenarioView' initializer is inaccessible due to "private"
// protection level - I don't know what is set to private in ScenarioView that could
// cause this
// second error message: missing argument for parameter 'scenarios' in call
}
.handlesExternalEvents(matching: ["scenario"])
}
}
I am at a loss on how to solve these 2 error messages and would be very grateful for any tips or guidance. Apologies if this question is very long, I scanned many other forum questions and could not find any good answers.
I have tried adding pro forma data in #main as follows
#main
struct FloodModelScenarioViewerApp: App {
#State var scenarios = Scenario(scenarioNumber: 1, date: Date.now)
var body: some Scene {
WindowGroup {
ContentView()
}
.handlesExternalEvents(matching: ["main"])
WindowGroup("Scenarios") {
ScenarioView(scenarios: scenarios)
}
.handlesExternalEvents(matching: ["scenario"])
}
}
This still gives 2 error messages:
same issue with regards to ScenarioView initialiser being inaccessible due to being 'private'
Cannot convert value of type 'Scenario' to expected argument type 'OutputOverview'
Just remove the private from
#ObservedObject private var scenarios: OutputOverview
The value is coming from he parent so the parent needs access. So put
#StateObject private var scenarios: OutputOverview = .init()
in FloodModelScenarioViewerApp
#StateObject is for initializing ObservableObjects and #ObservedObject is for passing them around.
I don't know if your code will work after you read this question, and that's because there are many things to correct, but you can start with these:
In Cell, you shouldn't use an id that is a variable, this may cause inconsistent behavior. Use something like:
let id = UUID()
When you initialize ContentView, you can't use currentGrid inside a variable because currentGrid will not be available before all variables are initialized. Meaning, you are trying to initialize rows before currentGrid actually exists. You can try using the .onAppear modifier:
var rows = [GridItem]()
var body: some View {
GeometryReader { geometry in
// ... view code in here
}
.onAppear {
var rows = Array(repeating: GridItem(.flexible()), count: currentGrid.rowTotal)
}
}
This creates the view and, before showing it, the grid is set to its proper value.
The message 'ScenarioView' initializer is inaccessible due to "private" protection level seems clear: you must provide a value to to the variable scenarios (it doesn't have a default value) but it's marked as private. Remove private.
#ObservedObject var scenarios: OutputOverview
Then, remember to pass a value of type OutputOverview for the variable when you call the view:
ScenarioView(scenarios: aVariableOfTypeOutputOverview)
The type mismatch error you get inside the #main code is also clear - you have defined a variable of type Scenario:
#State var scenarios = Scenario(scenarioNumber: 1, date: Date.now)
but ScenarioView requires another type:
#ObservedObject private var scenarios: OutputOverview
One of them needs change for your code to work.
I was taking a look at this :
tornadofx
and tried to expand on it with database connection and little more options, (not all of them make sense, but its just playing in a sandbox).
Even though table can be directly edited and the data will persist in database, i did try to do edit through text fields too. actual table editing would happen through different view and not table itself, as i said its just example.
Database used is Jetbrains Exposed.
object Categories : IntIdTable() {
val name = varchar("name", 64).uniqueIndex()
val description = varchar("description", 128)
}
class Category(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Category>(Categories)
var name by Categories.name
var description by Categories.description
override fun toString(): String {
return "Category(name=\"$name\", description=\"$description\")"
}
}
now controller looks something like this, functions are just rudimentary and picked as an example.
typealias ModelToDirtyState = Map.Entry<CategoryModel, TableColumnDirtyState<CategoryModel>>
class CategoryModel() : ItemViewModel<Category>() {
val name: SimpleStringProperty = bind(Category::name)
val description: SimpleStringProperty = bind(Category::description)
}
class DBController : Controller() {
val categories: ObservableList<CategoryModel> by lazy {
transaction {
SchemaUtils.create(Categories)
Category.all().map {
CategoryModel().apply {
item = it
}
}.observable()
}
}
init {
Database.connect(
"jdbc:mysql://localhost:3306/test", driver = "com.mysql.cj.jdbc.Driver",
user = "test", password = "test"
)
TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
}
fun deleteCategory(model: CategoryModel) {
runAsync {
transaction {
model.item.delete()
}
}
categories.remove(model)
}
fun updateCategory(model: CategoryModel) {
transaction {
Categories.update {
model.commit()
}
}
}
fun commitDirty(modelDirtyMappings: Sequence<ModelToDirtyState>) {
transaction {
modelDirtyMappings.filter { it.value.isDirty }.forEach {
it.key.commit()
println(it.key)// commit value to database
it.value.commit() // clear dirty state
}
}
}
Just to quickly comment on controller, delete method works as "intended" however the update one does not, it does not work in sense that after using delete item is remove both from database and tableview(underlying list) itself, and when i do update its not, now i know the reason, i call remove manually on both database and list, now for update perhaps i could do change listener, or maybe tornadofx can do this for me, i just cant set it up to do it. Following code will make things clearer i think.
class CategoryEditor : View("Categories") {
val categoryModel: CategoryModel by inject()
val dbController: DBController by inject()
var categoryTable: TableViewEditModel<CategoryModel> by singleAssign()
var categories: ObservableList<CategoryModel> by singleAssign()
override val root = borderpane {
categories = dbController.categories
center = vbox {
buttonbar {
button("Commit") {
action {
dbController.commitDirty(categoryTable.items.asSequence())
}
}
button("Roll;back") {
action {
categoryTable.rollback()
}
}
// This model only works when i use categorytable.tableview.selected item, if i use categoryModel, list gets updated but not the view itself
// Question #1 how to use just categoryModel variable without need to use categorytable.tableview.selecteditem
button("Delete ") {
action {
val model = categoryTable.tableView.selectedItem
when (model) {
null -> return#action
else -> dbController.deleteCategory(model)
}
}
}
//And here no matter what i did i could not make the view update
button("Update") {
action {
when (categoryModel) {
null -> return#action
else -> dbController.updateCategory(categoryModel)
}
categoryTable.tableView.refresh()
}
}
}
tableview<CategoryModel> {
categoryTable = editModel
items = categories
enableCellEditing()
enableDirtyTracking()
onUserSelect() {
//open a dialog
}
//DOES WORK
categoryModel.rebindOnChange(this) { selectedItem ->
item = selectedItem?.item ?: CategoryModel().item
}
// Question #2. why bindSelected does not work, and i have to do it like above
//DOES NOT WORK
// bindSelected(categoryModel)
//
column("Name", CategoryModel::name).makeEditable()
column("Description", CategoryModel::description).makeEditable()
}
}
right = form {
fieldset {
field("Name") {
textfield(categoryModel.name)
}
}
fieldset {
field("Description") {
textfield(categoryModel.description)
}
}
button("ADD CATEGORY") {
action {
dbController.addCategory(categoryModel.name.value, categoryModel.description.value)
}
}
}
}
}
I apologize for huge amount of code, also in last code snipped i left questions in form of comments where i fail to achive desired results.
I am sure i am not properly binding code, i just dont see why, also i sometimes use one variable to update data, my declared one "categoryModel" and sometimes i use tableview.selecteditem, it just seems hacky and i cant seem to grasp way.
Thank you!
I am trying to get a class to have a property bound to another class's list property, where the 1st property is derived from a summarizing calculation over the objects in the list. The code below is a simplified version of my production code. (The production code is doing a summary over DateTime objects -- the essential part of the code below is the binding between a list and an object property (here, it is a String for simplicity).)
I have tried various things. One approach was using addListener on the list in the Summary class below but I was running into weird bugs with the listener callback making updates on the Summary object. After doing a bunch of reading I think that a binding between the summary string and the list is more appropriate but I don't know exactly how to hook up the binding to the property?
package com.example.demo.view
import javafx.beans.Observable
import javafx.beans.binding.StringBinding
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleListProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import tornadofx.View
import tornadofx.button
import tornadofx.label
import tornadofx.vbox
class Thing(x: Int) {
val xProperty = SimpleIntegerProperty(x)
val yProperty = SimpleStringProperty("xyz")
}
class Collection {
private var things = FXCollections.observableList(mutableListOf<Thing>()) {
arrayOf<Observable>(it.xProperty)
}
val thingsProperty = SimpleListProperty<Thing>(things)
fun addThing(thing: Thing) {
things.add(thing)
}
}
class Summary(var collection: Collection) {
val summaryBinding = object : StringBinding() {
// The real code is more practical but
// this is just a minimal example.
override fun computeValue(): String {
val sum = collection.thingsProperty.value
.map { it.xProperty.value }
.fold(0, { total, next -> total + next })
return "There are $sum things."
}
}
// How to make this property update when collection changes?
val summaryProperty = SimpleStringProperty("There are ? things.")
}
class MainView : View() {
val summary = Summary(Collection())
override val root = vbox {
label(summary.summaryProperty)
button("Add Thing") {
summary.collection.addThing(Thing(5))
}
}
}
Keep in mind that I made this answer based on your minimal example:
class Thing(x: Int) {
val xProperty = SimpleIntegerProperty(x)
var x by xProperty
val yProperty = SimpleStringProperty("xyz")
var y by yProperty
}
class MainView : View() {
val things = FXCollections.observableList(mutableListOf<Thing>()) {
arrayOf<Observable>(it.xProperty)
}
val thingsProperty = SimpleListProperty<Thing>(things)
val totalBinding = integerBinding(listProperty) {
value.map { it.x }.fold(0, { total, next -> total + next })
}
val phraseBinding = stringBinding(totalBinding) { "There are $value things." }
override val root = vbox {
label(phraseBinding)
button("Add Thing") {
action {
list.add(Thing(5))
}
}
}
}
I removed your other classes because I didn't see a reason for them based on the example. If the collection class has more functionality than holding a list property in your real project, then add just add it back in. If not, then there's no reason to give a list its own class. The summary class is really just two bindings (or one if you have no need to separate the total from the phrase). I don't see the need to give them their own class either unless you plan on using them in multiple views.
I think your biggest problem is that you didn't wrap your button's action in action {}. So your code just added a Thing(5) on init and had no action set.
P.S. The var x by xProperty stuff will only work if you import tornadofx.* for that file.
I am a newbie to javafx, kotlin and obviously tornadofx.
Issue:
How to pass parameters to Fragment on every instance?
Lets say I have a table view layout as my fragment. Now this fragment is used at multiple places but with different datasets.
eg.
Adding a fragment in:
class SomeView : View() {
...
root += SomeViewFragment::class
}
class SomeAnotherView : View() {
...
root += SomeViewFragment::class
}
Declaring Fragment:
class SomeViewFragment : Fragment() {
...
tableview(someDataSetFromRestApiCall) {
...
}
}
How can I pass different someDataSetFromRestApiCall from SomeView and SomeAnotherView ?
Let's start with the most explicit way to pass data to Fragments. For this TableView example you could expose an observable list inside the Fragment and tie your TableView to this list. Then you can update that list from outside the Fragment and have your changes reflected in the fragment. For the example I created a simple data object with an observable property called SomeItem:
class SomeItem(name: String) {
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
}
Now we can define the SomeViewFragment with an item property bound to the TableView:
class SomeViewFragment : Fragment() {
val items = FXCollections.observableArrayList<SomeItem>()
override val root = tableview(items) {
column("Name", SomeItem::nameProperty)
}
}
If you later update the items content, the changes will be reflected in the table:
class SomeView : View() {
override val root = stackpane {
this += find<SomeViewFragment>().apply {
items.setAll(SomeItem("Item A"), SomeItem("Item B"))
}
}
}
You can then do the same for SomeOtherView but with other data:
class SomeOtherView : View() {
override val root = stackpane {
this += find<SomeViewFragment>().apply {
items.setAll(SomeItem("Item B"), SomeItem("Item C"))
}
}
}
While this is easy to understand and very explicit, it creates a pretty strong coupling between your components. You might want to consider using scopes for this instead. We now have two options:
Use injection inside the scope
Let the scope contain the data
Use injection inside the scope
We will go with option 1 first, by injecting the data model. We first create a data model that can hold our items list:
class ItemsModel(val items: ObservableList<SomeItem>) : ViewModel()
Now we inject this ItemsModel into our Fragment and extract the items from that model:
class SomeViewFragment : Fragment() {
val model: ItemsModel by inject()
override val root = tableview(model.items) {
column("Name", SomeItem::nameProperty)
}
}
Lastly, we need to define a separate scope for the fragments in each view and prepare the data for that scope:
class SomeView : View() {
override val root = stackpane {
// Create the model and fill it with data
val model= ItemsModel(listOf(SomeItem("Item A"), SomeItem("Item B")).observable())
// Define a new scope and put the model into the scope
val fragmentScope = Scope()
setInScope(model, fragmentScope)
// Add the fragment for our created scope
this += find<SomeViewFragment>(fragmentScope)
}
}
Please not that the setInScope function used above will be available in TornadoFX 1.5.9. In the mean time you can use:
FX.getComponents(fragmentScope).put(ItemsModel::class, model)
Let the scope contain the data
Another option is to put data directly into the scope. Let's create an ItemsScope instead:
class ItemsScope(val items: ObservableList<SomeItem>) : Scope()
Now our fragment will expect to get an instance of SomeItemScope so we cast it and extract the data:
class SomeViewFragment : Fragment() {
override val scope = super.scope as ItemsScope
override val root = tableview(scope.items) {
column("Name", SomeItem::nameProperty)
}
}
The View needs to do less work now since we don't need the model:
class SomeView : View() {
override val root = stackpane {
// Create the scope and fill it with data
val itemsScope= ItemsScope(listOf(SomeItem("Item A"), SomeItem("Item B")).observable())
// Add the fragment for our created scope
this += find<SomeViewFragment>(itemsScope)
}
}
Passing parameters
EDIT: As a result of this question, we decided to include support for passing parameters with find and inject. From TornadoFX 1.5.9 you can therefore send the items list as a parameter like this:
class SomeView : View() {
override val root = stackpane {
val params = "items" to listOf(SomeItem("Item A"), SomeItem("Item B")).observable()
this += find<SomeViewFragment>(params)
}
}
The SomeViewFragment can now pick up these parameters and use them directly:
class SomeViewFragment : Fragment() {
val items: ObservableList<SomeItem> by param()
override val root = tableview(items) {
column("Name", SomeItem::nameProperty)
}
}
Please not that this involves an unchecked cast inside the Fragment.
Other options
You could also pass parameters and data over the EventBus, which will also be in the soon to be released TornadoFX 1.5.9. The EventBus also supports scopes which makes it easy to target your events.
Further reading
You can read more about Scopes, EventBus and ViewModel in the guide:
Scopes
EventBus
ViewModel and Validation
I've been trying to figure this out recently, and that's what I got:
You need create button which will switch your components
button {
text = "open fragment"
action {
val params = Pair("text", MySting("myText"))
replaceWith(find<MyFragment>(params))
}
}
On second components
class MyFragment : Fragment("Test") {
var data = SimpleStringProperty()
override val root = hbox {
setMinSize(600.0, 200.0)
label(data) {
addClass(Styles.heading)
}
}
override fun onDock() {
data.value = params["text"] as String
}
}
As a result, we get the parameters in the second component