Ellipse Tutorial

Développement d'un composant générant des événements



ATTENTION : Tutorial en cours d'écriture ! N'hésitez pas à nous signaler toute erreur ou suggestion.

Accès rapide :
   Quelques rappels sur les listeners (les écouteurs) Java
   Introduction au modèle de gestion d'événements du framework Ellipse
   Spécification d'une interface d'écoute Ellipse et d'une classe d'événements Ellipse
   Génération d'un événement au sein d'un composant Ellipse
   Evénements Ellipse et gestion des priorités d'événements
   Interception d'un événement
   Traitement en lot des générateurs d'événements

Quelques rappels sur les listeners (les écouteurs) Java

Le modèle JavaBeans (nous en avons parlé dans les chapitres précédents) introduit une convention de codage Java. Cette convention permet, via le moteur de réflexion Java et le moteur d'introspection (une surcouche à la réflexion Java), de découvrir, au runtime, les propriétés (voir dans le document précédent), mais aussi les gestionnaires d'événements : c'est sur ce dernier point que nous allons débattre dans la première partie de ce document.

Le modèle des listeners est basé sur la notion d'interface. Pour mettre en oeuvre une nature (un type) d'événements particuliers, la première des choses à faire est de définir une interface d'écoute. Le composant, générateur de l'événement, typera ses écouteurs par le biais de cette interface et les récepteurs de l'événement l'implémenteront. Pour information, notez qu'une interface d'écoute doit normalement étendre l'interface java.util.EventListener (directement ou indirectement) et qu'elle doit de plus se terminer par le mot Listener.

 
01 public interface DemoListener extends java.util.EventListener {
02     public void eventFired( DemoEvent event );
03 }
Définition d'une interface d'écoute

Comme vous l'avez certainement remarqué, les éventuelles méthodes de l'interface (ici une seule : eventFired), qui seront invoquées pour traiter les évènements, acceptent justement un objet d'événement en paramètre. Cet aspect aussi est imposé par JavaBeans. Une classe d'événement doit étendre la classe java.util.EventObject. Cette classe définie un attribut permettant de retrouver l'objet émetteur de cet événement. Voici un petit exemple de code pour notre classe DemoEvent.

 
01 public class DemoEvent extends java.util.EventObject {
02 
03     public DemoEvent( Object source ) {
04         super( source );
05     }
06 
07 }
Définition d'une classe d'événement

Une fois la classe d'événement et l'interface d'écoute codées, il faut coder le déclenchement de l'événement au niveau du composant générant l'événement. Normalement l'événement est déclanché sous une condition particulière : nous ferons abstraction de cette condition. Focalisons nous uniquement sur le déclenchement à proprement parler.

 
01 private Vector<DemoListener> listeners = new Vector<DemoListener>();
02 
03 public void fireDemoEvent() {
04     DemoEvent event = new DemoEvent( this/*, ...*/ );
05     for( DemoListener listener : this.listeners ) {
06         listener.eventFired( event );
07     }
08 }
Exemple de code d'émission d'un événement

Il ne reste plus qu'à coder l'écouteur à proprement parler : celui qui reçoit l'événement. D'ailleurs, la plupart du temps, votre code est simplement écouteur d'un type d'événement particulier. Il est plus rare de coder un générateur d'événements. Quoi qu'il en soit, voici un exemple de code permettant de réagir à la réception d'un événement (notez l'utilisation de la classe anonyme).

 
01 generator.addDemoListener( new DemoListener() {
02     @Override public void eventFired( DemoEvent event ) {
03         System.out.println( "Event received !" );
04     }
05 });
Exemple d'implémentation d'un écouteur

Introduction au modèle de gestion d'événements du framework Ellipse

Un des principaux apport des frameworks Web modernes, consiste à permettre une programmation événementielle, coté serveur, pour le développement d'applications Web. Le framework Ellipse ne déroge pas à la règle. Comparé à certains autres framework Web Java, Ellipse utilise pleinement la technique des listeners Java, avec un enregistrement traditionnel des écouteurs. Une extension liée à l'architecture Web a néanmoins été introduite au niveau du déclanchement des événements. Nous allons y revenir.

