Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site
https://www.yesik.it !
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:
|
---|---|
Prérequis |
|
Moyens |
|
Sommaire
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:
- La page index.xhtml qui présente une question;
- La page failure.xhtml qui signale à l'utilisateur une réponse erronée;
- 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:
- http://localhost:8080/MathMath/index.faces
- http://localhost:8080/MathMath/failure.faces
- http://localhost:8080/MathMath/result.faces
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:
- si l'action renvoie continue il faut envoyer l'utilisateur sur la page index.xhtml
- si l'action renvoie done il faut envoyer l'utilisateur sur la page result.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:
- associer une feuille de style aux pages (et en particulier signaler les erreurs de conversion de manière plus visible - cherchez à ce propos l'attribut styleClass de l'élément h:message);
- proposer d'autres opérations en plus de l'addition (utilisez les énumérations - enum - de Java, ou pourquoi pas, créez une hiérarchie de classes pour représenter les différentes opérations);
- ou encore fournir d'autres informations statistiques sur la page des résultats: taux de bonnes réponses, temps mis pas l'utilisateur pour répondre à toutes les questions, temps moyen par question;
- etc.
Ressources
- (en) David Geary, Cary Horstmann. Core JavaServer Faces, 2nd edition. Prentice Hall, 2007. ISBN 0131738860.