Site icon Blog Zenika

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.

L’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.

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.

Auteur/Autrice

Quitter la version mobile