How to refresh grid data when using ConfigurableFilterDataProvider?

I’ve tried every way I can think of to refresh the data in my grid. In the logs I can see that the method is getting called, but on the rendered page, the grid remains empty.

There are 2 clues:

  1. The second refresh call does work. It comes at the end of the long async process that is running.
  2. The first call has a strange limit setting.

Here’s the method:

public void refreshGrid() {
        backendDataProvider.refreshAll();
    }

Here are the relevant logs:

KeywordGrid * starting refresh

  • KeywordDtoDataProvider starting refreshAll
  • Loaded 121 keywords for collection KeywordCollectionId[value=877dd040-1326-47de-b1c3-6a1b148d80b8]
  • KeywordDtoDataProvider calling super.refreshAll()
  • KeywordDtoDataProvider completed refreshAll
  • KeywordGrid completed refresh call
  • Fetching data from backend with offset: 0, limit: 2147483647

Later, when the second refresh is called, the limit is much different:

  • Fetching data from backend with offset: 0, limit: 49

I think that limit is handled by this method:

@Override
    protected int sizeInBackEnd(Query<KeywordDto, KeywordFilter> query) {
        KeywordFilter filter = query.getFilter().orElse(null);
        return (int) applyFiltersAndSorting(fullDataset.stream(), filter, null).count();
    }

Here’s the full class:

