Looking for Computer Science  & Information Technology online courses ?
Check my new web site: https://www.yesik.it !

Cet article est une reprise de l'article Navigation dans des pages JSF que j'avais précédemment publié sur http://wiki.esicom-st-malo.fr

JSF est une technologie de présentation qui prend en charge non seulement le rendu, mais aussi la navigation entre les pages d'une application Web. Ce tutoriel vous montre comment définir des règles de navigation qui permettent de passer d'une page à l'autre dans une application Web qui utilise JSF (ou une technologie de présentation compatible comme Facelets).

Objectifs A la fin de ce tutoriel, vous saurez:
  • Fixer des règles de navigation entre vos pages JSF
Prérequis
  • Premiers pas avec JSF
Moyens
  • Serveur d'application compatible JSF (JBoss AS, pas ex.)


Le programme

Description

Ce tutoriel s'appuie sur l'application MathMath qui permet de vérifier les capacités de calcul mental de l'utilisateur. L'application présente une opération et attend de l'utilisateur qu'il donne le résultat. Si l'utilisateur donne une mauvaise réponse, le logiciel le lui signale et la solution est affichée. Le processus se répète plusieurs fois, puis l'application affiche le score de l'utilisateur (nombre de bonnes réponses). Dans cette version, l'application ne présente que des additions.

Création du projet

Le projet va être créé de la manière décrite dans le tutoriel Un projet JSF/Facelets avec Ant et Eclipse. Si vous ne disposez pas d'Eclipse (ou si vous ne voulez pas l'utiliser), il vous suffit d'ignorer les instructions spécifiques. C'est ce que j'ai fait ici. Au final:

Créez le répertoire du projet:

sh$ mkdir MathMath
sh$ cd MathMath

Créez le fichier build.xml

<?xml version="1.0" encoding="UTF-8" ?>
<project name="MathMath" default="compile">
    <property name="project.name" value="MathMath" />
    <property name="project.src.dir" value="src" />
    <property name="project.web.dir" value="WebContent" />
    <property name="project.war.file" value="${project.name}.war" />
 
    <property name="JBOSS_HOME" value="../../jboss" />
    <path id="JBoss.libraryclasspath">
        <pathelement location="${JBOSS_HOME}/server/default/lib/servlet-api.jar"/>
    </path>
 
    <target name="init">
        <mkdir dir="${project.src.dir}" />
        <mkdir dir="${project.web.dir}" />
        <mkdir dir="${project.web.dir}/WEB-INF" />
        <mkdir dir="${project.web.dir}/WEB-INF/classes" />
        <mkdir dir="${project.web.dir}/WEB-INF/lib" />
    </target>
 
    <target name="clean">
        <delete dir="${project.web.dir}/WEB-INF/classes"/>
    </target>
 
    <target name="compile" depends="init" unless="eclipse.running">
        <javac srcdir="${project.src.dir}"
               destdir="${project.web.dir}/WEB-INF/classes">
            <classpath refid="JBoss.libraryclasspath" />
        </javac>
    </target>
    <target name="copyclasses" depends="init" if="eclipse.running">
        <copy todir="${project.web.dir}/WEB-INF/classes">
            <fileset dir="classes">
                <include name="**/*.class"/>
            </fileset>
        </copy>
    </target>
 
    <target name="build" depends="compile,copyclasses">
    </target>
 
    <target name="war" depends="build">
        <jar destfile="${project.war.file}" basedir="${project.web.dir}" />
    </target>
</project>

Laissez Ant créer les sous-répertoires:

sh$ ant init
Buildfile: build.xml 

init:
    [mkdir] Created dir: /home/sylvain/dev/MathMath/src
    [mkdir] Created dir: /home/sylvain/dev/MathMath/WebContent
    [mkdir] Created dir: /home/sylvain/dev/MathMath/WebContent/WEB-INF
    [mkdir] Created dir: /home/sylvain/dev/MathMath/WebContent/WEB-INF/classes
    [mkdir] Created dir: /home/sylvain/dev/MathMath/WebContent/WEB-INF/lib

BUILD SUCCESSFUL
Total time: 0 seconds

