Accueil > Code > Formulaires et validation en GWT

Formulaires et validation en GWT

samedi 31 août 2013, par Nicolas

Introduction

GWT étant orienté client [1], il est plus que logique de vouloir l’utiliser pour des formulaires de saisie, notamment la validation des données entrées par l’utilisateur.

Heureusement, il existe différents ensembles de classes qui rendent la génération du code requis très simple, et permet de se concentrer sur la logique propre à l’application plutôt que la plomberie basique.

Le premier jeu de classes est celui centré sur l’édition, décrit sur la page UI Editors du projet GWT. Les classes concernent la gestion de formulaire, à savoir les basiques « tel champ de mon objet est affiché par tel champ du formulaire ».

Le second jeu de classes est centré sur la validation des données dudit formulaire, et est décrit sur la page Validation de GWT.

Remarques préliminaires

Ce billet n’a pas vocation à expliquer en détail comment tout fonctionne, mais à donner quelques pistes et astuces, ou expliciter certains points qui ne me semblent pas très clair dans les différentes documentations.

Je suis redevable aux excellents billets « La validation des données » de Jean-Michel Doudoux et « La validation des données avec GWT 2.5 (Côté client) » de Sébastien Tauvel.

La version de GWT utilisée est la 2.5.1, mais d’autres fonctionnent probablement.

Le code ici présenté n’est pas garanti compiler et n’est pas complet non plus, à vous de combler les trous. UiBinder n’est pas non plus utilisé, pour simplifier.

Scénario

Pour les besoins de ce billet, le but du code sera d’éditer (et visualiser bien sûr) la fiche d’un ordinateur, qui regroupe les informations suivantes :
- nom de l’ordinateur (texte, obligatoire)
- emplacement physique (texte, facultatif)
- accès à Internet (oui/non)
- nom du fournisseur Internet (texte, facultatif)

Ceci donne une classe de données :

class Computer {
  private String name;
  private String location;
  private boolean internet;
  private String provider;
 
  /* tous les setters et getters selon les conventions habituelles :
  getName();
  getLocation();
  hasInternet();
  getProvider();
  setName();
  setLocation();
  setInternet();
  setProvider();
  */

};

Première étape : formulaire

Le formulaire doit implémenter Editor afin de pouvoir être lié à la classe de données, et comporter différents champs nommés comme ceux de l’objet Computer (ou avec le suffixe Editor) :

class ComputerEditor extends SomeWidget implements Editor<Computer> {
 
  interface FieldBinder extends SimpleBeanEditorDriver<Computer, ComputerEditor> {};
 
  final protected ValueBoxEditorDecorator<String> name;
  final protected ValueBoxEditorDecorator<String> location;
  final protected CheckBox internet;
  final protected ValueBoxEditorDecorator<String> provider;
  final protected FieldBinder fieldBinder;
 
  ComputerEditor() {
 
  /* initialiser tout ce qu'il faut */
 
  final TextBox n = new TextBox();
  name = new ValueBoxEditorDecorator<String>(n, n.asEditor());
  someContainer.insert(name);
 
  /* idem pour les autres champs */
 
  fieldBinder = GWT.create(FieldBinder.class);
  fieldBinder.initialize(this);
}  

ValueBoxEditorDecorator est choisi plutôt que TextBox car il affichera automatiquement les messages d’erreur éventuels.

Les valeurs du formulaire peuvent alors être simplement définies via fieldBinder.edit(someComputer);, et récupérées via final Computer data = fieldBinder.flush();

Validation de base

Comme décrit dans les billets de blogs (les avez-vous lus ?), la première étape est de mettre une annotation sur les champs de l’objet de données :

class Computer {
 
  @NotNull
  @Max(50)
  private String name;
 
  @Max(200)
  private String location;
 
  private boolean internet;
 
  @Max(200)
  private String provider;

Puis il faut instancier la bonne classe et valider l’objet récupéré du formulaire :

  final Computer data = fieldBinder.flush();
  Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
  Set<ConstraintViolation<Computer>> violations = validator.validate(computer);
  if (!violations.isEmpty()) {
    /* traiter les erreurs */
    return false;
  }

Valider les contraintes c’est bien, mais afficher à l’utilisateur qu’il y a des erreurs, c’est mieux. Rien de plus simple, avec un petit coup de conversion de données [2] :

