Jonatan Stenbacka
Jonatan Stenbacka

Reputation: 1864

Content from ObservableArrayList gets loaded (probably) but not shown

I have created a little program that is supposed to load data from a database and display it in a TableView element in a JavaFX application.

The problem is that when I start the application all I see is empty rows. The data of the different rows doesn't get displayed. But I am certain that they are handled somehow, because the number of empty rows shown matches the amount of rows loaded from the database.

Also my System.out.prints for debug prints out the correct data from the database, so I'm certain that the problem isn't within the DBLoader class.

I took most of it from this guide. What I've done so far is inspired by the first two parts of the tutorial.

Edit: And yes, I have added DatabaseOverviewControll.java as the controller class of the DatabaseOverview.fxml. And I have added itemNumberColumn, descriptionColumn and priceColumn as the fx:id of their respective TableColumn element.

My PrototypeApp class that extends application, and is the runnable class:

package prototype;

import java.io.IOException;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import prototype.util.DBLoader;
import javafx.collections.*;
import prototype.view.DatabaseOverviewController;

import prototype.model.Row;

public class PrototypeApp extends Application {

    private Stage primaryStage;
    private BorderPane rootLayout;

    private ObservableList<Row> databaseData = FXCollections
            .observableArrayList();

    /**
     * The constructor. Creates an dbLoader object and loads the database data
     * from that dbLoader into the ObservableArrayList databaseData;
     */
    public PrototypeApp() {
        //DBLoader dbLoader = new DBLoader();

        // databaseData = dbLoader.getData(); //This is used by me, but for the
        // sake of testability I added some mock data instead.

        // Some mock data for Stack Overflow users.
        databaseData.add(new Row("abc", "efg", "hij"));
        databaseData.add(new Row("abc2", "efg2", "hij2"));
        databaseData.add(new Row("abc3", "efg3", "hij3"));
        databaseData.add(new Row("abc4", "efg4", "hij4"));

        // For debugging.
        for (Row row : databaseData) {
            System.out.println(row.getItemNumber() + "\t" + row.getDescription() + "\t" + row.getPrice());
        }
    }

    public ObservableList<Row> getDatabaseData() {
        return databaseData;
    }

    @Override
    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;
        this.primaryStage.setTitle("Prototype");

        initRootLayout();

