MVP avec Vaadin 7 et Spring

Publié dans: 

De bonnes pratiques font de bons logiciels !

C'est pour ça que le patron de conception MVP (Modèle-vue-présentateur) devint une référence dans la mise en place d'une architecture logicielle interactive. 

Le but de ce billet est de présenter une démarche pour appliquer MVP à une application web à base du framework Vaadin 7 et via le conteneur léger Spring.


Cet article est destiné aux développeurs java web qui débutent avec Vaadin 7. Néanmoins, une connaissance de la mise en place d'une application Vaadin et de l'injection de dépendances du framework Spring est requise.

Je vous propose alors ces deux liens utiles pour ceux qui découvrent Vaadin ou Spring pour la première fois :

- Vaadin 7: https://vaadin.com/book/vaadin7/-/page/getting-started.maven.html
-Spring Ioc: http://www.vogella.com/articles/SpringDependencyInjection/article.html

L'interface graphique avec Vaadin 7

Avec Vaadin 7, l'interface proposée à l'utilisateur via la fenêtre du navigateur n'est autre qu'une instance de la classe "com.vaadin.ui.UI".

Une instance de cette classe UI présente une arborescence de composants graphiques Vaadin dont le rendue aboutie à une interface utilisateur riche (RIA).

Vaadin 7 + Spring + MVP

Le but est de séparer entre les données à afficher, la façon de présenter ces données via les composants Vaadin et les traitements résultat de l'interaction « utilisateur - interface ».


Ainsi l'interface d'une application Vaadin sera décomposée en un ensemble de vues (Views). Chaque vue présente une arborescence de composants Vaadin et aura comme la tâche principale d'afficher des données. Ces données forment le modèle (Model) souvent encapsulé dans de simple classe java (POJO). 

L'interaction utilisateur avec l'interface quant à elle, sera traduit en un ensemble d'événements que va intercepter les présentateurs (Presenters) et ainsi réaliser d'éventuels traitements métiers et guider les changements requis dans l'interface via les vues.


Il est important de noter que pour chaque vue, il y aura un présentateur dédié à l'interception des événements émanant des composants Vaadin de cette vue et qui sera en charge d'ordonner les changements requis à l'interface, soit en agissant sur la vue qu'il contrôle, soit en incitant d'autres présentateurs à mettre à jour les vues qu'ils contrôlent.


Le schéma suivant illustre la solution technique proposée pour mettre en en place le MVP avec Vaadin et Spring.

Ainsi d'après cette solution, chaque instance de la classe "com.vaadin.ui.UI" aura son contexte Spring, dans lequel les présentateurs et les vues sont enregistré en tant que « Beans ». 

Les présentateurs quant à eux forment aussi un ensemble d'écouteurs (Listeners) d'événements lancé par le contexte Spring.

Pour Mettre en place cette architecture, il faut suivre les étapes suivantes:

1- Injecter le contexte spring dans la classe UI:

L’instanciation de la classe UI est à la charge du framework Vaadin lors de la réception d'une requête HTTP invoquant l'application.

La classe UI comprend la méthode « protected void init(VaadinRequest request) », qui sert comme point d'entrée à la construction de l'interface utilisateur.

Il s'agit donc dans cette étape d'instancier un contexte Spring, et de l'injecter dans la classe UI (instancié par le framework Vaadin) et ceci avant l'exécution de la méthode « init ».


Pour cela, nous allons faire recours à la définition d'une classe héritant de la classe abstraite com.vaadin.server.UIProvider.
En fait Vaadin propose un UIProvider par défaut (com.vaadin.server.DefaultUIProvider), dont la responsabilité est de fournir l'instance de la classe UI adéquate lors de l’invocation de l'application. Voici alors notre UIProvider spécifique « MyVaadinUIProvider » :

 public class MyVaadinUIProvider extends UIProvider {

 private static final long serialVersionUID = 1L;

 @Override
 public Class<? extends UI> getUIClass(UIClassSelectionEvent event) {
 return MyVaadinUI.class;
 }
 @Override
 public UI createInstance(UICreateEvent event) {
 try {
 MyVaadinUI ui = (MyVaadinUI) event.getUIClass().newInstance();

 ConfigurableApplicationContext uiCtx = new ClassPathXmlApplicationContext(
 event.getService().getDeploymentConfiguration()
 .getInitParameters()
 .getProperty("UIContextConfigLocation"));

 ui.setUiContext(uiCtx);

 return ui;

 } catch (InstantiationException e) {
 throw new RuntimeException("Could not instantiate UI class", e);
 } catch (IllegalAccessException e) {
 throw new RuntimeException("Could not access UI class", e);
 }}}

