JavaFX 8 comes with two new classes, SortedList and FilteredList. In JavaFX 2 we had to manually do the filtering as I’ve described in an earlier post.

So, let’s see how we can use the new classes to sort and filter a TableView.

Example Setup

As an example we’ll create a simple table that displays Persons. The table should be filtered whenever the user enters something in the text field.

TableView Sorting and Filtering

The Files

The example contains four files. You can look at each file individually or download the entire source as an Eclipse project.

We will only take a closer look at the PersoTableController as this is where the sorting and filtering takes place.

Sorting and Filtering Explained

For the sorting and filtering to work, we need to wrap an ObservableList in a FilteredList and then in a SortedList.

Let’s take a look at the code:

PersonTableController.java

package ch.makery.sortfilter;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;


/**
 * View-Controller for the person table.
 * 
 * @author Marco Jakob
 */
public class PersonTableController {
    
    @FXML
    private TextField filterField;
    @FXML
    private TableView<Person> personTable;
    @FXML
    private TableColumn<Person, String> firstNameColumn;
    @FXML
    private TableColumn<Person, String> lastNameColumn;

    private ObservableList<Person> masterData = FXCollections.observableArrayList();

    /**
     * Just add some sample data in the constructor.
     */
    public PersonTableController() {
        masterData.add(new Person("Hans", "Muster"));
        masterData.add(new Person("Ruth", "Mueller"));
        masterData.add(new Person("Heinz", "Kurz"));
        masterData.add(new Person("Cornelia", "Meier"));
        masterData.add(new Person("Werner", "Meyer"));
        masterData.add(new Person("Lydia", "Kunz"));
        masterData.add(new Person("Anna", "Best"));
        masterData.add(new Person("Stefan", "Meier"));
        masterData.add(new Person("Martin", "Mueller"));
    }

    /**
     * Initializes the controller class. This method is automatically called
     * after the fxml file has been loaded.
     * 
     * Initializes the table columns and sets up sorting and filtering.
     */
    @FXML
    private void initialize() {
        // 0. Initialize the columns.
        firstNameColumn.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
        lastNameColumn.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
        
        // 1. Wrap the ObservableList in a FilteredList (initially display all data).
        FilteredList<Person> filteredData = new FilteredList<>(masterData, p -> true);
        
        // 2. Set the filter Predicate whenever the filter changes.
        filterField.textProperty().addListener((observable, oldValue, newValue) -> {
            filteredData.setPredicate(person -> {
                // If filter text is empty, display all persons.
                if (newValue == null || newValue.isEmpty()) {
                    return true;
                }
                
                // Compare first name and last name of every person with filter text.
                String lowerCaseFilter = newValue.toLowerCase();
                
                if (person.getFirstName().toLowerCase().contains(lowerCaseFilter)) {
                    return true; // Filter matches first name.
                } else if (person.getLastName().toLowerCase().contains(lowerCaseFilter)) {
                    return true; // Filter matches last name.
                }
                return false; // Does not match.
            });
        });
        
        // 3. Wrap the FilteredList in a SortedList. 
        SortedList<Person> sortedData = new SortedList<>(filteredData);
        
        // 4. Bind the SortedList comparator to the TableView comparator.
        sortedData.comparatorProperty().bind(personTable.comparatorProperty());
        
        // 5. Add sorted (and filtered) data to the table.
        personTable.setItems(sortedData);
    }
}

The steps explained

1. Wrap the ObservableList in a FilteredList

To allow filtering we must wrap our masterData in a FilteredList. The FilteredList filters the list depending on a specified Predicate. The initial Predicate is always true: p -> true (yes, this is a Java 8 lambda expression).

2. Set the filter Predicate whenever the filter changes

As a second step we add a ChangeListener to the filter text field. Whenever the user changes the text, the Predicate of our FilteredList is updated.

In this example I’m using a filter that matches whenever firstName or lastName contains the filter String.

3. Wrap the FilteredList in a SortedList

FilteredList is unmodifiable, so it cannot be sorted. We need to also wrap it in SortedList for this purpose.

4. Bind the SortedList comparator to the TableView comparator

A click on the column header changes the sorting of the TableView. But now that we have a separate SortedList we must bind the sorting of that list to the TableView. Notice that the TableView will return back to the original, unsorted state after three clicks on the column header (this was not the case in JavaFX 2).

5. Add sorted (and filtered) data to the table

As a last step we set the sorted and filtered data as items into the personTable.


Conclusion

Sorting and filtering has become more convenient in JavaFX 8. But not everything is straight forward: double-wrapping a list, binding a Comparator, and creating a lambda expression inside another lambda expression are things that are not all too common.