I use kotlinx.serialization for my models.
I'd like the idea of them to not depend on JavaFX, so they do not expose properties.
Given a model, I want a tableview for a quick representation of a list of instances, and additionally a more detailed Fragment as an editor.
consider the following model:
#Serializable
data class Person(
var name: String,
var firstname: String,
var complex: Stuff)
the view containing the tableview contains
private val personlist = mutableListOf<Person>().observable()
with a tableview that opens an instance of PersonEditor for the selected row when Enter is pressed:
tableview(personlist) {
column("name", Person::name)
column("first name", Person::firstname)
setOnKeyPressed { ev ->
selectedItem?.apply {
when (ev.code) {
KeyCode.ENTER -> PersonEditor(this).openModal()
}
}
}
}
I followed this gitbook section (but do not want the modelview to be rebound on selection of another row within the tableview)
The editor looks about like this:
class PersonEditor(person: Person) : ItemFragment<Person>() {
val model: Model = Model()
override val root = form {
fieldset("Personal information") {
field("Name") {
textfield(model.name)
}
field("Vorname") {
textfield(model.firstname)
}
}
fieldset("complex stuff") {
//... more complex stuff here
}
fieldset {
button("Save") {
enableWhen(model.dirty)
action { model.commit() }
}
button("Reset") { action { model.rollback() } }
}
}
class Model : ItemViewModel<Person>() {
val name = bind(Person::name)
val firstname = bind(Person::firstname)
//... complex stuff
}
init {
itemProperty.value = mieter
model.bindTo(this)
}
}
When I save the edited values in the detail view, the tableview is not updated.
Whats the best practize to solve this?
Also I'm unsure, if what I'm doing can be considered good practize, so i'd be happy for some advice on that too.
The best practice in a JavaFX application is to use observable properties. Not doing so is an uphill battle. You can keep your lean domain objects, but add a JavaFX/TornadoFX specific version with observable properties. This object can know how to copy data to/from your "lean" domain objects.
With this approach, especially in combination with ItemViewModel wrappers will make sure that your data is always updated.
The setOnKeyPressed code you posted can be changed to:
setOnUserSelect {
PersonEditor(it).openModal()
}
Notice though, that you are not supposed to instantiate Views and Fragments directly, as doing so skips certain steps in the TornadoFX life cycle. Instead you should pass the person as a parameter, or create a new scope and inject a PersonModel into that scope before opening the editor in that scope:
setOnUserSelect {
find<PersonEditor>(Scope(PersonEditor(it)))
}
Related
I have a controller with a string variable, and I would like the text value of a TextArea to change when the controller's string variable changes.
class MyView: View() {
...
button("Run Test").action {
runAsync {
for(test in testList){
controller.updateText = "running" + test.name
run(test)
}
}
}
...
scriptRanArea = textarea {
text = controller.updateText
}
...
}
This is the fastest way I know how to accomplish this, but I don't really know which design pattern you want to use:
class MyView: View() {
val controller: MyController by inject()
override val root = vbox {
textarea(controller.myTextProperty)
}
}
class MyController: Controller() {
val myTextProperty = SimpleStringProperty()
}
The inject method automatically finds the controller within the TornadoFX Scope, or creates one if not found, when it is first referenced. The TornadoFX text area builder function binds the string property from the controller to the TextArea when it is passed in as a param. Keep in mind though, writing in the text area will now automatically change the value in the controller's property and vice versa. If you don't want that functionality, you will have to update your question to be more specific to your needs.
How can I get a reference to the first tab? And furthermore how do I get its Stage?
class MainApp : App() {
override val primaryView = MainView::class
class MainView : View() {
override val root = VBox()
init {
with(root) {
tabpane {
tab("Report") {
hbox {
// TODO Want a reference to this tab here.
// Ideally something like tab.getStage()
this += Button("Hello 1")
}
}
tab("Data Entry") {
hbox {
this += Button("Hello 2")
}
}
}
}
}
}
}
Quickly: I've seen a lot of your posts here and they're pretty basic questions. These are things you could figure out on your own if you did your own digging. I'd recommend at least looking at the official guide to get a good grasp on most of what you need to know. Then, check out other posts on here to see if they've been answered already.
But to answer your question:
class MainView : View() {
override val root = vbox {
tabpane {
tab("Report") {
hbox {
val tab = this#tab //Here is your tab
button("Hello 1")
}
}
tab("Data Entry") {
hbox {
button("Hello 2")
}
}
}
}
}
Again, I would urge you to look at the guide, as you missed some helpful building tools (see how I built the buttons? see how I moved the root out of init?). I'd hate for you to code more than you need to then realize you could've done less work if you had known how.
Also: Tabs don't have references to stages. They just inherit Styleable and EventTarget, they're not like Views or Fragments.
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)
}
}
}
Good day.
I am trying to preserve a property of ItemViewModel via config helper. I am able to successfully save the property (conf directory with appropriate .properties file is generated), however upon next start, the property does not restore its value, just remains null. Here's a sample code to demonstrate my issue:
import javafx.beans.property.SimpleStringProperty
import tornadofx.*
data class Foo(val doNotPreserveMe: String, val preserveMe: String)
class FooModel : ItemViewModel<Foo>() {
val doNotPreserveMe = bind { item?.doNotPreserveMe?.toProperty() }
val preserveMe = bind { SimpleStringProperty(item?.preserveMe, "pm", config.string("pm")) }
}
class FooApp : App(FooView::class)
class FooView : View() {
private val model = FooModel()
override val root = form {
fieldset {
field("Do not preserve me") { textfield(model.doNotPreserveMe).required() }
field("Preserve me") { textfield(model.preserveMe).required() }
button("Do something") {
enableWhen(model.valid)
action {
model.commit {
// ...
with(config) {
set("pm" to model.preserveMe.value)
save()
}
}
}
}
}
}
}
Any ideas on why the model is not restoring the value?
Each Component has it's own config store, which is backed by a separate file. Either make sure to use the same config file, or the app global config file.
You can refer to other component's config store, so one solution would be to let the View access the ViewModel's config store like this:
button("Do something") {
enableWhen(model.valid)
action {
model.commit {
// ...
with(model.config) {
set("pm" to model.preserveMe.value)
save()
}
}
}
}
However, there is a much simpler and more contained solution, which is simply to handle save in the FooModel's onCommit callback
override fun onCommit() {
with(config) {
set("pm" to preserveMe.value)
save()
}
}
In this case you'd simply call model.commit() in the button callback.
You can also use a common, or even global config object. Either use a Controller's config store, or the global store. To use the global config object, just refer to app.config in both the model and the view.
I have the following code:
class ExampleView :View("My Example view") {
val model:ExampleModel by inject()
override val root= vbox {
textfield(model.data)
button("Commit") {
setOnAction {
model.commit()
closeModal()
}
}
button("Rollback") {
setOnAction {
model.rollback()
closeModal()
}
}
button("Just quit") {
setOnAction {
closeModal()
}
}
}
}
class Example() {
var data by property<String>()
fun dataProperty() = getProperty(Example::data)
}
class ExampleModel(example: Example) : ItemViewModel<Example>() {
init {
item = example
}
val data = bind { item?.dataProperty() }
}
class MainView : View() {
val example:Example
override val root = BorderPane()
init {
example = Example()
example.data = "Data for example"
val exampleModel = ExampleModel(example)
with(root){
top {
menubar {
menu("Test") {
menuitem("Example - 1") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
menuitem("Example - 2") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
}
}
}
}
}
}
I have two questions for this example:
1) If i change the value and close the window without a commit (User can do this with help [X] button) then only ViewModel will store changes (and it will be displayed in GUI even after the re-opening ), but model POJO object will keep the old data.
if I used instance of Example class (without DI) then this instance received all the changes at once.
For example i don't want commit/rollback functionality but i want DI and immediate updating. What i should do? (ofcource i can call "commit" for "textfield change value event")
2) ViewModel has constructor with parameter and if i try open ExampleView like this
find<ExampleView>(Scope()).openWindow()
then I got an an obvious RuntimeException. Can I avoid this for example, by a compiler warnings (or by something else)?
1) This is the correct default behavior of the ViewModel. If you bind a property of the view model to an input, the changes are immediately reflected in that bound property, but will only be flushed into the underlying model object once you commit it.
If you want to autocommit changes in the view model properties back into the underlying model object, you can create the binding with the autocommit property set to true:
val data = bind(true) { item?.dataProperty() }
You can also write bind(autocommit = true) if that looks clearer to you. This will cause any changes to be automatically flushed back into the underlying object.
I also want to make you aware that by requiring an item in the constructor of your view model, you're effectively preventing it from being used with injection unless you prime it like you do using setInScope. This might be fine for your use case, but worth noting.
2) The upcoming TornadoFX 1.5.10 will give you a better runtime error message if you forget to pass a parameter. It also introduces default values for parameters. See https://github.com/edvin/tornadofx/pull/227 for more info.