Vaadin 8 Grid - multiple filter configuration?

I’m planning to replace 40 SQLContainer-backed Grids with the new data model of Vaadin 8. A typical grid might look like this, with multiple user input filters of various types (normally String and java.sql.Timestamp).

Planning the service interface and implementing the backing SQL query is clear enough, but I can’t figure out how to configure the BackEndDataProvider and or ListDataProvider to handle the multiple user filter inputs. DataProvider’s query.getFilter() method seems to expect a single string filter. While ListDataProvider appears to provide for many, I’m not clear on how to cascade the filters nor on how to write the corresponding DataProvider queries.

An example outlining how multiple filters would be set would clear my confusion.

I have the same problem. How can real filtering for Grids can be achieved?

In https://github.com/vaadin/framework/blob/master/documentation/datamodel/datamodel-providers.asciidoc the documentation stops at the moment it would get interesting. There are only examples for ComboBox (single String filter) or Grid with no filter param at all.
What about converting and filtering on a per Column basis? In memory and in backend.
Sorting can be defined per Column but there seems to be no support for filtering per column.
I am missing informations about/functions to

  • convert the value of the Column filter Component to a value suitable for filtering (simple case: TextField-String → Integer). DataProvider.convertFilter() converts only one Object. If this should be used for filters with many optional criteries, it looks very cumbersome (create class/object for values from Components, create class/object for converted filter criteries, use converted criteries in DataProvider to filter in memory or in BackEndDataProvider to create backend query).
  • access the Cell value (respectively bean property value) for in memory filtering (a Column.getValueProvider() function would be great)
  • chaining DataProvider’s with different filter would not work with backend DataProviders. There has to by one Query with one filter object but how to create and use?

Please give us an advice, how to implement real Grid filtering.

Additionally I am currently looking for some examples how to migrate the Vaadin 7 Container approach to Vaadin 8 (DataProvider approach), since most of the interfaces changed/were replaced. Is there already something like that out there?

Perhaps this is a clue? Looking at a
recent documentaton commit
, it looks like Grid filtering in Vaadin 8 is still a work in progress. I suppose we will need to generate an event from a filter field component, then use that to trigger the generation of a query described in BackEndDataProvider which will perform a backend query that we write, loaded with specific user data that comes from all of the filter components. Or something like that.

It would be helpful if a Vaadin expert could tell us if we’re supposed to understand this from
existing data model documentation
, or if indeed there is still work being done on how this all operates.

Playing with Vaadin 8 I used DataCommunicator to set the filter for the grid

Here’s some sample code

final AtomicReference<PersonFilter> filter = new AtomicReference<>(new PersonFilter());

DataProvider<Person, PersonFilter> provider = new BackEndDataProvider<>(...)

Grid<Person> grid = new Grid<>();
Grid.Column<Person, String> c1 = grid.addColumn(Person::getFirstName);
Grid.Column<Person, String> c2 = grid.addColumn(Person::getLastName);
grid.setDataProvider(provider);

Supplier<DataCommunicator<Person, PersonFilter>> gridDataCommunicator = () ->
    (DataCommunicator<Person, PersonFilter>)grid.getDataCommunicator();

HeaderRow headerRow = grid.appendHeaderRow();
TextField firstNameFilter = new TextField("");
firstNameFilter.addValueChangeListener(event -> {
    gridDataCommunicator.get().setFilter(filter.updateAndGet( pf -> pf.withFirstName(event.getValue())));
});
headerRow.getCell(c1).setComponent(firstNameFilter);
TextField lastNameFilter = new TextField("");
lastNameFilter.addValueChangeListener(event -> {
    gridDataCommunicator.get().setFilter(filter.updateAndGet( pf -> pf.withLastName(event.getValue())));
});
headerRow.getCell(c2).setComponent(lastNameFilter);

Thanks Marco. This fills some gaps: You update and set a filter in a DataCommunicator instance in response to change listeners. I haven’t seen mention of DataCommunicator in existing documentation and wonder if Vaadin intends us to use it directly like this? Seems straightforward enough.

I presume your PersonFilter is an extension of String holding some developer-contrived concatenation of textfield values? It seems we need List as a type input to DataProvider instead of String. And is it fair to guess that your implementation of fetchPersons disassembles the PersonFilter string and constructs a backend query appropriate to the individual filters?

