Search code examples
genericsjavafxtableviewjavafx-8

How to genericise the creation of a Predicate for a TableView's FilteredList in JavaFX8?


A question from an experienced programmer who's still relatively new to Java.

I've created a prototype of a class that adds Excel-like data filters to columns in a TableView. I'm now making it generic so that it will work with any TableView and am stuck with how to "genericise" the creation of Predicates.

In my prototype (where I've hard-coded everything), I call a createPredicate() method for a String "Value" column, for example, like this:

createPredicate(TableViewModel::getTvValue);

and in the createPredicate() method, I set the Predicate like this:

private void createPredicate(Function<TableViewModel, String> columnGetter) {
    //...
    final String filter = "some value";
    Predicate<TableViewModel> predicate = tvm -> columnGetter.apply(tvm).contains(filter);
    //...
}

How can I make both the call to the method and the method itself generic, so that they will work for any TableColumn?

I'm stuck in two places. Firstly, how to get the getter for a TableColumn so I can pass it to the createPredicate() method ...

//Get a column
TableColumn<S, T> col = (TableColumn<S, T>) table.getColumns().get(0);

//Set a predicate based on the column
//==>STUCK HERE:  How do I get the getter for the column?
createPredicate(S::<WHAT_GOES_HERE?>);

... and secondly, how to make the contains() generic. I'm currently getting a "cannot find symbol method contains(T)" compiler error.

private void createPredicate(Function<S, T> columnGetter) {

    final T filter = (T) "some value";
    //STUCK HERE:  How to genericise the contains?
    Predicate<S> predicate = tvm -> columnGetter.apply(tvm).contains(filter);

}

Thanks in advance. If it helps, here is a MVCE. I'm using JavaFX8 (JDK1.8.0_181) and NetBeans 8.2.

The (bare bones!) TestColumnFilter.java class:

import java.util.function.Function;
import java.util.function.Predicate;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;

public class TestColumnFilter<S, T> {

    public TestColumnFilter(TableView<S> table) {

        //Get a column
        TableColumn<S, T> col = (TableColumn<S, T>) table.getColumns().get(0);

        //Set a predicate based on the column
        //STUCK HERE:  How to get the getter for the column?
        createPredicate(S::<WHAT_GOES_HERE?>);

    }

    private void createPredicate(Function<S, T> columnGetter) {

        final T filter = (T) "some value";
        //STUCK HERE:  How to genericise the contains?  
        //Currently getting a "cannot find symbol method contains(T)" compiler error
        Predicate<S> predicate = tvm -> columnGetter.apply(tvm).contains(filter);

    }

    public static <S, T> TestColumnFilter<S, T> createColumnFiltersForTableView(TableView<S> table) {

        return new TestColumnFilter<>(table);

    }

}

and Test55.java, a dummy class that uses TestColumnFilter.java.

import java.util.Arrays;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Test55 extends Application {

    private Parent createContent() {

        TableView<TableViewModel> table = new TableView<>();
        TableColumn<TableViewModel, String> valueCol = new TableColumn<>("Value");
        valueCol.setCellValueFactory(cb -> cb.getValue().tvValueProperty());

        table.getColumns().addAll(Arrays.asList(valueCol));

        TestColumnFilter.createColumnFiltersForTableView(table);

        BorderPane content = new BorderPane(table);

        return content;
    }

    private class TableViewModel {

        private final StringProperty tvValue;

        public TableViewModel(
            String tvValue
        ) {
            this.tvValue = new SimpleStringProperty(tvValue);
        }

        public String getTvValue() {return tvValue.get().trim();}
        public void setTvValue(String tvValue) {this.tvValue.set(tvValue);}
        public StringProperty tvValueProperty() {return tvValue;}

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle("Test");
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

Solution

  • I don't think the current design would eventually work out well.

    I'm going to assume that you could set filters on multiple columns, and the TableView should show the results based on all filters.

    You could try this approach:

    public class TableViewFilter<S> {
        private final TableView<S> tableView;
        private final FilteredList<S> filteredItems;
        private final ObservableList<Function<S, Boolean>> conditions;
    
        public TableViewFilter(TableView<S> tableView, ObservableList<S> items) {
            this.tableView = tableView;
            this.filteredItems = new FilteredList<>(items);
            this.tableView.setItems(filteredItems ).
    
            this.conditions = FXCollections.observableArrayList();
    
            this.filteredItems.predicateProperty().bind(
                Bindings.createObjectBinding(this::generatePredicate, this.conditions));
        }
    
        public static <S> TableViewFilter<S> forTableView(TableView<S> tableView) {
            return new TableViewFilter<>(tableView);
        }
    
        public void addCondition(Function<S, Boolean> condition) {
            conditions.add(condition);
        }
    
        private Predicate<S> generatePredicate() {
            return item -> {
                return conditions.stream().map(func -> func.apply(item)).allMatch(Boolean.TRUE::equals);
            };
        }
    }
    
    TableView<TableViewModel> table = new TableView<>();
        TableColumn<TableViewModel, String> valueCol = new TableColumn<>("Value");
        valueCol.setCellValueFactory(cb -> cb.getValue().tvValueProperty());
    
        table.getColumns().addAll(Arrays.asList(valueCol));
    
        ObservableList<TableViewModel> items = FXCollections.observableArrayList();
        items.addAll(/*items*/);
    
        //TestColumnFilter.createColumnFiltersForTableView(table);
        TableViewFilter filter = TableViewFilter.forTableView(table, items); // Keep a reference if you need to add more conditions later
        filter.addCondition(item -> item.getTvValue().contains("filtered string"));
    
        BorderPane content = new BorderPane(table);
    
        return content;
    }
    

    Notice that the condition does not depend on a particular TableColumn. It is easier to filter using the underlying data (i.e. TableViewModel).