Les deux méthodes de la classe UIProvider à redéfinir sont respectivement :

public Class<? extends UI> getUIClass(UIClassSelectionEvent event) ; et
public UI createInstance(UICreateEvent event) ;

- La méthode « getUIClass» doit retourner le « class literal » de la classe UI à instancier. Ainsi nous retournons le « class literal » de notre classe UI qu'on va présenter un peu plus tard et qu'on va nommer « MyVaadinUI ».
- La méthode «createInstance» quant à elle présente plus d’intérêt pour nous, puisque c'est elle que le framework Vaadin va invoquer pour avoir son instance de la classe UI. 

Dans cette méthode nous avons pris soin de :

  1. Instancier à partir de notre classe « MyVaadinUI » (Ligne 12)
  2. De construire un contexte Spring (Ligne 14) : à noter que le chemin du fichier de configuration du contexte Spring est fournit à partir d'un paramètre d'initialisation de la servlet Vaadin responsable de l'interception des requêtes HTTP. Ce paramètre est intitulé « UIContextConfigLocation » qu'on va initialiser un peu plus tard dans ce billet.
  3. D'injecter le contexte Spring créé dans l'instance de la classe MyVaadinUI. (Ligne 19)

Ainsi la classe UI que Vaadin vient de créer via notre UIProvider contient bel et bien un contexte Spring prêt à recevoir la déclaration des divers vues et présentateurs. (Voir schéma plus haut).

2 - Configurer Vaadin pour passer par « MyVaadinUIProvider »

Maintenant qu'on a défini un UIProvider qui injecte un contexte Spring dans la classe UI instancié par Spring, nous allons procéder à mettre en place la configuration nécessaire pour que Vaadin passe bien par « MyVaadinProvider » lors de la création d'une instance UI et afin de passer le chemin du fichier de configuration du contexte Spring.

 

Pour se faire, il faudra alors passer par le fichier descripteur de déploiement « web.xml ».

Ainsi dans la description de la servlet de Vaadin, on va rajouter les paramètres suivant :

- « UIProvider » : comprend comme valeur le chemin de la classe « MyVaadinUIProvider » (Lignes 5 - 8)

- « UIContextConfigLocation » : comprend comme valeur le chemin du fichier de configuration du contexte Spring (nommé ui-applicationContext.xml) (Lignes 10 - 13)

 <servlet>
 <servlet-name>Vaadin Application Servlet</servlet-name>
 <servlet-class>com.vaadin.server.VaadinServlet</servlet-class>

 <init-param>
 <param-name>UIProvider</param-name>
 <param-value>com.iptech.mvp.MyVaadinUIProvider</param-value>
 </init-param>

 <init-param>
 <param-name>UIContextConfigLocation</param-name>
 <param-value>/META-INF/spring/ui-applicationContext.xml</param-value>

3- Définir les vues : 

Une vue est une arborescence de composants graphiques Vaadin. Il est indispensable pour une vue de fournir une référence sur le composant racine de cette arborescence.

Ainsi, nous définissions l'interface View suivante, que chaque vue concrète de notre application doit implémenter.

 public interface View {

 ComponentContainer getViewRoot();

 @PostConstruct
 void initView();
 }

La méthode « ComponentContainer getViewRoot() » retourne une référence sur le composant Vaadin raine de l'arborescence de chaque vue. Ce composant est une instance d'une classe qui implémente l'interface « com.vaadin.ui.ComponentContainer ». (notez que tous les composants graphique de Vaadin ayant la capacité de contenir d'autres composants fils, implémente ComponentContainer ).


