I have a tornadoFX application following the MVVM pattern with the model:
data class Person (
val name: String,
val cars: List<Car>
)
data class Car (
val brand: String,
val model: String
)
The application defines the following view:
There is a list-view that lists all persons. Besides the listView is a details-view with a text-field for the person´s name and a table-view for the person´s cars.
A double click on a car entry in the table opens a dialog, in which one can edit the car´s properties.
I want, that if I open the car-details and edit an entry, the changes will be reflected in the table-view. Since i can´t alter the Car-model (which is an immutable type) by adding fx-properties, i came up with the following view-model:
class PersonViewModel(): ItemViewModel<Person> {
val name = bind(Person::name)
val cars = bind { SimpleListProperty<CarViewModel>(item?.cars?.map{CarViewModel(it)}?.observable()) }
override fun onCommit {
// create new person based on ViewModel and store it
}
}
class CarViewModel(item: Car): ItemViewModel<Car> {
val brand = bind(Car::name)
val model = bind(Car::model)
init {
this.item = item
}
}
This way, if double-click on a car-entry in the table-view and open the car-detail-view, an update on the car will be directly reflected in the table-view.
My Problem here is, that I can´t find a way to bind the dirty properties of all my CarViewModels in the table to the PersonViewModel. So if I change a car, the PersonViewModel is not marked as dirty.
Is there a way to bind the dirty-properties of PersonViewModel and CarViewModel? (And also rebind them, if another person is selected).
Or is there even a better way to define my view-models?
I've made a change to the framework to allow ViewModel bindings towards lists to observe ListChange events. This enables you to trigger the dirty state of a list property by altering the list somehow. Merely changing a property inside an item in the list will not trigger it, so in the following example I just get the index of the Car before committing, and reassigning the Car to the same index. This will trigger a ListChange event, which the framework now listens for.
The important action happens in the Car dialog save function:
button("Save").action {
val index = person.cars.indexOf(car.item)
car.commit {
person.cars[index] = car.item
close()
}
}
The index of the car is recorded before the values are committed (to make sure that equals/hashCode matches the same entry), then the newly committed item is inserted in the same index, thus triggering a change event on the list.
Here is a complete example, using mutable JavaFX properties, since they are the idiomatic JavaFX way. You can pretty easily adapt it to using immutable items, or use wrappers.
class Person(name: String, cars: List<Car>) {
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val carsProperty = SimpleListProperty<Car>(FXCollections.observableArrayList(cars))
var cars by carsProperty
}
class PersonModel : ItemViewModel<Person>() {
val name = bind(Person::nameProperty)
val cars: SimpleListProperty<Car> = bind(Person::carsProperty)
}
class Car(brand: String, model: String) {
val brandProperty = SimpleStringProperty(brand)
var brand by brandProperty
val modelProperty = SimpleStringProperty(model)
var model by modelProperty
}
class CarModel(car: Car? = null) : ItemViewModel<Car>(car) {
val brand = bind(Car::brandProperty)
val model = bind(Car::modelProperty)
}
class DataController : Controller() {
val people = FXCollections.observableArrayList<Person>()
init {
people.add(
Person("Person 1", listOf(Car("BMW", "M3"), Car("Ford", "Fiesta")))
)
}
}
class PersonMainView : View() {
val data: DataController by inject()
val selectedPerson: PersonModel by inject()
override val root = borderpane {
center {
tableview(data.people) {
column("Name", Person::nameProperty)
bindSelected(selectedPerson)
}
}
right(PersonEditor::class)
}
}
class PersonEditor : View() {
val person: PersonModel by inject()
val selectedCar : CarModel by inject()
override val root = form {
fieldset {
field("Name") {
textfield(person.name).required()
}
field("Cars") {
tableview(person.cars) {
column("Brand", Car::brandProperty)
column("Model", Car::modelProperty)
bindSelected(selectedCar)
onUserSelect(2) {
find<CarEditor>().openModal()
}
}
}
button("Save") {
enableWhen(person.dirty)
action {
person.commit()
}
}
}
}
}
class CarEditor : View() {
val car: CarModel by inject()
val person: PersonModel by inject()
override val root = form {
fieldset {
field("Brand") {
textfield(car.brand).required()
}
field("Model") {
textfield(car.model).required()
}
button("Save").action {
val index = person.cars.indexOf(car.item)
car.commit {
person.cars[index] = car.item
close()
}
}
}
}
}
The feature is available in TornadoFX 1.7.17-SNAPSHOT.
Related
I use MVVM and have a list of data elements in a database that is mapped through a DAO and repository to ViewModel functions.
Now, my problem is rather banal; I just want to use the data in fragment variables, but I get a type mismatch.
The MVVM introduces a bit of code, and for completeness of context I'll run through it, but I'll strip it to the essentials:
The data elements are of a data class, "Objects":
#Entity(tableName = "objects")
data class Objects(
#ColumnInfo(name = "object_name")
var objectName: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
}
In ObjectsDao.kt:
#Dao
interface ObjectsDao {
#Query("SELECT * FROM objects")
fun getObjects(): LiveData<List<Objects>>
}
My database:
#Database(
entities = [Objects::class],
version = 1
)
abstract class ObjectsDatabase: RoomDatabase() {
abstract fun getObjectsDao(): ObjectsDao
companion object {
// create database
}
}
In ObjectsRepository.kt:
class ObjectsRepository (private val db: ObjectsDatabase) {
fun getObjects() = db.getObjectsDao().getObjects()
}
In ObjectsViewModel.kt:
class ObjectsViewModel(private val repository: ObjectsRepository): ViewModel() {
fun getObjects() = repository.getObjects()
}
In ObjectsFragment.kt:
class ObjectsFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ObjectsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(ObjectsViewModel::class.java)
// I use the objects in a recyclerview; rvObjectList
rvObjectList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = ObjectsAdapter(listOf(), viewModel)
// And I use an observer to keep the recyclerview updated
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
}
}
The adapter:
class ObjectsAdapter(var objects: List<Objects>,
private val viewModel: ObjectsViewModel):
RecyclerView.Adapter<ObjectsAdapter.ObjectsViewHolder>() {
// Just a recyclerview adapter
}
Now, all the above works fine - but my problem is that I don't want to use the observer to populate the recyclerview; in the database I store some objects, but there are more objects that I don't want to store.
So, I try to do this instead (in the ObjectsFragment):
var otherObjects: List<Objects>
// ...
if (condition) {
adapter.objects = viewModel.getObjects()
} else {
adapter.objects = otherObjects
}
adapter.notifyDataSetChanged()
And, finally, my problem; I get type mismatch for the true condition assignment:
Type mismatch: inferred type is LiveData<List> but List was expected
I am unable to get my head around this. Isn't this pretty much what is happening in the observer? I know about backing properties, such as explained here, but I don't know how to do that when my data is not defined in the ViewModel.
We need something to switch data source. We pass switching data source event to viewModel.
mySwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.switchDataSource(isChecked)
}
In viewModel we handle switching data source
(To use switchMap include implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
class ObjectsViewModel(private val repository: ObjectsRepository) : ViewModel() {
// Best practice is to keep your data in viewModel. And it is useful for us in this case too.
private val otherObjects = listOf<Objects>()
private val _loadDataFromDataBase = MutableLiveData<Boolean>()
// In case your repository returns liveData of favorite list
// from dataBase replace MutableLiveData(otherObjects) with repository.getFavorite()
fun getObjects() = _loadDataFromDataBase.switchMap {
if (it) repository.getObjects() else MutableLiveData(otherObjects)
}
fun switchDataSource(fromDataBase: Boolean) {
_loadDataFromDataBase.value = fromDataBase
}
}
In activity/fragment observe getObjects()
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
You can do something like this:
var displayDataFromDatabase = true // Choose whatever default fits your use-case
var databaseList = emptyList<Objects>() // List we get from database
val otherList = // The other list that you want to show
toggleSwitch.setOnCheckedChangeListener { _, isChecked ->
displayDataFromDatabase = isChecked // Or the negation of this
// Update adapter to use databaseList or otherList depending upon "isChecked"
}
viewModel.getObjects.observe(viewLifecycleOwner) { list ->
databaseList = list
if(displayDataFromDatabase)
// Update adapter to use this databaseList
}
I have model for planning schedule of events.
PlanningEntity Event has PlanningVariable Slot and Slot has two properties FullTimeSlot and RoomSlot.
I hasn't FullTimeSlot and RoomSlot in Event as different PlanningVariables because they are dependent on each other in a complicated way, and I combine it in one variable Slot.
I want to have ShadowVariable with List of Events in FullTimeSlot. And I do it with
Add property FullTimeSlot in Event and CustomShadowVariable for this Property that depends on Slot
Add property List of Event in FullTimeSlot and #InverseRelationShadowVariable(sourceVariableName = "FullTimeSlot") on it
When I run solver I get an exeption
Caused by: java.lang.IllegalArgumentException: The entity (custis.modeus.commonAutoScheduler.model.FullTimeSlot#6c84ad0e) was never added to this ScoreDirector.
Maybe that specific instance is not in the return values of the PlanningSolution's entity members ([EventList, FullTimeSlotList]).
I think it is because of FullTimeSlot #PlanningEntityCollectionProperty clone in solution, but property FullTimeSlot in Slot didn't clone.
But I dont understand how to do this case.
Code on Kotlin.
#PlanningEntity
class Event {
#SerialName("id") #PlanningId
var Id: Long = 0
#ValueRangeProvider(id = "slotRange")
lateinit var SlotList: ArrayList<Slot>
#PlanningVariable(valueRangeProviderRefs = arrayOf("slotRange"), nullable = false)
var Slot: Slot? = null
#CustomShadowVariable(
variableListenerClass = SlotVariableListener::class,
sources = [PlanningVariableReference(variableName = "Slot")]
)
var FullTimeSlot: FullTimeSlot? = null
}
class SlotVariableListener : VariableListener<Event> {
override fun beforeEntityRemoved(scoreDirector: ScoreDirector<*>?, entity: Event?) {
}
override fun afterVariableChanged(scoreDirector: ScoreDirector<*>?, entity: Event?) {
if (entity != null) {
scoreDirector!!.beforeVariableChanged(entity!!, "FullTimeSlot");
entity!!.FullTimeSlot = entity.Slot?.FullTimeSlot
scoreDirector!!.afterVariableChanged(entity!!, "FullTimeSlot");
}
}
override fun beforeEntityAdded(scoreDirector: ScoreDirector<*>?, entity: Event?) {
}
override fun afterEntityAdded(scoreDirector: ScoreDirector<*>?, entity: Event?) {
}
override fun afterEntityRemoved(scoreDirector: ScoreDirector<*>?, entity: Event?) {
}
override fun beforeVariableChanged(scoreDirector: ScoreDirector<*>?, entity: Event?) {
}
}
I’m writing PC app using Kotlin and TornadoFX.
I’ll show you what I have
FooModel.kt:
class FooModel(val id: Int){
constructor(foo: Foo) : this(foo.id)
}
Foo.kt:
data class Foo(val id: Int)
MainController.kt:
val FoosList: ObservableList<FooModel> = FXCollections.observableArrayList<FooModel>()
//bellow I wanna describe what I’m already doing with val above
fun fooDifferentOperationsForExample(example: Int){
example1Object = FoosList.find{ it.id == example }
example2Object.removeIf { it.id == example }
example3Object.setAll(it.map {FooModel})
}
I’m showing described model in listview and want to add objects of another kind(s) in the same list. I have a solution with sealed class, example of DifferentKindsOfInstances.kt:
sealed class DifferentKindsOfInstances{
data class Foo(val foo: FooModel): DiffrentKindsOfInstances()
data class Bar(val bar: BarModel): DiffrentKindsOfInstances()
}
let’s think BarModel is exact copy of FooModel, nevermind.
But if I wanna continue working with FooModel in a way I worked before(in MainControl) - how would the code look then?
it.foo.id doesn’t work for me
Here's a solution that references the sealed class members in two places: rendering the ListCell in cellFragment{} and in the binding of the selected sealed class item to the textfield string content.
class ItemModel1 ( val dataField1 : String )
class ItemModel2 ( val dataField2 : String )
sealed class ItemViewInstance {
data class Item1(val item1: ItemModel1): ItemViewInstance()
data class Item2(val item2: ItemModel2): ItemViewInstance()
}
class SealedClassDemoView : View("Sealed Class") {
val instances = listOf(
ItemViewInstance.Item1(ItemModel1("One")),
ItemViewInstance.Item2(ItemModel2("Two"))
).observable()
val selectedItemString = SimpleStringProperty()
override val root = vbox {
listview(instances) {
cellFormat {
when( it ) {
is ItemViewInstance.Item1 -> text = it.item1.dataField1
is ItemViewInstance.Item2 -> text = it.item2.dataField2
}
}
selectedItemString.bind(
Bindings.createStringBinding(
Callable {
val sel = selectionModel.selectedItemProperty().value
when (sel) {
is ItemViewInstance.Item1 -> sel.item1.dataField1
is ItemViewInstance.Item2 -> sel.item2.dataField2
else -> ""
}
},
selectionModel.selectedItemProperty()
)
)
}
textfield(selectedItemString)
padding = Insets(2.0)
spacing = 4.0
}
}
class SealedClassDemoApp : App(SealedClassDemoView::class)
fun main(args: Array<String>) {
launch<SealedClassDemoApp>(args)
}
I'm writing a very simple TornadoFX table demo, trying to display the properties of some pojos in a table, but the cells are all empty.
The main code is:
data class User(val id: Int, val name: String)
private val data = listOf(User(111, "AAA"), User(222, "BBB"), User(333, "CCC"), User(444, "DDD")).observable()
class HelloWorld : View() {
override val root = vbox {
tableview(data) {
column("id", User::id.getter)
column("name", User::name.getter)
}
}
}
I use User::id.getter to make it compiling, but the cells are empty.
I did a lot of search, but can't find code to work with current latest tornado (1.7.16)
Here is a complete demo for this: https://github.com/javafx-demos/tornadofx-table-show-pojo-demo
You need to reference the property, not the getter, ie. User::id. To reference immutable properties you need to use the readonlyColumn builder:
readonlyColumn("id", User::id)
readonlyColumn("name", User::name)
That said, you really should use JavaFX properties in your domain objects instead. Not doing so in a JavaFX based application just makes everything harder, and you loose out on a lot of benefits, or at the very least you have to jump through hoops.
Here is the complete application written with observable JavaFX properties. Note that you would then access the idProperty and nameProperty properties instead. With this approach, changes to the underlying data item would automatically be visible in the tableview as well:
class User(id: Int, name: String) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
}
private val data = listOf(User(111, "AAA"), User(222, "BBB"), User(333, "CCC"), User(444, "DDD")).observable()
class HelloWorld : View() {
override val root = vbox {
tableview(data) {
column("id", User::idProperty)
column("name", User::nameProperty)
}
}
}
Consider folowing example:
class Item(name: String, number: Int) {
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val numberProperty by lazy { SimpleIntegerProperty(number) }
var number by numberProperty
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).makeEditable(NumberStringConverter())
enableCellEditing()
}
}
}
How can I add a validator while editing cells? Is the only way to do that is to add rowExpander with some textfield and try to validate a model there?
You can either implement your own cellfactory and return a cell that shows a textfield bound to a ViewModel when in edit mode and an label if not. Alternatively, if you're fine with always displaying a textfield, you can use cellFormat and bind the current item to an ItemModel so you can attach validation:
class ItemModel(item: Item) : ItemViewModel<Item>(item) {
val name = bind(Item::nameProperty)
val number = bind(Item::numberProperty)
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).cellFormat {
val model = ItemModel(rowItem)
graphic = textfield(model.number, NumberStringConverter()) {
validator {
if (model.number.value == 123) error("Invalid number") else null
}
}
}
}
}
}
It will look like this:
While it works, it's sort of wasteful since the nodes are recreated frequently. I would recommend approach number one if performance is a concern, until we get cellFragment support for TableView like we have for ListView.
EDIT: I implemented cellFragment support, so it's possible to create a more robust solution which will show a label when not in edit mode and a validating textfield when you enter edit mode.
class ItemModel : ItemViewModel<Item>() {
val name = bind(Item::nameProperty)
val number = bind(Item::numberProperty)
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).cellFragment(NumberEditor::class)
}
}
}
class NumberEditor : TableCellFragment<Item, Number>() {
// Bind our ItemModel to the rowItemProperty, which points to the current Item
val model = ItemModel().bindToRowItem(this)
override val root = stackpane {
textfield(model.number, NumberStringConverter()) {
removeWhen(editingProperty.not())
validator {
if (model.number.value == 123L) error("Invalid number") else null
}
// Call cell.commitEdit() only if validation passes
action {
if (model.commit()) {
cell?.commitEdit(model.number.value)
}
}
}
// Label is visible when not in edit mode, and always shows committed value (itemProperty)
label(itemProperty) {
removeWhen(editingProperty)
}
}
// Make sure we rollback our model to avoid showing the last failed edit
override fun startEdit() {
model.rollback()
}
}
This will be possible starting from TornadoFX 1.7.9.