Java: setCellValuefactory; Lambda vs. PropertyValueFactory; advantages/disadvantages
PropertyValueFactory
expects correctly named property getters. getAColumnsProperty
is probably not one.
In case of new PropertyValueFactory<Appointment, LocalDate>("date")
the Appointment
class needs to contain a dateProperty()
method; the returned values need to extend ReadOnlyProperty
for this to work and any edits will only lead to an update in the model automatically, if the returned object also WritableValue
.
Example Appointment
class that should work with PropertyValueFactory<>("date")
:
public class Appointment {
private final ObjectProperty<LocalDate> date = new SimpleObjectProperty<>();
public final LocalDate getDate() {
return this.date.get();
}
public final void setDate(LocalDate value) {
this.date.set(value);
}
public final ObjectProperty<LocalDate> dateProperty() {
return this.date;
}
}
If no such method exists, PropertyValueFactory
will use a getter to retrieve the value, i.e. getDate()
, but this case updates in the model will not be visible in the UI until it updates the Cell
, since the PropertyValueFactory
"does not know" where to add a listener.
Disadvantages of PropertyValueFactory
- Can only find
public
methods in apublic
class PropertyValueFactory
uses reflection- Not typesafe. In
new PropertyValueFactory<Appointment, LocalDate>("date")
the compiler does not check, if there is a appropriate method, if that method even returns a suitable class or if e.g. the property getter returns aString
instead of aReadOnlyProperty<LocalDate>
which can lead toClassCastException
s. - No compile time checking. In the lambda expression the compiler can do some checking if the method exists and returns a appropriate type; with
PropertyValueFactory
this is not done. - Does not work with records.
If you are sure to implement the appropriate methods in the item class correctly, there is nothing wrong with using PropertyValueFactory
, but as mentioned above it has it's disadvantages. Moreover implementing the Callback
is much more flexible. You could e.g. do some additional modifications:
TableColumn<Appointment, String> column = ...
column.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Appointment, String>, ObservableValue<String>> {
@Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<Appointment, String> cd) {
Appointment a = cd.getValue();
return Bindings.createStringBinding(() -> "the year: " + a.getDate().getYear(), a.dateProperty());
}
});
JavaFX: what to do if you need to use a PropertyValueFactory
If the property in the model class is an ObjectProperty<LocalDate>
, then the column should be a TableColumn<ItemEv, LocalDate>
, not a TableColumn<ItemEv, String>
.
Implementing the cellValueFactory
directly (typically with a lambda expression) is always preferable to using the legacy PropertyValueFactory
class. You never "need to use" a PropertyValueFactory
(and never should).
The cellValueFactory
is only used to determine what data to display. It is not used to determine how to display the data. For the latter, you should use a cellFactory
.
So:
private TableColumn<ItemEv, LocalDate> opendAtColumnEv ;
// ...
openedAtColumnEv.setCellValueFactory(cellData -> cellData.getValue().openedAtProperty());
openedAtColumnEv.setCellFactory(column -> new TableCell<ItemEv, LocalDate>() {
@Override
protected void updateItem(LocalDate openedAt, boolean empty) {
super.updateItem(openedAt, empty);
if (openedAt == null || empty) {
setText("");
} else {
if (openedAt.equals(LocalDate.MIN)) {
setText("---");
} else {
// Note you can use a different DateTimeFormatter as needed
setText(openedAt.format(DateTimeFormatter.ISO_LOCAL_DATE));
}
}
}
});
Java(fx) - TableView - Did I understood the usage of setCellValueFactory and PropertyValueFactory?
I cannot tell if you understood how it's used; The description seems seems to be mostly correct, but there are some details you did not get correct:
A
Callback
itself does not provide a value. ThecellValueFactory
is aCallback
that takes aCellDataFeatures
instance as parameter of it's method and returns aObservableValue
; The value stored in theObservableValue
instance is used asitem
for theTableCell
.
Furthermore the value is only passed to theTableCell
responsible for displaying the value. What this cell does with the new value is completely up to it's implementor. A change may or may not be visible on screen and a cell needs not be displayed on screen when the value is changed (It can be scrolled out of view using theTableView
's scrollbar).PropertyValueFactory
does not look for the field. It looks for anameProperty()
method that is used to retrieve theObservableValue
.
If there is no such method, it looks for a getter method:getName()
orisName()
. In the latter case anObservableValue
instance wrapping the value returned by the getter is used (; Note that this makes automatic updates impossible).
How do you use a JavaFX TableView with java records?
Solution
Use a lambda cell value factory instead of a PropertyValueFactory
.
For some explanation of the difference between the two, see:
- Java: setCellValuefactory; Lambda vs. PropertyValueFactory; advantages/disadvantages
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):
- Why does asObject allow use of SimpleLongProperty with setCellValueProperty?
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 ..
JavaFX TableView Integer not displaying while String items are
You have a “;” in the string passed to the property value factory, it should not be there.
This is wrong:
new PropertyValueFactory<Show, Integer>("EpsWatched;")
PropertyValueFactory lookup strings and property names should follow Java bean naming conventions. For more info, see:
- Javafx PropertyValueFactory not populating Tableview
Normally, I’d close the question as a duplicate of the above question, however, the asker requested a specific answer and had some additional questions, so this answer addresses those directly.
It is recommended to use a lambda rather than a PropertyValueFactory, as any errors will be detected and reported accurately by the compiler rather than failures at runtime. For a thorough analysis, see:
- Java: setCellValuefactory; Lambda vs. PropertyValueFactory; advantages/disadvantages
how would I write a lamba for this?
In your model class declare the property and provide an accessor for it:
public class Show {
private final IntegerProperty numEpisodes = new SimpleIntegerProperty();
public final IntegerProperty numEpisodesProperty() {
return numEpisodes;
}
}
Use the property accessor in a lambda to define the cell value factory:
TableColumn<Show, Integer> numEpisodesCol = new TableColumn<>();
numEpisodesCol.setCellValueFactory(
data -> data.getValue().numEpisodesProperty().asObject()
);
See also:
- Converting Integer to ObservableValue<Integer> in javafx
How to correctly set images inside TableView
As stated in the comments by @jewelsea, your model class (Apple
) should contain only data; it should not contain UI fields such as ImageView
. Any Node
can only appear once in the scene graph, which is why you only see the ImageView
in a single row in the table.
You should instead store either the path to the image, or the Image
(which is only data) itself in the model class. The trade-off between these two choices is a compute-time versus memory consumption trade-off. If you store the path to the image, the image will need to be loaded each time a cell updates (e.g. during scrolling), which takes time. On the other hand, if you store the Image
, then all images needed for the entire table will need to be stored in memory, whether or not they are displayed.
I would recommend storing the Image
in the model class if your table only needs a small number of images. This may happen if the table only has a few rows, or if there are a small number of images and multiple rows show the same image. Note that Image
s can be shared by multiple ImageView
s, so there is no need to load any single Image
more than once.
Using Image
in the model class would look like this:
public class Apple {
private String name;
private Image image;
public Apple(String name, Image image) {
this.name = name;
this.image = image;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Image getImage() {
return image;
}
public void setImage(Image image) {
this.image = image;
}
}
You will need a cell implementation to display the image:
public class ImageCell extends TableCell<Apple, Image> {
private static final IMAGE_SIZE = 20 ;
private final ImageView imageView ;
public TableCell() {
imageView = new ImageView();
imageView.setFitWidth(IMAGE_SIZE);
imageView.setFitHeight(IMAGE_SIZE);
imageView.setPreserveRatio(true);
}
@Override
protected void updateItem(Image image, boolean empty) {
if (empty || item == null) {
setGraphic(null);
} else {
imageView.setImage(image);
setGraphic(imageView);
}
}
}
And your application code looks like:
public class MainController implements Initializable {
public TableView<Apple> table;
public TableColumn<Apple, String> nameColumn;
public TableColumn<Apple, Image> imageColumn;
Image downloadImage = new Image("download.jpg");
Apple apple = new Apple("Bob",downloadImage);
Apple apple2 = new Apple("John",downloadImage);
@Override
public void initialize(URL location, ResourceBundle resources) {
imageColumn.setCellValueFactory(cellData -> new SimpleObjectProperty<>(cellData.getValue().getImage()));
imageColumn.setCellFactory(column -> new ImageCell());
nameColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
table.setItems(FXCollections.observableArrayList(apple2, apple));
}
}
If you have a large number of distinct images in the table, which is probably unlikely, you should represent the path to the image in the model class instead:
public class Apple {
private String name;
private String imagePath;
public Apple(String name, String imagePath) {
this.name = name;
this.imagePath = imagePath;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getImagePath() {
return imagePath;
}
public void setImage(String imagePath) {
this.imagePath = imagePath;
}
}
Then your cell class needs to load the image:
public class ImageCell extends TableCell<Apple, String> {
private static final IMAGE_SIZE = 20 ;
private final ImageView imageView ;
public TableCell() {
imageView = new ImageView();
imageView.setFitWidth(IMAGE_SIZE);
imageView.setFitHeight(IMAGE_SIZE);
imageView.setPreserveRatio(true);
}
@Override
protected void updateItem(String imagePath, boolean empty) {
if (empty || item == null) {
setGraphic(null);
} else {
imageView.setImage(new Image(imagePath));
setGraphic(imageView);
}
}
}
and your application code has the obvious modifications:
public class MainController implements Initializable {
public TableView<Apple> table;
public TableColumn<Apple, String> nameColumn;
public TableColumn<Apple, String> imageColumn;
Apple apple = new Apple("Bob", "download.jpg");
Apple apple2 = new Apple("John", "download.jpg");
@Override
public void initialize(URL location, ResourceBundle resources) {
imageColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getImagePath()));
imageColumn.setCellFactory(column -> new ImageCell());
nameColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
table.setItems(FXCollections.observableArrayList(apple2, apple));
}
}
Javafx PropertyValueFactory not populating Tableview
Suggested solution (use a Lambda, not a PropertyValueFactory)
Instead of:
aColumn.setCellValueFactory(new PropertyValueFactory<Appointment,LocalDate>("date"));
Write:
aColumn.setCellValueFactory(cellData -> cellData.getValue().dateProperty());
For more information, see this answer:
- Java: setCellValuefactory; Lambda vs. PropertyValueFactory; advantages/disadvantages
Solution using PropertyValueFactory
The lambda solution outlined above is preferred, but if you wish to use PropertyValueFactory, this alternate solution provides information on that.
How to Fix It
The case of your getter and setter methods are wrong.
getstockTicker
should be getStockTicker
setstockTicker
should be setStockTicker
Some Background Information
Your PropertyValueFactory remains the same with:
new PropertyValueFactory<Stock,String>("stockTicker")
The naming convention will seem more obvious when you also add a property accessor to your Stock class:
public class Stock {
private SimpleStringProperty stockTicker;
public Stock(String stockTicker) {
this.stockTicker = new SimpleStringProperty(stockTicker);
}
public String getStockTicker() {
return stockTicker.get();
}
public void setStockTicker(String stockticker) {
stockTicker.set(stockticker);
}
public StringProperty stockTickerProperty() {
return stockTicker;
}
}
The PropertyValueFactory uses reflection to find the relevant accessors (these should be public). First, it will try to use the stockTickerProperty accessor and, if that is not present fall back to getters and setters. Providing a property accessor is recommended as then you will automatically enable your table to observe the property in the underlying model, dynamically updating its data as the underlying model changes.
Adding a CheckBox column to an existing TableView
Summary: As noted here, this is likely a bug; steps to avoid the pitfall include these:
- Verify that the data model exports properties correctly, as shown here.
- Critically examine the value of replacing
PropertyValueFactory
with an explicitCallback
, when possible, as outlined here, here, here, here and here.
The problem is that CheckBoxTableCell
can't find or bind the ObservableProperty<Boolean>
based on the parameter supplied:
active.setCellFactory(CheckBoxTableCell.forTableColumn(active));
The CheckBoxTableCell
defers to the table column for access to the target Boolean
property. To see the effect, replace the active
parameter with a Callback
that returns the ObservableValue<Boolean>
for row i
explicitly:
active.setCellFactory(CheckBoxTableCell.forTableColumn(
(Integer i) -> data.get(i).active));
While this makes the checkboxes work, the underlying problem is that the Person
class needs an accessor for the active
property. Using JavaFX Properties and Binding discusses the property method naming conventions, and the Person
class of the Ensemble8 tablecellfactory
illustrates a working model class with a property getter for each attribute, also shown below.
With this change PropertyValueFactory
can find the newly added BooleanProperty
, and the original form of forTableColumn()
works. Note that the convenience of PropertyValueFactory
comes with some limitations. In particular, the factory's fall-through support for the previously missing property accessor goes unnoticed. Fortunately, the same accessor allows substitution of a simple Callback
for each column's value factory. As shown here, instead of PropertyValueFactory
,
active.setCellValueFactory(new PropertyValueFactory<>("active"));
Pass a lamda expression that returns the corresponding property:
active.setCellValueFactory(cd -> cd.getValue().activeProperty());
Note also that Person
can now be private
. Moreover, the use of explicit type parameters affords stronger type checking during compilation.
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
/**
* https://stackoverflow.com/a/68969223/230513
*/
public class TableViewSample extends Application {
private final TableView<Person> table = new TableView<>();
private final ObservableList<Person> data
= FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith@example.com"),
new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
new Person("Ethan", "Williams", "ethan.williams@example.com"),
new Person("Emma", "Jones", "emma.jones@example.com"),
new Person("Michael", "Brown", "michael.brown@example.com")
);
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
stage.setTitle("Table View Sample");
stage.setWidth(600);
stage.setHeight(400);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
table.setEditable(true);
TableColumn<Person, Boolean> active = new TableColumn<>("Active");
active.setCellValueFactory(cd -> cd.getValue().activeProperty());
active.setCellFactory(CheckBoxTableCell.forTableColumn(active));
TableColumn<Person, String> firstName = new TableColumn<>("First Name");
firstName.setCellValueFactory(cd -> cd.getValue().firstNameProperty());
TableColumn<Person, String> lastName = new TableColumn<>("Last Name");
lastName.setCellValueFactory(cd -> cd.getValue().lastNameProperty());
TableColumn<Person, String> email = new TableColumn<>("Email");
email.setCellValueFactory(cd -> cd.getValue().emailProperty());
table.setItems(data);
table.getColumns().addAll(active, firstName, lastName, email);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(8));
vbox.getChildren().addAll(label, table);
stage.setScene(new Scene(vbox));
stage.show();
}
private static class Person {
private final BooleanProperty active;
private final StringProperty firstName;
private final StringProperty lastName;
private final StringProperty email;
private Person(String fName, String lName, String email) {
this.active = new SimpleBooleanProperty(true);
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public BooleanProperty activeProperty() {
return active;
}
public StringProperty firstNameProperty() {
return firstName;
}
public StringProperty lastNameProperty() {
return lastName;
}
public StringProperty emailProperty() {
return email;
}
}
}
Related Topics
Java Change the Document in Documentlistener
What Is the Best/Simplest Way to Read in an Xml File in Java Application
"File Not Found" When Running New Libgdx Project
Stopping a Window from Displaying Till It Is Fully Drawn
Adding Points to Xyseries Dynamically with Jfreechart
How to Write an Arraylist of Strings into a Text File
What Is the Regex for "Any Positive Integer, Excluding 0"
Using Java to Decrypt Openssl Aes-256-Cbc Using Provided Key and Iv
Springboot - Making Jar Files - No Auto Configuration Classes Found in Meta-Inf/Spring.Factories
Documentlistener Java, How to Prevent Empty String in Jtextbox
Exception in Initializer Error in Java When Using Netbeans
How to Bundle Images in Jar File
Java Outofmemoryerror Strange Behaviour
Spring Boot Jackson Date and Timestamp Format