Editor Framework avec GWT

Depuis la version 2.0 de GWT, de nombreuses fonctionnalités sont apparues, Activities & Places, RequestFactory, UiBinder, CodeSplitting
Après avoir vu dans un précédent article différentes solutions permettant de faire transiter des beans hibernate entre le client et le serveur, notamment avec l’utilisation de RequestFactory, nous allons aujourd’hui nous intéresser à un autre aspect du développement GWT, le data binding entre nos objets métiers ou DTO(Data Transfer Object) et notre interface graphique.
Pour cela GWT fournit le framework Editor, qui a pour but d’éviter d’avoir à écrire plein de « boilerplate code » en simplifiant ce binding. Je vais donc vous présenter comment, en partant d’une application utilisant un binding classique (à base de getter et setter) mettre en place les Editor et ainsi faciliter le développement et limiter le risque d’erreur.

Toutes les sources de ce billet sont disponibles sur Github. Les différentes étapes ont été taguées et sont indiquées à chaque fois.

L’application initiale

(tag git : init-state)

Pour mettre en avant l’utilisation des Editor, j’ai décidé de partir d’une application simple, un formulaire affichant des informations signalétiques d’une personne. Cette personne ayant été récupérée depuis le serveur via un simple appel RPC. Voici une capture d’écran du formulaire que l’on souhaite remplir. Formulaire Editor

L’organisation du code

Organisation du code
Pour ceux qui ne sont pas familiers avec GWT, les trois classes et interfaces PersonService, PersonServiceImpl et PersonServiceAsync correspondent au service RPC (Remote Procedure Call) permettant la communication client/serveur.
Les objets ListItem et TextItem sont une simple composition d’un label et d’un input (ListBox pour le premier et TextBox pour le second). Ils nous seront utiles plus tard dans la démonstration.
Les objets Person et Address, dont voici le code en dessous, sont les DTO que nous allons devoir mapper dans notre formulaire. Celui-ci va être mis en place dans les vues PersonView et AddressView

public class Person implements Serializable {
    private String firstName;
    private String lastName;
    private String phoneNumber;
    private Address address;
    private String company;
    ...
}
public class Address implements Serializable {
    private String street;
    private String zipCode;
    private String city;
    private String country;
    ...
}

Mise en place du binding

Première chose à faire, récupérer notre objet métier côté serveur, dans ce cas un objet Person, via le service RPC mentionné précedemment.

personService.getPerson(new AsyncCallback<Person>() {
                    @Override
                    public void onSuccess(final Person result) {
                        setData(result);
                    }
                    @Override
                    public void onFailure(final Throwable caught) {
                        Window.alert("Call failed");
                    }
                });

Après avoir récupéré notre objet, nous appelons la méthode setData() sur la vue PersonView et en cascade sur la vue AddressView. Dans le code suivant, les objets sur lesquels nous appelons la méthode setValue sont des éléments graphiques de l’application GWT.

public void setData(final Person result) {
    firstName.setValue(result.getFirstName());
    lastName.setValue(result.getLastName());
    phoneNumber.setValue(result.getPhoneNumber());
    company.setValue(result.getCompany());
    address.setData(result.getAddress());
 }
public void setData(final Address address) {
    street.setValue(address.getStreet());
    zipCode.setValue(address.getZipCode());
    city.setValue(address.getCity());
    country.setValue(address.getCountry());
 }

Notre formulaire est ainsi rempli. Après avoir modifié les valeurs de ces champs, il faut maintenant faire le mapping inverse, créer un objet Person rempli avec les valeurs de chaque champ du formulaire et envoyer cet objet au service. De la même façon que précedemment, on appelle la méthode getData() sur la vue PersonView.

