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 component 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
One of the possible uses of ValueContext is for Locale.
// 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.
A simple usage is by means of the validate() method of a Binder and get the validation errors for utter solving.
You may also use do the same dealing with exceptions
And you can also check the return value using writeBeanIfValid(bean) instead of writeBean(bean) method.
But we can add a Lambda validator (or more)
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); } });
No hay comentarios:
Publicar un comentario