Tornadofx Javafx - How to reload a view / component - javafx

So its a basic question.
What I am trying to achieve is refreshing views from another views.
Lets say I have a view EmployeeTableView which shows a tabular representation of employees by doing a REST API call.
In another view, I have a the filter EmployeeFilterView wherein I have gender, salary range, employee type, etc.
I also have a userContext object in which I store the user preferences. So by default lets say I have stored the value of gender filter to be Male, salary range to be ALL, etc. This object is send as a parameter to the EmployeeTableView.
When the EmployeeTableView is loaded I do a restAPI call with the userContext values to get the employee details. So that works fine. Now I change the gender filter to Female and assign this value in my userContext.
Now if I could just reload the EmployeeTableView with the userContext object, the restapi call would get the updated values.
But how can I do that ?
Also suggest a better approach if you have.

The EventBus is one valid solution to this. Another would be to use a ViewModel or Controller as the UserContext object and let that include the actual observable list of employees and then bind that list to the TableView in EmployeeTableView. Whenever the list in the context is updated, the TableView will update as well.
The filter view would call a function in the UserContext to perform the actual REST call and update the list of employees based on that.
You could create a separate EmployeeQuery object that can be injected into both the EmployeeFilterView and the UserContext so it can extract the selected filter values to perform the query. This query object contains a list of all the search parameters you want to pass to the server.
You could also consider creating a separate scope to keep these components separated if that makes sense to your architecture.
Exactly how you define these components are mostly a matter of taste, here is one suggestion. I used the RangeSlider from ControlsFX for the mock search UI.
To make it easier to imagine how this ties together, here is a screenshot:
(All names and salaries are fiction :)
/**
* The employee domain model, implementing JsonModel so it can be fetched
* via the REST API
*/
class Employee : JsonModel {
val nameProperty = SimpleStringProperty()
var name by nameProperty
val salaryProperty = SimpleIntegerProperty()
var salary by salaryProperty
val genderProperty = SimpleObjectProperty<Gender>()
var gender by genderProperty
override fun updateModel(json: JsonObject) {
with (json) {
name = getString("name")
salary = getInt("salary")
gender = Gender.valueOf(getString("gender"))
}
}
}
enum class Gender { Male, Female }
/**
* Container for the list of employees as well as a search function called by the filter
* view whenever it should update the employee list.
*/
class EmployeeContext : Controller() {
val api: Rest by inject()
val query: EmployeeQuery by inject()
val employees = SimpleListProperty<Employee>()
fun search() {
runAsync {
FXCollections.observableArrayList(Employee().apply {
name = "Edvin Syse"
gender = Gender.Male
salary = 200_000
})
//api.post("employees/query", query).list().toModel<Employee>()
} ui {
employees.value = it
}
}
}
/**
* Query object used to define the query sent to the server
*/
class EmployeeQuery : ViewModel(), JsonModel {
val genderProperty = SimpleObjectProperty<Gender>(Gender.Female)
var gender by genderProperty
val salaryMinProperty = SimpleIntegerProperty(50_000)
var salaryMin by salaryMinProperty
val salaryMaxProperty = SimpleIntegerProperty(250_000)
var salaryMax by salaryMaxProperty
val salaryDescription = stringBinding(salaryMinProperty, salaryMaxProperty) {
"$$salaryMin - $$salaryMax"
}
override fun toJSON(json: JsonBuilder) {
with(json) {
add("gender", gender.toString())
add("salaryMin", salaryMin)
add("salaryMax", salaryMax)
}
}
}
/**
* The search/filter UI
*/
class EmployeeFilterView : View() {
val query: EmployeeQuery by inject()
val context: EmployeeContext by inject()
override val root = form {
fieldset("Employee Filter") {
field("Gender") {
combobox(query.genderProperty, Gender.values().toList())
}
field("Salary Range") {
vbox {
alignment = Pos.CENTER
add(RangeSlider().apply {
max = 500_000.0
lowValueProperty().bindBidirectional(query.salaryMinProperty)
highValueProperty().bindBidirectional(query.salaryMaxProperty)
})
label(query.salaryDescription)
}
}
button("Search").action {
context.search()
}
}
}
}
/**
* The UI that shows the search results
*/
class EmployeeTableView : View() {
val context: EmployeeContext by inject()
override val root = borderpane {
center {
tableview(context.employees) {
column("Name", Employee::nameProperty)
column("Gender", Employee::genderProperty)
column("Salary", Employee::salaryProperty)
}
}
}
}
/**
* A sample view that ties the filter UI and result UI together
*/
class MainView : View("Employee App") {
override val root = hbox {
add(EmployeeFilterView::class)
add(EmployeeTableView::class)
}
}

I ended up using Tornadofx -> EventBus
Basically, when I change any of the filters, I fire an even which rebuilds the Node with the updated values.
Not sure whether the approach is right, that's why still keeping it open for discussion.

Related

How can I concat a call that returns an array and multiple calls for each element of this array?

