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.
- 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. -->
- FXMLLoader creates the UI nodes from the FXML file
- It finds or creates an instance of the controller class (from
fx:controller) - It matches each
fx:idattribute with a field in the controller - It sets the field value to the corresponding UI node
- Fields can be public or private with @FXML annotation
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);
}
- Method must be public (or have @FXML annotation)
- Method name must match the FXML
onActionvalue - 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;
}
}
);
}
initialize() is Called:
- After FXML is loaded
- After all
@FXMLand 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
}
}
- 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);
}
// 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);
}
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:
- Open
AdminPanelController.java - Find the
initialize()method and trace through what it does - Find an event handler method (like
onAddAlbumClicked) and see how it accesses the model - Find how the controller stores its reference in a static field
- Look at
Jukebox.javaand see all the static controller references
Common Controller Patterns in Jukebox
1. Setup Methods
Most controllers have setup methods for configuring UI elements:
setupAlbumsList()- Configures ListView in AdminPanelControllersetupSongsList()- Configures another ListViewsetupGenreComboBox()- Populates ComboBox with enum valuessetupSelectionListeners()- Sets up listeners for selection changes
2. Helper Methods
Controllers often have helper methods for common tasks:
showConfirmation()- Shows a confirmation dialogshowAlert()- Shows an information alert
3. View Display Methods
The ApplicationWindowController has methods for displaying different views:
displayHomeView()displayChooseAlbumView()displayPlayAlbumView()displayAdminPanelView()
Key Takeaways
- Controllers handle user interactions and manage UI state
- FXML injection automatically sets controller fields from FXML
fx:idattributes - Event handlers are methods that match
onActionvalues in FXML - The
initialize()method is called automatically after FXML loading - Use
@FXMLannotation for better encapsulation (recommended) - Controllers communicate through static fields in Jukebox
- Controllers access data through
Jukebox.database