Perhaps this is more flexible and more memory-efficient, but certainly not simpler or more maintainable than adding and subtracting filters to SQLContainer… Anyway, thanks for shedding some light.

Hi Steve,
for my understanding the filter type could be anything you need to query your data: a POJO (containig strings, dates, enums …), a set of JPA criterias, a collection of Filter abstractions like Vaadin 7 filters, etc.

So PersonFilter in this case is a simple POJO but I think it could be for example:

  • a composable Predicate (or better SerializablePredicate) for an in memory data provider
  • a collection of backend specific filter objects (JPA criteria, querydsl predicates, JOOQ conditions, …) directly used by the backend provider
  • a collection of object representing a particular filter condition (like vaadin filters ) that the backend provider is able to convert (like FilterTranslator in SQLContainer)

I don’t know if this is the intended way for filtering data.

I agree with you and Marius that some more details and examples from Vaadin guys would be very helpful.

Best Regards
Marco

I just want to recognize and thank Leif Åstrand for all the
heavy lifting
he’s doing on this development. Check out
#8245
and related commits to see that filtering is getting a lot of attention.

Sorry for not discovering this discussion until now. I guess you could have been spared from some frustration if someone with insights into how things are supposed to work would have provided input earler on.

We quickly realized that there were some limitations in the filtering approach that was part of beta1, especially for the use case discussed here with multiple filters from different Grid columns. It did take us some time to figure out exactly what we wanted to do about that and also getting it done. I believe that the
last functional change
related to this was merged some minutes ago, but I’ll still also update the documentation to describe how to use the new functionality.

When using a back end data provider, there are two general approaches. One option is to create you own custom subclass with separate setXyzFilter methods that store the filter in state variables that are read by the fetch and size methods without looking at Query.getFilter() at all. Just remember to also do refreshAll() in each setter so that Grid can know that it should fetch items again. The other option is to create a DTO class that contains all the different values to filter by and making your data provider accept that DTO as the query filter. You can then use BackEndDataProvider.withConfigurableFilter() to get a wrapper instance with a setFilter method that accepts a DTO, handles all refreshing automatically and passes the DTO the query for the wrapped data provider to use.

If using a ListDataProvider, the most straightforward approach is most likely to do one addFilter for each filter component with a predicate that dynamically checks the value from the component. Whenever a filter component changes, you can then do dataProvider.refreshAll() to make it rerun all filters. You could also use some combination of addFilter, setFiler and/or clearFilters() whenever a filter input component changes, but you’d then need to do some additional bookeeping to also preserve filters from the other components. As a last option, you can also use a similar DTO as suggested for the back end case and then use convertFilter (which should probably be renamed to withConvertedFilter to be consistent with other similar methods) to translate that DTO into a single predicate. This options requries some additional boilerplate compared to the other alternatives, but it has the benefit of allowing you do things in mostly the same way regardless of whether you’re using in-memory or lazy-loaded data.

Thank you very much Leif for the explanation, but i really don’t get the filtering working with vaadin 8. There are one or two concepts i don’t understand yet and i try to explain why.

With Vaadin 7 i was able to generate a “generic” way to add and remove filter for the containers for every column in the grid.

    private void addGridFilters() {
        final HeaderRow filterRow = gridFacility.appendHeaderRow();
        for (final Object propertyId : gridFacility.getContainerDataSource().getContainerPropertyIds()) {
            final HeaderCell headerCell = filterRow.getCell(propertyId);
            if (headerCell != null) {
                if (!"idFacility".equals(propertyId) && !"facilityImageExists".equals(propertyId) && !"disabled".equals(propertyId)) {
                    headerCell.setComponent(createFilterTextField(propertyId));
                } else if ("facilityImageExists".equals(propertyId) || "disabled".equals(propertyId)) {
                    headerCell.setComponent(createFilterComboBox(propertyId));
                }
            }
        }
    }

private TextField createFilterTextField(final Object propertyId) {
        final TextField tfFilter = new TextField();
        tfFilter.setInputPrompt(tp.getText(TextKey.VIEW_TEXTFIELD_FILTER));
        tfFilter.addStyleName(ValoTheme.TEXTFIELD_TINY);
        tfFilter.setWidth(100, Unit.PERCENTAGE);
        tfFilter.addTextChangeListener(event -> {
            final String text = event.getText();

            bicGridFacility.removeContainerFilters(propertyId);
            if (!text.isEmpty()) {
                bicGridFacility.addContainerFilter(new SimpleStringFilter(propertyId, text, true, false));
            }
            bicGridFacility.sort(new Object { "facilityName" }, new boolean { true });
        });
        return tfFilter;
    }

