javajavafxfxmlscenebuilder

JavaFX TableView not displaying data (complex project structure)


I have a seemingly common problem with JavaFX's TableView not displaying my data, but my project structure is complicated enough that I think I am missing something or something is not being properly passed to a different class. I am using the CellValueFactory method to identify the columns of my TableView<Employee> object, where Employee is a class I use to represent employees (ID, name, email, and phone, but only the first two fields are to be displayed in the table). The project uses the H2 embedded SQL database package, but that seems to be working so I didn't include my database package. The relevant parts of this project are structured like this:

java
|__ myorg.scheduler
    |__ gui
        |__ [Abstract] Controller
        |__ Model
        |__ ControllerMain
        |__ ControllerEmployeeTab
        |__ ControllerAddEmployee
        |__ MainApp
resources
|__ myorg.scheduler
    |__ gui
        |__ tabHandler.fxml
        |__ employeeTab.fxml
        |__ addEmployeePopup.fxml

At this stage, the software should allow the user to add an employee to the database: enter image description here

Pressing the "Add" button brings up this menu, from addEmployeePopup.fxml enter image description here

The code is as follows:

GUI package:

Model Since I need to pass data from one controller to another, I have a Model class:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import myorg.scheduler.database.EmployeeDAO;
import myorg.scheduler.people.Employee;

import java.sql.SQLException;

public class Model {

    private final static Model INSTANCE = new Model();

    @FXML
    ObservableList<Employee> employeeList =  FXCollections.observableArrayList();

    public static synchronized Model getInstance() {
        return INSTANCE;
    }

    public void addEmployee(Employee employee) {
        employeeList.add(employee);
    }

    public ObservableList<Employee> getAllEmployees() {
        return employeeList;
    }

    public void setEmployeeList() throws SQLException, ClassNotFoundException {
        employeeList = EmployeeDAO.getAllEmployees();
    }
}

Controller

All of the classes starting with "Controller" inherit from the abstract Controller class.

public abstract class Controller {

    private Model model;
    public void setModel(Model model) {
        this.model = model;
    }

    public Model getModel() {
        return model;
    }
}

ControllerEmployeeTab

This is where the problem starts: populateTable() is supposed to get (from the synchronized Model class) the ObservableList of employees and populate the TableView with their IDs and Names. For some reason, it is not doing this.

import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import myorg.scheduler.people.Employee;

import java.io.IOException;

import static myorg.scheduler.gui.MainApp.loadEmployeePopup;


public class ControllerEmployeeTab extends Controller {

    @FXML
    public Button btnAddEmployee;
    @FXML
    TableView<Employee> employeeTable = new TableView<Employee>();
    @FXML
    TableColumn<Employee, Integer> employeeIDColumn;
    @FXML
    TableColumn<Employee, String> employeeNameColumn;



    @FXML
    public void populateTable() {
        employeeIDColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getID()).asObject());
        employeeNameColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
        employeeTable.setItems(super.getModel().getAllEmployees());
    }

    @FXML
    private void addEmployeePressed(ActionEvent ae) throws IOException {
        loadEmployeePopup();
    }
}

ControllerMain

import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;

public class ControllerMain extends Controller {

    @FXML
    private ControllerEmployeeTab controllerEmployeeTab;
    @FXML
    private ControllerAddEmployee controllerAddEmployee;

    public ControllerEmployeeTab getControllerEmployeeTab() { return controllerEmployeeTab;}

    public void setControllerEmployeeTab(ControllerEmployeeTab controllerEmployeeTab) {
        this.controllerEmployeeTab = controllerEmployeeTab;
    }
    public ControllerAddEmployee getControllerAddEmployee() { return controllerAddEmployee;}

    public void setControllerAddEmployee(ControllerAddEmployee controllerAddEmployee) {
        this.controllerAddEmployee = controllerAddEmployee;
    }
}

ControllerAddEmploye

Employees are added by pressing the add button which calls addEmployeePressed() from the ControllerEmployeeTab class. The ControllerAddEmployee class handles this, and as far as I can tell employees are added to the database, the ObservableList of employees, and even the TableView but for some reason are not displaying. Adding employees to the database is working, but the code for that menu is here:

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.stage.Stage;
import myorg.scheduler.database.EmployeeDAO;
import myorg.scheduler.people.Employee;

import java.sql.SQLException;

public class ControllerAddEmployee extends Controller {

    @FXML
    private TextField nameField;

    @FXML
    private TextField emailField;

    @FXML
    private TextField phoneField;

    @FXML
    private TextField idField;