Afin de bien comprendre le fonctionnement global de la gestion des événements par Ellipse, nous allons coder un composant Web de sélection d'un mois parmi une liste de 12. Ce composant déclenchera un événement dès lors que le mois sélectionné aura changé. N'oubliez pas que les extraits de codes suivants proviennent de l'application de démonstration du Framework Ellipse. Vous pouvez télécharger cette application en activant ce lien.

Spécification d'une interface d'écoute Ellipse et d'une classe d'événements Ellipse

Nous allons donc commencer par définir notre type d'événement ainsi que notre interface d'écoute relativement à la sélection d'un nouveau mois. Une classe d'événements Ellipse doit étendre la classe corelib.services.web.webapplications.events.WebEvent : cette classe dérive elle même de java.util.EventObject. Nous appellerons cette classe MonthSelectorEvent et nous allons y rajouter une propriété permettant de retrouver facilement le nouveau mois sélectionné : nous choisirons comme nom, pour cette propriété, newMonth (un get et un set devront donc y être définis).

 
01 package corelib.services.web.samples.virtualcaddy.webcomponents.samples;
02 
03 import corelib.services.web.webapplications.events.WebEvent;
04 
05 public class MonthSelectorEvent extends WebEvent {
06 
07     private String newMonth;
08 
09     public MonthSelectorEvent( Object source, String newMonth ) {
10         super( source );
11         this.setNewMonth( newMonth );
12     }
13 
14     public String getNewMonth() {
15         return newMonth;
16     }
17 
18     public void setNewMonth( String newMonth ) {
19         if ( newMonth == null ) throw new NullPointerException();
20         this.newMonth = newMonth;
21     }
22 
23 }
Classe "corelib.services.web.samples.virtualcaddy.webcomponents.samples.MonthSelectorEvent"

Nous pouvons maintenant définir notre interface d'écoute. Nous appellerons cette interface MonthSelectorListener et elle devra étendre l'interface corelib.services.web.webapplications.events.WebListener. Comme expliqué en première partie de ce document, la méthode définie par notre méthode acceptera obligatoirement un paramètre du type de l'événement associé (spécifié par le modèle JavaBeans).

 
01 package corelib.services.web.samples.virtualcaddy.webcomponents.samples;
02 
03 import corelib.services.web.webapplications.events.WebListener;
04 
05 public interface MonthSelectorListener extends WebListener {
06 
07     public void monthChanged( MonthSelectorEvent event );
08 
09 }
Classe "corelib.services.web.samples.virtualcaddy.webcomponents.samples.MonthSelectorListener"

Génération d'un événement au sein d'un composant Ellipse

Il nous faut maintenant mettre en oeuvre notre composant (du moins sa classe). Commençons par voir le code complet de la classe. Ensuite, nous reprendrons les principaux points un à un.

 
01 package corelib.services.web.samples.virtualcaddy.webcomponents.samples;
02 
03 import java.io.PrintWriter;
04 import java.util.Vector;
05 
06 import corelib.services.web.components.VisualComponent;
07 import corelib.services.web.components.events.WebComponentEventDescriptor;
08 import corelib.services.web.components.events.WebComponentEventLevel;
09 import corelib.services.web.webapplications.events.WebPageEvent;
10 
11 public class MonthSelector extends VisualComponent {
12 
13     private static final String [] MONTHS = {
14         "--SELECT--", "January", "February", "March", "April", "Mai", "June",
15         "July", "August", "September", "October", "November", "December" 
16     };
17 
18 
19     private String value = MonthSelector.MONTHS[0];
20     public String getValue() { return value; }
21     public void setValue( String value ) { this.value = value; }
22 
23     private String oldMonth = MonthSelector.MONTHS[0];
24     public String getOldMonth() { return oldMonth; }
25     public void setOldMonth( String oldMonth ) { this.oldMonth = oldMonth; }
26 
27 
28     private Vector<MonthSelectorListener> listeners = new Vector<MonthSelectorListener>();
29 
30     public void addMonthSelectorListener( MonthSelectorListener listener ) {
31         if ( listener == null ) throw new NullPointerException();
32         listeners.add( listener );
33     }
34 
35     public void fireMonthSelectorEvent() {
36         MonthSelectorEvent event = new MonthSelectorEvent( this, value );
37         for( MonthSelectorListener listener : listeners ) {
38             listener.monthChanged( event );
39         }
40     }
41 
42     @Override public void component_load( WebPageEvent event ) {
43         if ( value.equals( oldMonth ) == false  && 
44                 this.webPage.getRequest().getMethod().equals( "POST" ) ) {
45             WebComponentEventDescriptor eventDescriptor =
46                 new WebComponentEventDescriptor(
47                     this, "MonthSelectorEvent", WebComponentEventLevel.DEFAULT
48                 );
49             this.webPage.getEventDispatcher().addEventDescriptor( eventDescriptor );
50         }
51     }
52 
53     @Override public void component_renderBegin( WebPageEvent webPageEvent ) {
54         PrintWriter writer = this.getWebPage().getOut();
55         writer.println( "<select name=\"" + this.getId() + "\">" );
56         for( String month : MonthSelector.MONTHS ) {
57             String sel = month.equals( value ) ? " selected" : "";
58             writer.println( "<option " + sel + ">" + month + "</option>" );
59         }
60         writer.println( "</select>" );
61         writer.println( "<input type=\"hidden\" name=\"" + this.getId()
62                 + ":oldMonth\" value=\"" + value + "\">" );
63     }
64 
65 }
Classe "corelib.services.web.samples.virtualcaddy.webcomponents.samples.MonthSelector"

La ligne 21 commence par définir une collection de type Vector<MonthSelectorListener> : celle-ci permettra de stocker l'ensemble de tous les objets qui se seront inscrits en tant qu'écouteurs sur l'événement déclenché par notre composant. Pour réaliser ces inscriptions la méthode addMonthSelectorListener, déclarée en ligne 30, est proposée. Elle accepte, bien entendu, en paramètre un objet de type MonthSelectorListener.

Le framework Ellipse, applique un cycle de vie relativement complexe à chaque traitement d'une WebPage. L'invocation des gestionnaires d'événements est un des points de ce cycle de vie. Il en résulte que les appels aux écouteurs ne sont pas exécutés au moment où les conditions de déclenchement sont observées, mais différées dans le temps.

En ligne 42, la méthode component_load réalise le test qui permet de savoir si oui ou non, le mois sélectionné vient de changer depuis la requête précédente. Pour savoir si la valeur a changé, un champ de formulaire caché (<input type="hidden" ...>) a été adjoint : il contient l'ancienne sélection. La valeur de ce champ est portée par la propriété oldMonth (lignes 24 et 25) étant donné que le nom du champ caché est [componentName]:oldMonth (voir ligne 62). Donc, si les deux valeurs (month et oldMonth) sont différentes, c'est que l'événement doit être déclenché. Il faut donc préparer le déclenchement de l'événement : c'est ce qui est fait par les lignes 45-49. On décrit l'événement qui devra être déclenché : qui est le déclencheur de cet événement, quel est son type et surtout qu'elle est sa priorité. Effectivement, le framework Ellipse décrit cinq niveaux de priorités pour les événements de composant WEB (nous allons revenir sur ce point dans quelques instants). Enfin, le descripteur est ajouté à l'EventDispatcher qui sera chargé de diffuser l'ensemble des événements relatifs aux composants web et ce au bon moment dans le cycle de vie de la page Web.

Ensuite, la ligne 35 définie la méthode fireMonthSelectorEvent : elle sera invoquée par le framework Ellipse lors du déclenchement de l'événement. Il découle que son nom est, lui aussi, conventionné. Néanmoins ce n'est pas le modèle JavaBeans qui spécifie le nom de cette méthode, mais bien le framework Ellipse. Nous pouvons donc en déduire une seconde chose : une interface d'écoute Ellipse pourra difficilement exposer plus qu'une méthode. Toute méthode de déclenchement d'événements Ellipse sur un composant se doit de commencer par fire. Quant à son contenu, la méthode créé un objet d'événement et invoque, pour chaque écouteur, la méthode attendu en lui passant l'événement.