        showDatabaseOverview();
    }

    /**
     * Initializes the root layout.
     */
    public void initRootLayout() {
        try {
            // Load root layout from fxml file.
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(PrototypeApp.class
                    .getResource("view/RootLayout.fxml"));
            rootLayout = (BorderPane) loader.load();

            // Show the scene containing the root layout.
            Scene scene = new Scene(rootLayout);
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Shows the database overview inside the root layout.
     */

    public void showDatabaseOverview() {
        try {
            // Load database overview
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(PrototypeApp.class
                    .getResource("view/DatabaseOverview.fxml"));
            AnchorPane databaseOverview = (AnchorPane) loader.load();

            // Set the database overview into the center of root layout.
            rootLayout.setCenter(databaseOverview);

            // Give the controller access to the prototype app.
            DatabaseOverviewController controller = loader.getController();
            controller.setPrototypeApp(this);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Returns the main stage.
     * 
     * @return
     */

    public Stage getPrimaryStage() {
        return primaryStage;
    }

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

And here's my DatabaseOverviewController class:

package prototype.view;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import prototype.PrototypeApp;
import prototype.model.Row;

public class DatabaseOverviewController {

    @FXML
    private TableView<Row> databaseTable;
    @FXML
    private TableColumn<Row, String> itemNumberColumn;
    @FXML
    private TableColumn<Row, String> descriptionColumn;
    @FXML
    private TableColumn<Row, String> priceColumn;

    @FXML
    private Label itemNumberLabel;

    @FXML
    private Label descriptionLabel;

    @FXML
    private Label priceLabel;

    // Reference to the prototype application
    private PrototypeApp prototypeApp;

    /**
     * The constructor The constructor is called before the initialize() method.
     */
    public DatabaseOverviewController() {
    }

    /**
     * Initializes a controller class. This method is automatically called after
     * the fxml file has been loaded.
     */
    @FXML
    private void intialize() {
        // Initialize the database table with the three columns.
        itemNumberColumn.setCellValueFactory(cellData -> cellData.getValue()
                .itemNumberProperty());
        descriptionColumn.setCellValueFactory(cellData -> cellData.getValue()
                .descriptionProperty());
        priceColumn.setCellValueFactory(cellData -> cellData.getValue()
                .priceProperty());
    }

    /**
     * A method called by the prototype application to give a reference back to
     * itself.
     * 
     * @param prototypeApp
     */
    public void setPrototypeApp(PrototypeApp prototypeApp) {
        this.prototypeApp = prototypeApp;

        // Adds observable list data to the table
        databaseTable.setItems(prototypeApp.getDatabaseData());
    }
}

And here's my row class that represents a row in the SQL table:

package prototype.model;

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

/*
 * Model class for a SQL row.
 * 
 * @author Jonatan Stenbacka
 */

public class Row {

    private final StringProperty itemNumber;
    private final StringProperty description;
    private final StringProperty price;

    /**
     * Default constructor
     */
    public Row() {
        this(null, null, null);
    }

    /**
     * Constructor
     * @param itemNumber
     * @param description
     * @param price
     */
    public Row (String itemNumber, String description, String price) {
        this.itemNumber = new SimpleStringProperty(itemNumber);
        this.description = new SimpleStringProperty(description);
        this.price = new SimpleStringProperty(price);
    }

    public String getItemNumber() {
        return itemNumber.get();
    }

    public void setItemNumber(String itemNumber) {
        this.itemNumber.set(itemNumber);
    }

    public StringProperty itemNumberProperty() {
        return itemNumber;
    }

    public String getDescription() {
        return description.get();
    }

    public void setDescription(String description) {
        this.description.set(description);
    }

    public StringProperty descriptionProperty() {
        return description;
    }

    public String getPrice() {
        return price.get();
    }

    public void setPrice(String price) {
        this.price.set(price);
    }

    public StringProperty priceProperty() {
        return price;
    }
}

Edit: Here's my FXML files. Should be enough to run the program if you just add some mock Row data.

DatabaseOverview.fxml:

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

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

<AnchorPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="prototype.view.DatabaseOverviewController">
   <children>
      <BorderPane layoutX="191.0" layoutY="58.0" prefHeight="400.0" prefWidth="600.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
         <top>
            <Label text="Tables" BorderPane.alignment="CENTER">
               <font>
                  <Font size="24.0" />
               </font>
            </Label>
         </top>
         <bottom>
            <Button mnemonicParsing="false" onAction="#handleLoadDatabase" text="Load" BorderPane.alignment="CENTER">
               <BorderPane.margin>
                  <Insets bottom="10.0" />
               </BorderPane.margin>
            </Button>
         </bottom>
         <center>
            <TableView fx:id="databaseTable" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
              <columns>
                <TableColumn fx:id="itemNumberColumn" prefWidth="200.0" text="Item Number" />
                <TableColumn fx:id="descriptionColumn" prefWidth="200.0" text="Description" />
                  <TableColumn fx:id="priceColumn" prefWidth="200.0" text="Price" />
              </columns>
               <columnResizePolicy>
                  <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
               </columnResizePolicy>
            </TableView>
         </center>
      </BorderPane>
   </children>
</AnchorPane>

RootLayout.fxml:

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

<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.BorderPane?>


<BorderPane prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.40">
   <top>
      <MenuBar BorderPane.alignment="CENTER">
        <menus>
          <Menu mnemonicParsing="false" text="File">
            <items>
              <MenuItem mnemonicParsing="false" text="Close" />
            </items>
          </Menu>
          <Menu mnemonicParsing="false" text="Edit">
            <items>
              <MenuItem mnemonicParsing="false" text="Delete" />
            </items>
          </Menu>
          <Menu mnemonicParsing="false" text="Help">
            <items>
              <MenuItem mnemonicParsing="false" text="About" />
            </items>
          </Menu>
        </menus>
      </MenuBar>
   </top>
</BorderPane>

Upvotes: 0

Views: 1009

Answers (1)

James_D
James_D

Reputation: 209684

There's a simple typo in the DatabaseOverviewController: you have intialize() as the method name, instead of initialize(). Since the FXMLLoader uses reflection to search for a method named initialize, and just does nothing if it doesn't find one, your method silently fails to be invoked. Fixing the typo fixes the problem.

For what it's worth, here's how I figured this out, as it's really hard to see. I observed, as you did, that the correct number of rows existed in the table (you can select them), but weren't displaying anything. This is almost always called by not having set the cellValueFactory (or having set it incorrectly, which can happen if you use a PropertyValueFactory but rarely happens when you use the nice approach with lambdas that you show here).

Since the initialize() method appeared to be setting these correctly, I added logging, first to Row.itemNumberProperty(), and then when I observed that method wasn't getting invoked (it should be invoked by each TableCell in that column), to the initialize() method itself. When I observed the initialize() method wasn't getting invoked, I found the typo.

Not sure what the general message is from this, if any? Perhaps it's better to use the old (pre JavaFX 2.1) style approach of having the controller implement Initializable, so that the compiler will catch mistyped method names. I suppose what would have been nicer is if the post-FXML-loading method to be invoked had a specific annotation (@PostLoading or something).

Upvotes: 1

Related Questions