Rechercher
 

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 :

La gestion des événements côté serveur avec JSF

Validation d'un formulaire JSF par annotations Liaison aux données de vos formulaires JSF



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

Introduction à la gestion des événements côté serveur

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.

Implémentation explicite de listeners

Les interfaces d'écoute (les listeners)

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.

les API AWT et Swing définissent aussi une interface nommée 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.

Implémentation explicite de l'interface javax.faces.event.ActionListerner

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;
    }
    
}
Le code de notre backing bean

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" /> &#160;
                <h:commandButton value="Suivant" />
            </h:form>
        
        </body>
    </f:view>
</html>
La définition de notre vue

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 );
    }
    
}
Exemple d'implémentation explicite de l'interface javax.faces.ActionListener

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>
Exemple d'association d'une classe d'écoute sur un bouton

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.

un autre attribut, 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.

Implémentation explicite de l'interface javax.faces.ValueChangedListener

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 classe d'écoute basée sur l'interface javax.faces.event.ValueChangedListener

Implémentation implicite de listeners

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}" />
Définition d'une EL de liaison à une méthode de gestion d'événement
il est important de ne pas se tromper de syntaxe lors de l'association d'un gestionnaire d'événement dans votre vue. La première technique proposée requière l'ajout d'un sous-tag <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}" />
                &#160;
                <h:commandButton value="Suivant" 
                                 actionListener="#{catalogBrowserBean.processNextAction}" />
            </h:form>
        
        </body>
    </f:view>
</html>
La facelet complète
il ne faut pas oublier de définir un paramètre de type 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.

Quelle technique privilégier ?

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.

Cas d'un bouton avec exécution immédiate

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( "" );
    }
            
}
Exemple de réinitialisation d'un bean associé à plusieurs formulaire de saisie


Validation d'un formulaire JSF par annotations Liaison aux données de vos formulaires JSF