Ajoutez le descripteur de déploiement WebContent/WEB-INF/web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" >
 
   <!-- JSF -->
   <servlet>
      <servlet-name>Faces Servlet</servlet-name>
      <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
      <load-on-startup>1</load-on-startup>
   </servlet>
 
   <context-param>
      <param-name>javax.faces.DEFAULT_SUFFIX</param-name>
      <param-value>.xhtml</param-value>
   </context-param>
 
   <servlet-mapping>
      <servlet-name>Faces Servlet</servlet-name>
      <url-pattern>*.faces</url-pattern>
   </servlet-mapping>
 
   <security-constraint> 
       <display-name>Restrict raw XHTML Documents</display-name>
       <web-resource-collection>
           <web-resource-name>XHTML</web-resource-name>
           <url-pattern>*.xhtml</url-pattern>
       </web-resource-collection>
       <auth-constraint/>
   </security-constraint>
</web-app>

Copiez la bibliothèque de Facelets:

sh$ cp /path/to/jsf-facelets.jar WebContent/WEB-INF/lib

Ajoutez le fichier de configuration de JSF WebContent/WEB-INF/faces-config.xml:

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="1.2"
              xmlns="http://java.sun.com/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
   	<application>
    	<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
   	</application>
 
 	<managed-bean>
		<managed-bean-name>exam</managed-bean-name>
		<managed-bean-class>fr.esicom.demo.ExamBean</managed-bean-class>
		<managed-bean-scope>session</managed-bean-scope>
	</managed-bean>
</faces-config>

Vous pouvez vérifier que vous avez bien tous les fichiers nécessaires:

sh$ find .
.
./build.xml
./src
./WebContent
./WebContent/WEB-INF
./WebContent/WEB-INF/classes
./WebContent/WEB-INF/lib
./WebContent/WEB-INF/lib/jsf-facelets.jar
./WebContent/WEB-INF/web.xml
./WebContent/WEB-INF/faces-config.xml

Reste à écrire le managed bean chargé de la logique de notre application et les pages Facelets chargées de la présentation. Nous verrons ensuite comment les deux couches collaborent pour assurer la navigation entre les pages.

Le managed bean

Comme vous l'avez peut-être remarqué dans le fichier faces-config.xml, notre managed bean va être implémenté dans la classe fr.esicom.demo.ExamBean. Celui-ci va mémoriser les nombres présentés à l'utilisateur, et comptabiliser les bonnes réponses. Il est relativement aisé à coder:

package fr.esicom.demo;
 
public class ExamBean {
    private int	leftOperande;
    private int	rightOperande;
    private int	expectedResult;
    private int	userResponse;
 
    /**
     * Nombre de questions déjà posées
     */
    private int count; 
 
    /**
     * Total des bonnes réponses
     */
    private int	score;
 
 
    public ExamBean() {
	startExam();
    }
 
    /**
     * Initialise un nouvel examen.
     */
    public void startExam() {
	score = 0;
	count = 0;
	nextGuess();
    }
 
    private void nextGuess() {
	leftOperande = (int) Math.ceil(Math.random()*100);
	rightOperande = (int) Math.ceil(Math.random()*100);
	expectedResult = leftOperande+rightOperande;
	userResponse = 0;
    }
 
    public String startExamAction() {
	startExam();
	return "start";
    }
 
    /**
     * Traite les réponses de l'utilisateur.
     * 
     * Trois possibilité:
     * <ul>
     * <li>failure: mauvaise réponse</li>
     * <li>continue: bonne répone, on continue</li>
     * <li>done: bonne réponse, évaluation terminée</li>
     * </ul>
     */
    public String answerAction() {
	++count;
	if (userResponse != expectedResult)
	    return "failure";
 
	/* else */
	++score;
	return continueExam();
    }
 
    public String continueExam() {
	/* TODO remplacer cette valeur codée en dur */
	if (count == 10)
	    return "done";
 
	/* else */
	nextGuess();
	return "continue";
    }
 
    public int getLeftOperande() {
	return leftOperande;
    }
 
    public int getRightOperande() {
	return rightOperande;
    }
 
    public void setExpectedResult(int expectedResult) {
	this.expectedResult = expectedResult;
    }
 
    public int getExpectedResult() {
	return expectedResult;
    }
 
    public int getScore() {
	return score;
    }
 
    public int getCount() {
	return count;
    }
 
