0

I am trying to import a CSV file, set it to an ArrayList then populate a table using this data. I have a class setup where a user can open a CSV file then it's read and the contents are set using the setters which are below. My table is always empty but the studentData always gets printed out like com.example.project.CSV_Objects@4f591691

Below is my code:

@FXML
private TableView<CSV_Objects> tableView;

@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
    TableColumn<CSV_Objects, String> studentNumberColumn = new TableColumn<>("Student Number");
    studentNumberColumn.setCellValueFactory(new PropertyValueFactory<>("studentNumber"));

    TableColumn<CSV_Objects, String> q1Column = new TableColumn<>("Question 1");
    q1Column.setCellValueFactory(new PropertyValueFactory<>("q1"));

    TableColumn<CSV_Objects, String> q2Column = new TableColumn<>("Question 2");
    q2Column.setCellValueFactory(new PropertyValueFactory<>("q2"));

    TableColumn<CSV_Objects, String> q3Column = new TableColumn<>("Question 3");
    q3Column.setCellValueFactory(new PropertyValueFactory<>("q3"));

    TableColumn<CSV_Objects, String> q4Column = new TableColumn<>("Question 4");
    q4Column.setCellValueFactory(new PropertyValueFactory<>("q4"));

    TableColumn<CSV_Objects, String> q5Column = new TableColumn<>("Question 5");
    q5Column.setCellValueFactory(new PropertyValueFactory<>("q5"));

    tableView.getColumns().setAll(studentNumberColumn, q1Column, q2Column, q3Column, q4Column, q5Column);
    CSV_Objects csvObject = new CSV_Objects();
    ObservableList<CSV_Objects> studentData = FXCollections.observableArrayList();
    ArrayList<String> students = csvObject.getStudent();

    System.out.println(csvObject.getStudent());
    for (String student : students) {
        studentData.add(new CSV_Objects(student));
    }

    tableView.setItems(studentData);

In my CSV_Objects class I have the getters and setters setup:

private static ArrayList<String> student = new ArrayList<>();
public void setStudent(ArrayList<String> student) {
    this.student = student;
}
public ArrayList<String> getStudent() {
    return student;
}

Any advice is greatly appreciated:)

Cbrady
  • 93
  • 1
  • 2
  • 7
  • 1
    What is `CSV_Objects`? Why is the class name plural, and why does it have a `getStudent()` method whose name is singular yet returns a list? Why is it used as the item type of your table view? Why does an `ArrayList` apparently represent a single student (based on the name of the methods and variables)? Why do you create a `CSV_Objects`, call `getStudent()` on that instance, and then loop over each `String` in the returned list only to create a _new_ `CSV_Objects`? Perhaps if you provide a [mre] we will be able to help you better. – Slaw Jan 29 '23 at 18:29
  • CSV_Obects is my class for getters/setters. When I read a CSV file I use this class to store some data of the CSV file. getStudent should be students yes as it stores the students details. I call CSV_Objects so that I can access the getters in that particular class which is getStudent() in this instance. I then loop through everything that is stored in the getStudent() ArrayList then add that data to the studentData FXCollections.observableArrayList() as a tableView needs this format for that data to be passed into the table. – Cbrady Jan 29 '23 at 18:35
  • 2
    Post a [mre]. It seems this is too advanced for your level of Java knowledge right now. You really need to understand Java basics, including basic object modeling with classes and how to use Collections, before you try to use a library like JavaFX. – James_D Jan 29 '23 at 18:56
  • No [`PropertyValueFactory`](https://stackoverflow.com/q/72437983/230513) property name matches the corresponding `TableColumn` identifier, e.g. `q1Column` vs. "q1". Converting to callbacks is illustrated [here](https://stackoverflow.com/a/68969223/230513) – trashgod Jan 29 '23 at 18:57
  • ..and stick to java naming conventions please (no underscores) – kleopatra Jan 29 '23 at 23:51

1 Answers1

4

It's not clear what is wrong with your code. There's too much missing. There's also an indication of not enough familiarity with how JavaFX's TableView works nor how to separate concerns in a non-trivial application. This answer does not directly address your code, but hopefully the example provided here can help you solve your problem(s).

That said, I believe part of the problem is that you're not separating the data from how the data is retrieved enough. I don't quite understand what your TableView is supposed to be displaying, but the fact your model is named CSV_Objects would seem to support my belief. You should set up your UI code to display a model regardless of where the instances come from, then later worry about reading the data from a CSV file.

For example, let's say you want a table to display a number of students, where each student has an ID, a name, and a "year" (freshman, sophomore, junior, or senior). You first create a Student class which encapsulates those properties.

Student.java:

package sample;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Student {
    
    private final StringProperty id = new SimpleStringProperty(this, "id");
    public final void setId(String id) { this.id.set(id); }
    public final String getId() { return id.get(); }
    public final StringProperty idProperty() { return id; }

    private final StringProperty name = new SimpleStringProperty(this, "name");
    public final void setName(String name) { this.name.set(name); }
    public final String getName() { return name.get(); }
    public final StringProperty nameProperty() { return name; }

    private final ObjectProperty<Year> year = new SimpleObjectProperty<>(this, "year");
    public final void setYear(Year year) { this.year.set(year); }
    public final Year getYear() { return year.get(); }
    public final ObjectProperty<Year> yearProperty() { return year; }

    public Student() {}

    public Student(String id, String name, Year year) {
        setId(id);
        setName(name);
        setYear(year);
    }

    public enum Year {
        FRESHMAN,
        SOPHOMORE,
        JUNIOR,
        SENIOR
    }
}

Note: JavaFX properties are used to make interacting with a TableView easier.

Then you set up TableView (FXML is used since you are also using FXML).

Controller.java:

package sample;

import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;

public class Controller {
    
    @FXML private TableView<Student> studentTable;
    @FXML private TableColumn<Student, String> idColumn;
    @FXML private TableColumn<Student, String> nameColumn;
    @FXML private TableColumn<Student, Student.Year> yearColumn;

    @FXML
    private void initialize() {
        idColumn.setCellValueFactory(data -> data.getValue().idProperty());
        nameColumn.setCellValueFactory(data -> data.getValue().nameProperty());
        yearColumn.setCellValueFactory(data -> data.getValue().yearProperty());

        // mock data for example
        studentTable.getItems().addAll(
            new Student("1", "John Smith", Student.Year.FRESHMAN),
            new Student("2", "Jane Smith", Student.Year.SOPHOMORE)
        );
    }
}

Note: Used lambdas instead of PropertyValueFactory. See Why should I avoid using PropertyValueFactory in JavaFX?.

Main.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.layout.StackPane?>

<StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
           fx:controller="sample.Controller">
    <TableView fx:id="studentTable">
        <columnResizePolicy>
            <TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
        </columnResizePolicy>
        
        <columns>
            <TableColumn fx:id="idColumn" text="ID"/>
            <TableColumn fx:id="nameColumn" text="Name"/>
            <TableColumn fx:id="yearColumn" text="Year"/>
        </columns>
    </TableView>
</StackPane>

And here is the application class.

Main.java:

package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        Parent root = FXMLLoader.load(Main.class.getResource("Main.fxml"));
        primaryStage.setScene(new Scene(root, 600, 400));
        primaryStage.show();
    }
}

