1. General Binding process
The binder is the element that is between the business object and the control in the user interface. The process is :
1.
Create the binder instance of a binder that wraps the bean. As seen in a
previous post the binder can wrap extenders of a simple Base class
Binder<Person> binder = new Binder<>(Person.class);
2.
Use the binder instance to bind a UI component to an attribute of the bean. Note that the component must implement the
HasValue interface (as TextField and many other components do). First, a component should be created, for instance, a TextField
TextField titleField = new TextField();
But take into account that the binder uses callbacks that are functional interfaces. To bind the component "titleField" to the attribute title of the class customer can be done referencing the
component, a
getter, and a
setter.
// 01. Shorthand for cases without extra configuration. Using Mehod references of a KNOWN class
binder.bind(titleField, Person::getTitle, Person::setTitle);
// 02. Longer way using method references of a KNOWN class
binder.forField(titleField)
.bind(Person::getTitle, Person::setTitle);
//03. Shorthand method using lambda expressions of a KNOWN class
binder.bind(titleField,
person -> //getter
person.getTitle(),
(person, title) -> { //setter
person.setTitle(title);
logger.info("setTitle: {}", title);
otherStuff() ..
});
//04. Longer way method using lambda expressions of a KNOWN class
binder.forField(titleField)
.bind(titleField,
person ->
person.getTitle(),
(person, title) -> {
person.setTitle(title);
logger.info("setTitle: {}", title);
otherStuff() ..
});
If you want to use generics we can use this replacement of the shorthand method:
binder.bind(titleField,LambdaUtils.getter(fld), LambdaUtils.setter(fld));
Where the implementation of LambdaUtils class is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| package org.ximodante.vaadin.utils;
import java.lang.reflect.Field;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.ximodante.vaadin.entities.Base;
import com.vaadin.flow.data.binder.Setter;
import com.vaadin.flow.function.ValueProvider;
public class LambdaUtils {
/**
* Implementation of a Lambda of type ValueProvider that is similat to a bean getter
* Replacement of ClassName::MethodName required by Vaadin for:
* 1. grid.addColumn()
* 2. Binder.bind()
* 3. Etc
* @param fld
* @return
*/
public static <T extends Base> ValueProvider<T,?> getter(Field fld) {
return bean->{
try {
return FieldUtils.readField(fld, bean, true);
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
}
};
}
/**
* Implementation of a Lambda Setter that is similar to a bean setter
* @param fld
* @return
*/
public static <T extends Base> Setter<T,?> setter(Field fld) {
return (bean, value ) -> {
try {
FieldUtils.writeField(fld, bean, value, true);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
};
}
}
|
If we want the bean property non-modifiable then pass a
null value to the setter
binder.forField(titleField).bind(Person::getTitle, null);
There is a more
simplistic binding process, and consist of passing the component and
the name of the attribute of the class (or even the
attribute of an attribute). Also works in the
"forField" method
// 01. Super simple binding process based on property name
binder.bind(titleField, "title");
// 2. Forfield simple binding based on sub property path
binder.forfield(streetAddressField).bind("address.street");
But if the c
omponent name is the same as the attribute of the bean (or use the
@PropertyId("attributeName") and we need no conversion of types between the presentation and mode, we can bind
all these fields in the form with
binder.bindInstanceFields(thisForm)
public class MyForm extends VerticalLayout {
// Component name matches attributre name
private TextField firstName = new TextField("First name");
private TextField lastName = new TextField("Last name");
// Component name is "gender" but attribute name is "sex"
@PropertyId("sex")
private ComboBox<Gender> gender = new ComboBox<>("Gender");
public MyForm() {
Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(this);
}
}
3. Use
binder.readBean(bean) for updating the data in every bound UI component or
binder.writeBean(bean) for updating the bean from the information in the UI components.
These functions may be tied to the event of a button and may require some try-catch block for validation treatment of exceptions.
But there method
binder.setBean(bean) can be used instead of the readBean() and writeBean() methods, enabling binding values directly to the bean instance.But take care that:"
When using the setBean
method, the business object instance will be updated whenever the user changes the value in any bound field. If some other part of the application is also using the same instance, then that part might show changes before the user has clicked the save button"
4. Use a backend service to update the bean to the database( if no errors)
2. Field conversion implementation
In these cases, we should use "
forField" method, which enables us to include a validator.
2.1 Using existing Converters
// Using the predefined integer converter
binder.forField(yearOfBirthField)
.withConverter(
new StringToIntegerConverter("Please enter a number"))
.bind("yearOfBirth");
2.2 Simple enum converter
In this case, the transformation is from boolean to enum
// Checkbox for marital status
Checkbox marriedField = new Checkbox("Married");
binder.forField(marriedField)
//Transform a boolean into an enum
.withConverter(isMarried -> isMarried ? MaritalStatus.MARRIED : MaritalStatus.SINGLE,
marritalStatus -> MaritalStatus.MARRIED.equals(marritalStatus))
.bind(Person::getMaritalStatus, Person::setMaritalStatus);
2.3 Bidirectional conversion
Logically, a conversión carries out a double process: converting from presentation (components) to model (bean attributes) and vice versa. This can be made using callbacks:
// Bidirectional conversion
binder.forField(yearOfBirthField)
.withConverter(
Integer::valueOf, // Convert from string to integer (converting to model)
String::valueOf, // Convert from integer to string (Converting to presentation)
// Text to use instead of the NumberFormatException message
"Please enter a number")
.bind(Person::getYearOfBirth, Person::setYearOfBirth);
We can create classes that implement the
Converter interface, overriding the methods
convertToModel() and
convertToPresentation(). It also has the helper methods
ok() and
error() to simplify conversion
// Bidirectional conversion presentation (string) <---> model (integer)
class MyConverter implements Converter<String, Integer> {
@Override
public Result<Integer> convertToModel(String fieldValue, ValueContext context) {
// Produces a converted value or an error
try {
// ok is a static helper method that creates a Result
return Result.ok(Integer.valueOf(fieldValue));
} catch (NumberFormatException e) {
// error is a static helper method that creates a Result
return Result.error("Please enter a number");
}
}
@Override
public String convertToPresentation(Integer integer, ValueContext context) {
// Converting to the field type should always succeed,
// so there is no support for returning an error Result.
return String.valueOf(integer);
}
}
// Using the converter
binder.forField(yearOfBirthField)
.withConverter(new MyConverter())
.bind(Person::getYearOfBirth, Person::setYearOfBirth);
One of the possible uses of ValueContext is for Locale.
3. Validation implementation
Validation can be at Bean level or at Bean. Attribute level. We will cover other aspects as Validation status label and JSR 303 Bean Validation.
3.1 Whole Bean level validation
Before updating a Bean to a database, the validation should be used.
A simple usage is by means of the
validate() method of a
Binder and get the validation errors for utter solving.
// This will make all current validation errors visible
BinderValidationStatus<Person> status = binder.validate();
if (status.hasErrors()) {
notifyValidationErrors(status.getValidationErrors());
}
You may also use do the same dealing with exceptions
// This will make delegate validation to exception management
try {
binder.writeBean(person);
MyBackend.updatePersonInDatabase(person);
} catch (ValidationException e) {
notifyValidationErrors(e.getValidationErrors());
}
And you can also check the return value using
writeBeanIfValid(bean) instead of
writeBean(bean) method.
// Using a result operation
boolean saved = binder.writeBeanIfValid(person);
if (saved) {
MyBackend.updatePersonInDatabase(person);
} else {
notifyValidationErrors(binder.validate().getValidationErrors());
}
But we can add a Lambda validator (or more)
// Using a general bean validator for many fields
binder
.withValidator(
p -> p.getYearOfMarriage() > p.getYearOfBirth(),
"Marriage year must be bigger than birth year.")
.withValidator(
p -> p.getSalary() > 0,
"Salary must be bigger than 0.");
3.2 Attribute-level validation
3.2.1 Implementing new validators using withValidator method
// Defining a new validator using Lambdas
binder.forField(yearOfBirthField)
.withValidator(
yearOfBirth -> yearOfBirth <= 2000,
"Year of Birth must be less than or equal to 2000")
.bind(Person::getYearOfBirth, Person::setYearOfBirth);
We can have several
withValidator() methods and several
withConverters() methods within one
binder.forField(fieldName)
3.2.2 Required Field
binder.forField(titleField)
// Shorthand for requiring the field to be non-empty
.asRequired("Every employee must have a title")
.bind(Person::getTitle, Person::setTitle);
3.2.3 Validation depending on other fields
In this case, we should:
- Create a new Binder<BeanClass, FieldTypeClass> for the field to validate
- Include a withValidator() method that validates taking into account the value of depending fields.
- Add a valueChangeListener (that triggers the binder validator) to all the depending fields
Binder<Trip> binder = new Binder<>(Trip.class);
DatePicker departing = new DatePicker();
departing.setLabel("Departing");
DatePicker returning = new DatePicker();
returning.setLabel("Returning");
// Store return date binding so we can revalidate it later
Binder.Binding<Trip, LocalDate> returningBinding = binder
.forField(returning)
.withValidator( returnDate -> !returnDate.isBefore(departing.getValue()),
"Cannot return before departing")
.bind(Trip::getReturnDate, Trip::setReturnDate);
// Revalidate return date when departure date changes
departing.addValueChangeListener(event -> returningBinding.validate());
3.3 Validation Status Label
A status label is a label that is used to display messages. This label can be used and
shared by all components, using
setStatusLabel() method
// Shared status label
Label formStatusLabel = new Label();
Binder<Person> binder = new Binder<>(Person.class);
// Make a status label shared by all fields
binder.setStatusLabel(formStatusLabel);
Or maybe exclusive to a single component using a withValidationStatusHandler() method
// Status label reserved for only one component (nameField)
Label nameStatus = new Label();
binder.forField(nameField)
// Define the validator
.withValidator(
name -> name.length() >= 3,
"Full name must contain at least three characters")
// Define how the validation status is displayed
.withValidationStatusHandler(status -> {
nameStatus.setText(status.getMessage().orElse(""));
nameStatus.setVisible(status.isError());
})
// Finalize the binding
.bind(Person::getName, Person::setName);
3.4 JSR 303 Bean Validations
You should use
BeanValidationBinder instead of
Binder to use annotations like Max, Min, Size, etc. Note that BeanValidationBinder extends Binder, so it uses the same API.
We should include in the
pom.xml the validator dependency (
hibernate-validator)
// A class with Bean Validator annotations
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");
3.5 Keeping track of updates and invalid states
The method
addStatusChangeListener can detect if
errors and if there has been
changes. This let us enable disabling save and reset buttons.
// Detecting if valid binding and if changes have been produced
binder.addStatusChangeListener(event -> {
boolean isValid = event.getBinder().isValid(); // Success
boolean hasChanges = event.getBinder().hasChanges(); // Changes in components
saveButton.setEnabled(hasChanges && isValid);
resetButton.setEnabled(hasChanges);
});
3.6 Authomatic saving by using binder.setBean() instead of binder.readBean() + binder.writeBean()
The method binder.validate.isOk() indicates the correct bind.
binder.addStatusChangeListener(event -> {
boolean isValid = event.getBinder().isValid(); // Binder<Person> binder = new Binder<>();
// Field binding configuration omitted, it should be done here
Person person = new Person("John Doe", 1957);
// Loads the values from the person instance
// Sets person to be updated when any bound field is updated (automatic binding...)
binder.setBean(person);
Button saveButton = new Button("Save", event -> {
if (binder.validate().isOk()) {
// person is always up-to-date as long as there are no
// validation errors
MyBackend.updatePersonInDatabase(person);
}
});