public class KeywordDtoDataProvider extends AbstractBackEndDataProvider<KeywordDto, KeywordFilter> {
private final ResearchRepository repository;
private final KeywordCollectionId keywordCollectionId;
private Set fullDataset;
private static final Logger log = LoggerFactory.getLogger(KeywordDtoDataProvider.class);

public KeywordDtoDataProvider(ResearchRepository repository, KeywordCollectionId keywordCollectionId) {
    this.repository = Objects.requireNonNull(repository, "Repository must not be null");
    this.keywordCollectionId = Objects.requireNonNull(keywordCollectionId, "KeywordCollectionId must not be null");
    reloadData();
}

private void reloadData() {
    Optional<KeywordCollection> keywordCollection = repository.findById(keywordCollectionId);

    if (keywordCollection.isEmpty()) {
        log.debug("No keyword collection found for id: {}", keywordCollectionId);
        this.fullDataset = Collections.emptySet();
    } else {
        log.debug("Loaded {} keywords for collection {}",
                keywordCollection.get().keywordResults().size(),
                keywordCollectionId);
        this.fullDataset = keywordCollection.get().keywordResults();
    }
}

@Override
public void refreshAll() {
    log.debug("KeywordDtoDataProvider starting refreshAll");
    reloadData();
    log.debug("KeywordDtoDataProvider calling super.refreshAll()");
    super.refreshAll();
    log.debug("KeywordDtoDataProvider completed refreshAll");
}

@Override
protected Stream<KeywordDto> fetchFromBackEnd(Query<KeywordDto, KeywordFilter> query) {
    log.debug("Fetching data from backend with offset: {}, limit: {}", query.getOffset(), query.getLimit());
    int offset = query.getOffset();
    int limit = query.getLimit();
    KeywordFilter filter = query.getFilter().orElse(null);
    List<QuerySortOrder> sortOrders = getQuerySortOrders(query);
    Comparator<KeywordResult> comparator = createComparator(sortOrders);
    return assignRowNumber(applyFiltersAndSorting(fullDataset.stream(), filter, comparator)
            .skip(offset)
            .limit(limit))
            .map(pair -> KeywordDto.from(pair.element()))
            .toList()
            .stream();
}

@NotNull
private Comparator<KeywordResult> createComparator(List<QuerySortOrder> sortOrders) {
    return sortOrders.stream()
            .map(this::getComparatorForSortOrder)
            .reduce(Comparator::thenComparing)
            .orElse((_, _) -> 0);
}

private Stream<KeywordResult> applyFiltersAndSorting(Stream<KeywordResult> keywordStream,
                                                     KeywordFilter filter,
                                                     Comparator<KeywordResult> comparator) {
    if (filter != null) {
        keywordStream = keywordStream.filter(filter::test);
    }
    if (comparator != null) {
        keywordStream = keywordStream.sorted(comparator);
    }
    return keywordStream;
}

private List<QuerySortOrder> getQuerySortOrders(Query<KeywordDto, KeywordFilter> query) {
    List<QuerySortOrder> sortOrders = query.getSortOrders();
    if (sortOrders == null || sortOrders.isEmpty()) {
        sortOrders = QuerySortOrder.desc("createdAt").build();
    }
    return sortOrders;
}

@Override
protected int sizeInBackEnd(Query<KeywordDto, KeywordFilter> query) {
    KeywordFilter filter = query.getFilter().orElse(null);
    return (int) applyFiltersAndSorting(fullDataset.stream(), filter, null).count();
}

private <T> Stream<Pair<T>> assignRowNumber(Stream<T> stream) {
    AtomicInteger index = new AtomicInteger();
    return stream.map(element -> new Pair<>(element, index.getAndIncrement()));
}

private record Pair<T>(T element, int index) {
}

private Comparator<KeywordResult> getComparatorForSortOrder(QuerySortOrder order) {
    Objects.requireNonNull(order, "QuerySortOrder must not be null");
    Objects.requireNonNull(order.getSorted(), "Sort field must not be null");
    Objects.requireNonNull(order.getDirection(), "Sort direction must not be null");

    Comparator<KeywordResult> comparator;

    switch (order.getSorted()) {
        case "keyword" -> comparator = Comparator.comparing(
                kr -> kr.keyword().replaceAll("(\\D*)(\\d+)(.*)", "$1$2"),
                String.CASE_INSENSITIVE_ORDER
        );
        case "avgMonthlySearches" -> comparator = Comparator.comparing(
                KeywordResult::searchVolume, Comparator.nullsLast(Comparator.naturalOrder())
        );
        case "compositeTrend" -> comparator = Comparator.comparing(
                KeywordResult::compositeTrendScore, Comparator.nullsLast(Comparator.naturalOrder())
        );
        case "competition" -> comparator = Comparator.comparing(
                KeywordResult::competitionIndex, Comparator.nullsLast(Comparator.naturalOrder())
        );
        case "createdAt" -> comparator = Comparator.comparing(
                KeywordResult::createdAt, Comparator.nullsLast(Comparator.naturalOrder())
        );
        case "industryAlignmentConfidence" -> comparator = Comparator.comparing(
                KeywordResult::minIndustryAlignmentConfidence, Comparator.nullsLast(Comparator.naturalOrder())
        );
        case "avgDomainAuth" -> comparator = Comparator.comparing(
                KeywordResult::avgDomainAuthority, Comparator.nullsLast(Comparator.naturalOrder())
        );
        case "serpUri" -> comparator = Comparator.comparing(
                KeywordResult::serpUri, Comparator.nullsLast(Comparator.naturalOrder())
        );
        case "difficulty" -> comparator = Comparator.comparing(
                KeywordResult::difficulty, Comparator.nullsLast(Comparator.naturalOrder())
        );
        case "commercialValue" -> comparator = Comparator.comparing(
                KeywordResult::commercialValue, Comparator.nullsLast(Comparator.naturalOrder())
        );
        case "searchIntent" -> comparator = Comparator.comparing(
                KeywordResult::searchIntent, Comparator.nullsLast(Comparator.naturalOrder())
        );
        default -> throw new IllegalArgumentException("Unsupported sorting field: " + order.getSorted());
    }

    return order.getDirection() == SortDirection.DESCENDING ? comparator.reversed() : comparator;
}

}

Can you give us some sample code on how the data provider is set to the grid and how your refreshGrid is called?

First thing to check is if either the acces from the async call is done without a ui access or that push is not activated, but I assume, you already did that?

Also do you maybe use some setting like “show all rows” on the grid?

1 Like

Thanks Stefan!

how the data provider is set to the grid

setDataProvider(filterDataProvider);

