Skip to content
This repository has been archived by the owner on May 6, 2021. It is now read-only.

Latest commit

 

History

History
596 lines (476 loc) · 22.8 KB

tutorial-flow-data-provider.asciidoc

File metadata and controls

596 lines (476 loc) · 22.8 KB
title order layout
Binding Items to Components
5
page

Binding Items to Components

You often have lists of items that you want to display in your application, and may want to let the user select one or more of them. You can use basic components such as HTML elements to display such lists, or components especially designed for the purpose, such as Grid, ComboBox, and ListBox.

// Create a listing component for a bean type
Grid<Person> grid = new Grid<>(Person.class);

// Sets items using vararg beans
grid.setItems(
        new Person("George Washington", 1732),
        new Person("John Adams", 1735),
        new Person("Thomas Jefferson", 1743),
        new Person("James Madison", 1751)
);

All listing components in Vaadin have a number of overloaded setItems() methods to define the items to display. Items can be basic objects, such as strings or numbers, or POJOs, such as DTOs or JPA entities, from your domain model. The easiest way is to provide a List of objects to be shown in such a component.

If the number of items is large and reserves a lot of memory, Grid and ComboBox allow you to do a lazy data binding using callbacks to fetch only the required set of items from the back end.

Configuring How Items Are Displayed

Component-specific APIs allow adjusting how items are displayed. Listing components use the toString() method to display items by default. If this is not suitable, you can change the behavior by configuring the component. Listing components have one or more callbacks that define how to display the items.

For example, consider the ComboBox component that lists status items. You can configure it to use Status::getLabel method to get a label for each status item.

ComboBox<Status> comboBox = new ComboBox<>();
comboBox.setItemLabelGenerator(Status::getLabel);

In a Grid, you can use addColumn() to define the columns and configure the getter that returns the content for the column. The setHeader() method sets the column header.

// A bean with some fields
final class Person implements Serializable {
    private String name;
    private String email;
    private String title;
    private int yearOfBirth;

    public Person(String name, int yearOfBirth) {
        this.name = name;
        this.yearOfBirth = yearOfBirth;
    }

    public String getName() {
        return name;
    }

    public int getYearOfBirth() {
        return yearOfBirth;
    }

    public String getTitle() {
        return title;
    }

    // other getters and setters
}

// Show such beans in a Grid
Grid<Person> grid = new Grid<>();
grid.addColumn(Person::getName)
    .setHeader("Name");
grid.addColumn(Person::getYearOfBirth)
    .setHeader("Year of birth");

It’s possible to define the Grid columns to display also by the property names. In this case, you need to get the column objects to configure the headers.

Grid<Person> grid = new Grid<>(Person.class);
grid.setColumns("name", "email", "title");
grid.getColumnByKey("name").setHeader("Name");
grid.getColumnByKey("email").setHeader("Email");
grid.getColumnByKey("title").setHeader("Title");

Check the component examples for more details on configuring how the listed data is displayed.

Assigning a List or Array of In-Memory Data

The easiest way to pass data to the listing component is to use an array or List. You can create those easily by yourself or pass values directly from your service layer.

Example: Passing in-memory data to components using the setItems method.

// Sets items as a collection
List<Person> persons = getPersonService().findAll();
comboBox.setItems(persons);

// Sets items using vararg beans
grid.setItems(
        new Person("George Washington", 1732),
        new Person("John Adams", 1735),
        new Person("Thomas Jefferson", 1743),
        new Person("James Madison", 1751)
);

// Pass all Person objects to a grid from a Spring Data repository object
grid.setItems(personRepository.findAll());

Lazy Data Binding Using Callbacks

Using callback methods is a more advanced way to bind data to components. This way, only the required portion of the data is loaded from your back end to the server memory. This approach is harder to implement and provides fewer features out of the box, but can save a lot of resources in the back end and the UI server. The component gives a query object as a parameter to your callback methods, where you can check that what part of the data set is needed.

Currently, only Grid and ComboBox properly support lazy data binding.

For example, to bind data lazily to a Grid:

grid.setItems(query -> // (1)
    getPersonService() // (2)
            .fetchPersons(query.getOffset(), query.getLimit()) // (3)
            .stream() // (4)
);
  1. To create a lazy binding, use an overloaded version of the setItems() method that uses a callback instead of passing data directly to the component.

  2. Typically, you call your service layer from the callback, as is done here.

  3. Use the query object’s parameters to limit the data you pass from the back end to the component.

  4. The callbacks return the data as a Stream. In this example, the back end returns a List, so we need to convert it to a Stream.

The example above works well with JDBC back ends, where you can request a set of rows from a given index. Vaadin calls your data binding call in paged manner, so it is possible to bind also to the back ends with paging, such as Spring Data based solutions.