Evénements Ellipse et gestion des priorités d'événements

Afin de garantir la cohérence dans les déclenchements de vos événements, le framework Ellipse introduit la notion de priorités d'exécution. Les différents niveaux de priorités sont introduits par la classe WebComponentEventLevel : il y en a cinq au total. L'ordre de priorité est le même que celui de leurs définitions dans le type énuméré présenté ci-dessus. C'est d'abord les événements de priorité WebComponentEventLevel.FIRST qui sont déclenchés, puis ceux de priorité WebComponentEventLevel.BEFORE et ainsi de suite. Par contre pour tous les événements utilisant une même priorité, on ne peut pas présager de l'ordre de déclenchement.

 
01 package corelib.services.web.components.events;
02 
03 public enum WebComponentEventLevel {
04 
05     FIRST, BEFORE, DEFAULT, AFTER, LAST
06 
07 }
Classe "corelib.services.web.components.events.WebComponentEventLevel"

A titre d'information, sachez que les événements de type corelib.services.web.components.events.FormInputEvent (en cas de modification de la valeur saisie par un champ de formulaire) ont une priorité WebComponentEventLevel.BEFORE. Les événements de type corelib.services.web.components.events.ActionEvent (en cas d'actionnement d'un bouton de formulaire) ont une priorité WebComponentEventLevel.AFTER.

Interception d'un événement

L'interception d'un événement Ellipse se passe exactement de la même manière que pour une application SWING. Il vous suffit d'enregistrer votre écouteur sur le composant Web considéré et pour peu que ce dernier déclenche des événements, votre écouteur le recevra. Il vous est préconisé de coder vos enregistrements d'écouteurs au sein de l'événement page_load de la page Web considérée. Le code suivant correspond à la classe de la page Web de démonstration.

01 package corelib.services.web.samples.virtualcaddy.webpages.samples;
02 
03 import corelib.services.web.components.OutputText;
04 import corelib.services.web.samples.virtualcaddy.webcomponents.samples.MonthSelector;
05 import corelib.services.web.samples.virtualcaddy.webcomponents.samples.MonthSelectorEvent;
06 import corelib.services.web.samples.virtualcaddy.webcomponents.samples.MonthSelectorListener;
07 import corelib.services.web.webapplications.WebPage;
08 import corelib.services.web.webapplications.events.WebPageEvent;
09 
10 public class EventCoding extends WebPage {
11 
12     private MonthSelector monthSelector;
13     private OutputText txtSelectedMonth;
14 
15     @Override public void page_load( WebPageEvent webPageEvent ) {
16         monthSelector.addMonthSelectorListener( new MonthSelectorListener() {
17             @Override public void monthChanged( MonthSelectorEvent event ) {
18                 txtSelectedMonth.setText( event.getNewMonth() );
19             }
20         });
21     }
22 
23 }
Classe "corelib.services.web.samples.virtualcaddy.webpages.samples.EventCoding"