Concernant la méthode « void initView() », ell est optionnelle. Elle est destiné à contenir l'initialisation de l'arborescence de composants graphiques Vaadin de chaque vue. L'annoter avec « @PostConstruct » indiquera à Spring d'invoquer directement cette méthode juste après l’instanciation de la vue par le contexte. (N'oublier pas qu'une vue est un Bean Spring : c'est le contexte Spring qu'est responsable de son instanciation). 

Cette méthode nous épargne en fait de mettre le code d'initialisation graphique de la vue dans le constructeur de la classe.

4 - Définir les présentateurs :

Le rôle d'un présentateur est d'intercepter l'interaction de l'utilisateur avec l'interface. 

Les composants graphiques de Vaadin présentent des méthodes pour enregistrer des écouteurs à des événements qu'ils sont capable de lancer. 

 

A titre d'exemple, le composant « com.vaadin.ui.Button » de Vaadin, présente une méthode « void addListener(Button.ClickListener listener) » afin d'enregistrer un écouteur de type ClickListener pour les événements ClickEvent que le bouton est susceptible de lancer à chaque click fait par l'utilisateur sur ce bouton via l'interface.

 

Si jamais ce bouton fait partie de l'arborescence de composant d'une vue, l'enregistrement d'un écouteur de type ClickListener pour l'événement ClickEvent est une tache affecté au présentateur de cette vue. On voit bien la séparation entre affichage et gestion des événements entre vue et présentateur. Pour ça, le présentateur doit impérativement avoir une référence sur la vue qu'il contrôle. 

Via cette référence, le présentateur pourra avoir une référence sur le bouton Vaadin et anisi lui attacher un écouteur pour les événements de click.

 

D’où cette interface que chaque présentateur de notre application devra implémenter :

 public interface Presenter extends ApplicationListener<ApplicationEvent>{

 View getDisplay();

 @PostConstruct
 void bind();
 }

La méthode «View getDisplay()» retourne une référence sur la vue que le présentateur contrôle. Pour la méthode «void bind()» elle sert à attacher les écouteurs requis pour les divers composants vaadin de la vue qu'il contrôle. 

Cette dernière méthode sera lancée juste après instanciation du présentateur via le contexte Spring grâce à l'annotation «@PostConstruct».

 

Les présentateurs d'une application doivent aussi communiquer entre eux dans le cas où un événement intercepté par l'un doit apporter une modification sur une vue contrôlé par un autre. C'est ainsi que chaque présentateur est un écouteur d’événements « ApplicationEvent » propre au contexte Spring. Ceci est désormais possible puisque nos présentateurs sont des Beans Spring et implémentent l'interface « ApplicationListener » via l'interface « Presenter » juste au-dessus (Ligne 1). 

Pour cette communication inter-présentateur, on notera bien la différentiation entre des « ApplicationEvent » porteur de donnée, notamment dans le cas ou un présentateur doit notifier un autre présentateur d'un changement dans la vue de ce dernier, en lui passant des données qui lui seront utiles dans la mise à jour de sa vue, et entre des « ApplicationEvent » non porteur de données.

Pour cela, nous allons spécifier deux événements qui en héritent de « ApplicationEvent », à savoir « UIEvent » et sa classe fille « AppEvent » porteuse de données.

 public class UIEvent extends ApplicationEvent{

 private static final long serialVersionUID = 1L;

 private UIEventTypes eventType;


 public <T extends Presenter> UIEvent(T source, UIEventTypes eventType) {
 super(source);
 this.eventType = eventType;
 }

 public UIEventTypes getEventType() {
 return eventType;
 }

 }

 

 public class AppEvent extends UIEvent {

 private static final long serialVersionUID = 1L;

 private Object data;
 private Map<String, Object> dataMap;

 public <T extends Presenter> AppEvent(Object data,T source, UIEventTypes eventType) {
 super(source, eventType);
 this.data = data;
 }

 public <T extends Presenter> AppEvent(Map<String, Object> dataMap,
 T source, UIEventTypes eventType) {
 super(source, eventType);
 this.dataMap = dataMap;
 }

 @SuppressWarnings("unchecked")
 public <X> X getData() {
 return (X) data;
 }

 @SuppressWarnings("unchecked")
 public <X> X getData(String key) {
 if (dataMap == null)
 return null;
 return (X) dataMap.get(key);
 }
 }

