TornadoFX: How to preserve `ItemViewModel`'s property? - kotlin

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.

Related

How to improve my code to show a data array in a table, with TornadoFX?

This is my way to display an array of data:
private val data = observableArrayList(
arrayOf("AAA", "111"),
arrayOf("BBB", "222"),
arrayOf("CCC", "333")
)
class HelloWorld : View() {
override val root = tableview<Array<String>>(data) {
column("name") { cellDataFeatures: TableColumn.CellDataFeatures<Array<String>, String> ->
SimpleStringProperty(cellDataFeatures.value[0])
}
column("value") { cellDataFeatures: TableColumn.CellDataFeatures<Array<String>, String> ->
SimpleStringProperty(cellDataFeatures.value[1])
}
}
}
It works but the code is quite complex. Is there any better way to do it?
(Maybe define a class to hold the data will make it much simpler, but I just want to test some uncommon cases)
Update:
A complete demo project for this: https://github.com/javafx-demos/tornadofx-tableview-array-data-demo
Here is a simpler way of defining your columns:
class HelloWorld : View() {
override val root = tableview(data) {
column<Array<String>, String>("name", { it.value[0].toProperty() })
column<Array<String>, String>("value", { it.value[1].toProperty() })
}
}
That said, using a specialized data structure would yield less headache :)
An alternative approach would be to configure just the cell item type and then a value factory:
column("name", String::class) {
value { it.value[0] }
}
column("value", String::class) {
value { it.value[1] }
}

TableView and Fragment to edit Details with tornadofx

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

Specialized Singleton implementation

I am looking for specialized singleton implementation, probably I might be using wrong terminology and hence looking for expert suggestion. Here is my scenario:
There is common code which can be called by ComponentA or ComponentB. I need to push telemetry data from the common code. Telemetry needs to have information that whether this common code get called by ComponentA or ComponentB.
So common code will have just this line of code:
telemetry.pushData(this._area, data);
where this._area tells the telemetry data is getting pushed for which component
I need to push telemetry data from multiple places so it would be good if object got created once and used through out the code lifetime
One option I can think of passing component context to the common code which in mind doesn't look right, hence looking for suggestion what kind of pattern one should use in this case?
This is what I am thinking
// Telemetry.ts file present in shared code
export class Telemetry extends Singleton {
public constructor() {
super();
}
public static instance(): Telemetry {
return super.instance<Telemetry>(Telemetry);
}
public publishEvent(data): void {
if (!this.area) {
throw new Error("Error: Initialize telemetry class with right area");
}
pushtelemetryData(this.area, data);
}
public area: string;
}
// Create Telemetry object from component A
Telemetry.instance().area = "ComponentA";
// Shared code will call telemetry publishEvent
Telemetry.instance().publishEvent(data);
Thanks
It's not a good pattern to use in TypeScript where you would generally inject dependencies.
If you must absolutely do it then you can do it by faking it somewhat:
namespace Telemetry {
var instance : SingletonSomething;
export function push(data: Any) : void {
if (instance == null) {
instance = new SingletonSomething();
}
instance.push(data);
}
class SingletonSomething() { ... }
}
and then you could call
Telemetry.push(data);
You can imitate the singleton pattern in typescript easily:
class Telemetry {
private static instance: Telemetry;
public static getInstance(): Telemetry {
if (Telemetry.instance == null) {
Telemetry.instance = new Telemetry();
}
return Telemetry.instance;
}
...
}
If you have your code in some sort of closure (module, namespace, etc) then you can replace the static member with:
let telemetryInstance: Telemetry;
export class Telemetry {
public static getInstance(): Telemetry {
if (telemetryInstance == null) {
telemetryInstance = new Telemetry();
}
return telemetryInstance;
}
...
}
But then you can also replace the static method with:
let telemetryInstance: Telemetry;
export function getTelemetryInstance(): Telemetry {
if (telemetryInstance == null) {
telemetryInstance = new Telemetry();
}
return telemetryInstance;
}
export class Telemetry {
...
}
At this point, in case you are using some sort of closure, you might ask yourself if you really need the class at all?
If you use this as a module:
// telemetry.ts
export interface TelemetryData {
...
}
export function pushData(data: TelemetryData): void {
...
}
Then you get exactly what you're looking for, and this is more of the "javascript way" of doing it.
Edit
In the telemetry module there's no need to know the users of it.
If the Telemetry.pushData function needs to have information about the object that called it then define an interface for it:
// telemetry.ts
export interface TelemetryData {
...
}
export interface TelemetryComponent {
name: string;
...
}
export function pushData(data: TelemetryData, component: TelemetryComponent): void {
...
}
Then in the other modules, where you use it:
// someModule.ts
import * as Telemetry from "./telemetry";
class MyComponent implement Telemetry.TelemetryComponent {
// can also be a simple string property
public get name() {
return "MyComponent";
}
fn() {
...
Telemetry.pushData({ ... }, this);
}
}
2nd Edit
Because you are using a module system, your module files are enough to make singletons, there's no need for a class to achieve that.
You can do this:
// telemetry.ts
let area: string;
export interface TelemetryData {
...
}
export function setArea(usedArea: string) {
area = usedArea;
}
export function pushData(data: TelemetryData): void {
...
}
Then:
Telemetry.setArea("ComponentA");
...
Telemetry.publishEvent(data);
The telemetry module will be created only once per page, so you can treat the entire module as a singleton.
Export only the functions that are needed.