I’ll add the entire class at the bottom.

how your refreshGrid is called?

From the view class:

@Override
    protected void onAttach(AttachEvent attachEvent) {
        setupBroadcasterListener(attachEvent.getUI(), account.accountId());
    }

    private void setupBroadcasterListener(UI ui, AccountId accountId) {
        broadcasterRegistration = broadcaster.registerListener((message, _) -> ui.access(() -> {
            log.debug("ResearchView received broadcast message: {}", message);
            if ("REFRESH_GRIDS".equals(message)) {
                log.debug("ResearchView refreshing grids for account {}", accountId);
                keywordGrid.refreshGrid();
            }
            if ("REFRESH_INPUT_FORM".equals(message)) {
                log.debug("ResearchView refreshing input form for account {}", accountId);
                binder.populateForm();
            }
        }), ui, accountId);
    }

acces from the async call is done without a ui access

private void handleFormSubmission(BeanValidationBinder<StartResearchDto> binder, Account account) {
        try {
            StartResearchDto dto = binder.writeRecord();
            UI ui = UI.getCurrent();
            if (ui == null) {   // This can happen if the user navigated away.
                form.errorMessageField().setText("UI not available. Please refresh and try again.");
                return;
            }

            CompletableFuture
                    .supplyAsync(() -> researchService.start(dto, account))
                    .whenCompleteAsync((result, throwable) -> ui.access(() -> {
                        try {
                            if (throwable != null) {  // Some unhandled exception
                                handleUpdateFailure(throwable.getMessage());
                            } else {
                                switch (result) {
                                    case RichResult.Success<Void> success -> handleUpdateSuccess(success.message());
                                    case RichResult.Failure<Void> failure ->
                                            handleUpdateFailure(failure.errorMessage());
                                }
                            }
                        } finally {
                            form.startButton().setEnabled(true);
                        }
                    }));

        } catch (ValidationException exception) {
            form.errorMessageField().setText("Validation error: " + exception.getMessage());
            form.startButton().setEnabled(true);
        }
    }

push is not activated

@Push
@SpringBootApplication
@Theme(value = "keyword-compass", variant = Lumo.DARK)
public class Application implements AppShellConfigurator {

Here’s the entire grid class:

public class KeywordGrid extends Grid<KeywordDto> {
    private final ConfigurableFilterDataProvider<KeywordDto, Void, KeywordFilter> filterDataProvider;
    private final KeywordFilter keywordFilter;
    private final KeywordDtoDataProvider backendDataProvider;
    private static final Logger log = LoggerFactory.getLogger(KeywordGrid.class);

    public KeywordGrid(KeywordCollectionId keywordCollectionId, ResearchRepository repository, TextField searchField, KeywordFilter filter) {
        super(KeywordDto.class, false);
        backendDataProvider = new KeywordDtoDataProvider(repository, keywordCollectionId);
        this.filterDataProvider = backendDataProvider.withConfigurableFilter();
        this.keywordFilter = filter;
        initializeGrid();
        styleGrid();
        setupSearch(searchField);
    }

