> Markdown version of [Show Lists and Grids](https://vaadin.com/docs/latest/building-apps/views/mvvm/lists). Section index: [llms.txt](https://vaadin.com/docs/latest/building-apps/llms.txt)

# Show Lists and Grids

How a ViewModel holds a list depends on how the list changes: whether it’s reloaded as a whole, edited entry by entry, or too large to load at once. This guide shows each case.

> **Tip: Part of a Series**
>
> This is part 5 of the **Structure Views with MVVM** series. It uses the `SignalBindings` class from [Bind Grids & Error Messages](https://vaadin.com/docs/latest/building-apps/views/mvvm/bind-view.md#bind-grids-and-error-messages).

## <a id="show-a-list-that-reloads-as-a-whole"></a>Show a List That Reloads as a Whole

A list that the ViewModel reloads as a whole, such as search results, is an immutable list in a `ValueSignal`. A command replaces the list, as the `refresh()` command in [Write a ViewModel](https://vaadin.com/docs/latest/building-apps/views/mvvm/write-viewmodel.md#offer-commands) does.

Bind a grid to the list with `bindItems()`, and its selection with `bindValue()`:

```java
bindItems(grid, viewModel.tickets()).setIdentifierProvider(Ticket::id);
grid.asSingleSelect().bindValue(viewModel.selected(), viewModel::select);
```

`bindItems()` sets the items once, and refreshes them whenever the list changes. The identifier provider keeps the row selected when a save replaces its record with a new one.

## <a id="keep-the-selection-as-ids"></a>Keep the Selection as IDs

Keep a selection as the IDs of the selected items, and derive the selected items from the IDs and the current list. After a reload, the derived selection then holds the latest data of each item, and an item that is no longer listed drops out of the selection without any command having to remove it. This matters most for a multi-select grid, whose commands act on every selected row:

```java
private final ValueSignal<Set<Long>> selectedIds = new ValueSignal<>(
        Set.of());
private final Signal<Set<Ticket>> selectedTickets = Signal.computed(() -> {
    Set<Long> ids = selectedIds.get();
    return tickets.get().stream().filter(t -> ids.contains(t.id()))
            .collect(Collectors.toUnmodifiableSet());
});

Signal<Set<Ticket>> selectedTickets() {
    return selectedTickets;
}

void selectTickets(Set<Ticket> selection) {
    selectedIds.set(selection.stream().map(Ticket::id)
            .collect(Collectors.toUnmodifiableSet()));
}
```

The view binds the grid’s selection to the derived signal:

```java
grid.setSelectionMode(Grid.SelectionMode.MULTI);
bindItems(grid, viewModel.tickets()).setIdentifierProvider(Ticket::id);
grid.asMultiSelect().bindValue(viewModel.selectedTickets(),
        viewModel::selectTickets);
```

The commands that act on the selection, such as one that resolves every selected ticket, read `selectedTickets.peek()`, never the IDs. They then never act on a ticket that is no longer listed.

## <a id="edit-a-list-entry-by-entry"></a>Edit a List Entry by Entry

When the user edits a list entry by entry, such as the lines of an order, keep the list in a `ListSignal`. Each entry of the list is a signal of its own. A `bindChildren()` binding creates one row of UI components for each entry, and keeps the row when its entry changes or moves. The rows aren’t rebuilt while the user types, so the fields don’t lose the focus.

A `ListSignal` has no `asReadonly()` method. To keep the view from changing the entries without a command, expose the entries typed as read-only signals. They are the same signal instances, so `bindChildren()` still keeps track of them, and the commands find the entry that they receive by its identity:

LinesViewModel.java

`LinesViewModel.java`

```java
import java.math.BigDecimal;
import java.util.List;

import com.vaadin.flow.signals.Signal;
import com.vaadin.flow.signals.local.ListSignal;
import com.vaadin.flow.signals.local.ValueSignal;

class LinesViewModel {

    record Line(String product, BigDecimal price, int quantity) {

        Line withQuantity(int quantity) {
            return new Line(product, price, quantity);
        }

        BigDecimal total() {
            return price.multiply(BigDecimal.valueOf(quantity));
        }
    }

    private final ListSignal<Line> lines = new ListSignal<>();
    private final ValueSignal<List<Line>> opened = new ValueSignal<>(List.of());

    // The same entry signals, typed so that the view can't set them
    private final Signal<List<Signal<Line>>> readOnlyLines = lines
            .map(entries -> List.<Signal<Line>> copyOf(entries));
    private final Signal<BigDecimal> total = Signal
            .computed(() -> lines.getValues().map(Line::total)
                    .reduce(BigDecimal.ZERO, BigDecimal::add));
    private final Signal<Boolean> dirty = Signal
            .computed(() -> !lines.getValues().toList().equals(opened.get()));
    private final Signal<Boolean> valid = Signal
            .computed(() -> lines.getValues().allMatch(l -> l.quantity() >= 1));

    LinesViewModel(List<Line> initial) {
        lines.insertAllLast(initial);
        opened.set(List.copyOf(initial));
    }

    Signal<List<Signal<Line>>> lines() {
        return readOnlyLines;
    }

    Signal<String> quantityError(Signal<Line> line) {
        return line.map(l -> l.quantity() < 1 ? "At least 1" : null);
    }

    Signal<BigDecimal> total() {
        return total;
    }

    Signal<Boolean> dirty() {
        return dirty;
    }

    Signal<Boolean> valid() {
        return valid;
    }

    void addLine(String product, BigDecimal price) {
        lines.insertLast(new Line(product, price, 1));
    }

    void setQuantity(Signal<Line> line, Integer quantity) {
        entry(line)
                .update(l -> l.withQuantity(quantity == null ? 0 : quantity));
    }

    void removeLine(Signal<Line> line) {
        lines.remove(entry(line));
    }

    private ValueSignal<Line> entry(Signal<Line> line) {
        return lines.peek().stream().filter(e -> e == line).findFirst()
                .orElseThrow();
    }
}
```

A few things to note about this ViewModel:

- Reading `lines.getValues()` in a computed signal makes the signal depend on the list and on the value of every entry. The total therefore follows both a changed quantity and an added or removed line.

- The `dirty` signal compares the current values with a copy taken when the lines were opened.

- The rows are identified by their signals, not by an ID. A new line that hasn’t been saved yet doesn’t need a temporary ID.

- A rule about one row is a derived signal of the row’s entry, such as the one that `quantityError()` returns.

The view renders one row for each entry. The factory receives the read-only signal of the entry, and passes it back to the commands:

LinesEditor.java

`LinesEditor.java`

```java
VerticalLayout rows = new VerticalLayout();
rows.bindChildren(viewModel.lines(), line -> {
    Span product = new Span(line.map(Line::product));
    IntegerField quantity = new IntegerField();
    quantity.setAriaLabel("Quantity");
    quantity.bindValue(line.map(Line::quantity),
            value -> viewModel.setQuantity(line, value));
    bindError(quantity, viewModel.quantityError(line));
    Button remove = new Button("Remove",
            event -> viewModel.removeLine(line));
    return new HorizontalLayout(product, quantity, remove);
});
```

A list that the user only views, or that the ViewModel reloads as a whole, doesn’t need a `ListSignal`. Keep it as an immutable list in a `ValueSignal`, and show it in a grid.

## <a id="load-data-page-by-page"></a>Load Data Page by Page

A grid that loads its rows page by page needs a data provider, and a data provider belongs to one UI component. Several UI components may show the same ViewModel, so the ViewModel doesn’t hand out a data provider. Instead, it offers what every data provider needs:

- The `fetch()` and `count()` methods, which load one page of items and count the items. They read the filter with `peek()`.

- The `query()` signal, which changes whenever the contents of the list would change. It holds the filter, and a revision number that the commands increase after they change data.

The query is a record:

TicketQuery.java

`TicketQuery.java`

```java
// What the ticket list shows: the filter, and how often the data has changed
record TicketQuery(String text, long revision) {

    TicketQuery withText(String text) {
        return new TicketQuery(text, revision);
    }

    TicketQuery nextRevision() {
        return new TicketQuery(text, revision + 1);
    }
}
```

The ViewModel replaces the query when the user searches, or when a command has changed a ticket:

PagedTicketsViewModel.java

`PagedTicketsViewModel.java`

```java
import java.util.stream.Stream;

import com.vaadin.flow.signals.Signal;
import com.vaadin.flow.signals.local.ValueSignal;

class PagedTicketsViewModel {

    private final TicketService ticketService;

    private final ValueSignal<TicketQuery> query = new ValueSignal<>(
            new TicketQuery("", 0));
    private final ValueSignal<Ticket> selected = new ValueSignal<>(null);
    private final Signal<Boolean> canResolve = selected
            .map(t -> t != null && t.status() != Status.RESOLVED);

    PagedTicketsViewModel(TicketService ticketService) {
        this.ticketService = ticketService;
    }

    // Changes whenever the list's contents would: a new filter or a saved
    // ticket
    Signal<TicketQuery> query() {
        return query.asReadonly();
    }

    Signal<String> searchText() {
        return query.map(TicketQuery::text);
    }

    Signal<Ticket> selected() {
        return selected.asReadonly();
    }

    Signal<Boolean> canResolve() {
        return canResolve;
    }

    Stream<Ticket> fetch(int offset, int limit) {
        return ticketService.search(query.peek().text(), offset, limit)
                .stream();
    }

    int count() {
        return ticketService.count(query.peek().text());
    }

    void search(String text) {
        query.update(q -> q.withText(text == null ? "" : text));
    }

    void select(Ticket ticket) {
        selected.set(ticket);
    }

    void resolveSelected() {
        if (!canResolve.peek()) {
            return;
        }
        Ticket saved = ticketService
                .save(selected.peek().withStatus(Status.RESOLVED));
        selected.set(saved);
        query.update(TicketQuery::nextRevision);
    }
}
```

Each UI component that shows the tickets builds its own data provider with `bindItems()`. The view passes the same ViewModel to each of them:

TicketList.java

`TicketList.java`

```java
import static com.vaadin.demo.buildingapps.mvvm.SignalBindings.bindItems;

import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;

// One of the components that show the tickets. Each builds its own data
// provider.
class TicketList extends VerticalLayout {

    TicketList(PagedTicketsViewModel viewModel) {
        Grid<Ticket> grid = new Grid<>();
        grid.addColumn(Ticket::title).setHeader("Title");
        grid.addColumn(Ticket::status).setHeader("Status");
        bindItems(grid, viewModel.query(), viewModel::fetch, viewModel::count)
                .setIdentifierProvider(Ticket::id);
        grid.asSingleSelect().bindValue(viewModel.selected(),
                viewModel::select);
        add(grid);
    }
}
```

`bindItems()` sets the data provider once, and refreshes it whenever the query changes. The refresh keeps the selection, and the identifier provider keeps the resolved ticket selected after its record has been replaced. The binding ends when its grid is detached, so no UI component has to unsubscribe from the ViewModel.

For more about data providers, see [Paginated Data](https://vaadin.com/docs/latest/building-apps/forms-data/add-grid/paginated-data.md).

### <a id="pass-the-query-to-the-data-provider"></a>Pass the Query to the Data Provider

If you’d rather have `fetch()` and `count()` work only from their arguments, make them take the query as a parameter:

```java
Stream<Ticket> fetch(TicketQuery query, int offset, int limit) {
    return ticketService.search(query.text(), offset, limit).stream();
}

int count(TicketQuery query) {
    return ticketService.count(query.text());
}
```

The view then passes the query to the data provider as its filter. Setting the filter refreshes the data provider. `bindItems()` doesn’t cover this case, so the view creates the data provider and the effect itself:

```java
ConfigurableFilterDataProvider<Ticket, Void, TicketQuery> tickets =
        new CallbackDataProvider<Ticket, TicketQuery>(
                query -> viewModel.fetch(query.getFilter().orElseThrow(),
                        query.getOffset(), query.getLimit()),
                query -> viewModel.count(query.getFilter().orElseThrow()),
                Ticket::id).withConfigurableFilter();
grid.setItems(tickets);
Signal.effect(grid, () -> tickets.setFilter(viewModel.query().get()));
```

The effect sets the filter before the grid fetches its first page, so the filter is always present when the data provider asks for it.

### <a id="refresh-on-changes-by-other-users"></a>Refresh on Changes by Other Users

The revision number counts only the changes that this ViewModel makes. To refresh the list whenever any user saves a ticket, let the Model count the saves in a [shared signal](https://vaadin.com/docs/latest/flow/ui-state/shared-signals.md):

```java
private final SharedNumberSignal changes = new SharedNumberSignal();

public Ticket save(Ticket ticket) {
    // Store the ticket
    changes.incrementBy(1);
    return ticket;
}

// Changes whenever any user saves a ticket
public Signal<Integer> changes() {
    return changes.mapIntValue(count -> count);
}
```

The ViewModel derives its query from its own search text and the Model’s signal. It no longer increases a revision number itself, because its own saves go through the service as well:

```java
private final Signal<TicketQuery> query;

LiveTicketsViewModel(TicketService ticketService) {
    this.ticketService = ticketService;
    query = Signal.computed(() -> new TicketQuery(searchText.get(),
            ticketService.changes().get()));
}
```

The Model now depends on the signal classes of Vaadin. For the lists of the other users to refresh without an action of their own, the application needs [server push](https://vaadin.com/docs/latest/building-apps/server-push.md#enabling-push). [React to Changes in the Model](https://vaadin.com/docs/latest/building-apps/views/mvvm/background-work.md#react-to-changes-in-the-model) shows an alternative that keeps the Model free of signals.

### <a id="test-the-refresh"></a>Test the Refresh

A ViewModel test can’t see a grid, so test the refresh in a browserless test of the view. A browserless grid locator reads the rows from the data provider, so it shows the current data even when no refresh happened. To test that the grid refreshes, count the refresh events of its data provider:

```java
var refreshes = new AtomicInteger();
grid.getDataProvider()
        .addDataProviderListener(event -> refreshes.incrementAndGet());
```

Next, [handle dialogs and navigation](https://vaadin.com/docs/latest/building-apps/views/mvvm/dialogs-and-navigation.md).