    public int getUserResponse() {
	return userResponse;
    }
 
    public void setUserResponse(int userResponse) {
	this.userResponse = userResponse;
    }
}

Le code est un peu long, mais pas très compliqué. Remarquez comme "l'intelligence" du système est concentrée dans les méthodes answerAction et continueExam. Elles testent l'état du système (réponse de l'utilisateur, réponse attendue, nombre de questions posées), et selon les cas renvoient soit failure, soit continue, soit done. Nous verrons d'ici quelques instants à quoi servent ces valeurs.

Les pages Facelets

Laissons maintenant ExamBean et concentrons nous sur les pages JSF/Facelets. Notre application va présenter trois pages différentes:

  1. La page index.xhtml qui présente une question;
  2. La page failure.xhtml qui signale à l'utilisateur une réponse erronée;
  3. La page result.xhtml qui montre à l'utilisateur le résultat de son évaluation.

Tout d'abord WebContent/index.xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:h="http://java.sun.com/jsf/html">
<head><title>MathMath</title></head>
 
<body>
    <form jsfc="h:form" id="Question">
		<h1>MathMath Quiz</h1>
		<p>Donnez le résultat de l'opération suivante:</p>
		<p>#{exam.leftOperande} + #{exam.rightOperande} = 
                    <input type="text" jsfc="h:inputText" value="#{exam.userResponse}" /></p>
		<p><input type="submit" 
                          jsfc="h:commandButton" 
                          value="Valider" 
                          action="#{exam.answerAction}" /></p>
    </form>
</body>
</html>

Ensuite la page d'erreur WebContent/failure.xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:h="http://java.sun.com/jsf/html">
<head><title>MathMath</title></head>
 