For example, to do lazy data-binding from a Spring Data Repository to Grid:

grid.setItems(query -> {
    return repository.findAll( // (1)
            PageRequest.of(query.getPage(), // (2)
                           query.getPageSize()) // (3)
    ).stream(); // (4)
});
  1. Call a Spring Data repository to obtain the requested result set.

  2. The query object contains a shorthand for zero-based page index.

  3. The query object also contains page size.

  4. Return a stream of items from the Spring Data Page object.

Sorting with Lazy Data Binding

For efficient lazy data-binding, sorting needs to be done already in the back end. By default, Grid makes all columns appear sortable in the UI. You need to manually declare which columns are actually sortable. Otherwise, the UI may indicate that some columns are sortable, but nothing happens if you try to sort them. With lazy data binding, you need to pass the hints that Grid provides in the Query object to your back end logic.

For example, to enable sortable lazy data-binding to a Spring Data repository:

public void bindWithSorting() {
    Grid<Person> grid = new Grid<>(Person.class);
    grid.setSortableColumns("name", "email"); // (1)
    grid.addColumn(person -> person.getTitle())
        .setHeader("Title")
        	.setKey("title").setSortable(true); // (2)
    grid.setItems(
        query -> {
            Sort springSort = toSpringDataSort(query.getSortOrders()); // (3)
            return repo.findAll(
                    PageRequest.of(
                            query.getPage(),
                            query.getPageSize(),
                            springSort // (4)
            )).stream();
    });
}

/**
 * A method to convert given Vaadin sort hints to Spring Data specific sort
 * instructions.
 *
 * @param vaadinSortOrders a list of Vaadin QuerySortOrders to convert to
 * @return the Sort object for Spring Data repositories
 */
public static Sort toSpringDataSort(List<QuerySortOrder> vaadinSortOrders) {
    return Sort.by(
            vaadinSortOrders.stream()
                    .map(sortOrder ->
                            sortOrder.getDirection() == SortDirection.ASCENDING ?
                                    Sort.Order.asc(sortOrder.getSorted()) : // (5)
                                    Sort.Order.desc(sortOrder.getSorted())
                    )
                    .collect(Collectors.toList())
    );
}
  1. If you are using property name based column definition, Grid columns can be made sortable by their property names. The setSortableColumns() method makes columns with given identifiers sortable and all other non-sortable.

  2. Alternatively, define a key to your columns, which will be passed to the callback, and define the column to be sortable.

  3. In the callback, you need to convert the Vaadin specific sort information to whatever your back end understands. In this example, we are using Spring Data and using a separate method to convert the values. The method body is shown below. Note that the conversion becomes simpler if you only want to support sorting based on a single property. Vaadin Grid supports sorting based on multiple columns.

  4. Here the back-end compatible sort information is passed to our back-end call.

  5. The getSorted() method in QuerySortOrder returns the columns property name or a key you have assigned to the column.

Filtering with Lazy Data Binding

Note that, for the lazy data to be efficient, filtering needs to be done in the back end. For example, if you provide a text field to limit the results shown in a Grid, you need to make your callbacks handle the filter.

For example, to handle filterable lazy data binding to a Spring Data repository in Grid:

public void initFiltering() {
    filterTextField.setValueChangeMode(ValueChangeMode.LAZY); // (1)
    filterTextField.addValueChangeListener(e -> listPersonsFilteredByName(e.getValue())); // (2)
}

private void listPersonsFilteredByName(String filterString) {
    String likeFilter = "%" + filterString + "%";// (3)
    grid.setItems(q -> repo
        .findByNameLikeIgnoreCase(
            likeFilter, // (4)
            PageRequest.of(q.getPage(), q.getPageSize()))
        .stream());
}
  1. The lazy data binding mode is optimal for filtering purposes. Queries to the back end are only done when a user makes a small pause while typing.

  2. When a value change event occurs, you should reset the data binding to use the new filter.

  3. The example back end uses SQL behind the scenes, so % is appended to the beginning and to the end to match anywhere in the text.

  4. Pass the filter to your back end in the binding.

You can combine both filtering and sorting in your data binding callbacks.

Let’s consider a ComboBox as an another example of lazy loaded data filtering. The lazy loaded binding in ComboBox is always filtered by the string typed in by the end user. Initially, when there is no filter input yet, the filter is an empty string.

The ComboBox examples below use the new data API available since Vaadin 18 where the item count query is not needed for fetching items.

For example, you can handle filterable lazy data binding to a Spring Data repository as follows:

ComboBox<Person> cb = new ComboBox<>();
cb.setItems(
        query -> repo.findByNameLikeIgnoreCase(
                // Add `%` marks to filter for an SQL "LIKE" query
                "%" + query.getFilter().orElse("") + "%",
                PageRequest.of(query.getPage(), query.getPageSize()))
                .stream()
);

The above example uses a fetch callback for lazy loading items, and the ComboBox will fetch more items as the user scrolls the dropdown, until there are no more items returned. In case it is desired to have the dropdown’s scrollbar reflect the exact number of items matching the filter, an optional item count callback can be used as shown in the example below:

ComboBox<Person> cb = new ComboBox<>();
cb.setItems(
        query -> repo.findByNameLikeIgnoreCase(
                "%" + query.getFilter().orElse("") + "%",
                PageRequest.of(query.getPage(), query.getPageSize()))
                .stream(),
        query -> (int) repo.countByNameLikeIgnoreCase(
                "%" + query.getFilter().orElse("") + "%"));

In case the filtering of items should be done with another type than a string, you can provide a filter converter with the fetch callback to get the type of the filter right for the fetch query:

ComboBox<Person> cb = new ComboBox<>();
cb.setPattern("\\d+");
cb.setPreventInvalidInput(true);
cb.setItemsWithFilterConverter(
        query -> getPersonService()
                .fetchPersonsByAge(query.getFilter().orElse(null), // (1)
                        query.getOffset(), query.getLimit())
                .stream(),
        textFilter -> textFilter.isEmpty() ? null // (2)
                : Integer.parseInt(textFilter));
  1. Query object contains the filter of type returned by given converter.

  2. The second callback is used to convert the combo box’s text client-side filter into the appropriate value, used by back end.

Improving Scrolling Behaviour

In the case of the simple lazy data binding, the component does not know how many items there are actually available. When a user scrolls to the end of the scrollable area, Grid polls your callbacks for more items. If new items are found, those are added to the component. This causes the relative scrollbar to behave in a strange way as new items are added on the fly. The usability can be improved by giving an estimate or the actual number of items in the binding code. The adjustment happens through a DataView instance, which is returned by the setItems() method.

For example, to configure the estimate of rows and how the "virtual row count" is adjusted when the user scrolls down:

GridLazyDataView<Person> dataView = grid.setItems(query -> { // (1)
    return getPersonService()
            .fetchPersons(query.getOffset(), query.getLimit())
            .stream();
});

dataView.setItemCountEstimate(1000); // (2)
dataView.setItemCountEstimateIncrease(500); // (3)
  1. When assigning the callback, a data view object is returned. It can be configured directly or saved for later adjustments.

  2. If you know a rough estimate or rows, giving that to the component increases the user experience. Users can, for example, scroll directly to the end of the result set.

  3. You can also configure how Grid adjusts its estimate of available rows. With this configuration, if the back end returns an item for index 1000, the scrollbar is adjusted as if there were 1500 items in the Grid.

A count callback has to be provided to get similar user experience as when assigning data directly. Note that in many back ends, counting the number of results can be a heavy operation.

dataView.setItemCountCallback(q -> getPersonService().getPersonCount());

Accessing Currently Shown Items

You may need to get a handle to all items shown in a listing component. For example, add-ons or generic helpers might want to do something with the data that is currently listed in the component. For such a purposes, the supertype of data views can be accessed with the getGenericDataView() method.

Caution
Calling certain methods in data views can be an expensive operation. Especially with lazy data binding, calling for example grid.getGenericDataView().getItems() will cause the whole data set to be loaded from the back end.

For example, you can export persons listed in a Grid to a CSV file as follows:

private void exportToCsvFile(Grid<Person> grid)
        throws FileNotFoundException, IOException {
    GridDataView<Person> dataView = grid.getGenericDataView();
    FileOutputStream fout = new FileOutputStream(new File("/tmp/export.csv"));

    dataView.getItems().forEach(person -> {
        try {
            fout.write((person.getFullName() + ", " + person.getEmail() +"\n").getBytes());
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    });
    fout.close();
}

If you have assigned your items as in memory data you have more methods available in a list data view object. You can get the reference to that as a return value of the setItems() method or through the getListDataView() method. It is possible then to get the next or previous item of a certain item. This can be done, of course, by saving the original data structure, but this way you can implement a generic UI logic without dependencies to the assigned data.

For example, you can programmatically select the next item in a Grid, if a current value is selected and there is a next item after it.

List<Person> allPersons = repo.findAll();
GridListDataView<Person> gridDataView = grid.setItems(allPersons);

Button selectNext = new Button("Next", e -> {
    grid.asSingleSelect().getOptionalValue().ifPresent(p -> {
        gridDataView.getNextItem(p).ifPresent(
                next -> grid.select(next)
        );
    });
});

Updating the Shown Data

A typical scenario in Vaadin apps is that data displayed in, for example, a Grid component, is edited elsewhere in the application. Editing the item elsewhere does not automatically update the UI in a listing component. An easy way to refresh the component’s content is to call setItems() again with the fresh data. Alternatively, you can use more fine-grained APIs in the DataView to update just a portion of the dataset.

For example, you can modify one or more fields of a displayed item and notify Grid about the updates to the item through the DataView::refreshItem. This would modify only one particular item, not the whole data set.

Person person = new Person();
person.setName("Jorma");
person.setEmail("old@gmail.com");

GridListDataView<Person> gridDataView = grid.setItems(person);

Button modify = new Button("Modify data", e -> {
    person.setEmail("new@gmail.com");

    // The component shows the old email until notified of changes
    gridDataView.refreshItem(person);
});

If you have bound a mutable List to your component, you can alternatively use helper methods in the list data view to add or remove items or obtain item count by hooking to item count change event or request the item count directly.

For example, it’s possible to use a mutation methods and listening to item count change through the list data view as follows:

// The initial data
ArrayList<String> items = new ArrayList<>(Arrays.asList("foo", "bar"));

// Get the data view when binding it to a component
Select<String> select = new Select<>();
SelectListDataView<String> dataView = select.setItems(items);

TextField newItemField = new TextField("Add new item");
Button addNewItem = new Button("Add", e -> {
        // Adding through the data view API mutates the data source
        dataView.addItem(newItemField.getValue());
});
Button remove = new Button("Remove selected", e-> {
        // Same for removal
        dataView.removeItem(select.getValue());
});

// Hook to item count change event
dataView.addItemCountChangeListener(e ->
        Notification.show(" " + e.getItemCount() + " items available"));

// Request the item count directly
Span itemCountSpan = new Span("Total Item Count: " + dataView.getItemCount());

Sorting of In-memory Data

Let’s consider the Grid as an example of component with a sorting API. Grid rows are automatically sortable by columns that have property type that implements Comparable. By defining a custom Comparator, you can make also other columns sortable, or you can override the default behavior for columns with comparable types.

For example, to make sorting string-typed columns case-insensitive:

grid.addColumn(Person::getName)
        .setHeader("Name")
        // Override the default sorting
        .setComparator(Comparator.comparing(person ->
                    person.getName().toLowerCase()));

Note that this kind of sorting is only supported for in-memory data. See Sorting with Lazy Data Binding for how to sort lazy-loaded data.

It’s possible to sort a collection of bound items with the DataView API, either by setting a Comparator or a sort order for a given bean field. Sort orders or Comparator can be added or removed completely as well.

For example, you can define a custom sorting through the DataView API as follows:

// You get a DataView when setting the items
GridListDataView<Person> dataView = grid
        .setItems(personRepository.findAll());

// Change the sort order of items collection
dataView.setSortOrder(Person::getName, SortDirection.ASCENDING);

// Add a secondary sort order to the existing sort order
dataView.addSortOrder(Person::getTitle, SortDirection.ASCENDING);

// Remove sorting completely (undoes the settings done above)
dataView.removeSorting();

Filtering of In-Memory Data

If you are using an in-memory data set, you can also apply filters through the data view object. The filtered list is automatically updated to the UI.

For example, you can use a list data view to filter items based on a property as follows:

List<Person> allPersons = repo.findAll();
GridListDataView<Person> gridDataView = grid.setItems(allPersons);

// Filter Persons younger 20 years
gridDataView.setFilter(p -> p.getAge() < 20);

// Remove filters completely (undoes the settings done above)
gridDataView.removeFilters();

Recycling Data Binding Logic

In large applications, you typically have multiple places where you display the same data type in a listing component. Multiple approaches can be used to share the lazy data binding logic.

One way is to use a domain-object specific component implementation by extending a listing component to handle the application-specific data binding. This approach allows sharing also other common configuration.

@SpringComponent
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PersonGrid extends Grid<Person> {

    public PersonGrid(@Autowired PersonRepository repo) {
        super(Person.class);

        // Make the lazy binding
        setItems(q -> repo.findAll(
                PageRequest.of(q.getPage(), q.getPageSize())).stream());

        // Make other common/default configuration
        setColumns("name", "email");
    }

}

You can also use a static helper method to bind the data as follows:

public static void listItems(Grid<Person> grid, PersonRepository repository) {
    grid.setItems(query -> repository.findAll(
            PageRequest.of(query.getPage(), query.getPageSize())).stream());
}

You can create a separate data provider class. The following example uses only the FetchCallBack, but you can also implement a full data provider by, for example, extending AbstractBackEndDataProvider.

@SpringComponent
public class PersonDataProvider implements CallbackDataProvider.FetchCallback<Person, Void> {

    @Autowired
    PersonRepository repo;

    @Override
    public Stream<Person> fetch(Query<Person, Void> query) {
        return repo.findAll(PageRequest.of(query.getPage(),
                query.getPageSize())).stream();
    }

}

personGrid.setItems(dataProvider);