personService.setPerson(personView.getData(), new AsyncCallback<Void>() {
...
}
public Person getData() {
    Person person = new Person();
    person.setAddress(address.getData());
    person.setFirstName(firstName.getValue());
    person.setLastName(lastName.getValue());
    person.setPhoneNumber(phoneNumber.getValue());
    person.setCompany(company.getValue());
    return person;
 }
public Address getData() {
    Address address = new Address();
    address.setCity(city.getValue());
    address.setCountry(country.getValue());
    address.setStreet(street.getValue());
    address.setZipCode(zipCode.getValue());
    return address;
 }

Nous nous rendons bien compte avec cet exemple très simple que le coût de développement, le risque d’erreur ainsi que le coût de maintenance sont tous trois élevés. C’est à cet instant qu’intervient le framework Editor.

Mise en place du framework Editor

(tag git : state1)

Définition des termes

Avant de montrer comment mettre en place ce framework sur une application GWT, une explication des différents termes utilisés s’impose.

  • Bean : l’objet métier ou le DTO qui va servir au binding
  • Editor : l’objet sur lequel vont être mappés les attributs du Bean. Les editors sont en général des widgets GWT ou une composition de widgets.
  • Driver : il s’agit du contrôleur qui va gérer le mapping entre le bean et l’editor. Il va le faire les liens entre les attributs du bean et les valeurs des widgets de l’editor

Mise en place

Contrairement à ce qui est indiqué dans la documentation officielle, il n’est pas nécessaire d’importer le module Editor dans la configuration du projet, car celui-ci est désormais inclus dans le module com.google.gwt.user.User

Tout d’abord, il faut déclarer une interface qui va étendre SimpleBeanEditorDriver (pour les habitués de GWT, le principe ici est le même que pour les interfaces de type ClientBundle)

interface EditorDriver extends SimpleBeanEditorDriver<Person, PersonView> {
}

Puis créer une implémentation via la méthode GWT.create()

EditorDriver editorDriver = GWT.create(EditorDriver.class);

Nous venons donc de créer le Driver. Nous avions déjà le bean (récupéré depuis le serveur). Il nous manque donc l’Editor, l’objet graphique qui va accueillir les attributs du bean.
Il faut s’imaginer la hiérarchie des widgets Editor comme une arborescence . L’Editor PersonView est un noeud qui contient différentes feuilles, les ListItem et TextItem, ainsi que d’autres noeuds, ici AddressView, qui lui-même contient de la même façon d’autres feuilles.
Nous allons donc transformer nos vues PersonView et AddressView en Editor. Il va également falloir transformer nos widgets TextItem et ListItem, ce que nous verrons juste après.

public class PersonView extends Composite implements Editor<Person> {
...
}
public class AddressView extends Composite implements Editor<Address> {
...
}

PersonView implémente Editor typé avec l’objet Person. Cela siginifie que PersonView permet le mapping des attributs de l’objet Person. AddressView, de la même façon, va permettre de mapper les attributs de l’objet Address.

Attention, il y a certaines règles de nommage à respecter afin que le driver sache quels attributs mapper sur quels widgets. Ces règles sont détaillées dans la documentation de GWT. Si l’on ne souhaite pas suivre ces règles, il est possible de les surcharger avec l’annotation @Path qui prend en paramètre le « chemin de l’attribut » (par exemple @Path(« address.street ») va permettre de mapper l’attribut getAddress().getStreet() de l’objet en cours d’édition)

Il faut également déclarer nos widgets “feuille” comme étant editables via le framework Editor. Pour cela, ils doivent implémenter LeafValueEditor, paramétré par le type de valeur qu’ils éditent. LeafValueEditor est une sous interface de Editor, qui permet de déclarer un widget comme “final”, c’est à dire qu’il va contenir une valeur, valeur correspondant à un attribut du bean. De plus, un LeafValueEditor ne contient pas de sous-Editor, comme peut l’être au contraire AddressView.

public class ListItem extends Composite implements LeafValueEditor<String> {
...
}

Maintenant que nos widgets et nos vues sont prêts, il ne nous reste plus qu’à remplacer le code de binding précédent par l’Editor. Nous supprimons donc les méthodes getData et setData des vues PersonView et AddressView. Nous remplaçons l’appel à ces méthodes par l’appel au driver. La méthode setData contient donc le code suivant.

editorDriver.initialize(personView);
editorDriver.edit(result);

La méthode initialize permet de « lier » le Driver avec l’Editor (l’objet graphique qui va accueillir les donneés). Tandis que la méthode edit est l’équivalent de la méthode setData précédente.
Notre formulaire est rempli. Désormais, lorsque l’on voudra récupérer les données de celui-ci, il suffit d’appeler la méthode flush sur le driver, les méthodes getData ne sont plus nécessaires.

personService.setPerson(editorDriver.flush(), new AsyncCallback<Void>() {
...
}

Allons un peu plus loin

Les champs non mappés

Un autre avantage intrinsèque au framework Editor, est le fait qu’il n’est pas nécessaire de mapper tous les champs du bean aux champs du formulaire. En effet, la référence du bean passé à la méthode edit() du driver est conservée et est la même référence retournée par la méthode flush. Ainsi, il n’est pas utile de se préoccuper de tous les champs de notre bean en ayant peur de perdre des données comme cela peut être le cas lorsque l’on réalise un binding manuel. (voir tag git : state2)

la méthode isDirty()

Si vous utilisez un peu le framework Editor, vous vous rendrez vite compte que le driver possède de nombreuses petites méthodes utilitaires pour vous faciliter encore plus le développement. La méthode isDirty, qui renvoie un booléen, permet de savoir si les attributs de votre bean ont été modifiées depuis l’appel à la méthode edit. C’est à dire, savoir si votre formulaire a été modifié ou non. Cela peut permettre d’éviter d’appeler un service distant dans le cas où cela n’est pas nécessaire. (voir tag git : state3)

protected void saveData() {
        if (editorDriver.isDirty()) {
            // appel du service RPC
        } else {
            Window.alert("Data has not changed");
        }
    }

La gestion des erreurs

(tag git : state4)

La dernière fonctionnalité que nous allons voir est la gestion des erreurs au sein même du framework.
Chaque Editor peut enregistrer ses propres erreurs au niveau du Driver, quand il le souhaite. Dans notre cas, nous allons enregistrer des erreurs lorsqu’un champ du formulaire est vide. Ensuite, nous récupérerons ces erreurs au niveau de la classe principale pour les afficher à l’utilisateur.
Afin d’enregistrer des erreurs, il est nécessaire d’avoir accès à l’objet EditorDelegate, qui possède la méthode recordError. Nous allons donc modifier la classe TextItem afin de récupérer l‘EditorDelegate. Pour cela, la classe doit implémenter l’interface HasEditorDelegate et définir la méthode setDelegate.

public class TextItem extends Composite implements LeafValueEditor<String>, HasEditorDelegate<String> {
    ...
    private EditorDelegate<String> editorDelegate;
    ...
    @Override
    public void setDelegate(EditorDelegate<String> delegate) {
        this.editorDelegate = delegate;
    }
}

Nous pouvons désormais enregistrer des erreurs. Ce que nous allons faire dans la méthode getValue(). Ainsi si la valeur du widget est vide, nous enregistrons une erreur

@Override
public String getValue() {
    checkNotNull();
    return textBox.getValue();
}
private void checkNotNull() {
    if (textBox.getValue().isEmpty()) {
        editorDelegate.recordError("TextItem " + label.getText() + " is empty", null, null);
    }
}

Le fait d’enregistrer des erreurs n’a aucune incidence sur le fonctionnement si on ne les prend pas en compte manuellement
Nous allons afficher un message d’alerte à l’utilisateur pour chaque erreur.

protected void saveData() {
    ...
    Person person = editorDriver.flush();
    if (editorDriver.hasErrors()) {
        StringBuilder errorBuilder = new StringBuilder();
        for (EditorError error : editorDriver.getErrors()) {
            errorBuilder.append(error.getMessage() + "\n");
        }
        Window.alert(errorBuilder.toString());
    }
    ...
}

Les Editor fournissent également d’autres fonctionnalités, tels que les Decorator ou l’intégration avec RequestFactory via un driver spécifique, RequestFactoryEditorDriver, que je ne détaillerai pas dans cet article.

Conclusion

Le framework Editor fourni par GWT est donc une façon élégante de mettre en place le binding objet métier/widget graphique, en réduisant fortement la quantité de code nécessaire. Certaines fonctionnalités supplémentaires telles que la gestion des erreurs sont un vrai plus quant à son adoption. On peut cependant regretter un manque de simplicité dans la documentation officielle de GWT.

9 pensées sur “Editor Framework avec GWT

  • 9 juillet 2012 à 17 h 50 min
    Permalink

    Bonjour,

    J’essaie de mettre en place ce framework Editor mais en vain.
    Je ne pense pas avoir tout bien implémenté.
    Avez-vous l’exemple complet ?
    Est-il possible d’utiliser de simple textbox ?

    C’est dommage car ce framework a l’air très intéressant 🙂

    Je vous remercie.
    Fabien

    Répondre
  • 10 juillet 2012 à 10 h 42 min
    Permalink

    Bonjour Fabien,

    Le code source complet de cet article est disponible sur github ici : https://github.com/julienvey/BlogGW
    Oui, il est tout a fait possible d’utiliser les Editor directement avec l’objet TextBox car celui-ci implémente (indirectement) l’interface IsEditor et peut donc être inclus facilement dans une hiérarchie d’Editor

    Répondre
  • 11 juillet 2012 à 9 h 45 min
    Permalink

    Effectivement je n’avais pas vu le lien Git. Bouhhhhh
    Je vais essayé de repartir d’un exemple simple alors en utilisant des TextBox.
    Dans ce cas je trouverai mon bonheur avec ce framework 🙂

    Merci de votre réponse.

    Répondre
  • 11 juillet 2012 à 15 h 36 min
    Permalink

    Bon ben je n’y arrive toujours pas à mettre en place ce framework 🙁

    Voici les classes que j’utilise :

    public class Pers implements Serializable {
    public String nom;
    public String prenom;

    public Pers() {
    }

    public Pers(String n, String p) {
    nom = n;
    prenom=p;
    }

    public String getNom() {
    return nom;
    }
    public void setNom(String nom) {
    this.nom = nom;
    }
    public String getPrenom() {
    return prenom;
    }
    public void setPrenom(String prenom) {
    this.prenom = prenom;
    }

    }

    Puis mon editor :
    public class PersTestEditor extends Composite implements Editor<Pers>{
    private Grid grid;

    private TextBox nom = new TextBox();
    private TextBox prenom = new TextBox();

    public PersTestEditor() {
    initWidget(getGrid());
    }

    public PersTestEditor(Pers l) {
    initWidget(getGrid());

    }

    public Grid getGrid() {
    if (grid == null) {
    grid = new Grid(6, 1);
    grid.setSize(« 100% », « 100% »);
    grid.setWidget(0, 0, nom);
    grid.setWidget(1, 0, prenom);
    }
    return grid;
    }
    }

    Et enfin quand je clique sur un bouton depuis un panel j’exécute ceci :
    Pers p = new Pers(« Nom », »Prenom »);
    PersTestEditor pers = new PersTestEditor();
    dockPanel.add(pers, DockPanel.SOUTH);
    editorDriver.initialize(pers);
    editorDriver.edit(p);

    L’interface et son instance définis dans ce Panel
    interface EditorDriver extends SimpleBeanEditorDriver<Pers, PersTestEditor> {}
    EditorDriver editorDriver = GWT.create(EditorDriver.class);

    J’ai surement du oublier un truc quelque part car le panel avec mes 2 champs s’affiche bien mais pas les données (Nom, Prenom)
    Je n’utilise pas l’UiBinder. Peut-être est-ce à cause de cela ?

    Si vous avez une idée je suis preneur 🙂

    Merci encore et désolé du dérangement
    Fabien

    Répondre
  • 11 juillet 2012 à 18 h 40 min
    Permalink

    Bon ben finalement j’ai réussi 🙂
    Il fallait rajouter le path sur mes getter de champs texte
    @Path(« usage »)

    Maintenant j’essaie de me dépatouiller avec les ListBox mais je galère un peu.
    J’ai vu qu’il existait le ValueListBox mais je chercher encore comment l’utiliser.

    Si vous avez des pistes je suis preneur 🙂

    Merci
    Fabien

    Répondre
  • 12 juillet 2012 à 15 h 32 min
    Permalink

    Bonjour Fabien,

    Le problème dans votre cas vient du fait que les champs TextBox nom et prenom sont private. En effet, pour que le framework Editor puisse fonctionner, la visibilité de ces champs doit être package-protected, protected ou public

    Le comportement de l’Editor avec les ValueListBox va être identique, les méthodes setValue() vont être appelées lors de l’edit et les méthodes getValue() lors du flush.

    En ce qui concerne l’utilisation des ValueListBox, il suffit de leur passer un objet Renderer en paramètre, qui va convertir l’objet en question en chaîne de caractères. Il existe plusieurs implémentations par défaut de Renderer pour les objets standards (DateTimeFormatRenderer, DoubleRenderer, IntegerRenderer ….). Sinon il est possible de définir vos propres renderer.

    Voici un exemple d’utilisation simple http://c.gwt-examples.com/home/ui/v

    Répondre
  • 12 juillet 2012 à 16 h 25 min
    Permalink

    Bonjour Julien et merci de ton aide.

    Je progresse un peu chaque jour mais dur dur quand même.
    Ce matin j’ai réussi à implémenter une ValueListBox avec mon propre renderer. Youpiii 🙂

    Mais j’ai un autre problème maintenant.
    En effet j’ai implémenté la gestion des erreurs (comme dans ton exemple) mais le driver me dit tout le temps qu’il y a des erreurs.
    Quand je passe en debug, je vois que la méthode getValue() est appelé même avant que j’appelle le fluch.
    Du coup il me dit forcément qu’il y a des erreurs 🙁
    Comment cela se fait ?
    Voici comment j’utilise le driver pour afficher mon panel vide pour la création
    //New local est mon panel qui implemente Editor<Locaux>
    editorDriveLocaux.initialize(newLocal);
    editorDriveLocaux.edit(new Locaux());
    _

    Autre question. Je préfère passer par des getters pour récupérer et initialiser mes objets (gwt code style Lazy)
    Dois-je mettre le @Path(« … ») sur le getter ou l’attribut, ou les deux ?

    Merci encore pour ton aide
    je vais faire péter le champagne quand tout va fonctionner 😉

    Répondre
  • 24 juillet 2012 à 17 h 27 min
    Permalink

    Bonjour Julien.

    Très bon article. Je vais m’empresser d’utiliser ce framework et je ferai un retour dessus.

    Merci.

    Répondre
  • 15 décembre 2016 à 19 h 06 min
    Permalink

    Bonjour,
    Pas trop tard pour poser une question supplémentaire ?
    J’ai un problème avec un exemple presque identique sauf que Person à 2 Address : Address adress, Adress adress2
    dans l’editor PersonView, j’ai 2 lignes pour les adresses :
    @UiField(provided=true) AddressView address = new AddressView();
    @UiField(provided=true) AddressView address2= new AddressView();
    Seul le 2ieme (address2) a ses données bindées correctement.
    Que fait de faux ? est possible de faire un composant générique et l’utilisé plusieurs fois ?
    Merci d’avance
    Pierre

    Répondre

Répondre à Pierre Annuler la réponse.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :