Java: Setcellvaluefactory; Lambda VS. Propertyvaluefactory; Advantages/Disadvantages

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 a public 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 a String instead of a ReadOnlyProperty<LocalDate> which can lead to ClassCastExceptions.
  • 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. The cellValueFactory is a Callback that takes a CellDataFeatures instance as parameter of it's method and returns a ObservableValue; The value stored in the ObservableValue instance is used as item for the TableCell.

    Furthermore the value is only passed to the TableCell 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 the TableView's scrollbar).

  • PropertyValueFactory does not look for the field. It looks for a nameProperty() method that is used to retrieve the ObservableValue.

    If there is no such method, it looks for a getter method: getName() or isName(). In the latter case an ObservableValue 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.

example

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 Images can be shared by multiple ImageViews, 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 explicit Callback, 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.

image

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



Leave a reply



Submit