@SuppressWarnings("unchecked")
    private ComboBox createFilterComboBox(final Object propertyId) {
...
}

So you see i was able to remove the container filter for an explicit property id and if the text of the filter field is not empty i am setting an new container filter. This was very easy and not complicated.

In Vaadin 8 i am not able to do this. I try to get this done in the same way but there are some obstructions.

[code]
private void addGridFilters() {
final HeaderRow filterRow = gridFunctionalAreaType.appendHeaderRow();
for (final Column<FunctionalAreaTypeDto, ?> column : gridFunctionalAreaType.getColumns()) {
final HeaderCell headerCell = filterRow.getCell(column);
if (“idFunctionalAreaType”.equals(column.getId())) {
headerCell.setComponent(createFilterTextField(column));
} else if (“functionalAreaTypeImageExists”.equals(column.getId())) {
// headerCell.setComponent(createFilterComboBox(headerCell.getColumnId()));
}
}
}

private TextField createFilterTextField(final Column<FunctionalAreaTypeDto, ?> column) {
    final TextField tfFilter = new TextField();
    tfFilter.setPlaceholder(tp.getText(TextKey.VIEW_TEXTFIELD_FILTER));
    tfFilter.addStyleName(ValoTheme.TEXTFIELD_TINY);
    tfFilter.setWidth(100, Unit.PERCENTAGE);
    tfFilter.addValueChangeListener(event -> {
        final ValueProvider<FunctionalAreaTypeDto, String> valueProvider = (ValueProvider<FunctionalAreaTypeDto, String>) column.getValueProvider();

        listDataProviderFunctionalAreaType.addFilter(valueProvider, functionalAreaId -> {
            return functionalAreaId.toLowerCase().contains(event.getValue().toLowerCase());
        });
    listDataProviderFunctionalAreaType.refreshAll();
    });
    return tfFilter;
}

[/code]I loop over the columns and get the headercell. and if its the right column i generate a textfield or a combobox. For this i have to sett an id for the column. This is a pity because i was hoping with vaadin 8 to do whitout a string in the class. In the create method i try to set with the valueprovider of the column the SerializablePredicate with addfilter.

The problem here is i can’t set in addfilter the Valueprovider directly with “column.getValueProvider()” because it returns “SerializableFunction<T, ? extends V>” and not a valueProvider. So i have to cast it. The other problem is i can’t remove a filter. i am missing “removefilter(ValueProvider<T, V> valueProvider)”. i only can call “clearFilters” for all filters. so the other filters won’t be active anymore.

So my question here is, am i missing some things or is my use case not possible in the moment? I think there are some methods missing to get this done!

My suggestion for you would be to attack the problem from the opposite direction: Use a single filter instance that takes care of all columns. The filter implementation can iterate through all columns and determine whether each item should be included based on the value of the filter component in that column’s header (if any). You’d then also have to call
dataProvider.refreshAll()
whenever the value in any filtering component changes so that e.g. Grid can know that it should fetch a new set of items from the data provider. It might feel inefficent to do
refreshAll()
every time a filter value changes, but this is also what all
addFilter
methods internally do.

Methods for removing filters are left out on purpose because each lambda expression is unique. This in turn means that something like
myDataProvider.removeFilter(foo → false)
wouldn’t remove a filter added using
myDataProvider.addFilter(foo → false)
. The different overloads of
addFilter
would further complicate the way filters are removed. What we could do is to use the same pattern that we use for removing event listeners, i.e. that
addXyzListener
methods return a registration instance that can be used for removing that particual listener. I created
https://github.com/vaadin/framework/issues/8985
about this.

Oops. I created a Pull Request for that type inconsistency:
https://github.com/vaadin/framework/pull/8983

Do you have any other approach in mind that would allow identifying columns without relying on a (string) id?

Thank you very much Leif. I tried to implement your suggestion. This is my result:

[code]
private Grid initResponsibilityGrid() {
gridResponsibility = new Grid();
gridResponsibility.setSelectionMode(SelectionMode.SINGLE);
gridResponsibility.setSizeFull();

    gridResponsibility.addColumn(responsibilityDto -> IdNumberFormatConverterV8.convertToPresentation(responsibilityDto.getIdResponsibility()))
            .setCaption(tp.getText(TextKey.RESPONSIBILITYVIEW_GRID_COLUMN_RESPONSIBILITY_ID)).setWidth(PropertyConstants.GRID_ID_COLUMN_WIDTH)
            .setResizable(false).setHidden(true);
    gridResponsibility.addColumn(responsibilityDto -> responsibilityDto.getNameI18nDto().getText())
            .setCaption(tp.getText(TextKey.RESPONSIBILITYVIEW_GRID_COLUMN_RESPONSIBILITY_NAME)).setExpandRatio(1);
    gridResponsibility.addColumn(responsibilityDto -> responsibilityDto.getDescriptionI18nDto().getText())
            .setCaption(tp.getText(TextKey.RESPONSIBILITYVIEW_GRID_COLUMN_RESPONSIBILITY_DESCRIPTION)).setMaximumWidth(600).setExpandRatio(2);
    gridResponsibility.addColumn(ResponsibilityDto::getSelectedOperatingModes)
            .setCaption(tp.getText(TextKey.RESPONSIBILITYVIEW_GRID_COLUMN_RESPONSIBILITY_OPERATINGMODES)).setMaximumWidth(600).setExpandRatio(2);
    gridResponsibility
            .addColumn(responsibilityDto -> BooleanToFontIconConverterV8.convertToPresentation(
                    responsibilityDto.getResponsibilityPoolTypeDto().getResponsibilityPoolType() == ResponsibilityPoolType.BASE_POOL))
            .setCaption(tp.getText(TextKey.RESPONSIBILITYVIEW_GRID_COLUMN_RESPONSIBILITY_BASEPOOL)).setId(columnResponsibilityPool)
            .setStyleGenerator(responsbilityDto -> "fontIconColumn").setRenderer(new HtmlRenderer()).setWidth(150);
    gridResponsibility.addColumn(responsibilityDto -> BooleanToFontIconConverterV8.convertToPresentation(responsibilityDto.isDisabled()))
            .setCaption(tp.getText(TextKey.RESPONSIBILITYVIEW_GRID_COLUMN_RESPONSIBILITY_DISABLED)).setId(columnResponsibilityDisabled)
            .setStyleGenerator(responsbilityDto -> "fontIconColumn").setRenderer(new HtmlRenderer()).setWidth(150);

    gridResponsibility.getColumns().stream().forEach(column -> column.setHidable(true));

    addSelectionListener();
    addGridFilters();

    return gridResponsibility;
}


private void addGridFilters() {
final HeaderRow filterRow = gridResponsibility.appendHeaderRow();
for (final Column<ResponsibilityDto, ?> column : gridResponsibility.getColumns()) {
final HeaderCell headerCell = filterRow.getCell(column);

        if (!columnResponsibilityPool.equals(column.getId()) && !columnResponsibilityDisabled.equals(column.getId())) {
            headerCell.setComponent(createFilterTextField(column));
        } else if (columnResponsibilityPool.equals(column.getId()) || columnResponsibilityDisabled.equals(column.getId())) {
            headerCell.setComponent(createFilterComboBox(column));
        }
    }
}

private TextField createFilterTextField(final Column<ResponsibilityDto, ?> column) {
    final TextField tfFilter = new TextField();
    tfFilter.setPlaceholder(tp.getText(TextKey.VIEW_TEXTFIELD_FILTER));
    tfFilter.addStyleName(ValoTheme.TEXTFIELD_TINY);
    tfFilter.setWidth(100, Unit.PERCENTAGE);
    tfFilter.addValueChangeListener(event -> {
        updateAllFilter();
    });
    return tfFilter;
}

private ComboBox<BooleanFilterBean> createFilterComboBox(final Column<ResponsibilityDto, ?> column) {
    final ComboBox<BooleanFilterBean> cbFilter = new ComboBox<BooleanFilterBean>();
    cbFilter.setPlaceholder(tp.getText(TextKey.VIEW_COMBOBOX_FILTER));
    cbFilter.setItemIconGenerator(BooleanFilterBean::getFilterIcon);
    cbFilter.setItemCaptionGenerator(BooleanFilterBean::getFilterName);
    cbFilter.addStyleName(ValoTheme.COMBOBOX_TINY);
    cbFilter.setWidth(100, Unit.PERCENTAGE);
    cbFilter.setTextInputAllowed(false);

    cbFilter.setItems(new BooleanFilterBean(true, tp.getText(TextKey.VIEW_COMBOBOX_FILTER_YES), VaadinIcons.CHECK_SQUARE_O),
            new BooleanFilterBean(false, tp.getText(TextKey.VIEW_COMBOBOX_FILTER_NO), VaadinIcons.THIN_SQUARE));

    cbFilter.addValueChangeListener(event -> {
        updateAllFilter();
    });
    return cbFilter;
}

