Binding Beans to Forms

The business objects used in an application are in most cases implemented as Java beans or POJOs. There is special support for that kind of business object in Binder. It can use reflection based on bean property names to bind values. This reduces the amount of code you have to write when binding to fields in the bean.

Binder<Person> binder = new Binder<>(Person.class);

// Bind based on property name
binder.bind(nameField, "name");
// Bind based on sub property path
binder.bind(streetAddressField, "address.street");
// Bind using forField for additional configuration
binder.forField(yearOfBirthField)
  .withConverter(
    new StringToIntegerConverter("Please enter a number"))
  .bind("yearOfBirth");
Note
Code using strings to identify properties will cause exceptions during runtime if the string contains a typo or if the name of the setter and getter methods have been changed without also updating the string.

If you want to use JSR 303 Bean Validation annotations such as Max, Min, Size, etc. then a BeanValidationBinder should be used. BeanValidationBinder extends Binder class so it has the same API but its implementation automatically adds a bean validator which takes care of JSR 303 constraints. Constraints defined for properties in the bean will work in the same way as if configured when the binding is created. Note that to use Bean Validation annotations you should have a JSR 303 implementation like Hibernate Validator available in your classpath. E.g. if you use Maven, the following dependency can be used.

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.1.Final</version>
</dependency>

Constraints defined for properties in the bean will work in the same way as if configured when the binding is created.

public class Person {
  @Max(2000)
  private int yearOfBirth;

  //Non-standard constraint provided by Hibernate Validator
  @NotEmpty
  private String name;

  // + other fields, constructors, setters, and getters
}
BeanValidationBinder<Person> binder = new BeanValidationBinder<>(Person.class);

binder.bind(nameField, "name");
binder.forField(yearOfBirthField)
  .withConverter(
    new StringToIntegerConverter("Please enter a number"))
  .bind("yearOfBirth");

Constraint annotations can also be defined on the bean level instead of being defined for any specific property.

There are some number of predefined constraint annotations that mark a bound field as required using BeanValidationBinder.setRequiredIndicatorVisible. By default @NotNull, @NotEmpty and @Size (if min() value is greater than 0) configures the field as required. It’s possible to change this behavior using the BeanValidationBinder.setRequiredConfigurator method.

Note
When using unbuffered binding (with the setBean method), validation will be triggered automatically on all change events. If you use buffered binding (with the readBean and writeBean methods), validation is only triggered automatically while calling writeBean. You can trigger it manually at any time by calling validate() on the Binder, for instance in a ValueChange handler.

Validation errors caused by that bean level validation might not be directly associated with any field component shown in the user interface, so Binder cannot know where such messages should be displayed.

Similarly to how the withStatusLabel method can be used for defining where messages for a specific binding should be showed, we can also define a Label that is used for showing status messages that are not related to any specific field.

Label formStatusLabel = new Label();

Binder<Person> binder = new Binder<>(Person.class);

binder.setStatusLabel(formStatusLabel);

// Continue by binding fields

We can also define our own status handler to provide a custom way of handling statuses.

BinderValidationStatusHandler<Person> defaultHandler = binder
                .getValidationStatusHandler();

binder.setValidationStatusHandler(status -> {
    // create an error message on failed bean level validations
    List<ValidationResult> errors = status
            .getBeanValidationErrors();

    // collect all bean level error messages into a single string,
    // separating each message with a <br> tag
    String errorMessage = errors.stream()
            .map(ValidationResult::getErrorMessage)
            // sanitize the individual error strings to avoid code
            // injection
            // since we are displaying the resulting string as HTML
            .map(errorString -> Jsoup.clean(errorString,
                    Whitelist.simpleText()))
            .collect(Collectors.joining("<br>"));

    // finally, display all bean level validation errors in a single
    // label
    formStatusLabel.getElement().setProperty("innerHTML", errorMessage);
    setVisible(formStatusLabel, !errorMessage.isEmpty());

    // Let the default handler show messages for each field
    defaultHandler.statusChange(status);
});

Automatic Data Binding

In this tutorial, we will explain a way to automatically bind properties of a business object to form fields.

The bindInstanceFields method

Usually, we define UI fields as members of a UI Java class in order to access them in different methods of the class. In such a case, binding those fields is really easy because the bindInstanceFields method can do this job for us. We simply need to pass an object of the UI Class to it and it matches fields of that object to the properties of the related business object based on their names.

public class MyForm extends VerticalLayout {
    private TextField firstName = new TextField("First name");
    private TextField lastName = new TextField("Last name");
    private ComboBox<Gender> gender = new ComboBox<>("Gender");

    public MyForm() {
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(this);
    }
}

This binds the firstName TextField to the "firstName" property in the item, lastName TextField to the “lastName” property and the gender ComboBox to the “gender” property.

Without this method, we would have to bind all the fields separately like the following example:

binder.forField(firstName)
    .bind(Person::getFirstName, Person::setFirstName);
binder.forField(lastName)
    .bind(Person::getLastName, Person::setLastName);
binder.forField(gender)
    .bind(Person::getGender, Person::setGender);

bindInstanceFields processes all Java member fields whose type extends HasValue (such as TextField) and that can be mapped to a property name. In case the field name does not match the corresponding property name in business object, we can use an annotation named @PropertyId to specify the property name. For example, if the related property of gender field in Person class is “sex”, we need to use the @PropertyId like the following:

@PropertyId("sex")
private ComboBox<Gender> gender = new ComboBox<>("Gender");

It’s not always possible to automatically bind all the fields to their corresponding properties because the value type of the field may not match the property type and bindInstanceFields doesn’t automatically add a converter to the binding. E.g. consider an “age” field which is a TextField whose value type is String, while the type of the “age” property in Person class is Integer. In such case an IllegalStateException will be thrown when calling bindInstanceFields. To avoid this exception, the “age” field should be configured manually to specify its converter before calling the bindInstanceFields method:

TextField yearOfBirthField = new TextField("Year of birth");

binder.forField(yearOfBirthField)
.withConverter(
       new StringToIntegerConverter("Must enter a number"))
.bind(Person::getYearOfBirth, Person::setYearOfBirth);

binder.bindInstanceFields(this);

The bindInstanceFields does not support validations and if you want to add validation you need to use the BeanValidationBinder instead of the bindInstanceFields.