I would like to implement a solution that avoids the PropertyValueFactory and also be able to reference all the class's properties in the TableCell updateItem method not just the bound property display value.
Note :
I've specifically set col_2 to be declared as public TableColumn<myclass,myclass> col_2;
This allows all the properties of the class to be available in the cells updateItem method. As you will see the display output of cell 2 is a combination of 2 properties.
Pressing the setup button displays this
Pressing the GO button changes the List origlist's fld2 property value.
Pressing the break button confirms to the console that the value has been changed in List origlist.
However the change does not propogate to the display.
The columns setup using PropertyValueFactory do update.
So in summary the column 2 is doing other than it is not observable to changes in the bound data.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox alignment="CENTER" prefHeight="473.0" prefWidth="923.0" spacing="20.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.test.tableview.HelloController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<TableView fx:id="TableView1" editable="true" prefHeight="200.0" prefWidth="400.0">
<columns>
<TableColumn fx:id="col_1" prefWidth="200.0" text="C1" />
<TableColumn fx:id="col_2" prefWidth="200.0" text="C2" />
<TableColumn fx:id="col_3" prefWidth="200.0" text="C3" />
<TableColumn fx:id="col_4" prefWidth="200.0" text="C4" />
</columns>
</TableView>
<Button onAction="#onsetup" text="setup" />
<Button onAction="#ongoClick" text="GO" />
<Button onAction="#onbreak" text="break" />
</VBox>
package org.test.tableview;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
public class myclass
{
private IntegerProperty fld1 = new SimpleIntegerProperty();
private StringProperty fld2 = new SimpleStringProperty();
private StringProperty fld3 = new SimpleStringProperty();
private StringProperty fld4 = new SimpleStringProperty();
public myclass(Integer fld1,String fld2,String fld3,String fld4)
{
this.fld1.set(fld1);
this.fld2.set(fld2);
this.fld3.set(fld3);
this.fld4.set(fld4);
}
// Getter methods
public IntegerProperty fld1Property() {
return fld1;
}
public StringProperty fld2Property() {
return fld2;
}
public StringProperty fld3Property() {
return fld3;
}
public StringProperty fld4Property() {
return fld4;
}
}
package org.test.tableview;
import javafx.beans.InvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import java.util.ArrayList;
import java.util.List;
public class HelloController
{
public TableView<myclass> TableView1;
public TableColumn<myclass,Integer> col_1;
public TableColumn<myclass,myclass> col_2;
public TableColumn<myclass,String> col_3;
public TableColumn<myclass,String> col_4;
List<myclass> origlist = new ArrayList<>();
public HelloController()
{
init();
}
private void init()
{
origlist.clear();
myclass m = new myclass(1,"A1","B1","C1");
origlist.add(m);
m= new myclass(2,"A2","B2","C2");
origlist.add(m);
m= new myclass(3,"A3","B3","C3");
origlist.add(m);
m= new myclass(4,"A4","B4","C4");
origlist.add(m);
}
@FXML
public void onsetup(ActionEvent actionEvent)
{
init();
col_1.setCellValueFactory(new PropertyValueFactory<>("fld1"));
col_2.setCellValueFactory(data -> new ObservableValue<>()
{
@Override
public void addListener(ChangeListener<? super myclass> listener) {}
@Override
public void removeListener(ChangeListener<? super myclass> listener) {}
@Override
public myclass getValue()
{
return data.getValue();
}
@Override
public void addListener(InvalidationListener listener) {}
@Override
public void removeListener(InvalidationListener listener) {
}
});
col_3.setCellValueFactory(new PropertyValueFactory<>("fld3"));
col_4.setCellValueFactory(new PropertyValueFactory<>("fld4"));
col_2.setCellFactory(p ->
{
TableCell<myclass,myclass> cell = new TableCell<>()
{
@Override
protected void updateItem(myclass item, boolean empty)
{
if (item != null)
{
Label l = new Label();
l.setText("combination display " + item.fld2Property().getValue() + " " + item.fld4Property().getValue());
setGraphic(l);
}
else
{
setGraphic(null);
setText(null);
}
}
};
return cell;
});
final ObservableList<myclass> olpc = FXCollections.observableArrayList();
olpc.addAll(origlist.stream().toList());
TableView1.setItems(olpc);
}
@FXML
public void ongoClick(ActionEvent actionEvent)
{
origlist.get(0).fld2Property().setValue("Z");
origlist.get(0).fld4Property().setValue("ZZZ");
}
@FXML
public void onbreak(ActionEvent actionEvent)
{
System.out.println(origlist.get(0).fld2Property().get());
}
}
in reference to how is it running comment. IntelliJ IDEA does it somehow.
To condense the comments from under the OP into an answer, there are really three ways to do create a table column that displays values depending on multiple other values in the model class for the
To demo, start with a variant of the table example from the standard Oracle documentation:
Table row model class:
package org.jamesd.example.compositecell;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
public Person(String firstName, String lastName) {
this.firstName.set(firstName);
this.lastName.set(lastName);
}
public StringProperty firstNameProperty() {
return firstName;
}
public StringProperty lastNameProperty() {
return lastName;
}
}
and the application:
package org.jamesd.example.compositecell;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class App extends Application {
@Override
public void start(Stage stage) throws Exception {
TableView<Person> table = new TableView<>();
table.setEditable(true);
TableColumn<Person, String> firstNameColumn = new TableColumn<>("First Name");
firstNameColumn.setCellValueFactory(data -> data.getValue().firstNameProperty());
firstNameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
TableColumn<Person, String> lastNameColumn = new TableColumn<>("Last Name");
lastNameColumn.setCellValueFactory(data -> data.getValue().lastNameProperty());
lastNameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
/* TODO:
Create full name column
*/
table.getColumns().add(firstNameColumn);
table.getColumns().add(lastNameColumn);
// table.getColumns().add(fullNameColumn);
ObservableList<Person> data = createPersonList();
populateData(data);
table.setItems(data);
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
private ObservableList<Person> createPersonList() {
return FXCollections.observableArrayList();
}
private void populateData(ObservableList<Person> personList) {
personList.addAll(
new Person("Jacob", "Smith"),
new Person("Isabella", "Johnson"),
new Person("Ethan", "Williams"),
new Person("Emma", "Jones"),
new Person("Michael", "Brown")
);
}
public static void main(String[] args) {
Application.launch(args);
}
}
For the first option, we can add a new, read-only, property to the model for the full name:
public class Person {
// existing code...
private final ReadOnlyStringWrapper fullName = new ReadOnlyStringWrapper();
public Person(String firstName, String lastName) {
this.firstName.set(firstName);
this.lastName.set(lastName);
fullName.bind(this.firstName.concat(" ").concat(this.lastName));
}
public ReadOnlyStringProperty fullNameProperty() {
return fullName.getReadOnlyProperty();
}
// existing code
}
And then just create the full name column exactly as usual:
/*
Create full name column
*/
TableColumn<Person, String> fullNameColumn = new TableColumn<>("Full Name");
fullNameColumn.setCellValueFactory(data -> data.getValue().fullNameProperty());
For the second option, leave the Person
class in its original form, and use a cell value factory that creates the appropriate binding:
/*
Create full name column
*/
TableColumn<Person, String> fullNameColumn = new TableColumn<>("Full Name");
fullNameColumn.setCellValueFactory(data -> {
Person person = data.getValue();
return person.firstNameProperty().concat(" ").concat(person.lastNameProperty());
});
or (maybe more generally):
/*
Create full name column
*/
TableColumn<Person, String> fullNameColumn = new TableColumn<>("Full Name");
fullNameColumn.setCellValueFactory(data -> {
Person person = data.getValue();
return Bindings.createStringBinding(
() -> person.firstNameProperty().get() + " " + person.lastNameProperty().get(),
person.firstNameProperty(),
person.lastNameProperty()
);
});
For option 3, again use the original Person
class, and create the composite column as:
/*
Create full name column
*/
TableColumn<Person, Person> fullNameColumn = new TableColumn<>("Full Name");
fullNameColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue()));
fullNameColumn.setCellFactory(tc -> new TableCell<>() {
// not sure you need a label here, but to match OP:
private final Label label = new Label();
@Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty || person == null) {
label.textProperty().unbind();
setGraphic(null);
} else {
label.textProperty().bind(
person.firstNameProperty().concat(" ").concat(person.lastNameProperty())
);
setGraphic(label);
}
}
});
Alternatively, using JavaFX19+ mapping:
fullNameColumn.setCellFactory(tc -> {
TableCell<Person, Person> cell = new TableCell<>();
Label label = new Label();
label.textProperty().bind(cell.itemProperty().flatMap(person -> person.firstNameProperty().concat(" ").concat(person.lastNameProperty())));
cell.setGraphic(label);
// or, omit the label entirely and do
// cell.textProperty().bind(/* same binding as above*/);
return cell;
});
The point here is that the updateItem()
method will not be called when the individual properties change. To force the text of the label (and you can do the same using the textProperty()
of the cell directly) to update, you need to explicitly bind it to the other properties.
Overall, I prefer the first option. Even if you have an existing model object you are using throughout your application, you can create a simple wrapper for it specifically for the table, and delegating to the existing object.
public class PersonTableModel {
private final Person person ;
private final ReadOnlyStringWrapper fullName = new ReadOnlyStringWrapper();
public PersonTableModel(Person person) {
this.person = person ;
fullName.bind(person.firstNameProperty().concat(" ").concat(person.lastNameProperty()));
}
public StringProperty firstNameProperty() {
return person.firstNameProperty();
}
public StringProperty lastNameProperty() {
return person.lastNameProperty();
}
public ReadOnlyStringProperty fullNameProperty() {
return fullName;
}
}
If the list can be added to or removed from, some wiring (using listeners on lists, or transformation lists) may be necessary to keep the lists in sync.
The second option is also a good one; it basically creates an "on the fly" model for the cell from the larger table model.
The third might violate the responsibilities of MVC, depending on how you break these up.