Using TableView

TableView is a JavaFX control for displaying tabular data (rows and columns). While ListView is great for simple lists, TableView provides a more structured way to display multiple properties of each item. In the Jukebox project, TableView is used in the Choose Album view to display albums with their name and artist. This cookbook section explains how to work with TableView.

What You'll Learn:
  • Setting up TableView in FXML
  • Defining TableColumn for each property
  • Using cell value factories
  • Configuring selection mode
  • Displaying related data (album to songs)
  • Handling selection changes

TableView in FXML

In the Jukebox project, the Choose Album view uses a TableView. Let's look at the FXML:

<!-- In choose-album-view.fxml -->

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

<TableView fx:id="albumsTable" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
    <columns>
        <TableColumn fx:id="nameColumn" prefWidth="150.0" text="Album Name" />
        <TableColumn fx:id="artistColumn" prefWidth="150.0" text="Artist" />
    </columns>
    <columnResizePolicy>
        <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
    </columnResizePolicy>
</TableView>
TableView FXML Structure:
  • TableView - The main container for the table
  • columns - Contains the TableColumn definitions
  • TableColumn - Defines each column with fx:id, text (header), and prefWidth
  • columnResizePolicy - Controls how columns can be resized
Column Resize Policy:
  • CONSTRAINED_RESIZE_POLICY - Columns maintain their relative widths as the table is resized
  • UNCONSTRAINED_RESIZE_POLICY - Columns can be freely resized

Setting Up TableView in Controller

In ChooseAlbumController.java, the TableView is configured in the initialize() method:

public void initialize() {
    // 1. Populate the table with all albums from the database
    albumsTable.getItems().addAll(Jukebox.database.albums);
    
    // 2. Set up the name column to display the album's name property
    nameColumn.setCellValueFactory(param -> 
        new SimpleStringProperty(param.getValue().name));
    
    // 3. Set up the artist column to display the album's artist property
    artistColumn.setCellValueFactory(param -> 
        new SimpleStringProperty(param.getValue().artist));
    
    // 4. Configure the table to allow only single selection
    albumsTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
    
    // 5. Add a listener to the selected item property
    albumsTable.getSelectionModel().selectedItemProperty().addListener(
        (obs, oldSelection, newSelection) -> {
            if (newSelection != null) {
                albumSongsList.getItems().setAll(newSelection.songs);
                albumTitleLabel.setText(newSelection.name + " - " + newSelection.artist);
                Jukebox.selectedAlbum = newSelection;
            }
        }
    );
}