questions about DI, ViewModel etc

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.

Can typescript external modules have circular dependencies?

It looks like this is not allowed. requireJS is throwing an error on the following (this post is different as it was resolved with internal modules):
element.ts:
import runProperties = require('./run-properties');
export class Element {
public static factory (element : IElement) : Element {
switch (element.type) {
case TYPE.RUN_PROPERTIES :
return new runProperties.RunProperties().deserialize(<runProperties.IRunProperties>element);
}
return null;
}
}
run-properties.ts:
import element = require('./element');
export class RunProperties extends element.Element implements IRunProperties {
}
No, modules can't have circular dependencies unless they are in the same file. Each file is being processed in sequence, synchronously, so the full file definition (including all of the exports for example) hasn't been completed when it goes to second file, which immediately tries to require/reference the first file, and so on.
Normally, you can break a circular dependency by introducing an interface or base class into a common definition file(s) (basically interfaces only) and having the other files use that as a common "interface" rather than directly referencing the classes. This is a typical pattern in many platforms.
I have same issue, I was able to fix it by creating factory class that allows registration of child classes and used Generics for instantiation.
Reference: https://www.typescriptlang.org/docs/handbook/generics.html#using-class-types-in-generics
See sample code below:
Base Class (abstract.control.ts)
export type AbstracControlOptions = {
key?:string;
}
export abstract class AbstractControl {
key:string;
constructor(options:AbstracControlOptions){
this.key = options.key;
}
}
Parent Class (container.ts)
import { AbstractControl, AbstracControlOptions } from './abstract.control';
import { Factory } from './factory';
export { AbstracControlOptions };
export abstract class Container extends AbstractControl {
children: AbstractControl[] = [];
constructor(options: AbstracControlOptions) {
super(options);
}
addChild(options: { type: string }) {
var Control:any = Factory.ControlMap[options.type];
if (Control) {
this.children.push(Factory.create(Control, options));
}
}
}
I don't have to import the child classes any more, because I'm using factory.ts to instantiate the child classes.
Factory Class(factory.ts)
import {AbstractControl, AbstracControlOptions} from './abstract.control';
type ControlMap<T extends AbstractControl> = {
[type:string]:T
};
export class Factory{
static ControlMap: ControlMap<any> = {};
static create<T extends AbstractControl>(c: { new ({}): T; }, options: AbstracControlOptions): T {
return new c(options);
}
}
Although class constructor seems to be called at c: { new ({}): T } but it does not actually calls it. But gets the reference to the constructor via new operator. The parameter {} to the constructor in my case is required because the base class AbstractControl requires it.
(1) Child Class(layout.ts)
import { Factory } from './factory';
import { Container, AbstracControlOptions } from './container';
export type LayoutlOptions = AbstracControlOptions & {
type:"layout";
}
export class Layout extends Container {
type: string = "layout";
constructor(options:LayoutlOptions) {
super(options);
}
}
Factory.ControlMap["layout"] = Layout;
(2) Child Class(repeater.ts)
import { Factory } from './factory'
import { Container, AbstracControlOptions } from './container';
export type RepeaterOptions = AbstracControlOptions & {
type: "repeater";
}
export class Repeater extends Container {
type: string = "repeater";
constructor(options:RepeaterOptions) {
super(options);
}
}
Factory.ControlMap["repeater"] = Repeater;