private void updateAllFilter() {
    listDataProviderResponsibility.clearFilters();

    final HeaderRow filterRow = gridResponsibility.getHeaderRow(1);
    for (final Column<ResponsibilityDto, ?> column : gridResponsibility.getColumns()) {
        final ValueProvider<ResponsibilityDto, String> valueProvider = (ValueProvider<ResponsibilityDto, String>) column.getValueProvider();
        final HeaderCell headerCell = filterRow.getCell(column);

        if (!columnResponsibilityPool.equals(column.getId()) && !columnResponsibilityDisabled.equals(column.getId())) {
            final TextField textField = (TextField) headerCell.getComponent();

            if (textField.getValue() != null && !textField.getValue().isEmpty()) {
                listDataProviderResponsibility.addFilter(valueProvider, value -> {
                    return value.toLowerCase().contains(textField.getValue().toLowerCase());
                });
            }
        } else if (columnResponsibilityPool.equals(column.getId()) || columnResponsibilityDisabled.equals(column.getId())) {
            final ComboBox<BooleanFilterBean> comboBox = (ComboBox<BooleanFilterBean>) headerCell.getComponent();

            if (comboBox.getValue() != null) {
                listDataProviderResponsibility.addFilter(valueProvider, value -> {
                    if (value.toString().contains(VaadinIcons.CHECK_SQUARE_O.getHtml()) && comboBox.getValue().isFilterValue()) {
                        return true;
                    } else if (value.toString().contains(VaadinIcons.THIN_SQUARE.getHtml()) && !comboBox.getValue().isFilterValue()) {
                        return true;
                    }
                    return false;
                });
            }
        }
    }
    listDataProviderResponsibility.refreshAll();
}

@Override
public void addAllResponsibilities(List<ResponsibilityDto> responsibilityDtos) {
    listDataProviderResponsibility = new ListDataProvider<ResponsibilityDto>(responsibilityDtos);
    gridResponsibility.setDataProvider(listDataProviderResponsibility);

    lLastUpdate.setValue(tp.getText(TextKey.VIEW_LABEL_LASTUPDATE, new Date()));
}

[/code]I have a problem with the following two designs. Maybe you can help me out with this problems:

  1. If you want a converter for a Boolean to a String you have to write a static converter method in a class to reuse it in other grids and cases. So it looks something like this:

gridResponsibility.addColumn(responsibilityDto -> BooleanToFontIconConverterV8.convertToPresentation(responsibilityDto.isDisabled())) .setCaption(tp.getText(TextKey.RESPONSIBILITYVIEW_GRID_COLUMN_RESPONSIBILITY_DISABLED)).setId(columnResponsibilityDisabled) .setStyleGenerator(responsbilityDto -> "fontIconColumn").setRenderer(new HtmlRenderer()).setWidth(150); The Problem here is, that you have a dependency on BooleanToFontIconConverterV8 with the static method from every class you use it. Do you know some better approach for this? Can i relize it with a lamdba expression?

  1. The second problem is about filtering this column. I have a combobox in the headerrow and try to filter it with the boolean value from the bean and not the String value of the column. But i can’t get the value from the bean for a specific row. I only get the font icon so i have to check it against this. It’s not very nice. Is there also a better approach?
final ComboBox<BooleanFilterBean> comboBox = (ComboBox<BooleanFilterBean>) headerCell.getComponent();

                if (comboBox.getValue() != null) {
                    listDataProviderResponsibility.addFilter(valueProvider, value -> {
                        if (value.toString().contains(VaadinIcons.CHECK_SQUARE_O.getHtml()) && comboBox.getValue().isFilterValue()) {
                            return true;
                        } else if (value.toString().contains(VaadinIcons.THIN_SQUARE.getHtml()) && !comboBox.getValue().isFilterValue()) {
                            return true;
                        }
                        return false;
                    });
                }

Thank you or your help!!