Step by Step Explanation:

  • Step 1: Populate with Data
    albumsTable.getItems().addAll(Jukebox.database.albums);

    This adds all albums from the database to the TableView's items list. The TableView will display each album as a row.

  • Step 2 & 3: Set Up Cell Value Factories
    nameColumn.setCellValueFactory(param -> 
        new SimpleStringProperty(param.getValue().name));
    
    artistColumn.setCellValueFactory(param -> 
        new SimpleStringProperty(param.getValue().artist));

    The cell value factory is what tells each column how to extract the value to display from each row's data object.

    Understanding Cell Value Factory:
    • param is a TableColumn.CellDataFeatures<Album, String>
    • param.getValue() returns the Album object for that row
    • We extract a property (name or artist) and wrap it in a SimpleStringProperty
    • The TableView uses this property to get the value for display
  • Step 4: Set Selection Mode
    albumsTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);

    This configures the table to allow only single row selection at a time.

  • Step 5: Add Selection Listener

    When a user selects a row, we want to display the songs from that album:

    albumsTable.getSelectionModel().selectedItemProperty().addListener(
        (obs, oldSelection, newSelection) -> {
            if (newSelection != null) {
                albumSongsList.getItems().setAll(newSelection.songs);
                albumTitleLabel.setText(newSelection.name + " - " + newSelection.artist);
                Jukebox.selectedAlbum = newSelection;
            }
        }
    );

    This listener:

    • Gets the selected album
    • Populates the songs ListView with that album's songs
    • Updates the album title label
    • Stores the selected album in the global field
  • TableView vs ListView

    While both TableView and ListView display collections of items, they have different use cases:

    When to Use TableView:
    • You need to display multiple properties of each item
    • You want column headers
    • You want sortable columns
    • You want resizable columns
    • You want a more spreadsheet-like display
    When to Use ListView:
    • You only need to display one string per item
    • Your model's toString() provides sufficient information
    • You want a simpler setup
    • You want selection to work with single clicks

    Cell Value Factory in Detail

    The cell value factory is a crucial concept for TableView. It's a function that extracts the value for a specific column from each row's data object.

    // Lambda expression version (used in Jukebox)
    nameColumn.setCellValueFactory(param -> 
        new SimpleStringProperty(param.getValue().name));
    
    // Equivalent method reference version
    nameColumn.setCellValueFactory(param -> 
        new SimpleStringProperty(param.getValue().getName()));
    
    // Long form with explicit types
    nameColumn.setCellValueFactory(
        new Callback<TableColumn.CellDataFeatures<Album, String>, ObservableValue<String>>() {
            @Override
            public ObservableValue<String> call(TableColumn.CellDataFeatures<Album, String> param) {
                return new SimpleStringProperty(param.getValue().name);
            }
        }
    );
    Why SimpleStringProperty?

    The cell value factory must return an ObservableValue<T> where T is the type of the column. For simple string display:

    • SimpleStringProperty - For String values
    • SimpleIntegerProperty - For int values
    • SimpleBooleanProperty - For boolean values

    You can also use other ObservableValue implementations or even create your own.

    Alternative: Using Property Methods

    If your model classes use JavaFX properties (with getters), you can simplify the cell value factory:

    // If Album had property methods:
    public class Album {
        private StringProperty name = new SimpleStringProperty("");
        
        public StringProperty nameProperty() {
            return name;
        }
        
        public String getName() {
            return name.get();
        }
        
        public void setName(String name) {
            this.name.set(name);
        }
    }
    
    // Then the cell value factory becomes:
    nameColumn.setCellValueFactory(param -> param.getValue().nameProperty());
    
    // Or even simpler with method reference:
    nameColumn.setCellValueFactory(Album::nameProperty);
    Using Properties in Model Classes:
    • Pros: Simpler cell value factories, automatic change notification
    • Cons: More boilerplate code in model classes
    • Jukebox Approach: Uses public fields for simplicity, which works fine for a learning project

    Displaying Related Data

    One of the powerful features of the Choose Album view is that when you select an album, it displays that album's songs in a separate ListView. This is a great example of displaying related data:

    <!-- In choose-album-view.fxml -->
    <VBox>
        <Label fx:id="albumTitleLabel" text="Select an album" />
        <TableView fx:id="albumsTable" ...>
            <columns>
                <TableColumn fx:id="nameColumn" ... />
                <TableColumn fx:id="artistColumn" ... />
            </columns>
        </TableView>
        <ListView fx:id="albumSongsList" prefHeight="200" prefWidth="200" />
    </VBox>

    In ChooseAlbumController:

    // In the selection listener
    albumsTable.getSelectionModel().selectedItemProperty().addListener(
        (obs, oldSelection, newSelection) -> {
            if (newSelection != null) {
                // Update the songs ListView with the selected album's songs
                albumSongsList.getItems().setAll(newSelection.songs);
                
                // Update the label
                albumTitleLabel.setText(newSelection.name + " - " + newSelection.artist);
                
                // Store the selected album globally
                Jukebox.selectedAlbum = newSelection;
            }
        }
    );
    Related Data Display Pattern:
    1. User selects a row in the TableView (album)
    2. Selection listener gets the selected album
    3. Listener accesses the album's songs list (newSelection.songs)
    4. Listener sets the songs ListView to display that list
    5. Now the songs ListView shows only songs from the selected album

    Play Button and Navigation

    The Choose Album view also has a Play button that allows the user to navigate to the Play Album view for the selected album:

    public void onPlayAlbumButtonClicked(ActionEvent actionEvent) {
        Album selected = albumsTable.getSelectionModel().getSelectedItem();
        
        if (selected != null) {
            // Set the selected album globally
            Jukebox.selectedAlbum = selected;
            
            // Navigate to play album view
            Jukebox.applicationWindowController.displayPlayAlbumView();
        }
        // If no album is selected, the button click is silently ignored
    }
    Passing Data Between Views:

    The Play button demonstrates a common pattern in Jukebox:

    1. Get the selected item from the current view
    2. Store it in a global/static field (Jukebox.selectedAlbum)
    3. Navigate to the target view
    4. The target view's controller reads from the global field when it's displayed

    Creating Your Own TableView

    Let's create a simple TableView to display a list of people:

  • Step 1: Create Model Class with Properties

    public class Person {
        public String firstName = "";
        public String lastName = "";
        public int age = 0;
        
        @Override
        public String toString() {
            return firstName + " " + lastName;
        }
    }
  • Step 2: Create FXML with TableView and Columns

    <?import javafx.scene.control.TableColumn?>
    <?import javafx.scene.control.TableView?>
    
    <TableView fx:id="peopleTable" prefHeight="200" prefWidth="400">
        <columns>
            <TableColumn fx:id="firstNameColumn" prefWidth="150" text="First Name" />
            <TableColumn fx:id="lastNameColumn" prefWidth="150" text="Last Name" />
            <TableColumn fx:id="ageColumn" prefWidth="100" text="Age" />
        </columns>
        <columnResizePolicy>
            <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
        </columnResizePolicy>
    </TableView>
  • Step 3: Set Up Controller

    @FXML
    private TableView<Person> peopleTable;
    
    @FXML
    private TableColumn<Person, String> firstNameColumn;
    
    @FXML
    private TableColumn<Person, String> lastNameColumn;
    
    @FXML
    private TableColumn<Person, Number> ageColumn;
    
    @FXML
    private void initialize() {
        // Populate with data
        peopleTable.getItems().addAll(
            new Person("John", "Doe", 30),
            new Person("Jane", "Smith", 25),
            new Person("Bob", "Johnson", 35)
        );
        
        // Set up cell value factories
        firstNameColumn.setCellValueFactory(param -> 
            new SimpleStringProperty(param.getValue().firstName));
        
        lastNameColumn.setCellValueFactory(param -> 
            new SimpleStringProperty(param.getValue().lastName));
        
        ageColumn.setCellValueFactory(param -> 
            new SimpleIntegerProperty(param.getValue().age));
        
        // Configure selection
        peopleTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
    }
  • Try This:

    1. Run the Jukebox application and click "Play Mode" from the home screen
    2. Notice the TableView showing all albums with their name and artist
    3. Click on different albums and see how the songs list updates
    4. Click on an album, then click Play to go to the Play Album view
    5. Open choose-album-view.fxml and examine the TableView structure
    6. Open ChooseAlbumController.java and find the initialize() method
    7. Trace through how the cell value factories work
    8. Try adding a new album in Admin Panel, then return to Play Mode to see it in the table

    Common Pitfalls

    Pitfall 1: Forgetting to Set Cell Value Factory

    If you don't set a cell value factory for a TableColumn, that column will be empty. Always set cell value factories for all columns.

    Pitfall 2: Type Mismatch in Cell Value Factory

    The cell value factory must return an ObservableValue of the type specified in the TableColumn's generic parameters. For example, if your TableColumn is TableColumn<Person, Number>, the factory must return ObservableValue<Number>.

    Pitfall 3: Not Setting Column Widths

    If you don't set preferred widths for your columns, they might be too narrow or too wide. Always set appropriate widths.

    Pitfall 4: Null Values in TableView

    If your model objects have null values for properties displayed in the table, you might see null or get errors. Always ensure your model objects have valid values.

    Pitfall 5: Memory with Large Datasets

    TableView can handle large datasets, but performance might degrade. For very large datasets, consider using pagination or virtualization.

    Key Takeaways

    Related Cookbook Sections