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.
Java
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.
XML
<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.
Java
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
}
Java
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.
Java
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.
Java
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.
Java
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:
Java
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:
Java
@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:
Java
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
.