I just start JavaFx and am a bit stuck with TableView, it shows very long string representation of each column like:
StringProperty [bean: com.plcsim2.PlcSimModel$ErpSheet#2c1ffe7b, name:name, value: big]
IntegerProperty [bean: com.plcsim2.PlcSimModel$ErpSheet#2c1ffe7b, name:sheet_long, value: 5000]
IntegerProperty [bean: com.plcsim2.PlcSimModel$ErpSheet#2c1ffe7b, name:sheet_short, value: 3000]
while I am expecting only "big", "5000", "3000" to appear in the cells.
Here is my model:
object PlcSimModel {
class ErpSheet {
val name = SimpleStringProperty(this, "name")
val sheet_long = SimpleIntegerProperty(this, "sheet_long")
val sheet_short = SimpleIntegerProperty(this, "sheet_short")
}
val erpSheets = ArrayList<ErpSheet>()
}
The fxml:
<VBox alignment="CENTER" prefHeight="562.0" prefWidth="812.0" spacing="20.0"
xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.plcsim2.PlcSimController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<TableView fx:id="table_1" prefHeight="400.0" prefWidth="200.0">
</TableView>
<Button onAction="#onHelloButtonClick" text="Hello!" />
</VBox>
And finally the controller:
#FXML
private fun onHelloButtonClick() {
val rs = DB.populateSql("select name, sheet_long, sheet_short from erp_sheet")
PlcSimModel.erpSheets.clear()
if (rs != null) {
while (rs.next()) {
val sheet = PlcSimModel.ErpSheet()
sheet.name.set(rs.getString("name"))
sheet.sheet_long.set(rs.getInt("sheet_long"))
sheet.sheet_short.set(rs.getInt("sheet_short"))
PlcSimModel.erpSheets.add(sheet)
}
}
table_1.columns.clear()
val col0 = TableColumn<PlcSimModel.ErpSheet, String>("name")
col0.cellValueFactory = PropertyValueFactory("name")
table_1.columns.add(col0)
val col1 = TableColumn<PlcSimModel.ErpSheet, Int>("sheet_long")
col1.cellValueFactory = PropertyValueFactory("sheet_long")
table_1.columns.add(col1)
val col2 = TableColumn<PlcSimModel.ErpSheet, Int>("sheet_short")
col2.cellValueFactory = PropertyValueFactory("sheet_short")
table_1.columns.add(col2)
table_1.items = FXCollections.observableArrayList(PlcSimModel.erpSheets)
}
It seems controller is good, it is able to get the values from database and add rows to TableView, but why TableView shows Property object's string representation, instead of just show the value?
Thanks a lot!
JavaFX Properties
When a class exposes a JavaFX property, it should adhere to the following pattern:
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
public class Foo {
// a field holding the property
private final StringProperty name = new SimpleStringProperty(this, "name");
// a setter method (but ONLY if the property is writable)
public final void setName(String name) {
this.name.set(name);
}
// a getter method
public final String getName() {
return name.get();
}
// a "property getter" method
public final StringProperty nameProperty() {
return name;
}
}
Notice that the name of the property is name, and how that is used in the names of the getter, setter, and property getter methods. The method names must follow that format.
The PropertyValueFactory class uses reflection to get the needed property. It relies on the method naming pattern described above. Your ErpSheet class does not follow the above pattern. The implicit getter methods (not property getter methods) return the property objects, not the values of the properties.
Kotlin & JavaFX Properties
Kotlin does not work especially well with JavaFX properties. You need to create two Kotlin properties, one for the JavaFX property object, and the other as a delegate (manually or via the by keyword) for the JavaFX property's value.
Here is an example:
import javafx.beans.property.IntegerProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
class Person(name: String = "", age: Int = 0) {
#get:JvmName("nameProperty")
val nameProperty: StringProperty = SimpleStringProperty(this, "name", name)
var name: String
get() = nameProperty.get()
set(value) = nameProperty.set(value)
#get:JvmName("ageProperty")
val ageProperty: IntegerProperty = SimpleIntegerProperty(this, "age", age)
var age: Int
get() = ageProperty.get()
set(value) = ageProperty.set(value)
}
You can see, for instance, that the name Kotlin property delegates its getter and setter to the nameProperty Kotlin property.
The #get:JvmName("nameProperty") annotation is necessary for Kotlin to generate the correct "property getter" method on the Java side (the JVM byte-code). Without that annotation, the getter would be named getNameProperty(), which does not match the pattern for JavaFX properties. You can get away with not using the annotation if you never plan to use your Kotlin code from Java, or use any class that relies on reflection (e.g., PropertyValueFactory) to get the property.
See the Kotlin documentation on delegated properties if you want to use the by keyword instead of manually writing the getter and setter (e.g., var name: String by nameProperty). You can write extension functions for ObservableValue / WritableValue (and ObservableIntegerValue / WritableIntegerValue, etc.) to implement this.
Runnable Example
Here is a runnable example using the above Person class. It also periodically increments the age of each Person so you can see that the TableView is observing the model items.
import javafx.animation.PauseTransition
import javafx.application.Application
import javafx.beans.property.IntegerProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.scene.Scene
import javafx.scene.control.TableColumn
import javafx.scene.control.TableView
import javafx.scene.control.cell.PropertyValueFactory
import javafx.stage.Stage
import javafx.util.Duration
fun main(args: Array<String>) = Application.launch(App::class.java, *args)
class App : Application() {
override fun start(primaryStage: Stage) {
val table = TableView<Person>()
table.columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY
table.items.addAll(
Person("John Doe", 35),
Person("Jane Doe", 42)
)
val nameCol = TableColumn<Person, String>("Name")
nameCol.cellValueFactory = PropertyValueFactory("name")
table.columns += nameCol
val ageCol = TableColumn<Person, Number>("Age")
ageCol.cellValueFactory = PropertyValueFactory("age")
table.columns += ageCol
primaryStage.scene = Scene(table, 600.0, 400.0)
primaryStage.show()
PauseTransition(Duration.seconds(1.0)).apply {
setOnFinished {
println("Incrementing age of each person...")
table.items.forEach { person -> person.age += 1 }
playFromStart()
}
play()
}
}
}
class Person(name: String = "", age: Int = 0) {
#get:JvmName("nameProperty")
val nameProperty: StringProperty = SimpleStringProperty(this, "name", name)
var name: String
get() = nameProperty.get()
set(value) = nameProperty.set(value)
#get:JvmName("ageProperty")
val ageProperty: IntegerProperty = SimpleIntegerProperty(this, "age", age)
var age: Int
get() = ageProperty.get()
set(value) = ageProperty.set(value)
}
Avoid PropertyValueFactory
With all that said, you should avoid using PropertyValueFactory, whether you're writing your application in Java or Kotlin. It was added when lambda expressions were not yet part of Java to help developers avoid writing verbose anonymous classes everywhere. However, it has two disadvantages: it relies on reflection and, more importantly, you lose compile-time validations (e.g., whether the property actually exists).
You should replace uses of PropertyValueFactory with lambdas. For example, from the above code, replace:
val nameCol = TableColumn<Person, String>("Name")
nameCol.cellValueFactory = PropertyValueFactory("name")
table.columns += nameCol
val ageCol = TableColumn<Person, Number>("Age")
ageCol.cellValueFactory = PropertyValueFactory("age")
table.columns += ageCol
With:
val nameCol = TableColumn<Person, String>("Name")
nameCol.setCellValueFactory { it.value.nameProperty }
table.columns += nameCol
val ageCol = TableColumn<Person, Number>("Age")
ageCol.setCellValueFactory { it.value.ageProperty }
table.columns += ageCol
Now I know PropertyValueFactory uses reflection to find the property. I thought it is the key defined to IntegerProperty or StringProperty. So simply changing the model class to following fixed the problem:
class ErpSheet {
var name = ""
var sheet_long = 0
var sheet_short = 0
}
The member variable name is the key to PropertyVaueFactory.
In python you can get a unique numeric ID for any object via id(object):
person = Person()
person_id = id(person)
What is the equivalent function in Kotlin?
Classes in Kotlin do not automatically have a unique ID. As mentioned in the comments, you can get the identityHashCode. It is not guaranteed unique, but in practice if you are just using it to compare items in a log, it is probably sufficient.
class Person() {
val id: Int get() = System.identityHashCode(this)
}
If you need unique IDs, you could assign them at construction time using a counter in a companion object.
class Person() {
val id: Long = nextId
companion object {
private var nextId: Long = 0L
get() = synchronized(this) { ++field }
set(_) = error("unsupported")
}
}
// Or simpler on JVM:
class Person() {
val id: Long = idCounter.getAndIncrement()
companion object {
private val idCounter = AtomicLong(1L)
}
}
Or if you are on JVM, you can use the UUID class to generate a statistically unique ID for each class as it is instantiated, but this is probably not very useful just for logging.
class Person() {
val id = UUID.randomUUID()
}
Consider following Kotlin-Code:
class Foo(input: Int) {
private var someField: Int = input
get() = -field
set(value) {
field = -value
}
fun bar() {
println(someField)
}
}
fun main() {
Foo(1).bar()
}
This prints -1 in the console which means that inside method bar() someField references the attribute and not the corresponding getter. Is there a way that allows me to use the get()-method as if I was referencing this field from outside?
Perhaps you could track the "raw" value separately from the negative value? Something like this:
class Foo(input: Int) {
private var _someField: Int = input
var someField: Int
get() = -_someField
set(value) {
_someField = -value
}
fun bar() {
println(someField)
}
}
Now the class internals can reference _someField to deal directly with the raw value, while outside clients can only "see" someField.
In my quarkus application I have an endpoint that takes in a DTO, with a field that has a default value. When I don't send that field, I still get the exception
com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of
`FooDTO`, problem: Parameter specified as non-null is null: method
io.otherstuff.FooDTO.<init>, parameter someListVariable
at [Source: (io.quarkus.vertx.http.runtime.VertxInputStream); line: 4, column: 1]
The class looks like this:
class FooDTO(
override var someStringVar: String,
override var someListVariable: List<Int> = emptyList(),
): BarDTO
---------------------------------------------
interface BarDTO {
var someStringVar: String
var someListVar: List<Int>
}
Now if I send a payload like this
{
"someStringVar": "Hello Stackoverflow",
"someListVar": []
}
it is working perfectly fine, but when I drop "someListVar" I get the exception from above, even though it should just initialize it as an empty list.
Any help is much appreciated!
The problem is, that during desalinization, the library (fasterxml) calls the primary constructor with null: FooDTO("Hello Stackoverflow", null). The call ends up with the exception as the someListVariable parameter is not nullable (default value is used only when the paremeter is not provided at all, not when it's null).
One option of solving the problem would be providing an explicit JsonCreator:
class FooDTO(
override var someStringVar: String,
override var someListVariable: List<Int> = emptyList()) : BarDTO {
companion object {
#JvmStatic
#JsonCreator
fun of(
#JsonProperty("someStringVar") someStringVar: String,
#JsonProperty("someListVariable") someListVariable: List<Int>?) =
FooDTO(someStringVar, someListVariable ?: emptyList())
}
}
Another posibility is using secondary constructor instead of the default value:
class FooDTO : BarDTO {
override var someStringVar: String
override var someListVariable: List<Int>
#JsonCreator
constructor(
#JsonProperty("someStringVar") someStringVar: String,
#JsonProperty("someListVariable") someListVariable: List<Int>?) {
this.someStringVar = someStringVar
this.someListVariable = someListVariable ?: emptyList()
}
}
Both options are unfortunately a bit verbose.
I'm trying to pass data class to the service-proxy of Vert.x like this:
data class Entity(val field: String)
#ProxyGen
#VertxGen
public interface DatabaseService {
DatabaseService createEntity(Entity entity, Handler<AsyncResult<Void>> resultHandler);
}
However, the service-proxy requires a DataObject as the parameter type.
Below are what I've tried so far.
First, I rewrite the data class as:
#DataObject
data class Entity(val field: String) {
constructor(json: JsonObject) : this(
json.getString("field")
)
fun toJson(): JsonObject = JsonObject.mapFrom(this)
}
Although this works, the code is redundant, so I tried the kapt with the following generator:
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
roundEnv.getElementsAnnotatedWith(ProxyDataObject::class.java).forEach { el ->
val className = el.simpleName.toString()
val pack = processingEnv.elementUtils.getPackageOf(el).toString()
val filename = "Proxy$className"
val classBuilder = TypeSpec.classBuilder(filename)
val primaryConstructorBuilder = FunSpec.constructorBuilder()
val secondaryConstructorBuilder = FunSpec.constructorBuilder().addParameter("json", JsonObject::class)
val secondaryConstructorCodeBlocks = mutableListOf<CodeBlock>()
el.enclosedElements.forEach {
if (it.kind == ElementKind.FIELD) {
val name = it.simpleName.toString()
val kClass = getClass(it) // get the corresponding Kotlin class
val jsonTypeName = getJsonTypeName(it) // get the corresponding type name in methods of JsonObject
classBuilder.addProperty(PropertySpec.builder(name, kClass).initializer(name).build())
primaryConstructorBuilder.addParameter(name, kClass)
secondaryConstructorCodeBlocks.add(CodeBlock.of("json.get$jsonTypeName(\"$name\")"))
}
}
secondaryConstructorBuilder.callThisConstructor(secondaryConstructorCodeBlocks)
classBuilder
.addAnnotation(DataObject::class)
.addModifiers(KModifier.DATA)
.primaryConstructor(primaryConstructorBuilder.build())
.addFunction(secondaryConstructorBuilder.build())
.addFunction(
FunSpec.builder("toJson").returns(JsonObject::class).addStatement("return JsonObject.mapFrom(this)").build()
)
val generatedFile = FileSpec.builder(pack, filename).addType(classBuilder.build()).build()
generatedFile.writeTo(processingEnv.filer)
}
return true
}
Then I can get the correct generated file by simply writing the original data class, but when I execute the building after cleaning, I still get the following error:
Could not generate model for DatabaseService#createEntity(ProxyEntity,io.vertx.core.Handler<io.vertx.core.AsyncResult<java.lang.Void>>): type ProxyEntity is not legal for use for a parameter in proxy
It seems that the generated annotation #DataObject is not processed.
So what should I do? Is there a better solution?