jeudi 30 mai 2013

Architecture MVP (Model View Presenter) GWT avec Activity & Place

En développant avec le framework GWT on remarque qu'il n'est pas toujours évident de bien séparer les différentes parties de son application. On aimerait également que chaque page possède sa propre URL (afin que l'utilisateur puisse utiliser les boutons suivant/précédent du navigateur). L'architecture MVP permet de diviser son application en plusieurs pages et de bien séparer le code métier de l'interface graphique. Grâce au Framework Activity and Place on pourra définir une url différente pour chaque page créée et ainsi permettre au navigateur d'utiliser l'historique de navigation.

Adopter cette stratégie permet de réaliser un projet bien structuré et facile à maintenir. De plus les utilisateurs de votre application apprécieront le fait de pouvoir accéder aux différentes pages en utilisant la barre d'adresse du navigateur.

Google décrit sur cette page la mise en place de cette architecture, je présenterai ici la création d'une petite application de gestion de produits contenant simplement deux pages permettant d'ajouter un produit et de lister les produits.

 Etape 1 : Création des vues

Qu'est-ce qu'une vue ? Cela correspond tout simplement à l'interface graphique d'une page, elle ne doit pas contenir de code métier mais doit se contenter de créer et d'afficher les différents composants qui seront contenus dans la page.

Nous allons donc créer deux vues : une vue pour la page de création de produit et une vue pour la page de visualisation de produits.

Création de la première vue permettant de créer un produit

Nous allons dans un premier temps créer une interface nommée CreateProductView. Cette interface doit étendre l'interface IsWidget afin de s'assurer que les implémentations de la classe CreateProductView fourniront bien un widget. Nous définissons une méthode setPresenter qui nous servira pour communiquer avec la classe qui s'occupera de gérer le code métier. Nous verrons cela par la suite.


A quoi sert le Presenter ? Cette interface sera le point de communication entre la vue et l'activité (Une activité correspond à la classe qui sera charger de traiter le code métier correspondant à la vue). Ainsi dans l'interface Presenter, vous devez spécifier toutes les fonctions qui devront être implémentées par l'activité. Ici on aura besoin d'ajouter un produit, ce n'est pas à la vue de s'en charger (son rôle est uniquement d'afficher les composants graphiques de la page), c'est à l'activité que nous verrons par la suite de s'occuper d'ajouter un produit à l'application.

Maintenant nous allons créer la classe qui implémente cette interface afin de construire le widget.


Pas de surprise ici, nous nous contentons d'afficher un formulaire permettant de saisir les informations relatives à un produit (nom, description) ainsi qu'un bouton de validation qui appellera la fonction addProduct du Presenter. A noter que nous étendons la classe Composite permettant définir la classe comme étant un Widget.

Pourquoi avons-nous créé une interface pour définir la vue ? C'est vrai que nous aurions pu directement créer une classe (sans interface) pour définir notre vue. Cependant, le fait de créer une interface nous permet de définir plusieurs implémentations pour une vue. Ainsi nous pourrons par exemple afficher une vue différente selon que l'utilisateur soit sur un smartphone, une tablette où sur un ordinateur.

Création de la vue de visualisation de produits

Avant de créer la vue de visualisation des produits, nous allons créer deux classes permettant de faire fonctionner l'application. Premièrement nous aurons une classe Product contenant deux champs (nom et description) puis nous aurons une classe Shop permettant de sauvegarder nos produits. En temps normal la sauvegarde doit se faire en base de données, or ici pour simplifier les choses nous utiliserons la classe Shop comme étant un Singleton. Voici le descriptif de ces deux classes :



Nous pouvons désormais créer la seconde vue qui affichera la liste des produits. Elle ne contiendra pas de Presenter étant donné qu'il n'y a aucune utilité à ce qu'elle communique avec son activité (le code métier).


L'interface définie une méthode setProducts(List<Product> products) permettant de fournir les produits à afficher sur la vue.



L'implémentation affichera la liste des produits dès lors que l'on appellera la méthode setProducts.

Voila pour ce qui est des vues. Bien évidemment dans une application plus grande on pourra créer les vues en utilisant uiBinder, cela reste la même choses sauf que la partie graphique sera déclarée en xml.
Ce qu'il faut retenir c'est que l'unique rôle de la vue doit être d'afficher des composants graphiques, elle ne doit pas contenir de code métier. C'est l'activité que nous allons voir par la suite qui va s'en charger.

Etape 2 : Création de la factory

La mise en place d'une factory nous permettra de fournir le bus d'évènement, le placeController et les différentes implémentations des vues.

Grâce à cette factory que nous créerons via Deferred binding (Client Factory clientFactory = GWT.create(ClientFactory.class);), nous pourrons choisir qu'elle implémentation nous souhaitons obtenir en fonction du User Agent par exemple.


Ici nous n'avons qu'une seule implémentation de notre factory, mais nous pourrions très bien en avoir plusieurs si nous avions différentes implémentations de vues pour notre application. En ajoutant la balise <when-property-is name="user.agent" value="" /> dans le fichier de description du projet (.gwt.xml) , vous pouvez ainsi sélectionner l'implémentation de la factory à utiliser. Pour cette petite application je vais faire au plus simple et utiliser qu'une seule implémentation de la factory.

Etape 3 : Création des activités

Les activités dans GWT permettent de gérer le code métier des vues de l'application. Une activité peut être perçu comme représentant une fonctionnalité de votre application. Elle peut être démarrée et stoppée, son but est d'afficher la vue correspondante à la fonctionnalité (via la méthode start) et de gérer la logique métier en amont.





Ici la méthode, addProduct permet d'ajouter un produit à l'application puis de naviguer vers la page qui liste les produits en utilisant la méthode goTo du placeController. Pour naviguer parmi les différentes pages de l'application, on utilisera cette méthode, c'est la clientFactory qui nous permettra d'obtenir le placeController.



Ici on appelle la méthode setProducts permettant d'afficher la liste des produits présents dans l'application.

Etape 4 : Création des places

Les places sont de paires avec les activités, en fait chaque activité est associée à une place.
Une place correspond à l'endroit où l'on se trouve dans l'application, elle est la représentation d'une page, ainsi on lui associe généralement un nom spécifique ainsi qu'un token. Ces derniers seront définis dans un objet que l'on appellera le Tokenizer.

Représentation de l'url : http://addresse_serveur:port_serveur/nom_page.html#nom_place:token

Le nom_place se définit grâce à l'annotation @Prefix du Tokenizer

Le token permet d'ajouter une information supplémentaire sur la page. Ici on ne l'utilisera pas car il nous serait inutile. Imaginons que nous avons une page affichant les informations d'un produit. Le nom de la page serait showProduct et le token pourrait représenter une information utile comme par exemple l'id du produit.

L'url de la place sera : createProduct:null


Le nom de la place ici sera : showProducts:null


On peut déclarer le Tokenizer comme étant un champ statique de la Place, mais ce n'est pas une obligation ! On pourrait très bien le définir à l'extérieur de la Place.

Deux méthodes doivent être redéfinies lorsque l'on déclare le Tokenizer : getPlace et getToken.

- getPlace(String token) permettra de retourner la place correspondante à l'url saisi par l'utilisateur.

- getToken(Place place) permettra d'afficher le token (correspondant à la place) dans l'url. Ceci intervient lorsque l'on navigue dans l'application par l’intermédiaire de la méthode goTo du placeController

Etape 5 : Création des mappers

Le premier Mapper permet de déclarer les différents Tokenizer que l'on a dans notre application, pour faire le lien entre URL et Place.


Ce second Mapper permet de faire le lien entre les Places et les Activités notamment avec la méthode getActivity qui retourne une Activité en fonction de la Place passée en paramètre.



Etape 6 : La mise en place du système


L'initialisation se passera dans le point d'entrée de l'application.
Dans un premier temps, on définie notre clientFactory. Rappelez vous dans le fichier de configuration du projet (le fichier .gwt.xml), on avait spécifier le remplacement de la classe ClientFactory par ClientFactoryImpl lors de son instanciation. La clientFactory nous permet d'initialiser le bus d'évènement, le placeController et les différentes vues de l'application.

Ensuite je vous propose de détailler ce petit schémas pour mieux comprendre le fonctionnement du framework.

L'avitityMapper permet de déclencher l'activité en fonction de la place qui a été demandée par l'utilisateur. Entre alors en jeu l'activityManager qui va pouvoir récupérer les places qui ont été diffusées sur l'eventBus afin de les redistribuer à l'activityMapper pour instancier l'activité correspondante.

La déclaration des différents tokenizer de l'application se fait en instanciant notre classe PlaceHistoryMapper qui va définir les liens à effectuer entre URL et Place.

C'est à ce moment là que le PlaceHistoryHandler fait son apparition ! Il va permettre lier de façon bidirectionnelle l'Url et la Place. La navigation va pouvoir alors se faire de deux manières. 
Soit en saisissant l'url dans la barre d'adresse, alors le PlaceHistoryHandler fournira la Place correspondante au PlaceController, soit en appelant la méthode goTo(Place place) du placeController. Le PlaceHistoryHandler se chargera de mettre à jour l'url dans la barre d'adresse. 

C'est ensuite le PlaceController qui prendra la main afin de diffuser la Place sur le bus d'évènement et d'avertir l'utilisateur au préalable dans le cas où il y aurait un autre traitement encore en cours. 

La dernière instruction historyHandler.handleCurrentHistory();  permet d'aller à la place correspondante à l'url saisi par l'utilisateur au démarrage de l'application, si le nom de la place ne correspond à aucune place alors l'application affichera la place par défaut (defaultPlace qui correspond ici à la place de création de produit).

Etape 7 : Test de l'application

Compilons maintenant notre projet afin de passer au test de notre application. Nous allons naviguer entre les deux pages que nous avons créées. La page html de mon projet se nomme ProductMvp.html.

Lancement de la page par défaut :


L'url saisi (127.0.0.1:8888/ProductMvp.html) ne correspond à aucune place, c'est donc la defaultPlace qui est chargée. Comme nous avons défini la place par défaut comme étant de type CreateProductPlace, c'est donc la page de création de produit qui s'affiche.

Complétons le formulaire afin d'ajouter un produit :

Lors du clic sur le bouton ajouter, nous tombons sur la page d'affichage des produits (dont l'url correspond à #showProducts:null)


Maintenant pour revenir à la page de création de produit, on peut soit appuyer sur le bouton précédent du navigateur, soit saisir le nom de l'url (#createProduct:null) dans la barre d'adresse.

 

 







2 commentaires:

  1. Ce commentaire a été supprimé par l'auteur.

    RépondreSupprimer
  2. Bonsoir,

    Je vous remercie pour ce magnifique article. Toutefois il y a un petit truc plutôt important qui manque. Il s'agit de l'héritage des modules Activity et Place dans le fichier .gwt.xml.

    Les lignes à rajouter donc sont les suivantes:

    inherits name='com.google.gwt.activity.Activity'
    inherits name='com.google.gwt.place.Place'

    Ces lignes sont à mettre en tant que balises.

    Bon courage pour la suite :)

    RépondreSupprimer