Creating Controllers

Controllers are the glue between your UI (FXML files) and your application logic. They handle user interactions, manage data display, and coordinate between different parts of your application. This cookbook section explains how controllers work in the Jukebox project.

What You'll Learn:
  • How controllers connect to FXML files
  • FXML injection of UI elements
  • Event handling methods
  • The initialize() lifecycle method
  • Controller organization patterns

Controller Basics

A controller is a plain Java class that works with an FXML file. In the Jukebox project, all controllers follow a similar pattern:

package jukebox.controller;

import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.layout.Region;
import java.io.IOException;

public class SomeController {
    
    // ============================================
    // FXML INJECTED ELEMENTS
    // ============================================
    
    // UI elements from FXML are injected here
    // They match fx:id attributes in the FXML file
    
    public SomeUIElement someElement;
    
    // ============================================
    // VIEW FACTORY METHOD
    // ============================================
    
    public static Region createViewInstance() {
        FXMLLoader loader = new FXMLLoader(
            SomeController.class.getResource("/fxml/some-view.fxml")
        );
        try {
            Parent root = loader.load();
            // Store controller reference if needed
            return (Region) root;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    // ============================================
    // INITIALIZATION
    // ============================================
    
    public void initialize() {
        // Called automatically after FXML is loaded
        // Set up data, listeners, etc.
    }
    
    // ============================================
    // EVENT HANDLERS
    // ============================================
    
    public void onSomeAction(ActionEvent event) {
        // Handle button click or other action
    }
}

FXML Injection

When FXMLLoader loads an FXML file, it automatically injects UI elements into the controller's fields. The field names must match the fx:id attributes in the FXML file.

Example from AdminPanelController.java:

// In AdminPanelController.java
public class AdminPanelController {
    
    // These field names match fx:id attributes in admin-panel-view.fxml
    
    public ListView<Album> albumsList;
    public ListView<Song> songsList;
    public TextField albumNameField;
    public TextField albumArtistField;
    public ComboBox<Genre> genreComboBox;
    public TextField songTitleField;
    public TextField songLengthField;
    public Button addAlbumButton;
    public Button deleteAlbumButton;
    public Button addSongButton;
    public Button deleteSongButton;
    
    // ... rest of class
}

Matching FXML:

<!-- In admin-panel-view.fxml -->
<ListView fx:id="albumsList" prefHeight="200" prefWidth="300" />
<ListView fx:id="songsList" prefHeight="200" prefWidth="300" />
<TextField fx:id="albumNameField" promptText="Album name" />
<TextField fx:id="albumArtistField" promptText="Artist" />
<ComboBox fx:id="genreComboBox" prefWidth="150" />
<!-- etc. -->
How Injection Works:
  1. FXMLLoader creates the UI nodes from the FXML file
  2. It finds or creates an instance of the controller class (from fx:controller)
  3. It matches each fx:id attribute with a field in the controller
  4. It sets the field value to the corresponding UI node
  5. Fields can be public or private with @FXML annotation
Important: Field Naming

The field names in the controller MUST exactly match the fx:id values in the FXML file. JavaFX/FXML is case-sensitive, so albumsList in the controller must match fx:id="albumsList" in the FXML.

Event Handling

Controllers handle user interactions through event handler methods. These methods are called when events occur in the UI.

Connecting Events in FXML:

<!-- In admin-panel-view.fxml -->
<Button fx:id="addAlbumButton" 
        onAction="#onAddAlbumClicked" 
        text="Add New Album" />

Handling the Event in Controller:

// In AdminPanelController.java
public void onAddAlbumClicked(ActionEvent actionEvent) {
    // Create new album with default values
    Album newAlbum = new Album();
    newAlbum.name = "New Album";
    newAlbum.artist = "Unknown Artist";
    newAlbum.genre = Genre.UNKNOWN;
    
    // Add to both database and observable list
    Jukebox.database.albums.add(newAlbum);
    albums.add(newAlbum);
    
    // Select the new album
    albumsList.getSelectionModel().select(newAlbum);
}
Event Handler Requirements:
  • Method must be public (or have @FXML annotation)
  • Method name must match the FXML onAction value
  • Method signature must match the event type
  • Common parameter types: ActionEvent, MouseEvent, KeyEvent

The initialize() Method

The initialize() method is a special lifecycle method that's automatically called by FXMLLoader after the FXML is loaded and all injections are complete. This is where you should set up initial data, configure controls, and add listeners.

Example from ChooseAlbumController.java:

public void initialize() {
    // Populate the table with all albums from the database
    albumsTable.getItems().addAll(Jukebox.database.albums);
    
    // Set up the name column to display the album's name property
    nameColumn.setCellValueFactory(param -> 
        new SimpleStringProperty(param.getValue().name));
    
    // Set up the artist column to display the album's artist property
    artistColumn.setCellValueFactory(param -> 
        new SimpleStringProperty(param.getValue().artist));
    
    // Configure the table to allow only single selection
    albumsTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
    
    // 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;
            }
        }
    );
}
When initialize() is Called:
  • After FXML is loaded
  • After all @FXML and public field injections
  • Before any event handlers are triggered
  • Only once per controller instance

Note: If you need to pass parameters to the controller during initialization, you'll need to add a separate method since initialize() has a fixed signature.

Using @FXML Annotation

While the Jukebox project uses public fields for injection, you can also use the @FXML annotation on private fields. This is actually the recommended approach for production code as it provides better encapsulation.