Notons bien les points suivants : 

  1. « UIEvent » et « AppEvent » hérite de « ApplicationEvent » ce qui leurs rend des événements que le contexte Spring peux déclencher et que les présentateurs peuvent nintercepter.
  2. « UIEvent » et « AppEvent » contiennent une propriété de type « UIEventTypes » qui est une énumération. Elle sert aux présentateurs de différentier entre deux événements « UIEvent ».
  3. « AppEvent » pourra transporter des données via le Map « Map<String, Object> dataMap »

5- Et le modèle du MVP ?

Comme je l'ai indiqué précédemment, le modèle à fournir pour une vue se résume en des simple classes java (POJO) , qui, de préférence, respecte la spécification javaBean.

Dans l'exemple qui va accompagner finalement ce billet, le modèle n'est autre que la classe « Contact », qui représente un contact via son nom, prénom et identifiant.

1. public class Contact {
2.
3. private Integer id;
4. private String firstName;
5. private String lastName;
6. …

6 - Que fait enfin « MyVaadinUI avec le contexte Spring qu'on lui a injecté ?

Bonne question ! 

 

L'idée est de récupérer via le contexte Spring un présentateur et de lui demander d'afficher la vue qu'il contrôle.

Ce présentateur de démarrage est un peu spéciale : la vue qu'il contrôle met en place généralement des conteneurs pour d'autres vues possible. Ainsi si on a une vue responsable d'afficher un menu (MenuView), une autre pour afficher un tableau de données (TableView), on aura besoin d'une autre vue (AppView) retournant des références sur les composants Vaadin destinées à contenir les composants racines respectives de MenuView et TableView.

Chacune de ces vues est contrôlé par un présentateur. Celui de AppView entre le premier en scène via la classe UI, et il va mettre en place les composants conteneurs de MenuView et TableView. 

Quand il termine la restitution de sa vue, il notifie les présentateurs de MenuView et TableView pour qu'ils attachent leurs vues (via leurs composants racine) à l'interface. Cette notification (communication inter-présentateur) est assuré par le contexte Spring : des événements UIEvent et la nature d'écouteurs des présentateurs.

 

Voici enfin le code source de « MyVaadinUI » :

 public class MyVaadinUI extends UI{

 private transient ApplicationContext uiContext;

 @Override
 protected void init(VaadinRequest request) {
 IApplicationPresenter appPresenter = (IApplicationPresenter) uiContext.getBean("applicationPresenter");
 setContent(appPresenter.getDisplay().getViewRoot());
 }

 public void setUiContext(ApplicationContext uiContext) {
 this.uiContext = uiContext;
 }
 }

A la ligne 7, c'est la récupération d'un Bean Spring nommé « applicationPresenter ». C'est le présentateur de démarrage. 

A la ligne 8, c'est l'initialisation de l'interface par le composant Vaadin racine de la vue qu'il contrôle. 

La récupération de la vue se fait via la méthode « getDisplay()» imposé par l'interface « Presenter » et la récupération du composant racine de la vue se fait par la méthode « getViewRoot() » imposé par l'interface « View ».

Et maintenant, place à un exemple:

L'exemple consiste à afficher une liste de contacts dans un tableau. Le choix d'un contact de cette liste remplis un formulaire par ses coordonnées.

 

Ainsi la décomposition de l'interface de cet exemple produit les possibles vues suivantes :

 

 

Vous allez trouver dans le projet Eclipse qui accompagne ce billet, l'implémentation des vues de la figure précédente et leurs présentateurs respective afin d'assurer le fonctionnel souhaité à base de l'approche MVP présenté et grâce au framework Spring et Vaadin. 

Bonne découverte et n'hésitez surtout pas à laisser vos commentaires et critiques.

Pièces jointe

1- Projet eclipse de l'exemple.

2- Fichier "war" de l'exemple.

 

Commentaires

Soumis le 14 Septembre 2013 - 12:49
Comment: 
le fichier des sources(projet eclipse) est corrompu
Soumis le 19 Septembre 2013 - 10:16
Comment: 
Bonjour, le lien est mis à jour. vous pouvez télécharger le projet eclipse.