Controller Communication
In a JavaFX application with multiple views and controllers, it's essential to have a way for controllers to communicate with each other and share data. The Jukebox project uses a combination of static fields, direct references, and event-driven patterns. This cookbook section explains how controllers communicate in the Jukebox application.
- The Jukebox static field pattern for global access
- How controllers access each other's methods and data
- Passing data between views using static fields
- Cross-controller method calls
- Event-driven communication patterns
- When to use different communication approaches
The Jukebox Static Field Pattern
The Jukebox project uses a simple but effective pattern: static fields in the main Jukebox class that hold references to all controllers and important data. This allows any controller to access any other controller or shared data.
// In Jukebox.java
public class Jukebox extends Application {
// ============================================
// GLOBAL CONTROLLER INSTANCES
// ============================================
public static ApplicationWindowController applicationWindowController;
public static ChooseAlbumController chooseAlbumController;
public static PlayAlbumController playAlbumController;
public static HomeScreenController homeScreenController;
public static AdminPanelController adminPanelController;
// ============================================
// GLOBAL DATA
// ============================================
public static Database database;
public static Album selectedAlbum;
// ============================================
// Main method and start method
// ============================================
}
- Simplicity: Easy to understand and implement for beginners
- Global Access: Any controller can access any other controller or shared data
- No Complex Infrastructure: No need for event buses, dependency injection, or complex patterns
- Clear Flow: Data flow is explicit and traceable
- Good for Learning: Perfect for educational projects where clarity is important
- Each view's controller has a
createViewInstance()factory method - This method loads the FXML and gets the controller instance from the loader
- The controller is stored in the corresponding static field in
Jukebox.java - This happens during application startup and view creation
// In ApplicationWindowController.java
public static Region createViewInstance() {
FXMLLoader loader = new FXMLLoader(
ApplicationWindowController.class.getResource("/fxml/application-window.fxml")
);
try {
Parent root = loader.load();
// Store the controller in the static field
Jukebox.applicationWindowController = loader.getController();
return (Region) root;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// In ChooseAlbumController.java
public static Region createViewInstance() {
FXMLLoader loader = new FXMLLoader(
ApplicationWindowController.class.getResource("/fxml/choose-album-view.fxml")
);
try {
Parent root = loader.load();
// Store the controller in the static field
Jukebox.chooseAlbumController = loader.getController();
return (Region) root;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Same pattern for all other controllers
Communication Pattern 1: Direct Method Calls
The most common communication pattern in Jukebox is direct method calls between controllers using the static fields.
Navigation: Controllers Calling ApplicationWindowController
All view controllers can trigger navigation by calling methods on ApplicationWindowController:
// In HomeScreenController.java
public void onPlayModeClicked(ActionEvent actionEvent) {
// Direct method call to ApplicationWindowController
Jukebox.applicationWindowController.displayChooseAlbumView();
}
public void onAdminModeClicked(ActionEvent actionEvent) {
// Direct method call to ApplicationWindowController
Jukebox.applicationWindowController.displayAdminPanelView();
}
// In ChooseAlbumController.java
public void onPlayAlbumButtonClicked(ActionEvent actionEvent) {
Album selected = albumsTable.getSelectionModel().getSelectedItem();
if (selected != null) {
Jukebox.selectedAlbum = selected;
// Direct method call to navigate
Jukebox.applicationWindowController.displayPlayAlbumView();
}
}
// In PlayAlbumController.java
public void onBackButtonClicked(ActionEvent actionEvent) {
// Direct method call to go back
Jukebox.applicationWindowController.displayChooseAlbumView();
}
- User interacts with a view (clicks a button)
- View's controller handles the event
- Controller accesses
Jukebox.applicationWindowController - Controller calls the appropriate display method
- ApplicationWindowController handles the view switch
Data Passing: Using Static Data Fields
Controllers share data through static fields in the Jukebox class:
// In ChooseAlbumController.java - Setting data
public void onPlayAlbumButtonClicked(ActionEvent actionEvent) {
Album selected = albumsTable.getSelectionModel().getSelectedItem();
if (selected != null) {
// Set the selected album in static field
Jukebox.selectedAlbum = selected;
// Navigate
Jukebox.applicationWindowController.displayPlayAlbumView();
}
}
// In PlayAlbumController.java - Reading data
public void initialize() {
// Read the selected album from static field
if (Jukebox.selectedAlbum != null) {
albumTitleLabel.setText(Jukebox.selectedAlbum.name);
albumArtistLabel.setText(Jukebox.selectedAlbum.artist);
songsList.getItems().setAll(Jukebox.selectedAlbum.songs);
}
}
- Source Controller: Sets data in static field before navigation
- Navigate: Calls display method on ApplicationWindowController
- Target Controller: Reads data from static field in its
initialize()method or when displayed
Controller-to-Controller Method Calls
In some cases, controllers call methods directly on each other:
// In ApplicationWindowController.java - displayPlayAlbumView()
public void displayPlayAlbumView() {
// Check if coming from admin panel (for auto-save)
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData();
}
// Re-initialize the play album controller
// This ensures it has fresh data when displayed
Jukebox.playAlbumController.initialize();
// Clear and add the view
container.getChildren().clear();
container.getChildren().add(playAlbumView);
}
Some controllers need to refresh their data when they're displayed. The displayPlayAlbumView() method in ApplicationWindowController calls initialize() on the PlayAlbumController before showing it. This ensures the view displays current data.
Communication Pattern 2: Shared Database
All controllers in Jukebox have access to the same Database instance through the static field Jukebox.database. This provides a shared data model that all controllers can read from and modify.
// In AdminPanelController.java - Modifying database
public void onAddAlbumClicked(ActionEvent actionEvent) {
Album newAlbum = new Album();
newAlbum.name = "New Album";
newAlbum.artist = "Unknown Artist";
newAlbum.genre = Genre.UNKNOWN;
// Modify the shared database
Jukebox.database.albums.add(newAlbum);
albums.add(newAlbum);
albumsList.getSelectionModel().select(newAlbum);
}
public void onDeleteAlbumClicked(ActionEvent actionEvent) {
Album selected = albumsList.getSelectionModel().getSelectedItem();
if (selected != null && showConfirmation("Delete Album", ...)) {
// Modify the shared database
Jukebox.database.albums.remove(selected);
albums.remove(selected);
// ... rest of logic
}
}
// In ChooseAlbumController.java - Reading database
public void initialize() {
// Populate table with data from shared database
albumsTable.getItems().addAll(Jukebox.database.albums);
// Set up columns
nameColumn.setCellValueFactory(param ->
new SimpleStringProperty(param.getValue().name));
artistColumn.setCellValueFactory(param ->
new SimpleStringProperty(param.getValue().artist));
// ... rest of initialization
}
- Single Source of Truth: All controllers work with the same database instance
- Immediate Updates: Changes made by one controller are immediately visible to others
- No Synchronization Needed: Since everything runs on the JavaFX thread
- Consistent State: All views show the same data
Communication Pattern 3: Auto-Save Triggering
One clever communication pattern in Jukebox is the auto-save mechanism. When leaving the Admin Panel, changes are automatically saved. This is implemented by checking the current view before switching:
// In ApplicationWindowController.java - All display methods
public void displayHomeView() {
// Check if coming from admin panel
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData(); // Auto-save!
}
container.getChildren().clear();
container.getChildren().add(homeScreenView);
}
public void displayChooseAlbumView() {
// Check if coming from admin panel
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData(); // Auto-save!
}
container.getChildren().clear();
container.getChildren().add(chooseAlbumView);
}
public void displayPlayAlbumView() {
// Check if coming from admin panel
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData(); // Auto-save!
}
// Re-initialize play album controller
Jukebox.playAlbumController.initialize();
container.getChildren().clear();
container.getChildren().add(playAlbumView);
}
- User is working in Admin Panel, making changes to albums/songs
- These changes are made directly to
Jukebox.databaseobjects - User navigates to another view (Home, Choose Album, Play Album)
- Each display method checks if the current view is the Admin Panel
- If it is,
database.saveData()is called - This saves all changes to the JSON file automatically
- User doesn't need to remember to click Save
- Prevents Data Loss: Users don't lose changes by forgetting to save
- Transparent: Happens automatically without user intervention
- Conditional: Only saves when actually leaving the admin panel
- Consistent: Applied to all navigation paths from admin
Communication Pattern 4: Event-Driven Updates
While Jukebox primarily uses direct method calls and static fields, it also uses listeners for automatic updates. This is especially evident in the Admin Panel where changes to fields automatically update the model objects:
// In AdminPanelController.java - Event-driven updates
private void setupSelectionListeners() {
// ... album and song selection listeners ...
// Album field change listeners - auto-update on change
albumNameField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null) {
selectedAlbum.name = newVal;
albumsList.refresh();
}
});
albumArtistField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null) {
selectedAlbum.artist = newVal;
albumsList.refresh();
}
});
genreComboBox.valueProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null && newVal != null) {
selectedAlbum.genre = newVal;
albumsList.refresh();
}
});
// Song field change listeners - auto-update on change
songTitleField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedSong != null) {
selectedSong.title = newVal;
songsList.refresh();
}
});
songLengthField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedSong != null && !newVal.isEmpty()) {
try {
selectedSong.lengthSeconds = Integer.parseInt(newVal);
songsList.refresh();
} catch (NumberFormatException e) {
// Ignore invalid input
}
}
});
}
- User selects an album from the ListView
- Selection listener updates the edit fields with the album's data
- User types in the name field
- Text property listener fires, updating the album's name field
- Listener calls
refresh()on the ListView - ListView updates to show the new name
- Since the album is in
Jukebox.database.albums, the change is visible everywhere
The Admin Panel implements a two-way binding pattern:
- Selection -> Fields: When user selects an album, fields are populated with album data
- Fields -> Model: When user edits a field, the album's property is updated
- Model -> View: The ListView is refreshed to show the updated data
This creates a seamless editing experience where changes are immediately visible.
Communication Pattern 5: GO HOME Button
The GO HOME button in the application window provides consistent navigation from any view. This is implemented through a direct method call:
// In application-window.fxml
<Button mnemonicParsing="false" onAction="#onHomeButtonClicked" text="GO HOME" />
// In ApplicationWindowController.java
public void onHomeButtonClicked(ActionEvent actionEvent) {
displayHomeView();
}
// In ApplicationWindowController.java
public void displayHomeView() {
// Auto-save if coming from admin panel
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData();
}
container.getChildren().clear();
container.getChildren().add(homeScreenView);
}
- Consistent Navigation: Users always have a way to return home
- Predictable: Always in the same location (top right)
- Smart: Handles auto-save when coming from Admin Panel
- Simple: Direct method call to ApplicationWindowController
Creating Your Own Communication System
Let's create a simple communication pattern between controllers:
Step 1: Create a Central Data Holder
Create a class to hold shared data and controller references:
public class AppData {
// Controller references
public static MainController mainController;
public static SettingsController settingsController;
public static ProfileController profileController;
// Shared data
public static User currentUser;
public static Settings appSettings;
public static List<RecentFile> recentFiles = new ArrayList<>();
}
Step 2: Set Up Controllers with Static References
In each controller's factory method, store the controller instance:
// In MainController.java
public static Region createViewInstance() {
FXMLLoader loader = new FXMLLoader(
MainController.class.getResource("/fxml/main.fxml")
);
try {
Parent root = loader.load();
AppData.mainController = loader.getController();
return (Region) root;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// In SettingsController.java
public static Region createViewInstance() {
FXMLLoader loader = new FXMLLoader(
SettingsController.class.getResource("/fxml/settings.fxml")
);
try {
Parent root = loader.load();
AppData.settingsController = loader.getController();
return (Region) root;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Step 3: Use Static References for Communication
In your controllers, use the static references to communicate:
// In SettingsController.java - Navigate to profile
public void onViewProfileClicked(ActionEvent event) {
// Update shared data
AppData.appSettings = settingsForm.getSettings();
// Call main controller to switch view
AppData.mainController.displayProfileView();
}
// In MainController.java - Display profile
public void displayProfileView() {
container.getChildren().clear();
container.getChildren().add(profileView);
// Initialize profile controller with current settings
AppData.profileController.initializeWithSettings(AppData.appSettings);
}
// In ProfileController.java
public void initializeWithSettings(Settings settings) {
this.settings = settings;
updateUI();
}
Step 4: Use Listeners for Automatic Updates
Add listeners to automatically update when data changes:
// In MainController.java
public void initialize() {
// Listen for user changes
AppData.currentUserProperty().addListener(
(obs, oldUser, newUser) -> {
if (newUser != null) {
userLabel.setText(newUser.getName());
}
}
);
}
// In ProfileController.java - Update user
public void onSaveUserClicked(ActionEvent event) {
User updatedUser = userForm.getUser();
AppData.currentUser = updatedUser;
// All listeners will be notified automatically
showConfirmation("User updated successfully!");
}
Advanced Patterns: Beyond Static Fields
For larger applications, you might want to use more sophisticated patterns:
1. Event Bus Pattern
// Simple event bus
public class EventBus {
private static Map<String, List<Consumer<Object>>> listeners = new HashMap<>();
public static void subscribe(String eventType, Consumer<Object> listener) {
listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
}
public static void publish(String eventType, Object data) {
List<Consumer<Object>> eventListeners = listeners.get(eventType);
if (eventListeners != null) {
eventListeners.forEach(listener -> listener.accept(data));
}
}
}
// Usage
// In Controller A
EventBus.subscribe("userUpdated", user -> updateUserDisplay(user));
// In Controller B
EventBus.publish("userUpdated", newUser);
2. Dependency Injection
Pass controller references as constructor parameters:
// Instead of static fields, pass dependencies
public class ProfileController {
private MainController mainController;
private Settings settings;
public ProfileController(MainController mainController, Settings settings) {
this.mainController = mainController;
this.settings = settings;
}
public void onDone() {
mainController.returnFromProfile(settings);
}
}
3. Callback Pattern
Pass callback functions to child controllers:
// In MainController.java
public void showSettings(Runnable onSaveCallback, Runnable onCancelCallback) {
SettingsController controller = new SettingsController(
settings,
onSaveCallback,
onCancelCallback
);
// Display settings dialog
}
// In SettingsController.java
public class SettingsController {
private Runnable onSave;
private Runnable onCancel;
public SettingsController(Settings settings, Runnable onSave, Runnable onCancel) {
this.settings = settings;
this.onSave = onSave;
this.onCancel = onCancel;
}
public void onSave() {
saveSettings();
onSave.run();
}
public void onCancel() {
onCancel.run();
}
}
| Pattern | Complexity | Use Case |
|---|---|---|
| Static Fields (Jukebox) | Low | Simple applications, learning projects, global state |
| Direct Method Calls | Low | Navigation, simple parent-child communication |
| Shared Data Model | Low-Medium | Multiple views showing the same data |
| Event Bus | Medium | Decoupled communication, publish-subscribe patterns |
| Dependency Injection | Medium | Testable code, explicit dependencies |
| Callback Functions | Medium | Dialog-like patterns, result callbacks |
Try This:
- Run the Jukebox application and trace the communication flow:
- Go from Home -> Admin Panel and notice the view switch
- Add a new album in Admin Panel and notice it appears immediately
- Go from Admin Panel -> Home and notice the auto-save message in console
- Go from Home -> Play Mode -> Select an album -> Play
- Notice how the selected album information flows from ChooseAlbumController to PlayAlbumController
- Open
Jukebox.javaand find all the static controller fields - Open
ApplicationWindowController.javaand find where controllers are stored in static fields - Trace through how a button click in ChooseAlbumController results in PlayAlbumView being displayed
Common Pitfalls
The static field pattern creates tight coupling between controllers. While fine for a learning project, in larger applications consider using more decoupled patterns like events or dependency injection.
JavaFX is single-threaded (JavaFX Application Thread), so the static field pattern works. But if you introduce background threads, you need to be careful about thread safety. Never access JavaFX controllers from background threads.
Static fields hold references permanently. If your controllers have listeners or hold large data, this can cause memory leaks. Make sure to clean up resources when they're no longer needed.
Static fields make unit testing more difficult. Each test needs to set up the static state, and tests can interfere with each other. Consider using dependency injection for better testability.
With static fields, you need to ensure controllers are created before they're accessed. The Jukebox pattern creates all controllers during application startup, but be careful if you add lazy initialization.
Static fields store one instance. If you need multiple instances of a controller, you can't use static fields. Each instance needs its own reference.
Key Takeaways
- Jukebox uses static fields in
Jukebox.javafor global controller access - Controllers store themselves in static fields during
createViewInstance() - Direct method calls allow controllers to trigger navigation and actions
- Static data fields (
Jukebox.database,Jukebox.selectedAlbum) provide shared state - Auto-save is implemented by checking current view before navigation
- Event-driven updates use property listeners for automatic UI updates
- The GO HOME button provides consistent navigation from any view
- Two-way binding pattern in Admin Panel: Selection -> Fields -> Model -> View
- Static field pattern is simple and works well for learning projects
- For larger applications, consider event bus, dependency injection, or callbacks