    private void initializeGrid() {
        setId("keyword-grid");
        setEmptyStateText("No results found.");
        setMultiSort(true, MultiSortPriority.APPEND, true);
        addThemeVariants(GridVariant.LUMO_WRAP_CELL_CONTENT);

        addColumn(KeywordDto::keyword)
                .setKey("keyword")
                .setHeader("Keyword")
                .setSortable(true)
                .setAutoWidth(true);

        addColumn(KeywordDto::avgMonthlySearches)
                .setKey("avgMonthlySearches")
                .setHeader("Avg Monthly Searches")
                .setSortable(true)
                .setAutoWidth(true);

        addColumn(KeywordDto::avgDomainAuth)
                .setKey("avgDomainAuth")
                .setHeader("Avg Domain Auth")
                .setSortable(true)
                .setAutoWidth(true);

        addColumn(createSerpLink())
                .setKey("serp")
                .setHeader("SERP")
                .setSortable(true)
                .setAutoWidth(true);

        addColumn(KeywordDto::difficulty)
                .setKey("difficulty")
                .setHeader("Difficulty")
                .setSortable(true)
                .setAutoWidth(true);

        addColumn(KeywordDto::competition)
                .setKey("competition")
                .setHeader("Competition")
                .setSortable(true)
                .setAutoWidth(true);

        addColumn(KeywordDto::commercialValue)
                .setKey("commercialValue")
                .setHeader("Commercial Value")
                .setSortable(true)
                .setAutoWidth(true);

        addColumn(KeywordDto::searchIntent)
                .setKey("searchIntent")
                .setHeader("Search Intent")
                .setSortable(true)
                .setAutoWidth(true);

        addColumn(KeywordDto::compositeTrend)
                .setKey("compositeTrend")
                .setHeader("Trend")
                .setSortable(true)
                .setAutoWidth(true);

        addColumn(KeywordDto::industryAlignmentConfidence)
                .setKey("alignment")
                .setHeader("Industry Alignment")
                .setAutoWidth(true);

        setDataProvider(filterDataProvider);
    }

    @NotNull
    private ComponentRenderer<Anchor, KeywordDto> createSerpLink() {
        return new ComponentRenderer<>(dto -> {
            if (dto.serpUri() != null) {
                Icon linkIcon = new Icon(VaadinIcon.EXTERNAL_LINK);
//                linkIcon.getStyle().set("cursor", "pointer");
                Anchor anchor = new Anchor(dto.serpUri().toString(), linkIcon);
                anchor.setTarget("_blank");
                anchor.setAriaLabel("Open SERP in new tab");
                return anchor;
            }
            return null;
        });
    }

    private void styleGrid() {
        setWidthFull();
    }

    private void setupSearch(TextField searchField) {
        searchField.setValueChangeMode(ValueChangeMode.LAZY);
        searchField.setValueChangeTimeout(300);
        searchField.addValueChangeListener(event -> {
            keywordFilter.setSearchTerm(event.getValue());
            filterDataProvider.setFilter(keywordFilter);
        });
    }

    public List<KeywordDto> fetchCurrentItems() {
        return backendDataProvider.fetch(new Query<>()).toList();
    }

    public void refreshGrid() {
        log.debug("KeywordGrid starting refresh");
        backendDataProvider.refreshAll();
        log.debug("KeywordGrid completed refresh call");
    }

    public ConfigurableFilterDataProvider<KeywordDto, Void, KeywordFilter> filterDataProvider() {
        return filterDataProvider;
    }
}

Forgive the question, but have you checked that your fetchFromBackEnd is effectively returning the required data? Is there any chance that a filter is preventing it to returning any item?

The code is quite complicated, but at a first look, I could not spot anything suspicious.

1 Like

have you checked that your fetchFromBackEnd is effectively returning the required data?

I’m not 100% sure I understand you correctly, but if I do, then yes. As I mentioned in the original post, “The second refresh call does work.”

Maybe that’s what you mean, though? Maybe you mean that during that first call, that there may be some error in the retrieval? It’s possible. Let me see if I can figure out how to test that.

a filter is preventing it to returning any item?

oh, good idea! I’m testing it right now.

But, that were true, then why is the limit value 2147483647 and not 49?

You were totally right. One of the filters was causing no data to be shown on that first call to refresh. I’ll have to figure out how to solve it. Maybe I can temporarily by pass that one filter or somehow alert the user that they will see data if they change the filter.

Thank you!

Have you checked with a debugger breakpoint where that call comes from?

1 Like

Just a guess. You did not provide a ItemCountCallback to the GridLazyDataView (https://vaadin.com/docs/latest/flow/binding-data/data-provider#improving-scrolling-behavior), so the DataCommunicator, to get the total number of the items, calls the size() method in your DataProvider implementation without pagination information (meaning limit is Integer.MAX_VALUE). Since you have extended AbstractBackEndDataProvider, this will end up in a call to sizeInBackEnd method.

2 Likes