StuartM
StuartM

Reputation: 125

Cannot display data in JavaFX TableView when using Properties

I have read several threads on using Properties to display data in a JavaFX TableView and I can get all of these to work. Up until now I have been displaying data without problem in my TableView without Properties. However, moving forward I think I need to start using Properties, particularly as I want my TableView to be editable. I suspect that the problem is because all the examples I have seen use one object to display 1 row of data, whereas in my model I have one object (a Field object) per cell, though I can't think why this should be a problem.

Below is a VERY cut-down version of my Field class, enough for this test, but not enough to show its real use. I also have a test class TableViewDemo. Any help with this irksome problem would be much appreciated. Either it will be a silly mistake, or a fundamental problem with my design.

package javafxtest;
import java.sql.SQLException;
import javafx.beans.property.SimpleObjectProperty;

public class Field {

    private final SimpleObjectProperty objectValue; //All the data is stored as an object, then cast to appropriate data type

    public Field(Object objValue) throws SQLException {
        if (objValue == null) {
            throw new SQLException("Field value is null");
        }
        this.objectValue = new SimpleObjectProperty(objValue);
    }

    public final String getStringValue() {
        return (String) this.objectValue.get();
    }

    public final int getIntValue() {
        return (Integer) this.objectValue.get();
    }

    public final double getDoubleValue() {
        return (Double) this.objectValue.get();
    }

    public final boolean getBooleanValue() {
        return (Boolean) this.objectValue.get();
    }
}

package javafxtest;

