0

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!

AIMIN PAN
  • 1,563
  • 1
  • 9
  • 13
  • 3
    Your domain model object is wrong. I don’t do Kotlin, but it should be implemented according to the [JavaFX properties pattern](https://docs.oracle.com/javafx/2/binding/jfxpub-binding.htm). Also, there is no need to use `PropertyValueFactory` in a modern JVM language. – James_D May 27 '22 at 03:15
  • 1
    My advice as you are “just starting JavaFX” is to write JavaFX applications only in Java. Once you know how to do that well then you can swap out Java for another language, but I don’t advise learning JavaFX using another language. – jewelsea May 27 '22 at 05:52

2 Answers2

3

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
Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Those kotlin+JavaFX properties are ugly. I guess that is to be expected when trying to get interoperability between two independent property systems developed in different languages. Perhaps [TornadoFX property delegates](https://edvin.gitbooks.io/tornadofx-guide/content/part2/Property_Delegates.html), might make it a bit neater, but it is nice to have an example that doesn't rely on a third party framework. – jewelsea May 27 '22 at 19:39
  • Yeah, I find them ugly, too. Though at the same time, I almost prefer them over regular Java just because it's more "compact", especially if you use delegated properties. I was going to mention _TornadoFX_, but I couldn't tell if it was still actively maintained, and the project's README claims "_TornadoFX is not yet compatible with Java 9/10_". – Slaw May 27 '22 at 20:38
  • @Slaw thanks for the great answer. But in this way we have to add columns at run time? it is no longer possible to add columns in fxml file? – AIMIN PAN Jun 08 '22 at 08:53
  • @AIMINPAN You can define the `TableColumn`s in the FXML file. But you still have to set the `cellValueFactory` for each column, and that's typically done in code (i.e., the FXML controller). You can get a reference to the column the same way as always—by defining an `fx:id` attribute and a corresponding `@FXML`-annotated field/Kotlin property in the FXML controller. Since this is Kotlin, you'll need to make the Kotlin property `lateinit`. – Slaw Jun 08 '22 at 09:28
  • @Slaw I can define TableColumn the generic class in fxml, but how can I define TableColumn ? FXML does not allow this. – AIMIN PAN Jun 11 '22 at 00:37
  • @AIMINPAN You can't. This is, in my opinion, a weakness of FXML—you can't define generic types in the FXML file. You'll just have to put e.g., `@FXML lateinit var nameColumn: TableColumn` in the controller and let FXML inject column, while making sure you don't mess up the types between the `TableView` and its `TableColumn`s. – Slaw Jun 11 '22 at 00:43
-1

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.

AIMIN PAN
  • 1,563
  • 1
  • 9
  • 13
  • 2
    While this will work, it is better to use a lambda than a PropertyValueFactory and, without JavaFX properties in the model, you won't be able to bind to it or listen to changes on it. Also (unrelated to the question), it is good style to follow [naming conventions](https://kotlinlang.org/docs/coding-conventions.html#naming-rules). – jewelsea May 27 '22 at 09:23
  • 2
    Don’t use `PropertyValueFactory` – James_D May 27 '22 at 11:10
  • 1
    For [example](https://stackoverflow.com/a/68969223/230513). – trashgod May 27 '22 at 11:51