import javafx.fxml.FXML;
import javafx.scene.control.Button;

public class MyController {
    
    // Using @FXML annotation on private fields
    @FXML
    private Button myButton;
    
    @FXML
    private ListView<String> myList;
    
    // This method can also be private with @FXML
    @FXML
    private void handleButtonAction(ActionEvent event) {
        // Handle action
    }
    
    // initialize() can also be private with @FXML
    @FXML
    private void initialize() {
        // Setup code
    }
}
Benefits of @FXML:
  • Encapsulation: Fields can be private
  • Clarity: Makes it explicit which fields are injected
  • Flexibility: Can annotate fields, methods, or the initialize method
  • Best Practice: Recommended for production code

Controller Communication

In the Jukebox project, controllers communicate through static fields in the Jukebox class. When a controller is created, it stores its reference in a static field:

// In AdminPanelController.java
public static Region createViewInstance() {
    FXMLLoader loader = new FXMLLoader(
        ApplicationWindowController.class.getResource("/fxml/admin-panel-view.fxml")
    );
    try {
        Parent root = loader.load();
        // Store the controller in the static field
        Jukebox.adminPanelController = loader.getController();
        return (Region) root;
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

Then, other controllers can access it through the static field:

// In ApplicationWindowController.java
public void displayAdminPanelView() {
    // Access the admin panel controller through the static field
    Jukebox.adminPanelController.initialize();
    
    container.getChildren().clear();
    container.getChildren().add(adminPanelView);
}
Static Controller References in Jukebox.java:
// These static fields enable communication between controllers
public static ApplicationWindowController applicationWindowController;
public static ChooseAlbumController chooseAlbumController;
public static PlayAlbumController playAlbumController;
public static HomeScreenController homeScreenController;
public static AdminPanelController adminPanelController;
public static Database database;
public static Album selectedAlbum;

Accessing the Model from Controllers

Controllers typically access the application's data model through static references. In Jukebox, the Database instance is stored in Jukebox.database:

// In AdminPanelController.java
public void initialize() {
    // Access the database from the static field
    albums = FXCollections.observableArrayList(Jukebox.database.albums);
    
    // Set up the albums ListView
    albumsList.setItems(albums);
}
// In ChooseAlbumController.java
public void initialize() {
    // Access albums from the database
    albumsTable.getItems().addAll(Jukebox.database.albums);
}
// In AdminPanelController.java - adding a new album
public void onAddAlbumClicked(ActionEvent actionEvent) {
    Album newAlbum = new Album();
    // Add to the database
    Jukebox.database.albums.add(newAlbum);
    albums.add(newAlbum);
}
Data Access Pattern:

In Jukebox, all data flows through the static Jukebox.database instance. This simple pattern works well for learning projects, but for larger applications, consider:

  • Dependency injection
  • Repository pattern
  • Event bus for communication

Creating Your Own Controller

Let's create a simple controller for a new view:

  • Step 1: Create the Controller Class

    package myapp;
    
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.event.ActionEvent;
    import javafx.fxml.FXML;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.control.ListView;
    import javafx.scene.layout.Region;
    import java.io.IOException;
    
    public class MyController {
        
        // FXML Injected Elements
        @FXML
        private Label messageLabel;
        
        @FXML
        private ListView<String> itemsList;
        
        @FXML
        private Button addButton;
        
        // Data
        private ObservableList<String> items;
        
        // View Factory Method
        public static Region createViewInstance() {
            FXMLLoader loader = new FXMLLoader(
                MyController.class.getResource("/fxml/my-view.fxml")
            );
            try {
                Parent root = loader.load();
                MyController controller = loader.getController();
                controller.initializeData();
                return (Region) root;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        
        // Initialization (called by FXMLLoader)
        @FXML
        private void initialize() {
            // Set up the list
            items = FXCollections.observableArrayList();
            itemsList.setItems(items);
        }
        
        // Additional initialization with data
        public void initializeData() {
            items.add("Item 1");
            items.add("Item 2");
            items.add("Item 3");
        }
        
        // Event handler
        @FXML
        private void onAddButtonClicked(ActionEvent event) {
            String newItem = "Item " + (items.size() + 1);
            items.add(newItem);
            messageLabel.setText("Added: " + newItem);
        }
    }
  • Step 2: Create the FXML File

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Button?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.control.ListView?>
    <?import javafx.scene.layout.VBox?>
    
    <VBox alignment="CENTER" spacing="20" 
         xmlns="http://javafx.com/javafx/25" 
         xmlns:fx="http://javafx.com/fxml/1"
         fx:controller="myapp.MyController">
        
        <children>
            <Label fx:id="messageLabel" text="My View" />
            <ListView fx:id="itemsList" prefHeight="200" prefWidth="200" />
            <Button fx:id="addButton" onAction="#onAddButtonClicked" text="Add Item" />
        </children>
    </VBox>
  • Try This:

    1. Open AdminPanelController.java
    2. Find the initialize() method and trace through what it does
    3. Find an event handler method (like onAddAlbumClicked) and see how it accesses the model
    4. Find how the controller stores its reference in a static field
    5. Look at Jukebox.java and see all the static controller references

    Common Controller Patterns in Jukebox

    1. Setup Methods

    Most controllers have setup methods for configuring UI elements:

    2. Helper Methods

    Controllers often have helper methods for common tasks:

    3. View Display Methods

    The ApplicationWindowController has methods for displaying different views:

    Key Takeaways

    Related Cookbook Sections