Sorry if that title is not clear enough but I didn't know how to sum it up in one sentence.
I have a webservice that returns an ArrayList of objects named Father.
The Father object is structured like this:
class Father {
ArrayList<Child> children;
}
I have another webservice that returns me the detail of the object Child.
How can I concat the first call that returns me the arraylist of Father and the multiple calls for the multiple objects Child ?
So far I can make the calls separately, like this:
Call for ArrayList of Father
myRepository.getFathers().subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()).subscribeWith(new DisposableSingleObserver<List<Father>>() {
})
multiple call for ArrayList of Child
childListObservable
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.flatMap((Function<List<Child>, ObservableSource<Child>>) Observable::fromIterable)
.flatMap((Function<Child, ObservableSource<Child>>) this::getChildDetailObservable)
.subscribeWith(new DisposableObserver<Child>() {
// do whatever action after the result of each Child
}))
Prerequisite
Gradle
implementation("io.reactivex.rxjava2:rxjava:2.2.10")
testImplementation("io.mockk:mockk:1.10.0")
testImplementation("org.assertj:assertj-core:3.11.1")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.1")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.3.1")
Classes / Interfaces
interface Api {
fun getFather(): Single<List<Father>>
fun childDetailInfo(child: Child): Single<ChildDetailInfo>
}
interface Store {
fun store(father: Father): Completable
fun store(child: ChildDetailInfo): Completable
}
class ApiImpl : Api {
override fun getFather(): Single<List<Father>> {
val child = Child("1")
val child1 = Child("2")
return Single.just(listOf(Father(listOf(child, child1)), Father(listOf(child))))
}
override fun childDetailInfo(child: Child): Single<ChildDetailInfo> {
return Single.just(ChildDetailInfo(child.name))
}
}
data class Father(val childes: List<Child>)
data class Child(val name: String)
data class ChildDetailInfo(val name: String)
Solution
val fathersStore = api.getFather()
.flatMapObservable {
Observable.fromIterable(it)
}.flatMapCompletable {
val detailInfos = it.childes.map { child ->
api.childDetailInfo(child)
.flatMapCompletable { detail -> store.store(detail) }
}
store.store(it)
.andThen(Completable.concat(detailInfos))
}
On each emit of a List of fathers, the list is flatten. The next opreator (flatMapCompletable) will take an Father. The completable will get the details of each Child with Api#childDetailInfo. The result is build by calling the API one by one. There is no concurrency happening wegen "concat". When the father is stored sucessfully, the childs will be stored as-well, when retrieved successfully. If one of the API-calls fails (e.g. network) everything fails, because the onError will be propgated to the subscriber.
Test
#Test
fun so62299778() {
val api = ApiImpl()
val store = mockk<Store>()
every { store.store(any<Father>()) } returns Completable.complete()
every { store.store(any<ChildDetailInfo>()) } returns Completable.complete()
val fathersStore = api.getFather()
.flatMapObservable {
Observable.fromIterable(it)
}.flatMapCompletable {
val detailInfos = it.childes.map { child ->
api.childDetailInfo(child)
.flatMapCompletable { detail -> store.store(detail) }
}
store.store(it)
.andThen(Completable.concat(detailInfos))
}
fathersStore.test()
.assertComplete()
verify { store.store(eq(Father(listOf(Child("1"), Child("2"))))) }
verify { store.store(eq(Father(listOf(Child("1"))))) }
verify(atLeast = 2) { store.store(eq(ChildDetailInfo("1"))) }
verify(atLeast = 1) { store.store(eq(ChildDetailInfo("2"))) }
}
Please provide next time some classes/ interfaces. When your question contains all vital information, you will get an answer quicker.

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)
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.

javafx binding from list property to arbitrary object property

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.

Tornadofx - How to pass parameter to Fragment on every instance

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

Get all descendants types of base class

I have a base class called BaseEvent and several descendants classes:
public class BaseEvent {
// the some properties
// ...
}
[MapInheritance(MapInheritanceType.ParentTable)]
public class Film : BaseEvent {
// the some properties
// ...
}
[MapInheritance(MapInheritanceType.ParentTable)]
public class Concert : BaseEvent {
// the some properties
// ...
}
I have a code which create the BaseEvent instance at runtime:
BaseEvent event = new BaseEvent();
// assign values for a properties
// ...
baseEvent.XPObjectType = Database.XPObjectTypes.SingleOrDefault(
t => t.TypeName == "MyApp.Module.BO.Events.BaseEvent");
Now, this event will be shows in BaseEvent list view.
I want to do the following: when a user click Edit button then show in list view lookup field with all descendants types. And when user saves record change ObjectType to selected value.
How can I do this?
Thanks.
PS. this is asp.net app.
I'm not sure that your approach is correct for what you are trying to achieve. First, I'll answer the question you have asked, and afterwards I'll try to explain how the XAF already provides the functionality you are trying to achieve, namely how to choose which subclass of record to create from the user interface.
In order to create a property which allows the user to choose a Type within the application, you can declare a TypeConverter:
public class EventClassInfoTypeConverter : LocalizedClassInfoTypeConverter
{
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
List<Type> values = new List<Type>();
foreach (ITypeInfo info in XafTypesInfo.Instance.PersistentTypes)
{
if ((info.IsVisible && info.IsPersistent) && (info.Type != null))
{
// select BaseEvent subclasses
if (info.Type.IsSubclassOf(typeof(BaseEvent)))
values.Add(info.Type);
}
}
values.Sort(this);
values.Insert(0, null);
return new TypeConverter.StandardValuesCollection(values);
}
}
And then your base event class would look like:
public class BaseEvent: XPObject
{
public BaseEvent(Session session)
: base(session)
{ }
private Type _EventType;
[TypeConverter(typeof(EventClassInfoTypeConverter))]
public Type EventType
{
get
{
return _EventType;
}
set
{
SetPropertyValue("EventType", ref _EventType, value);
}
}
}
However, I suspect this is not the functionality you require. Modifying the value of the property will NOT change the base type of the record. That is, you will end up with a record of type BaseEvent which has a property Type equal to 'Concert' or 'Film'.
XAF already provides a mechanism for selecting the type of record to create. In your scenario, you will find that the New button is a dropdown with your different subclasses as options:
Therefore you do not need to create a 'type' property within your object. If you need a column to show the type of event in the list view, you can declare a property as follows
[PersistentAlias("XPObjectType.Name")]
public string EventType
{
get
{
return base.ClassInfo.ClassType.Name;
}
}

Resources