Note: Expects the Main.fxml file to be in the same package as the sample.Main class.

The above example will run and display two students in a table view. But these students are created in code (specifically, the initialize method of the FXML controller). You want to read the students from a CSV file. For that, we need to write code to parse a CSV file into Student instances.

For example, StudentCsvReader.java:

package sample;

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class StudentCsvReader {

    /* 
     * This is really basic CSV-reading code. It assumes a perfectly formatted file
     * that has no commas in the actual data (i.e., there's no delimiter-escape
     * handling). You may want to find an existing library to parse CSV files; they
     * are likely to be more robust. Also, there are libraries out there that will
     * "directly" parse a CSV file into POJOs. It could look something like:
     * 
     * List<Student> students = CSVParser.parse(csvFile, Student.class);
     *
     * 
     * Note this code also assumes there is no header row.
     */
    
    public List<Student> read(Path csvFile) throws IOException {
        try (BufferedReader br = Files.newBufferedReader(csvFile)) {
            List<Student> students = new ArrayList<>();

            String line;
            while ((line = br.readLine()) != null) {
                if (!line.isBlank()) {
                    String[] columns = line.split(",");

                    String id = columns[0];
                    String name = columns[1];
                    String year = columns[2];
                    students.add(new Student(id, name, Student.Year.valueOf(year.toUpperCase())));
                }
            }

            return students;
        }
    }
}

And then we modify the Controller class to make use of this StudentCsvReader. Note we use a background thread and a Task to avoid doing I/O work on the JavaFX Application Thread.

Modified Controller.java:

package sample;

import java.nio.file.Path;
import java.util.List;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;

public class Controller {
    
    @FXML private TableView<Student> studentTable;
    @FXML private TableColumn<Student, String> idColumn;
    @FXML private TableColumn<Student, String> nameColumn;
    @FXML private TableColumn<Student, Student.Year> yearColumn;

    @FXML
    private void initialize() {
        idColumn.setCellValueFactory(data -> data.getValue().idProperty());
        nameColumn.setCellValueFactory(data -> data.getValue().nameProperty());
        yearColumn.setCellValueFactory(data -> data.getValue().yearProperty());

        Path csvFile = Path.of(".", "students.csv");
        Thread t = new Thread(new ParseStudentCsvTask(csvFile), "parse-csv-thread");
        t.setDaemon(true);
        t.start();
    }

    private class ParseStudentCsvTask extends Task<List<Student>> {

        private final Path csvFile;

        ParseStudentCsvTask(Path csvFile) {
            this.csvFile = csvFile;
        }

        @Override
        protected List<Student> call() throws Exception {
            return new StudentCsvReader().read(csvFile);
        }

        @Override
        protected void succeeded() {
            studentTable.getItems().setAll(getValue());
        }

        @Override
        protected void failed() {
            getException().printStackTrace();
        }
    }
}

Note: Expects a CSV file named students.csv to exist in the working directory.

And here's an example CSV file that will work with the above code:

1,Kate Todd,FRESHMAN
2,Leroy Gibbs,SENIOR
3,Tony DiNozzo,JUNIOR
4,Ellie Bishop,FRESHMAN
5,Nick Torres,FRESHMAN
6,Ziva David,SOPHOMORE

Notice the modifications to the code did nothing to Student or how the TableView is configured. The only difference was how the Student instances were created. Basically, the UI should not know where or how the data is created/found.

You should also consider using an application architecture such as Model-View-Controller (MVC) or Model-View-ViewModel (MVVM). It encourages separation of concerns, making it easier to reason about and develop code. Check out Applying MVC With JavaFx.

Slaw
  • 37,820
  • 8
  • 53
  • 80