Collaborative Form Editing

The recommended way of binding data from Java beans to forms in Vaadin applications is to use Binder (read Binding Data to Forms to learn more). Collaboration Engine provides a Binder extension called CollaborationBinder, which adds the following real-time features on top of the binder’s data binding and validation APIs:

  • Field value synchronization: When a user enters a new value to a field, the value is updated for the other users as well.

  • Field highlight: When a user is focused on a field, other users see a highlight around the field and a user name indicating who is currently editing it.

Example of a collaborative form bound with CollaborationBinder

Constructing a CollaborationBinder

The constructor of CollaborationBinder requires the bean type to bind values to, similarly to the regular Binder when binding by property names. As the second argument, you must provide some information about the end user. The same UserInfo object is required by all Collaboration Engine features. CollaborationBinder uses the name in UserInfo, when indicating that another user is editing a field with the field highlight.

User userEntity = userService.getCurrentUser();

UserInfo userInfo = new UserInfo(userEntity.getId(),
        userEntity.getName());

CollaborationBinder<Person> binder = new CollaborationBinder<>(
        Person.class, userInfo);

Connecting to Collaboration Engine and Populating the Form

The CollaborationBinder::setTopic method serves two purposes:

  1. Connecting to Collaboration Engine in the scope of the edited item

  2. Populating the form with initial values loaded from a backend

Here is an example use case. When selecting an item to edit (an instance of Person bean class in this case), setTopic is used to connect to the topic and to populate the form:

public void personSelected(long personId) {
    binder.setTopic("person/" + personId,
            () -> personService.findById(personId));
}

The first parameter is the id of the topic to connect to. By using unique topic ids for different items, we make sure that edits on different items don’t interfere with each other.

The second parameter is a callback for providing the bean that populates the fields. The regular Binder has the readBean method for populating the fields based on bean properties. This method is not supported by CollaborationBinder. The reason is, that calling readBean when a new view instance is constructed would have an unwanted effect: Every time a new user would join editing the form, the field values would reset for every user. The setBean method is unsupported for the same reason.

The callback provided for setTopic replaces readBean. It will be used to populate the form if the topic doesn’t have any data yet, which means that the user is the first one to edit that form. Otherwise, the field values are loaded from the topic in Collaboration Engine. In this case, the callback is not even called, possibly avoiding an unnecessary database request.

Note
If you explicitly want to override all field values for all collaborators (for example implement a reset button), you can use CollaborationBinder::reset method, which takes a bean instance and uses its properties for setting the field values.

Binding Collaborative Fields

Here is an example of how we can bind the Person bean’s name property to a text field, enabling also the collaborative features (value synchronization and field highlight):

TextField name = new TextField();
binder.forField(name).bind("name");
CheckboxGroup<String> pets = new CheckboxGroup<>();
pets.setItems("Dog", "Cat", "Parrot");

binder.forField(pets, Set.class, String.class).bind("pets");

You could write exactly the same code with the regular Binder. CollaborationBinder just adds the collaborative features on top of the regular data binding. Note that binding based on a property name ("name" in this case) requires the bean class (Person) to have standard getter and setter methods getName and setName.

The other bind variant, which takes the getter and setter callbacks as arguments, is not supported by CollaborationBinder. The technical reason for this is, that some unique key is needed per each field/property to store the data in the underlying CollaborationMap data structure. The property name is required for that purpose, to be used as the key.

Because the data used to communicate with Collaboration Engine is serialized as JSON, there are some limitations to what CollaborationBinder can do automatically. Next, we’ll cover the special cases where you need to do a little bit more than simply binding a property to a field.

Non-Primitive Value Types

Collaboration Engine supports only a limited set of primitive-like value types, that it knows how to serialize and deserialize. When using some other field value type, you must explicitly provide the serializer and deserializer functions.

When the field is used for selecting a bean object that has a unique identifier, you can serialize the value by converting the bean to its identifier, and deserialize by fetching the bean object that matches the id.

In this example, the Person bean that we are editing, has a reference to the person’s supervisor, which is another Person. We’ll use a ComboBox component for selecting the person’s supervisor:

ComboBox<Person> supervisor = new ComboBox<>();
supervisor.setItems(personService.findAllSupervisors());

binder.setSerializer(Person.class,
        person -> String.valueOf(person.getId()),
        id -> personService.findById(Long.parseLong(id)));

binder.bind(supervisor, "supervisor");

The person ids are stored as longs in this case, and the serialized value needs to be a String. In this case, we need to do a bit of converting between strings and longs.

Converters

When a Converter is used, you must provide the field’s value type in forField.

In this example, we are binding an enum property of the bean to a Checkbox, so the checkbox’s value type boolean needs to be provided:

Checkbox married = new Checkbox();
binder.forField(married, Boolean.class)
        .withConverter(
                fieldValue -> fieldValue ? MaritalStatus.MARRIED
                        : MaritalStatus.SINGLE,
                MaritalStatus.MARRIED::equals)
        .bind("maritalStatus");

This is necessary, because CollaborationBinder uses the bean property type (MaritalStatus enum in this case) for deserializing the field value by default.

Multi Select Fields

When the field’s value type is a collection, you must provide the type of the collection as well as the type of its contents in forField.

For example, the value type of CheckboxGroup is Set. In the example below, we must provide the collection type Set and the content type String.

TextField name = new TextField();
binder.forField(name).bind("name");
CheckboxGroup<String> pets = new CheckboxGroup<>();
pets.setItems("Dog", "Cat", "Parrot");

binder.forField(pets, Set.class, String.class).bind("pets");

This is necessary, because CollaborationBinder can’t automatically infer the generic type for deserializing the value. Note that if the element type is not supported by Collaboration Engine (for example CheckboxGroup<Person>), you need to implement custom (de)serializer functions anyway.

Propagate Values Eagerly in Text Fields

On text fields, the default and recommended mode for propagating values from one user to others is when the user blurs the field or presses the enter key. You can configure how eagerly the field sends data through its own API, using the setValueChangeMode(ValueChangeMode) method.

For example, to instantly send each keystroke to other users, you would do the following:

TextField textField = new TextField();
textField.setValueChangeMode(ValueChangeMode.EAGER);

Modes like ValueChangeMode.LAZY and ValueChangeMode.TIMEOUT can also be used together with the setValueChangeTimeout(int) method to reduce the amount of traffic.

Resetting Fields When There Are No Editors

By default, the edited field values are stored in Collaboration Engine indefinitely. Even after a long time has passed since someone has visited a form and you open the form again, you will be presented with the field values that the previous user might have left there. In this scenario, you might want the field values to be re-populated from the backend instead.

This can be achieved by setting an expiration timeout on the binder. If there hasn’t been any connected users for the set period of time, binder will call the bean supplier callback (provided in setTopic) again to populate the fields.

binder.setExpirationTimeout(Duration.ofMinutes(15));

You can use Duration.ZERO to reset the fields immediately when there’s a moment with no connected users. However, it might be a good idea to have a small time window. This enables, for example, a user to recover from a temporary network issue without having to start over with the form editing.