Accès rapide :
Introduction à la gestion des événements côté serveur
Implémentation explicite de listeners
Les interfaces d'écoute (les listeners)
Implémentation explicite de l'interface javax.faces.event.ActionListerner
Implémentation explicite de l'interface javax.faces.ValueChangedListener
Implémentation implicite de listeners
Quelle technique privilégier ?
Cas d'un bouton avec exécution immédiate
JSF est un framework de développement d'applications Web « Server Side » : cela veut dire qu'il ne prend en charge que les traitements côté serveur. Pour les traitements côté client, vous pouvez compléter le projet avec un framework JavaScript « Client Side » tel que jQuery ou autre.
Du coup, si une page Web contient, par exemple, un bouton, que se passera-t-il si vous cliquez dessus avec la souris ? En premier lieu, un événement JavaScript est lancé dans le navigateur. Vous pouvez y souscrire avec un peu de code JavaScript pour effectuer une action quelconque. Comprenez bien que ce cours JSF ne traite absolument pas de ce sujet (je vous renvoie vers la section « Dev. Web » du site KooR.fr pour de plus amples informations sur JavaScript ou jQuery). Une fois le(s) gestionnaire(s) d'événements côté client exécuté(s), les données saisies dans votre formulaire seront envoyées par HTTP au serveur. Celui-ci va devoir récupérer la requête HTTP, y extraire les informations soumises par votre navigateur et les traiter. C'est là que les gestionnaires d'événements côté serveur vont pouvoir nous aider.
Nous avons déjà vu, dans les chapitres précédents, qu'il existait la notion d'action JSF. Mais cette possibilité est plus liée à la gestion de la navigation dans le site. Si vous souhaitez juste exécuter un traitement côté serveur sans forcément rediriger vers une autre page, alors la notion de « listeners JSF » sera certainement plus adaptée. JSF propose deux manières de définir des listeners : étudions ces deux possibilités une à une.
En Java, un listener consiste en une interface décrivant la (ou les) méthode(s) associée(s) à l'événement considéré. Chaque méthode de l'interface accepte un unique paramètre : l'objet d'événement contenant les informations qualifiant l'événement constaté. JSF ne déroge par à ces règles et propose de les mettre en oeuvre dans le framework.
Il existe deux principales interfaces de listeners dans JSF : javax.faces.event.ActionListerner
et
javax.faces.event.ValueChangedListener
. La première, javax.faces.event.ActionListerner
, permet de définir un traitement en cas
de clic sur un bouton (par exemple). La seconde, javax.faces.event.ValueChangedListener
, permet de détecter un changement de valeur sur un
champ de saisie entre deux allers/retours sur le serveur.
ActionListener
, mais elle est localisée dans le package
java.awt.events
. De plus, elle ne contient pas la même signature de méthode.
Il est donc important de bien vérifier quel package vous allez importer quand vous utiliserez votre IDE pour produire l'import.
Pour notre premier exemple, nous allons coder une nouvelle page Web. Dans les prochains chapitres, cette page nous permettra de parcourir les articles proposés par notre site Web de vente en ligne. Dans un premier temps, nous allons juste gérer une donnée de type numérique qui correspondra à l'indice de l'article à afficher. Deux boutons nous permettront de passer à l'article précédent ou suivant : ce sont ces boutons qui nous intéressent pour notre gestion d'événements.
Cette nouvelle page Web doit, bien entendu, respecter l'architecture MVC de JSF : le contrôleur étant unique et déjà configuré, il nous reste à produire notre « backing bean » pour le modèle de données et une facelet pour la vue. Voici le code de notre « backing bean ». Nous l'enrichirons progressivement dans les futurs chapitres.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package fr.koor.webstore.ihm; import java.io.Serializable; import javax.enterprise.context.SessionScoped; import javax.inject.Named; @Named @SessionScoped public class CatalogBrowserBean implements Serializable { private static final long serialVersionUID = 2729758432756108274L; private int index; public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } } |
Comme vous le constatez, cette classe est très basique. L'attribut index
représente l'index de l'article (dans une collection, nous parlerons de
ça plus tard) à afficher dans la vue. Pour l'heure la vue, n'affiche que cet index et les deux boutons. Voici une proposition de code pour la vue.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!DOCTYPE html> <html xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:h="http://xmlns.jcp.org/jsf/html"> <f:view> <head> <title>View Article</title> <link rel="stylesheet" type="text/css" href="../styles.css" /> </head> <body> <h1>View Article</h1> <h:form> Identifiant : #{catalogBrowserBean.index} <br/> <!-- TODO: à finir ultérieurement --> <br/> <h:commandButton value="Précédent" />   <h:commandButton value="Suivant" /> </h:form> </body> </f:view> </html> |
Nous allons maintenant implémenter une classe de listener qui sera associée au bouton « Suivant ». Je vous propose de commencer par implémenter ce listener. Quelques explications suivront.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package fr.koor.webstore.ihm; import java.io.Serializable; import javax.enterprise.context.SessionScoped; import javax.faces.event.AbortProcessingException; import javax.faces.event.ActionEvent; import javax.faces.event.ActionListener; import javax.inject.Inject; import javax.inject.Named; @Named @SessionScoped public class NextListener implements Serializable, ActionListener { private static final long serialVersionUID = -7752358388239085979L; @Inject private CatalogBrowserBean catalogBrowserBean; @Override public void processAction( ActionEvent event ) throws AbortProcessingException { catalogBrowserBean.setIndex( catalogBrowserBean.getIndex() + 1 ); } } |
Avec cette technique, la difficulté consiste à retrouver le backing bean associé au formulaire, étant donné que la classe de listener est disjointe
de la classe de backing bean. Pour résoudre ce problème, on peut utiliser une injection de dépendance via l'annotation @Inject
: c'est le
framework CDI (Context And Dependency Injection) qui se chargera de retrouver l'instance de la classe CatalogBrowserBean
dans votre
session utilisateur.
Pour que CDI puisse correctement réaliser l'injection de dépendance, il faut absolument qu'il connaisse votre instance de la classe NextListener.
C'est pour cela qu'on l'associe à CDI via l'annotation @Named
. Comme pour un backing bean, le nom de l'instance de listener sera déduite
du nom de la classe (la première lettre du nom de la classe sera automatiquement mise en minuscule). Vous pouvez aussi explicitement nommé votre
gestionnaire d'événement en fournissant ce nom à l'annotation @Named
.
Le fait que la durée de vie du gestionnaire d'événements soit de niveau session permet de réaliser l'injection de dépendance qu'une seule fois par utilisateur, étant donné que le backing bean à la même espérance de vie.
Maintenant il nous faut associer cette classe de listener avec le bouton : cela se fait directement dans la facelet viewArticle.xhtml
en
ajoutant un sous tag <f:actionListener />
dans le tag correspondant au champ de saisie. Voici la modification à apporter sur votre vue.
1 2 3 |
<h:commandButton value="Suivant"> <f:actionListener binding="#{nextListener}" /> </h:commandButton> |
Le tag <f:actionListener />
doit porter un attribut binding
: c'est une expression EL qui doit référencer votre instance
de gestionnaire d'événements.
type
, permet de définir la classe du gestionnaire d'événement à utiliser si l'instanciation est réalisée par JSF (et non CDI).
Dans ce cas, l'attribut binding
ne doit plus être spécifié. Mais comprenez bien que dans cette situation, vous aurez du mal à lier
votre gestionnaire d'événements avec votre backing bean.
Voici un exemple de code montrant comment mettre en oeuvre une classe de gestionnaire d'événements basée sur l'interface
javax.faces.event.ValueChangedListener
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package fr.koor.webstore.ihm; import javax.faces.event.AbortProcessingException; import javax.faces.event.ValueChangeEvent; import javax.faces.event.ValueChangeListener; public class TextListener implements ValueChangeListener { @Override public void processValueChange(ValueChangeEvent arg0) throws AbortProcessingException { System.out.println( "Value changed" ); } } |
Une autre solution, certainement plus simple, consiste à laisser JSF produire le listener. Celui-ci aura pour responsabilité de rappeler une
méthode particulière sur votre « backing bean ». La méthode en question doit respecter une signature bien précise (la même que pour la
méthode processAction
). Par contre, vous aurez toute latitude sur le choix du nom de la méthode.
Voici un exemple de définition de deux gestionnaires d'événements (pour les deux boutons « Précédent » et « Suivant ») en utilisant cette technique.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package fr.koor.webstore.ihm; import java.io.Serializable; import javax.enterprise.context.SessionScoped; import javax.faces.event.ActionEvent; import javax.inject.Named; @Named @SessionScoped public class CatalogBrowserBean implements Serializable { private static final long serialVersionUID = 2729758432756108274L; private int index; public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public void processPreviousAction( ActionEvent event ) { index--; } public void processNextAction( ActionEvent event ) { index++; } } |
L'avantage indéniable de cette technique est que, comme la méthode est portée directement par votre backing bean, il est trivial d'en manipuler son
contenu : on peut donc accéder en direct à l'attribut index
.
Pour lier ces méthodes à vos boutons, il faut, pour chaque bouton, ajouter un attribut actionListener
.
Cet attribut utilise l'EL (l'Expression Language) pour établir la liaison.
1 2 |
<h:commandButton value="Précédent" actionListener="#{catalogBrowserBean.processPreviousAction}" /> |
<h:actionListener />
alors la second technique requière un
attribut de tag nommé actionListener
. Les deux syntaxes sont proches, je vous l'accorde.
Voici le code complet de la facelet avec ses deux boutons et les associations des gestionnaires d'événements (lignes 18 à 22).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<!DOCTYPE html> <html xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:h="http://xmlns.jcp.org/jsf/html"> <f:view> <head> <title>View Article</title> <link rel="stylesheet" type="text/css" href="../styles.css" /> </head> <body> <h1>View Article</h1> <h:form> Identifiant : #{catalogBrowserBean.index} <br/> <!-- TODO: à finir --> <br/> <h:commandButton value="Précédent" actionListener="#{catalogBrowserBean.processPreviousAction}" />   <h:commandButton value="Suivant" actionListener="#{catalogBrowserBean.processNextAction}" /> </h:form> </body> </f:view> </html> |
ActionEvent
sur votre méthode. Le véritable listener, produit par JSF, interceptera
l'objet d'événement et le repassera systématiquement à votre méthode. En cas d'oublie, une erreur sera produite.
Faites aussi attention sur le package à importer pour avoir accès à la classe ActionEvent
, car il en existe plusieurs :
il faut importer javax.faces.event.ActionEvent
.
Comme vous l'avez constaté, la seconde technique est bien plus simple à utiliser. Tant que possible, je vous recommande de l'utiliser. Toutes fois, si vous avez un code complexe et relativement long pour votre gestionnaire d'événement, le fait d'utiliser la première technique vous permettra d'isoler ce bloc de code dans une classe autonome.
Pour clore ce chapitre, je souhaitais vous présenter un cas subtil : celui d'un bouton avec exécution immédiate (attribut immediate="true"
)
pour court-circuiter le processus de validation de votre formulaire. Effectivement dans ce cas, et après traitement de votre gestionnaire d'événements,
le formulaire est retourné au navigateur avec les données soumises. Du coup, si vous cherchez à modifier l'état de vos backing beans dans le gestionnaire
d'événements, les modifications produites ne seront pas visibles dans le navigateur ! L'exemple suivant vous montre comment contourner ce problème.
Imaginez un ensemble de formulaires, présentés successivement, permettant de saisir un grand nombre d'informations. On peut passer au formulaire suivant, mais aussi revenir au précédent afin de corriger les informations déjà acquises. A tout moment, vous avez aussi la possibilité de réinitialiser l'ensemble des données, y compris celles renseignées dans les formulaires précédents : ces données étant stockées dans un ou plusieurs backing beans, les mises à jours devraient être relativement faciles à produire. Par contre, il en va autrement pour les données du formulaire courant. Si vous vous contentez de modifier vos beans, les données présentes lors de la soumission du formulaire resteront affichées dans la page malgré l'état des backing beans.
Il faut savoir que tous les composants web JSF (les tags JSF) présents dans votre page ont une existence, sous forme d'objets, au niveau du serveur. On peut récupérer ces composants web et changer leurs états. Voici un exemple de code permettant de modifier les champs du formulaire courant en plus des modifications sur un backing bean.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
class ABackingBean { private String dataForm1Field1; private String dataForm1Field2; private String dataForm1Field3; private String dataForm2Field1; private String dataForm2Field2; private String dataForm2Field3; private String dataForm3Field1; private String dataForm3Field2; private String dataForm3Field3; // --- Getters et setters --- // Ce gestionnaire d'événement est associé à un champ de saisie // possédant un attribut immediate="true"; public void btnCancelForm2Action( ActionEvent event ) { // --- Remise à l'état initial de tous les champs de saisies des 3 formulaires. this.dataForm1Field1 = ""; this.dataForm1Field2 = ""; this.dataForm1Field3 = ""; this.dataForm2Field1 = ""; this.dataForm2Field2 = ""; this.dataForm2Field3 = ""; this.dataForm3Field1 = ""; this.dataForm3Field2 = ""; this.dataForm3Field3 = ""; // Accès aux composants Web JSF du second formulaire : // Le tags JSF de formulaire doit avoir un attribut id="form2". // Les champs du formulaire se nomment field1, field2, field3. UIViewRoot viewRoot = context.getViewRoot(); // --- Accès au premier champ (field1) du formulaire courant (form2) --- HtmlInputText field1 = (HtmlInputText) viewRoot.findComponent( "form2:field1" ); field1.setSubmittedValue( "" ); // --- Accès au premier champ (field2) du formulaire courant (form2) --- HtmlInputText field2 = (HtmlInputText) viewRoot.findComponent( "form2:field2" ); field2.setSubmittedValue( "" ); // --- Accès au premier champ (field3) du formulaire courant (form2) --- HtmlInputText field3 = (HtmlInputText) viewRoot.findComponent( "form2:field3" ); field3.setSubmittedValue( "" ); } } |
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :