Editing Items in a List
Editing items in a ListView is a common requirement for data management applications. The Jukebox project implements a powerful two-way editing pattern where users can both select items from a list and edit them in form fields, with changes automatically reflected in the list. This cookbook section explains this pattern, referencing AdminPanelController.
- Selection listeners for populating edit fields
- Property listeners on TextFields for auto-update
- Two-way binding between UI and model
- Refreshing ListView after edits
- Tracking selected items
- Handling ComboBox selections
Overview of the Editing Pattern
The Jukebox Admin Panel implements a complete editing workflow:
User Selects an Item
When a user clicks on an album in the ListView, the selection listener populates the edit fields with the album's data.
User Edits Fields
As the user types in the TextFields, property listeners automatically update the underlying album object.
ListView Updates
After the album object is updated, the ListView is refreshed to show the new values.
Data is Persisted
When the user navigates away or clicks Save, the changes are saved to the database.
- UI to Model: Changes in the UI (TextFields) automatically update the model (Album/Song objects)
- Model to UI: Changes in the model (through selection) automatically update the UI (TextFields)
- Automatic: No Save button needed - changes happen as the user types
Selection Listeners: Model to UI
When a user selects an item from the ListView, we want to populate the edit fields with that item's data. This is done with selection listeners:
private void setupSelectionListeners() {
// Album selection listener - Model to UI
albumsList.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> {
selectedAlbum = newSelection;
if (newSelection != null) {
// Populate album fields from the album object
albumNameField.setText(newSelection.name);
albumArtistField.setText(newSelection.artist);
genreComboBox.getSelectionModel().select(newSelection.genre);
// Populate songs list
songs.setAll(newSelection.songs);
} else {
// Clear fields if no album selected
albumNameField.clear();
albumArtistField.clear();
genreComboBox.getSelectionModel().select(Genre.ROCK);
songs.clear();
}
}
);
// Song selection listener - Model to UI
songsList.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> {
selectedSong = newSelection;
if (newSelection != null) {
// Populate song fields from the song object
songTitleField.setText(newSelection.title);
songLengthField.setText(String.valueOf(newSelection.lengthSeconds));
} else {
// Clear fields if no song selected
songTitleField.clear();
songLengthField.clear();
}
}
);
}
- User selects an item in ListView
- ListView's selection model fires a change event
- Listener receives the new selection
- Listener updates the TextFields with data from the selected object
- Listener stores reference to the selected object for later use
Notice the use of selectedAlbum and selectedSong fields to track which items are currently selected. These are used by the property listeners (which we'll see next) to know which objects to update.
private Album selectedAlbum; // Tracks currently selected album
private Song selectedSong; // Tracks currently selected song
Property Listeners: UI to Model
The reverse direction - updating the model when the UI changes - is handled by property listeners on the TextField's textProperty():
private void setupSelectionListeners() {
// ... selection listeners from above ...
// Album field change listeners - UI to Model
albumNameField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null) {
selectedAlbum.name = newVal; // Update model
albumsList.refresh(); // Refresh UI
}
});
albumArtistField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null) {
selectedAlbum.artist = newVal; // Update model
albumsList.refresh(); // Refresh UI
}
});
genreComboBox.valueProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null && newVal != null) {
selectedAlbum.genre = newVal; // Update model
albumsList.refresh(); // Refresh UI
}
});
// Song field change listeners - UI to Model
songTitleField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedSong != null) {
selectedSong.title = newVal; // Update model
songsList.refresh(); // Refresh UI
}
});
songLengthField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedSong != null && !newVal.isEmpty()) {
try {
selectedSong.lengthSeconds = Integer.parseInt(newVal); // Update model
songsList.refresh(); // Refresh UI
} catch (NumberFormatException e) {
// Ignore invalid input
}
}
});
}
- User types in a TextField (or selects from ComboBox)
- TextField's
textProperty()fires a change event - Listener receives the new value
- Listener checks if there's a selected item
- Listener updates the appropriate field on the selected object
- Listener calls
refresh()on the ListView to update display
The Complete Two-Way Binding
Let's trace through a complete editing scenario:
Scenario: User edits an album name
- Step 1: User clicks on an album in the albums ListView
- Step 2: Selection listener fires, populates
albumNameFieldwith album's name - Step 3:
selectedAlbumis set to reference the album object - Step 4: User types a new name in
albumNameField - Step 5: TextProperty listener fires, sees that
selectedAlbum != null - Step 6: Listener sets
selectedAlbum.name = newVal - Step 7: Listener calls
albumsList.refresh() - Step 8: ListView redraws, showing the new album name
Result: The album name is updated in both the model (Album object) and the UI (ListView), all automatically as the user types.
ListView (album selected)
↓
Selection Listener
↓
TextField (populated)
↓
User types
↓
Property Listener
↓
Album Object (updated)
↓
ListView.refresh()
↓
ListView (updated display)
Why refresh() is Needed
An important detail is that we call refresh() on the ListView after updating the model object. This is because:
- ObservableList detects when items are added/removed from the list
- ObservableList does NOT detect when an object's properties change (like
album.name) - Solution: Call
refresh()to force the ListView to redraw all its cells
Example: Without refresh(), this would happen:
- User selects "My Album by Artist" from ListView
- User changes artist to "New Artist" in the TextField
- Album object is updated:
album.artist = "New Artist" - ListView still shows "My Album by Artist" (WRONG!)
With refresh():
- User selects "My Album by Artist" from ListView
- User changes artist to "New Artist" in the TextField
- Album object is updated:
album.artist = "New Artist" albumsList.refresh()is called- ListView correctly shows "My Album by New Artist"
Number Input Validation
Notice that the song length field has special handling for number parsing:
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
}
}
});
- Empty Check:
!newVal.isEmpty()prevents parsing empty string - Try/Catch:
Integer.parseInt()throwsNumberFormatExceptionif the input isn't a valid integer - Silent Failure: Invalid input is simply ignored, keeping the previous value
- Alternative: You could show an error message or highlight the field in red
ComboBox Editing
The genre field uses a ComboBox instead of a TextField. The editing pattern is similar but uses valueProperty() instead of textProperty():
// Setting up the genre ComboBox
private void setupGenreComboBox() {
genreComboBox.setItems(FXCollections.observableArrayList(Genre.values()));
genreComboBox.getSelectionModel().select(Genre.ROCK);
}
// Listening for genre changes
genreComboBox.valueProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null && newVal != null) {
selectedAlbum.genre = newVal;
albumsList.refresh();
}
});
| Aspect | TextField | ComboBox |
|---|---|---|
| Property | textProperty() |
valueProperty() |
| Value Type | String |
Any type (in this case, Genre enum) |
| Input | Free-form text | Predefined choices |
| Use Case | Album name, artist (free text) | Genre (from fixed set of values) |
Creating Your Own Two-Way Editing
Let's implement two-way editing for a simple ListView:
Step 1: Set Up Model and FXML
// Model class
public class Person {
public String firstName = "";
public String lastName = "";
public int age = 0;
@Override
public String toString() {
return firstName + " " + lastName + " (" + age + ")";
}
}
// FXML
<ListView fx:id="peopleList" prefHeight="200" prefWidth="300" />
<TextField fx:id="firstNameField" promptText="First Name" />
<TextField fx:id="lastNameField" promptText="Last Name" />
<TextField fx:id="ageField" promptText="Age" />
Step 2: Set Up Controller Fields and Lists
@FXML
private ListView<Person> peopleList;
@FXML
private TextField firstNameField;
@FXML
private TextField lastNameField;
@FXML
private TextField ageField;
private ObservableList<Person> people;
private Person selectedPerson;
Step 3: Initialize and Set Up Listeners
@FXML
private void initialize() {
// Create and set the ObservableList
people = FXCollections.observableArrayList();
peopleList.setItems(people);
// Add some sample data
people.add(new Person("John", "Doe", 30));
people.add(new Person("Jane", "Smith", 25));
// Selection listener - Model to UI
peopleList.getSelectionModel().selectedItemProperty().addListener(
(obs, oldVal, newVal) -> {
selectedPerson = newVal;
if (newVal != null) {
firstNameField.setText(newVal.firstName);
lastNameField.setText(newVal.lastName);
ageField.setText(String.valueOf(newVal.age));
} else {
firstNameField.clear();
lastNameField.clear();
ageField.clear();
}
}
);
// Property listeners - UI to Model
firstNameField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedPerson != null) {
selectedPerson.firstName = newVal;
peopleList.refresh();
}
});
lastNameField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedPerson != null) {
selectedPerson.lastName = newVal;
peopleList.refresh();
}
});
ageField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedPerson != null && !newVal.isEmpty()) {
try {
selectedPerson.age = Integer.parseInt(newVal);
peopleList.refresh();
} catch (NumberFormatException e) {
// Ignore invalid input
}
}
});
}
Try This:
- Run the Jukebox application and go to Admin Panel
- Add a new album by clicking "Add New Album"
- Notice that the new album is automatically selected and the fields are populated
- Edit the album name in the text field and see how the ListView updates automatically
- Edit the artist name and see the same effect
- Change the genre using the ComboBox
- Select an album, add a song, then edit the song title
- Open
AdminPanelController.javaand find thesetupSelectionListeners()method - Trace through how the two-way editing works
Common Pitfalls
If you modify an object's properties but forget to call refresh() on the ListView, the display won't update. The ListView only automatically updates when the ObservableList itself changes (items added/removed), not when object properties change.
Always check if selectedAlbum (or equivalent) is not null before trying to update it in property listeners. Otherwise, you'll get NullPointerException when no item is selected.
Make sure you're updating the correct object. In the property listeners, we use selectedAlbum which tracks the currently selected album. Don't accidentally create a new object instead of updating the existing one.
If your ListView cell factory or toString() method accesses the UI, calling refresh() could cause an infinite loop. Make sure your model classes don't depend on the UI.
For numeric fields, always check that the input isn't empty before trying to parse it. Integer.parseInt("") throws a NumberFormatException.
Key Takeaways
- Selection listeners populate UI from model (Model to UI)
- Property listeners on TextFields update model from UI (UI to Model)
- This creates two-way binding between UI and model
- Track selected items with controller fields (selectedAlbum, selectedSong)
- Call refresh() on ListView after modifying object properties
- Use textProperty() for TextFields, valueProperty() for ComboBox
- Handle numeric input with try/catch for NumberFormatException
- Always check for null selection before updating