Search code examples
javajavafxobservabletableviewtablecolumn

How can i make a custom TableColumn display cell observable?


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

after pressing setup button

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.

after go button pressed

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.

enter image description here


Solution

  • 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

    1. Create an observable value corresponding to the composite value you want to display. This is probably not as invasive as you think; if you have an existing model class you can always create a wrapper for it solely for the purposes of your table if needed.
    2. Create a binding in the cell value factory that generates the composite value you want.
    3. Use the whole table row model as the value for the cell, and display the composite value in a cell created by the cell factory. To ensure the cell stays updated, use bindings in the cell implementation.

    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.