  Set<ConstraintViolation<Computer>> violations = validator.validate(computer);
  final Set<?> convert = violations;
  fieldBinder.setConstraintViolations((Set<ConstraintViolation<?>>) convert);

Cela suffira pour que les erreurs soient affichées à gauche des champs concernés.

Validation avancée

Le code précédent suffit pour valider les champs uns à uns, cependant il serait pratique de rendre obligatoire le nom du fournisseur Internet si l’ordinateur a une connexion.

Il suffit pour cela de définir une contrainte personnalisée, tel que décrit dans les billets de blogs cités précédemment.

@Target( { ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ComputerValidator.class)
@Documented
public @interface ComputerConstraints {
 
  String message() default "{validation.invalid.parameters}";
 
  Class<?>[] groups() default {};
 
  Class<? extends Payload>[] payload() default {};
 
  String providerMandatoryMessage() default "{validation.provider.mandatory}";
}

Les trois premiers champs sont obligatoires pour les contraintes, le quatrième est un message supplémentaire qui sera affiché à côté du nom du fournisseur s’il est obligatoire.

Ensuite il suffit de définir la classe appliquant la validation :

public class ComputerValidator
  implements ConstraintValidator<ComputerConstraints, Computer> {
 
  private String providerMessage;
 
  @Override
  public void initialize(ComputerConstraints constraintAnnotation) {
    providerMessage = constraintAnnotation.providerMandatoryMessage();
  }
 
  @Override
  public boolean isValid(Computer value, ConstraintValidatorContext context) {
    if (value.hasInternet() == false) {
      return true;
    }
 
    if (value.getProvider() == null || value.getProvider().isEmpty()) {
      context
        .buildConstraintViolationWithTemplate(providerMessage)
        .addNode("provider")
        .addConstraintViolation();
      return false;
    }
 
    return true;
  }
}

Grâce au context, il est possible de remonter une erreur associée à la propriété provider, c’est-à-dire le nom du fournisseur Internet. Via le standard de nommage respecté, l’erreur remontera jusqu’au champ dans le formulaire. [3]

Enfin, si le formulaire n’est pas valide (isValid() retourne false), le message ayant pour clé « validation.invalid.parameters » est également remonté, et, n’étant associé à aucune propriété, peut être récupéré comme suit :

  final StringBuffer error = new StringBuffer();
  String sep = "";
  for (final ConstraintViolation<NewsSource> violation : violations) {
    if ("".equals(violation.getPropertyPath().toString())) {
      error.append(violation.getMessage()).append(sep);
      sep = " ; ";
    }
  }
  /* error contient le(s) message(s) */

Remarques finales

Si vous utilisez IntegerBox et ValueBoxEditorDecorator pour un champ, il semble que des valeurs null se propagent au moment de la validation si le champ de saisie est vide (pas 0 mais bel et bien vide). C’est lié à la sémantique de getValue() qui retourne null. Afin d’éviter tout souci, il conviendra que la propriété associée soit de type Integer et non int, au moins pour le setter, sinon des exceptions risquent de se produire.

Pour les listes de choix, il existe ValueListBox qui permet de facilement sélectionner des éléments, mais un seul. Pour les choix multiples, il faut apparemment réimplémenter un Editor soi-même.

Le ValueBoxEditorDecorator affiche le message d’erreur à gauche, ce qui n’est pas forcément le comportement voulu. Une solution est de surcharger le fichier .xml utilisé, en remplaçant (via replace-with dans le fichier de configuration du module) le Binder de cette classe.

Pour conclure

Ces mécanismes offrent à mon avis de nombreux avantages :
- les annotations (au moins celles de base) sont explicites, et indiquent tout de suite au lecteur du code les valeurs autorisées pour les champs
- en respectant les conventions de nommage, le code est énormément réduit, et plus facile à maintenir
- si le serveur est également en Java, la validation pourra y être utilisée


[1Même s’il contient des classes côté serveur, celles-ci ne servent qu’à faire le lien avec le côté client de façon simplifiée.

[2Requis pour une sombre histoire de génériques apparemment.

[3Apparemment un bug empêche ce comportement de fonctionner sur la version 2.5.0 de GWT.