import java.sql.SQLException;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class TableViewDemo extends Application {

    @Override
    public void start(Stage myStage) {
        // Set up the stage
        myStage.setTitle("Demonstrate TableView");
        FlowPane rootNode = new FlowPane(10, 10);
        rootNode.setAlignment(Pos.CENTER);
        Scene myScene = new Scene(rootNode, 600, 450);
        myStage.setScene(myScene);

        //Set up some test data - 4 rows of 4 columns
        ObservableList<Field[]> dataModel = FXCollections.observableArrayList();
        ObservableList<Field[]> dataModel2 = FXCollections.observableArrayList();
        Field[] fields1;
        Field[] fields2;
        Field[] fields3;
        Field[] fields4;
        try {
            fields1 = new Field[4];
            fields1[0] = new Field("R1C1");
            fields1[1] = new Field(25);
            fields1[2] = new Field(100.00);
            fields1[3] = new Field(true);

            fields2 = new Field[4];
            fields2[0] = new Field("R2C1");
            fields2[1] = new Field(30);
            fields2[2] = new Field(150.00);
            fields2[3] = new Field(false);

            fields3 = new Field[4];
            fields3[0] = new Field("R3C1");
            fields3[1] = new Field(397);
            fields3[2] = new Field(1005.99);
            fields3[3] = new Field(true);

            fields4 = new Field[4];
            fields4[0] = new Field("R4C1");
            fields4[1] = new Field(1085);
            fields4[2] = new Field(1.00);
            fields4[3] = new Field(false);

            //Add the data to the dataModel
            dataModel.addAll(fields1, fields2, fields3, fields4);

            //Set up the columns ===== This is where the likely problem is, or at least part of the problem ====
            TableColumn<Field[], String> colString = new TableColumn<>("String");
            colString.setCellValueFactory(new PropertyValueFactory<>("stringValue"));
            colString.setPrefWidth(150);

            TableColumn<Field[], Integer> colInt = new TableColumn<>("int");
            colInt.setCellValueFactory(new PropertyValueFactory<>("intValue"));
            colInt.setPrefWidth(100);

            TableColumn<Field[], Double> colDouble = new TableColumn<>("double");
            colDouble.setCellValueFactory(new PropertyValueFactory<>("doubleValue"));
            colDouble.setPrefWidth(100);

            TableColumn<Field[], Boolean> colBoolean = new TableColumn<>("boolean");
            colBoolean.setCellValueFactory(new PropertyValueFactory<>("booleanValue"));
            colBoolean.setPrefWidth(75);

            //Set up the TableView - it takes an Array of Field objects. Add the data and columns to it
            TableView<Field[]> tableView = new TableView<Field[]>();
            tableView.setItems(dataModel);
            tableView.getColumns().addAll(colString, colInt, colDouble, colBoolean);

            //Print out the datamodel
            dataModel2 = tableView.getItems();
            for (int i = 0; i < dataModel2.size(); i++) {
                Field[] fields = new Field[4];
                fields = dataModel2.get(i);
                for (int j = 0; j < fields.length; j++) {
                    Field field = fields[j];
                    switch (j) {
                        case 0: //Column is a String data type
                            System.out.println(field.getStringValue());
                            break;
                        case 1:  //Column is a int data type
                            System.out.println(field.getIntValue());
                            break;
                        case 2:  //Column is a double data type
                            System.out.println(field.getDoubleValue());
                            break;
                        case 3:  //Column is a boolean data type
                            System.out.println(field.getBooleanValue());
                            break;
                        default:
                            break;
                    }
                }
            }

            // Set the tableView size
            tableView.setPrefWidth(500);
            tableView.setPrefHeight(300);

            rootNode.getChildren().add(tableView);
        } catch (SQLException e) {
            System.out.println(e.getLocalizedMessage());
        }
        myStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

This is what I currently use to populate a cell with a String value:

    final int x = i;
    tableColumn<Field[], String> columnString = (TableColumn<Field[], String>) this.columnModel.get(i);
    columnString.setCellValueFactory((TableColumn.CellDataFeatures<Field[], String> fieldArray) -> {
        Field arrayField = fieldArray.getValue()[x];
        return new ReadOnlyObjectWrapper(arrayField.getStringValue());
    });

Upvotes: 0

Views: 2277

Answers (1)

James_D
James_D

Reputation: 209225

This simply isn't the way PropertyValueFactory works. From the documentation:

An example of how to use this class is:

TableColumn<Person,String> firstNameCol = new TableColumn<Person,String>("First Name");
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person,String>("firstName"));

In this example, Person is the class type of the TableView items list. The class Person must be declared public. PropertyValueFactory uses the constructor argument, "firstName", to assume that Person has a public method firstNameProperty with no formal parameters and a return type of ObservableValue<String>.

If such a method exists, then it is invoked, and additionally assumed to return an instance of Property<String>. The return value is used to populate the TableCell. In addition, the TableView adds an observer to the return value, such that any changes fired will be observed by the TableView, resulting in the cell immediately updating.

If no such method exists, then PropertyValueFactory assumes that Person has a public method getFirstName or isFirstName with no formal parameters and a return type of String. If such a method exists, then it is invoked, and its return value is wrapped in a ReadOnlyObjectWrapper and returned to the TableCell.

So, basically, the PropertyValueFactory retrieves the object representing the row, and uses reflection to look for a method xxxProperty(), where xxx is the property name assigned to the PropertyValueFactory. If no such method exists, then as a fall-back it looks for a method getXxx() instead, and wraps the result, if it exists, in a ReadOnlyObjectWrapper.

In your case, the value representing the row is a Field[], i.e. the object is an array of Fields. An array does not have a stringValueProperty() or getStringValue() method (etc), so this fails. (The individual elements of the array have those methods, of course, but the PropertyValueFactory isn't going to look at the array elements, it's just going to look at the value for the row, i.e. the array itself.)

It's hard to know how to "solve" this problem, as there's no really obvious reason you'd want to approach things this way anyway. You could, of course, just do

colString.setCellValueFactory(cellData -> 
    new SimpleStringProperty(cellData.getValue()[0].getStringValue()));

etc., but at that point it's not clear why you wouldn't just define a class with four regular fields - or even just use an Object[] and get rid of your Field class - anyway.

Upvotes: 1

Related Questions