jueves, 3 de enero de 2019

04. Binding data to forms & Validation

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


// 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:

  1. Create a new Binder<BeanClass, FieldTypeClass> for the field to validate
  2. Include a withValidator() method that validates taking into account the value of depending fields.
  3. 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