Issue
Java records are a new feature with Java15. Suppose you have a record such as this one.
public record Person(String last, String first, int age)
{
public Person()
{
this("", "", 0);
}
}
This is a final class. It automatically generates the getter methods first(), last(), and age().
Now here is a TableView in JavaFX.
/**************************************************
* Author: Morrison
* Date: 10 Nov 202021
**************************************************/
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.control.TableView;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.PropertyValueFactory;
public class TV extends Application
{
public TV()
{
}
@Override
public void init()
{
}
@Override
public void start(Stage primary)
{
BorderPane root = new BorderPane();
TableView<Person> table = new TableView<>();
root.setCenter(table);
TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
lastColumn.setCellValueFactory(new PropertyValueFactory<Person,
String>("last"));
TableColumn<Person, String> firstColumn = new TableColumn<>("First");
firstColumn.setCellValueFactory(new PropertyValueFactory<Person,
String>("first"));
TableColumn<Person, Integer> ageColumn = new TableColumn<>("Age");
ageColumn.setCellValueFactory(new PropertyValueFactory<Person,
Integer>("age"));
table.getColumns().add(lastColumn);
table.getColumns().add(firstColumn);
table.getColumns().add(ageColumn);
table.getItems().add(new Person("Smith", "Justin", 41));
table.getItems().add(new Person("Smith", "Sheila", 42));
table.getItems().add(new Person("Morrison", "Paul", 58));
table.getItems().add(new Person("Tyx", "Kylee", 40));
table.getItems().add(new Person("Lincoln", "Abraham", 200));
Scene s = new Scene(root, 500, 500);
primary.setTitle("TableView Demo");
primary.setScene(s);
primary.show();
}
@Override
public void stop()
{
}
}
The problem lies here.
TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
lastColumn.setCellValueFactory(new PropertyValueFactory<Person,
String>("last"));
The TableView is expecting methods of name getFirst
, getLast
, and getAge
to do its work. Is there a workaround other than doing this horrible thing?
public record Person(String last, String first, int age)
{
public Person()
{
this("", "", 0);
}
public String getLast()
{
return last;
}
public String getFirst()
{
return first;
}
public int getAge()
{
return age;
}
}
Solution
Solution
Use a lambda cell value factory instead of a PropertyValueFactory
.
For some explanation of the difference between the two, see:
Why this works
The issue, as you note, is that record accessors don't follow standard java bean property naming conventions, which is what the PropertyValueFactory
expects. For example, a record uses first()
rather than getFirst()
as an accessor, which makes it incompatible with the PropertyValueFactory
.
Should you apply a workaround of "doing this horrible thing" of adding additional get methods to the record, just so you can make use of a PropertyValueFactory
to interface with a TableView
? -> Absolutely not, there is a better way :-)
What is needed to fix it is to define your own custom cell factory instead of using a PropertyValueFactory
.
This is best done using a lambda (or a custom class for really complicated cell value factories). Using a lambda cell factory has advantages of type safety and compile-time checks that a PropertyValueFactory
does not have (see the prior referenced answer for more information).
Examples for defining lambdas instead of PropertyValueFactories
An example usage of a lambda cell factory definition for a record String
field:
TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
lastColumn.setCellValueFactory(
p -> new SimpleStringProperty(p.getValue().last())
);
It is necessary to wrap the record field in a property or binding as the cell value factory implementation expects an observable value as input.
You may be able to use a ReadOnlyStringWrapper instead of SimpleStringProperty, like this:
lastColumn.setCellValueFactory(
p -> new ReadOnlyStringWrapper(p.getValue().last()).getReadOnlyProperty()
);
In a quick test that worked. For immutable records, it might be a better approach, but I haven't thoroughly tested it to be sure, so, to be safe, I have used simple read-write properties throughout the rest of this example.
Similarly, for an int
field:
TableColumn<Person, Integer> ageColumn = new TableColumn<>("Age");
ageColumn.setCellValueFactory(
p -> new SimpleIntegerProperty(p.getValue().age()).asObject()
);
The need to put asObject()
on the end of the lambda is explained here, in case you are curious (but it is just a weird aspect of the usage of java generics by the JavaFX framework, which isn't worth spending a lot of time investigating, just add the asObject()
call and move on IMO):
Similarly, if your record contains other objects (or other records), then you can define a cell value factory for SimpleObjectProperty<MyType>
.
Note: This approach for lambda cell factory definition and the patterns defined above also works for standard (non-record) classes. There is nothing special here for records. The only thing to be aware of is to take care to use the correct accessor name after the getValue()
call in the lambda. For example, use first()
rather than the standard getFirst()
call which you would usually define on a class to support the standard Java Bean naming pattern. The really great thing about this is that, if you define the accessor name wrong, you will get a compiler error and know the exact issue and location before you even attempt to run the code.
Example Code
Full executable example based on the code in the question.
Person.java
public record Person(String last, String first, int age) {}
RecordTableViewer.java
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;
public class RecordTableViewer extends Application {
@Override
public void start(Stage stage) {
TableView<Person> table = new TableView<>();
TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
lastColumn.setCellValueFactory(
p -> new SimpleStringProperty(p.getValue().last())
);
TableColumn<Person, String> firstColumn = new TableColumn<>("First");
firstColumn.setCellValueFactory(
p -> new SimpleStringProperty(p.getValue().first())
);
TableColumn<Person, Integer> ageColumn = new TableColumn<>("Age");
ageColumn.setCellValueFactory(
p -> new SimpleIntegerProperty(p.getValue().age()).asObject()
);
//noinspection unchecked
table.getColumns().addAll(lastColumn, firstColumn, ageColumn);
table.getItems().addAll(
new Person("Smith", "Justin", 41),
new Person("Smith", "Sheila", 42),
new Person("Morrison", "Paul", 58),
new Person("Tyx", "Kylee", 40),
new Person("Lincoln", "Abraham", 200)
);
stage.setScene(new Scene(table, 200, 200));
stage.show();
}
}
Should PropertyValueFactory be "fixed" for records?
Record field accessors follow their own access naming convention, fieldname()
, just like Java Beans do, getFieldname()
.
Potentially an enhancement request could be raised for PropertyValueFactory
to change its implementation in the core framework so that it can also recognize the record accessor naming standard.
However, I do not believe that updating PropertyValueFactory
to recognize record field accessors would be a good idea.
A better solution is not to update PropertyValueFactory for record support and to only allow the typesafe custom cell value approach which is outlined in this answer.
I believe this because of the explanation provided by kleopatra in comments:
a custom valueFactory is definitely the way to go :) Even if it might appear attractive to some to implement an equivalent to PropertyValueFactory - but: that would be a bad idea, looking at the sheer number of questions of type "data not showing in table" due to typos ..
Answered By - jewelsea
Answer Checked By - David Marino (JavaFixing Volunteer)