FX Experience Has Gone Read-Only

I've been maintaining FX Experience for a really long time now, and I love hearing from people who enjoy my weekly links roundup. One thing I've noticed recently is that maintaining two sites (FX Experience and JonathanGiles.net) takes more time than ideal, and splits the audience up. Therefore, FX Experience will become read-only for new blog posts, but weekly posts will continue to be published on JonathanGiles.net. If you follow @FXExperience on Twitter, I suggest you also follow @JonathanGiles. This is not the end - just a consolidation of my online presence to make my life a little easier!

tl;dr: Follow me on Twitter and check for the latest news on JonathanGiles.net.

This is a guest post by Thomas Nield, a contributor to ControlsFX. Enjoy 🙂


Last year I spent a decent amount of time contributing a TableFilter control to the ControlsFX project. It was inspired by Eugene Ryzhikov’s SwingBits project which contained a table filtering control for Swing. After recently making some enhancements, it seemed like a good time to share the TableFilter and what it can do. I hope it enriches your UI much like the rest of the ControlsFX project has done for me.

The TableFilter behaves very similarly to Excel's spreadsheet filter. Right-clicking on a column header will make it visible, and a searchable checklist of distinct values for that column allows you to quickly filter for data.

Here is how it works:

Step 1: Have a TableView with a TableFilter applied

TableView<Person> tableView = ...
//add TableColumns
TableFilter filter = new TableFilter(myTableView);

Step 2: Right click the header for the column you want to filter on

Step 3: Either search for items you want to filter on or uncheck values you want to filter out

Step 4: Click APPLY to execute the filter

You can always remove the filter by right-clicking any column and clicking RESET to clear the filter for that particular column, or RESET ALL to remove all filters for all columns.

How it Works

This filter control is highly intuitive to business users. From my experience, many expect it especially if they have worked with Excel. But for something that is so intuitive to use, there are a surprising number of complicated moving parts the developer has to account for:

  • A distinct set of values must be maintained for each column
  • Values that are selected/unselected must be tracked
  • If data is added or removed, the distinct values must be rebuilt while persisting the selected/unselected values
  • Each column can have a separate filter applied, and only mutually inclusive items that qualify with all filters can be displayed
  • Users may want distinct values to be grayed out if they are no longer visible due to a recent filter in another column
  • There may be a need to customize search "strategies" for distinct values, such as wildcards, regular expressions, date and number comparisons, etc.

The TableFilter control does all the above, and after a lot of work I have used it in production for about six months. My clients have been extremely satisfied with it and it has worked reliably.

The TableFilter will swap out the ObservableList of items with a FilteredList. This FilteredList will only display items that qualify on all column's respective filters. This means you can execute filters on more than one column in combination.

For instance, we have already filtered to records where the last name is "Warren". We can add another filter where the age is 16. Note that when I right-click the "Age" column, values that are no longer visible due to the "Last Name" filter are grayed out. You can use the "NONE" button to unselect all values and pick the value(s) you are interested in.

STEP 1: Right-Click "Age" and apply a second filter

STEP 2: Hit "Apply" to execute the second filter

You can remove all filters from all columns by right-clicking any column and clicking "RESET ALL".

Custom Search Strategies

One simple but highly flexible configuration you can do with the TableFilter is change the search behavior on the search box. By default, the search box will filter distinct values based on the inputted String, and pass it to the contains() method of each distinct value's toString(). Effectively, whatever input you type in the search box will match distinct values that contain it.

You can change this behavior easily by passing a BiPredicate<String,String> lambda, where the first String parameter is the "input" from the search box, and the second String value is the "target" representing each distinct value. Using these two String inputs, you can do any matching logic that returns a boolean.

For example, you can implement the search box behavior to use regular expressions for the table filters.

TableFilter<Person> tableFilter = new TableFilter<>(table);

tableFilter.setSearchStrategy((input,target) -> {
    try {
        return target.matches(input);
    } catch (Exception e) {
        return false;
    }
});

Now I can use regular expressions to search through the distinct values.

Note I had to use the try-catch because event-driven text inputs will likely contain broken regular expressions, especially as they are being typed. When the typed input is currently not a valid regular expression, I just default the qualification to false.

Not that all values for all columns are searched by their toString() values. This makes sense since you are searching by typing in String inputs, so the distinct values should be compared to the input as String values as well. You can do other behaviors, like using startsWith() instead of contains() or even a wildcard pattern system.

Currently, the API is set up to use one search strategy for all column filters. I may explore column-specific search behaviors later but for now I feel it would complicate the API. But with a little effort, you can create more complex search strategies that recognize numbers, dates, and even simple math expressions. For example, you could evaluate if the two String inputs are numbers. If the numeric input starts with a "less than" symbol, as in "<120", you would then only qualify distinct values that are less than 120. This sounds involved but I have found it is not much work, and it just requires a few nested case statements. I have even developed implementations that can work with date strings of various formats.

Updating Data in the TableView

Because the TableFilter commandeers the TableView's backing ObservableList and replaces it with a FilteredList, you will get errors when trying to modify the backing ObservableList of items returned from getItems().

To modify the "backing list" of items for a TableView, call the getBackingList() method on the TableFilter and modify that instead to update the TableView.

TableFilter<Person> tableFilter = new TableFilter<>(table);
ObservableList<Person> items = tableFilter.getBackingList();

Conclusions

I hope you find the TableFilter useful. It is currently available in the ControlsFX project but an improved version (which includes the custom search strategy and fixes for nullability issues) is available in the coming release. Please let us know what you think and do not hesitate to get involved if you have ideas or improvements.