→ UPDATED VERSION of this article: JavaFX 8 TableView Sorting and Filtering

The JavaFX 2 TableView lacks the ability for filtering. The intention before JavaFX 2.0 shipped was to include a FilteredList that would wrap an ObservableList (see Oracle forum Filter rows on TableView). Unfortunately, the filtering was removed again. It will appear in JavaFX 8 which won’t be released before late 2013.

In this post I will explain how we can manually do row filtering in JavaFX 2.

Example Set Up

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 Filter

I prefer to define the user interface in fxml (with Scene Builder). After creating the example in Scene Builder the fxml looks like this:

PersonTable.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<?import jfxtras.labs.scene.control.*?>

<AnchorPane minWidth="315.0" prefHeight="300.0" prefWidth="315.0" xmlns:fx="http://javafx.com/fxml" fx:controller="PersonTableController">
  <children>
    <TableView fx:id="personTable" prefHeight="-1.0" prefWidth="-1.0" tableMenuButtonVisible="false" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="40.0">
      <columns>
        <TableColumn maxWidth="5000.0" minWidth="10.0" prefWidth="120.0" text="First Name" fx:id="firstNameColumn" />
        <TableColumn maxWidth="5000.0" minWidth="10.0" prefWidth="120.0" text="Last Name" fx:id="lastNameColumn" />
      </columns>
    </TableView>
    <HBox id="HBox" alignment="CENTER" spacing="5.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
      <children>
        <Label text="Filter Table:" />
        <TextField fx:id="filterField" prefWidth="-1.0" HBox.hgrow="ALWAYS" />
      </children>
    </HBox>
  </children>
</AnchorPane>

We’ll need a class Person for the model:

Person.java

public class Person {

    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

We’ll need a MainApp to load everything:

MainApp.java

import java.io.IOException;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class MainApp extends Application {
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Table Filtering");
        
        try {
            FXMLLoader loader = new FXMLLoader(MainApp.class.getResource("PersonTable.fxml"));
            AnchorPane page = (AnchorPane) loader.load();
            Scene scene = new Scene(page);
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (IOException e) {
            System.err.println("Error loading PersonTable.fxml!");
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

The most interesting part is the PersonTableController which I’ll discuss a bit more now.


Filtering

For the filtering to work, we need two ObservableLists. One list contains the original master data while the other contains the filtered data that will be displayed in the table.

The constructor puts the same sample data in both the masterData and filteredData lists. In the beginning nothing is filtered and the two lists contain the same data.

We’ll also add a ListChangeListener to the masterData. Whenever something changes in masterData we’ll also have to update the filteredData.

Now let’s take a look at the code:

PersonTableController.java

import java.util.ArrayList;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;

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();
    private ObservableList<Person> filteredData = FXCollections.observableArrayList();
    
    /**
     * The constructor. The constructor is called before the initialize()
     * method.
     */
    public PersonTableController() {
        // Add some sample data to the master data
        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"));
        
        // Initially add all data to filtered data
        filteredData.addAll(masterData);
        
        // Listen for changes in master data.
        // Whenever the master data changes we must also update the filtered data.
        masterData.addListener(new ListChangeListener<Person>() {
            @Override
            public void onChanged(ListChangeListener.Change<? extends Person> change) {
                updateFilteredData();
            }
        });
    }

    /**
     * Initializes the controller class. This method is automatically called
     * after the fxml file has been loaded.
     */
    @FXML
    private void initialize() {
        // Initialize the person table
        firstNameColumn.setCellValueFactory(
                new PropertyValueFactory<Person, String>("firstName"));
        lastNameColumn.setCellValueFactory(
                new PropertyValueFactory<Person, String>("lastName"));
        
        // Add filtered data to the table
        personTable.setItems(filteredData);

        // Listen for text changes in the filter text field
        filterField.textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observable,
                    String oldValue, String newValue) {
                
                updateFilteredData();
            }
        });
    }
    
    /**
     * Updates the filteredData to contain all data from the masterData that
     * matches the current filter.
     */
    private void updateFilteredData() {
        filteredData.clear();
            
        for (Person p : masterData) {
            if (matchesFilter(p)) {
                filteredData.add(p);
            }
        }
        
        // Must re-sort table after items changed
        reapplyTableSortOrder();
    }

    /**
     * Returns true if the person matches the current filter. Lower/Upper case
     * is ignored.
     * 
     * @param person
     * @return
     */
    private boolean matchesFilter(Person person) {
        String filterString = filterField.getText();
        if (filterString == null || filterString.isEmpty()) {
            // No filter --> Add all.
            return true;
        }
        
        String lowerCaseFilterString = filterString.toLowerCase();
        
        if (person.getFirstName().toLowerCase().indexOf(lowerCaseFilterString) != -1) {
            return true;
        } else if (person.getLastName().toLowerCase().indexOf(lowerCaseFilterString) != -1) {
            return true;
        }
        
        return false; // Does not match
    }
    
    private void reapplyTableSortOrder() {
        ArrayList<TableColumn<Person, ?>> sortOrder = new ArrayList<>(personTable.getSortOrder());
        personTable.getSortOrder().clear();
        personTable.getSortOrder().addAll(sortOrder);
    }
}

Reacting to User Entering a Filter String

At the end of the method initialize() we add a ChangeListener to the text property of the TextField. Whenever the user changes the text, the updateFilteredData() method is called.

In updateFilteredData() we remove all items in filteredData and only add the data matching the current filter.

Changing Filter Behaviour

The method matchesFilter(...) determines which Persons will be displayed. I chose to look both in the firstName and lastName fields for a match of the String while ignoring the case.

You could a different kind of filter behaviour in this method like Regular Expressions.

Reapply Table Sort Order

Whenever we change the filtering, the table must be resorted. The method reapplyTableSortOrder() is responsible to remove and set the sort order again.


Conclusion

Even though this might not be the fastest and most generic filtering approach, it’s still sufficient for many cases. For a more comfortable filtering we’ll have to wait for JDK 8.