Voici maintenant la description XML de la page Web. Bien entendu celle-ci est associée à la classe de page ci-dessus. Notez bien que pour pouvoir réaliser l'enregistrement des écouteurs, les composants Web se doivent d'être identifiés dans la page Web et, qui plus est, être définis dans la classe de page (ligne 12 de l'exemple ci-dessus).

01 <?xml version="1.0" encoding="ISO-8859-1" ?>
02 <web:Html xmlns:web="corelib.services.web.components"
03 		  xmlns:samples="corelib.services.web.samples.virtualcaddy.webcomponents.samples"
04           codeBehind="corelib.services.web.samples.virtualcaddy.webpages.samples.EventCoding">
05 	<head>
06 		<title>Event Coding Sample</title>
07 		<link rel="stylesheet" type="text/css" href="../CssStyles.css" />
08 	</head>
09 	<body>
10 		<h1>Event Coding Sample</h1> <br />
11 	
12 		<web:Form method="POST">
13 			<samples:MonthSelector id="monthSelector" />
14 			<web:Button type="submit" value="Submit" /> <br/>
15 			
16 			Your selection is : <web:OutputText id="txtSelectedMonth" />
17 		</web:Form>
18 	</body>
19 </web:Html>
Fichier "samples/EventCoding.wp"

Traitement en lot des générateurs d'événements

Pour clore ce chapitre, il faut savoir qu'il est possible de répéter plusieurs fois un composant ou plusieurs composants Ellipse via l'utilisation d'un <web:Repeater>. Mais dans ce cas, comment affecter à ces composants répliqués un gestionnaire d'événement unique ? En fait la solution est simple, étant donné que le framework fournie une méthode utilitaire adaptée : EventUtility.addActionListener. L'extrait de code ci-dessous vous montre la définition de la page Web. Elle vous propose de saisir une valeur numérique : le serveur créera autant de boutons que demandé. Si, de plus, vous cliquez sur l'un de ces boutons, un gestionnaire d'événement sera déclenché.

01 <?xml version="1.0" encoding="ISO-8859-1" ?>
02 <web:Html xmlns:web="corelib.services.web.components"
03 		  xmlns:samples="corelib.services.web.samples.virtualcaddy.webcomponents.samples"
04           codeBehind="corelib.services.web.samples.virtualcaddy.webpages.samples.EventCoding2">
05 	<head>
06 		<title>Event Coding Sample</title>
07 		<link rel="stylesheet" type="text/css" href="../CssStyles.css" />
08 	</head>
09 	<body>
10 		<h1>Event Coding Sample</h1> <br />
11 	
12 		<web:Form method="POST">
13 			Button count : <web:TextBox id="txtButtonCount" value="3" autoPost="true" />
14 			<br /><br />
15 			
16 			<web:Repeater values="#{this.buttonNames}" elementAlias="name">
17 				<web:Button id="btnTest" value="#{name}" /> <br/>
18 			</web:Repeater> 
19 		</web:Form>
20 	</body>
21 </web:Html>
Fichier "samples/EventCoding2.wp"

Maintenant vous pouvez observer le code de la classe de page associée à la page Web présentée ci-dessus. Notez la ligne 24 qui associe un gestionnaire d'événement unique à toutes les instances de boutons qui ont étés répliquées par le repeater (il faut, pour ce faire, fournir le nom des composants ainsi que le nombre d'instances).

01 package corelib.services.web.samples.virtualcaddy.webpages.samples;
02 
03 import corelib.services.web.components.events.ActionEvent;
04 import corelib.services.web.components.events.ActionListener;
05 import corelib.services.web.components.events.EventUtility;
06 import corelib.services.web.webapplications.WebPage;
07 import corelib.services.web.webapplications.events.WebPageEvent;
08 
09 public class EventCoding2 extends WebPage {
10 
11     private int length = 3;
12 
13     @Override public void page_init( WebPageEvent webPageEvent ) {
14         // --- Necessary because of the life cycle of the Web page ---
15         // The DataBinding is made before the assignment of values
16         // stored in the HTTP request !!!
17         try {
18             length = Integer.parseInt( request.getParameter( "txtButtonCount" ) );
19         } catch( Exception exception ) {
20         }
21     }
22 
23     @Override public void page_load( WebPageEvent webPageEvent ) {
24         EventUtility.addActionListener( this, "btnTest", length, new ActionListener() {
25             @Override public void actionPerformed( ActionEvent event ) {
26                 System.out.println( "Event intercepted " + event.getRepeatedIndex() );
27             }
28         });
29     }
30 
31     public String [] getButtonNames() {
32         String [] names = new String[ length ];
33         for( int i=0; i<length; i++ ) names[i] = "Button " + i;
34         return names;
35     }
36 
37 }
Classe "corelib.services.web.samples.virtualcaddy.webpages.samples.EventCoding2"

ATTENTION : Ellipse Framework vous est proposé en version beta (d'évaluation) afin de vous permettre d'évaluer ce framework. Infini Software se dégage de toutes responsabilités relatives à l'utilisation de ce framework. De plus, Infini Software ne pourra nullement être tenu responsable de l'utilisation des informations présentes dans ces tutoriaux.

Dominique LIARD - © 2007..2010 SARL Infini Software - Tous droits réservés
Les autres marques et les noms de produits cités dans ces documents sont la propriété de leurs détenteurs respectifs.