    @FXML
    private void addEmployee(ActionEvent ae) throws SQLException, ClassNotFoundException {
        if (idField.getText() == null || nameField.getText() == null || emailField.getText() == null || phoneField.getText() == null ||
                idField.getText().equals("") || nameField.getText().equals("") || emailField.getText().equals("") || phoneField.getText().equals("")) {
            System.out.println("missing a field");
        } else {
            Employee newEmployee = new Employee(Integer.parseInt(idField.getText()), nameField.getText(), emailField.getText(), phoneField.getText());
            super.getModel().addEmployee(newEmployee);
            try {
                EmployeeDAO.insertEmployee(newEmployee);
            } catch (SQLException e) {
                System.out.println("Problem occurred while inserting employee: " + e);
                throw e;
            }
        }
    }
}

MainApp

This is where the model is instantiated, and where public void start is for the JavaFX package. populateTable() from the ControllerEmployeeTab class is called in ```start``

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

import java.io.IOException;

public class MainApp extends Application {

    private static final Model model= Model.getInstance();

    public void start(Stage stage) throws Exception {
        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(MainApp.class.getResource("tabHandler.fxml"));
        Parent root = loader.load();

        ControllerMain mainController = loader.getController();
        mainController.setModel(model);
        model.setEmployeeList();

        setControllers(mainController);
        mainController.getControllerEmployeeTab().populateTable();

        Scene mainScene = new Scene(root);
        stage.setScene(mainScene);
        stage.show();
    }


    public void setControllers(ControllerMain controller) throws IOException {
        FXMLLoader employeeTabLoader = new FXMLLoader();
        employeeTabLoader.setLocation(MainApp.class.getResource("employeeTab.fxml"));
        employeeTabLoader.load();
        controller.setControllerEmployeeTab(employeeTabLoader.getController());
        controller.getControllerEmployeeTab().setModel(model);

    }

    public static void loadEmployeePopup() throws IOException {
        FXMLLoader addEmployeeLoader = new FXMLLoader();
        addEmployeeLoader.setLocation(MainApp.class.getResource("addEmployeePopup.fxml"));
        Parent addEmployeeRoot = addEmployeeLoader.load();

        ControllerAddEmployee employeeController = addEmployeeLoader.getController();
        employeeController.setModel(model);

        Scene newEmployeeScene = new Scene(addEmployeeRoot);
        Stage newEmployeeMenu = new Stage();
        newEmployeeMenu.initModality(Modality.APPLICATION_MODAL);
        newEmployeeMenu.setScene(newEmployeeScene);
        newEmployeeMenu.setResizable(false);
        newEmployeeMenu.show();
    }

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

}

GUI references

tabHandler.fxml

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

<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="myorg.scheduler.gui.ControllerMain"
            prefHeight="600.0" prefWidth="800.0">

    <TabPane>
        <tabs>
            <Tab fx:id="employeeTab" text="Employees">
                <fx:include fx:id="employeeTabPage" source="employeeTab.fxml"/>
            </Tab>
            <Tab fx:id="eventTab" text="Events">
                <fx:include fx:id="eventTabPage" source="eventTab.fxml"/>
            </Tab>
        </tabs>

    </TabPane>
</AnchorPane>

employeeTab.fxml

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

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="myorg.scheduler.gui.ControllerEmployeeTab"
            prefHeight="600.0" prefWidth="800.0">
     <children>
            <HBox AnchorPane.bottomAnchor="531.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
                <children>
                    <ToolBar prefHeight="40.0" prefWidth="800.0">
                        <items>
                            <Button fx:id="btnAddEmployee" mnemonicParsing="false" onAction="#addEmployeePressed" text="Add" />
                        </items>
                    </ToolBar>
                </children>
            </HBox>
            <SplitPane dividerPositions="0.3" layoutY="39.0" prefHeight="532.0" prefWidth="800.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="39.0">
                <items>
                    <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
                        <children>
                            <TableView fx:id="employeeTable" layoutX="18.0" layoutY="34.0" prefHeight="530.0" prefWidth="235.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
                                <columns>
                                    <TableColumn fx:id="employeeIDColumn" prefWidth="73.0" text="ID" />
                                    <TableColumn fx:id="employeeNameColumn" prefWidth="161.0" text="Name" />
                                </columns>
                            </TableView>
                        </children></AnchorPane>
                    <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="557.0" />
                </items>
            </SplitPane>
        </children></AnchorPane>

addEmployeePopup.fxml

I don't think the issue is here but regardless:

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>

<AnchorPane prefHeight="500.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="myorg.scheduler.gui.ControllerAddEmployee">
   <children>
      <TitledPane animated="false" prefHeight="500.0" prefWidth="500.0" text="Add Employee" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
        <content>
          <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="200.0">
               <children>
                  <GridPane layoutX="87.6" layoutY="41.6" prefHeight="191.0" prefWidth="307.0" AnchorPane.bottomAnchor="251.0" AnchorPane.leftAnchor="78.0" AnchorPane.rightAnchor="113.0" AnchorPane.topAnchor="32.0">
                    <columnConstraints>
                      <ColumnConstraints hgrow="SOMETIMES" maxWidth="219.0" minWidth="10.0" prefWidth="156.0" />
                      <ColumnConstraints hgrow="SOMETIMES" maxWidth="304.0" minWidth="10.0" prefWidth="227.0" />
                    </columnConstraints>
                    <rowConstraints>
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                      <RowConstraints maxHeight="40.0" minHeight="10.0" prefHeight="40.0" vgrow="SOMETIMES" />
                      <RowConstraints maxHeight="39.0" minHeight="10.0" prefHeight="38.0" vgrow="SOMETIMES" />
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                    </rowConstraints>
                     <children>
                        <Label text="ID:" GridPane.halignment="RIGHT">
                           <font>
                              <Font size="18.0" />
                           </font>
                           <GridPane.margin>
                              <Insets right="10.0" />
                           </GridPane.margin>
                        </Label>
                        <Label text="Full Name:" textAlignment="RIGHT" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
                           <font>
                              <Font size="18.0" />
                           </font>
                           <GridPane.margin>
                              <Insets right="10.0" />
                           </GridPane.margin>
                        </Label>
                        <Label text="Email:" GridPane.halignment="RIGHT" GridPane.rowIndex="2">
                           <font>
                              <Font size="18.0" />
                           </font>
                           <GridPane.margin>
                              <Insets right="10.0" />
                           </GridPane.margin>
                        </Label>
                        <Label text="Phone:" GridPane.halignment="RIGHT" GridPane.rowIndex="3">
                           <GridPane.margin>
                              <Insets right="10.0" />
                           </GridPane.margin>
                           <font>
                              <Font size="18.0" />
                           </font>
                        </Label>
                        <TextField fx:id="idField" GridPane.columnIndex="1">
                           <GridPane.margin>
                              <Insets left="10.0" right="10.0" />
                           </GridPane.margin>
                        </TextField>
                        <TextField fx:id="nameField" GridPane.columnIndex="1" GridPane.rowIndex="1">
                           <GridPane.margin>
                              <Insets left="10.0" right="10.0" />
                           </GridPane.margin></TextField>
                        <TextField fx:id="emailField" GridPane.columnIndex="1" GridPane.rowIndex="2">
                           <GridPane.margin>
                              <Insets left="10.0" right="10.0" />
                           </GridPane.margin>
                        </TextField>
                        <TextField fx:id="phoneField" GridPane.columnIndex="1" GridPane.rowIndex="3">
                           <opaqueInsets>
                              <Insets />
                           </opaqueInsets>
                           <GridPane.margin>
                              <Insets left="10.0" right="10.0" />
                           </GridPane.margin>
                        </TextField>
                     </children>
                  </GridPane>
                  <ButtonBar layoutX="171.0" layoutY="435.0">
                     <buttons>
                        <Button fx:id="btnCloseAddEmployee" mnemonicParsing="false" onAction="#closeAddEmployeeMenu" text="Cancel" />
                        <Button mnemonicParsing="false" onAction="#addEmployee" prefHeight="25.0" prefWidth="137.0" text="Add Employee" />
                     </buttons>
                  </ButtonBar>
               </children></AnchorPane>
        </content>
      </TitledPane>
   </children>
</AnchorPane>

In summary, employees are added to the TableView but not displayed, and I cannot figure out why. Thank you in advance for any assistance.


Solution

  • @James_D's comment helped me find this solution:

    new tabHandler.fxml for clarity of variables

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Tab?>
    <?import javafx.scene.control.TabPane?>
    <?import javafx.scene.layout.AnchorPane?>
    <AnchorPane xmlns="http://javafx.com/javafx"
                xmlns:fx="http://javafx.com/fxml"
                fx:controller="myorg.scheduler.gui.ControllerMain"
                prefHeight="600.0" prefWidth="800.0">
    
        <TabPane>
            <tabs>
                <Tab text="Employees">
                    <fx:include fx:id="employeeTabPage" source="employeeTab.fxml"/>
                </Tab>
                <Tab text="Events">
                    <fx:include fx:id="eventTabPage" source="eventTab.fxml"/>
                </Tab>
            </tabs>
    
        </TabPane>
    </AnchorPane>
    

    I edited ControllerMain to instead include the FXML AnchorPane and corresponding ControllerEmployeeTab variables :

    import javafx.fxml.FXML;
    import javafx.scene.layout.AnchorPane;
    
    public class ControllerMain extends Controller {
        @FXML
        public AnchorPane employeeTabPage;
        @FXML
        private ControllerEmployeeTab employeeTabPageController;
    
        public ControllerEmployeeTab getControllerEmployeeTab() { return employeeTabPageController;}
    
    }
    

    And finally removed setControllers() entirely and changed that call (in start()) to mainController.getControllerEmployeeTab().setModel(model)