<body>
    <form jsfc="h:form" id="oups">
	<h1>MathMath Quiz</h1>
	<p>Vous vous êtes trompé:</p>
	<p>
           #{exam.leftOperande} + #{exam.rightOperande} = 
           <strong>#{exam.expectedResult}</strong>
           (votre réponse était <em>#{exam.userResponse}</em>)
        </p>
	<p><input type="submit" 
                  jsfc="h:commandButton" 
                  value="Continuer" 
                  action="#{exam.continueExam}" /></p>
    </form>
</body>
</html>

Et enfin la page de synthèse des résultats WebContent/result.xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:h="http://java.sun.com/jsf/html">
<head><title>MathMath</title></head>
 
<body>
    <form jsfc="h:form" id="result">
	<h1>MathMath Quiz</h1>
	<p>Votre score:</p>
	<p>
           #{exam.score} sur #{exam.count} questions.
        </p>
	<p><input type="submit" 
                  jsfc="h:commandButton" 
                  value="Recommencer" 
                  action="#{exam.startExamAction}" /></p>
    </form>
</body>
</html>

Vous pouvez maintenant compiler votre application et la déployer:

sh$ ant war && cp -f MathMath.war ${JBOSS_HOME}/server/default/deploy/

Enfin, pour tester vos pages, il vous faut charger manuellement les URL:

Vous devriez constater que les pages se chargent, mais que les boutons n'assurent pas la transition d'une page à l'autre. C'est ce que nous allons corriger maintenant.

La navigation

Dans JSF, la navigation entre les pages est configurée au niveau du fichier faces-config.xml. C'est dans ce fichier que nous allons donner les règles de navigation qui vont indiquer vers quelle page l'utilisateur sera envoyé.

Vous avez sans doute remarqué dans la classe fr.esicom.demo.ExamBean que les méthodes startExamAction, answerAction et continueExam renvoient des chaines de caractères. Ces chaines vont en fait servir de condition pour les transitions d'une page à l'autre.

Pour chaque page, nous allons dire où envoyer l'utilisateur en fonction du résultat des actions entreprises. Ces règles sont écrites dans le fichier faces-config.xml. Par exemple:

<!-- ... -->
        <navigation-rule>
                <from-view-id>/failure.xhtml</from-view-id>
                <navigation-case>
                        <from-outcome>continue</from-outcome>
                        <to-view-id>/index.xhtml</to-view-id>
                </navigation-case>
                <navigation-case>
                        <from-outcome>done</from-outcome>
                        <to-view-id>/result.xhtml</to-view-id>
                </navigation-case>
        </navigation-rule>
<!-- ... -->

La règle ci-dessus signifie qu'à partir de la page failure.xhtml:

Piège:

Notez le format utilisé pour désigner les pages: elles commencent par une barre oblique (slash) et utilisent l'extension .xhtml pas .faces!

Remarque:

Si une action renvoie une valeur sans qu'il n'y ait de règle spécifique, alors c'est la même page qui est ré-affichée.

L'ensemble des transitions peut être représenté graphiquement ainsi:

Au final, notre fichier faces-config.xml est simplement la traduction textuelle du graphique ci-dessus:

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="1.2"
              xmlns="http://java.sun.com/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
   	<application>
    	<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
   	</application>
 
 	<managed-bean>
		<managed-bean-name>exam</managed-bean-name>
		<managed-bean-class>fr.esicom.demo.ExamBean</managed-bean-class>
 
		<managed-bean-scope>session</managed-bean-scope>
	</managed-bean>
 
	<navigation-rule>
		<from-view-id>/index.xhtml</from-view-id>
		<navigation-case>
			<from-outcome>continue</from-outcome>
			<to-view-id>/index.xhtml</to-view-id>
 
		</navigation-case>
		<navigation-case>
			<from-outcome>failure</from-outcome>
			<to-view-id>/failure.xhtml</to-view-id>
		</navigation-case>
		<navigation-case>
			<from-outcome>done</from-outcome>
 
			<to-view-id>/result.xhtml</to-view-id>
		</navigation-case>
	</navigation-rule>
 
	<navigation-rule>
		<from-view-id>/failure.xhtml</from-view-id>
		<navigation-case>
			<from-outcome>continue</from-outcome>
 
			<to-view-id>/index.xhtml</to-view-id>
		</navigation-case>
		<navigation-case>
			<from-outcome>done</from-outcome>
			<to-view-id>/result.xhtml</to-view-id>
		</navigation-case>
	</navigation-rule>
 
	<navigation-rule>
		<from-view-id>/result.xhtml</from-view-id>
		<navigation-case>
			<from-outcome>start</from-outcome>
			<to-view-id>/index.xhtml</to-view-id>
		</navigation-case>
	</navigation-rule>
</faces-config>

Validation

Avant de terminer, nous allons faire une petite digression et aborder la notion de validation, histoire de rendre notre application plus conviviale.

Vous l'avez peut-être remarqué, si l'utilisateur saisit autre chose qu'un nombre, la page de réponse lui est représentée. En fait, JSF met automatiquement en place un mécanisme pour convertir le texte saisi par l'utilisateur en un entier (puisque notre bean attend un entier dans userResponse). Si la conversion échoue, JSF représente la même page.

Pour améliorer la convivialité de l'application, il est également possible de présenter un message à l'utilisateur pour lui dire ce qui ne va pas. Modifiez le fichier index.xhtml ainsi:

<!-- ... -->
		<p>
                    #{exam.leftOperande} + #{exam.rightOperande} = 
                    <input type="text" 
                           jsfc="h:inputText" 
                           value="#{exam.userResponse}" 
                           id="response"/>
                    <h:message for="response"
                           showSummary="true"
                           showDetail="false" />
                </p>
<!-- ... -->

Remarque:

Comme pour les éléments label et input en XHTML ordinaire, ce sont les attributs id et for qui associent un élément h:message à l'élément input correspondant.

Si vous re-déployez notre application après cette modification, et que vous saisissez autre chose qu'un entier dans la zone de saisie, la page est représentée (comme précédemment), mais maintenant avec la message:

Question:response: 'douze' must be a number consisting of one or more digits. 

C'est mieux, mais le message est en anglais. Il est cependant possible d'associer un message spécifique à chaque zone de saisie grâce à l'attribut converterMessage de l'élément input:

<!-- ... -->
                    <input type="text" 
                           jsfc="h:inputText" 
                           value="#{exam.userResponse}" 
                           id="response"
                           converterMessage="La réponse doit être un nombre." />
<!-- ... -->

Et voilà. Maintenant, en cas d'erreur de saisie, l'utilisateur voit un message en français.

A vous de jouer

Comme toujours, ce tutoriel laisse la place à l'expérimentation et à diverses améliorations. Par exemple, vous pourriez:

Ressources