Cette étude fait suite à l'étude précédente qui
nous a permis de prendre connaissance de JSF. Cette connaissance est un
pré-requis indispensable aux sujets que nous allons aborder tout au
long de cette nouvelle étude. Il est évident que les notions que nous
venons d'apprendre sont insuffisantes pour développer une application
de gestion complète. Effectivement, nous devons appréhender un certains
nombre de critères qui vont nous permettre d'aboutir à une démarche de
qualité. Nous devons aborder les points suivants :
Avons d'aborder tous ces sujets intéressants, je vous propose de bien revoir tout le cycle de traitement des requêtes JSF (sujet que nous avons déjà traité dans l'étude précédente).
Toutes les requêtes JSF suivent le même cycle de traitement, constitué de six étapes distinctes. Les lignes pleines décrivent le cycle normal et les lignes en pointillé rouge le traitement des erreurs.
Une page HTML est un fichier texte qui contient un assemblage de balises spécialisées pour le formattage de l'information. Par contre JSF utilise, côté serveur, un arbre d'objets pour représenter la vue équivalente de cette page Web. Il s'agit là d'une structure bien différente d'un fichier texte. Cet arbre d'objets est un mirroir (un représentant) de l'interface visuelle du client.
Cette phase va donc consister à reconstituer l'arbre de composants qui correspond à la vue HTML de l'utilisateur qui soumet la requête. Cette arbre se nomme : arbre de vue.
Lorsque le client soumet la requête pour la première fois, JSF doit créer la vue correspondante au travers de cet arbre d'objets. A chaque balise de type <h:...> correspond un objet Interface Utilisateur équivalent qui va donc être placé sur l'arbre à une branche qui correspond à l'imbrication des balises proposées sur la page Web. Chaque objet est donc stocké dans cet arbre à l'endroit convenable.
La racine de cet arbre de vue est systématiquement une instance de la classe UIViewRoot qui correspond en réalité à la balise <f:view>. Nous remarquons au passage que le placement de cette balise dans une page JSP devient prépondérante afin de permettre l'élaboration de cet arbre de vue.
Cet arbre d'objets, qui est une image de la vue côté client, se créer automatiquement. Je veux dire que c'est le serveur qui s'en occupe sans aucune intervention de notre part.
Indépendamment de cet arbre, ces objets sont bien entendus issus d'un ensemble de classes qui respecte la hiérarchie suivante :
Toutes les classes ne
sont pas représentées. Nous pouvons, à l'intérieur des Javabeans,
travailler directement avec les classes correspondantes aux balises
présentes sur la page Web, celles qui sont représentés en bleu.
Toutefois, il est possible de travailler plutôt avec une des classes
parentes, représentée en jaune. En effet, ces classes disposent déjà de
certaines propriétés correspondantes aux attributs proposés par les
balises équivalentes. Ainsi, par exemple, à partir de la balise
suivante :
<h:outputText value="Un texte"/>
Vous pouvez créer un objet de type UIOutput
qui dispose de la propriété équivalente à cet attribut value. Ainsi, vous pouvez solliciter les
méthodes getValue() et setValue()
qui travaillent donc en tâche de fond sur l'attribut value
(de type Object) de la classe UIOutput.
Que se passe-t-il lorsque le client propose la requête la première fois, ou lorsqu'il la propose de nouveau au serveur ?
Lorsque JSF a déterminé la vue courante, celle-ci
est accessible via la classe FacesContext.
.
Lorsque la phase précédente est terminée, nous disposons d'un arbre de composants dont la racine est UIViewRoot. La requête HTTP soumise par le navigateur est porteuse des actions de l'utilisateur sur la vue. Ceci implique qu'il faut synchroniser la vue côté JSF avec la vue côté client. En effet, si l'utilisateur modifie une valeur dans un champs de formulaire, il faut que le composant graphique correspondant côté serveur reflète ce changement d'état.
Le but de cette phase est donc de récpercuter les actions de l'utilisateur sur l'arbre de composants courant (arbre de vue courant). Il faut décoder les valeurs contenues dans l'objet HttpServletRequest (nous connaissons particulièrement bien cet objet depuis que nous manipulons les servlets) et récupérer les changement d'états de la vue sur les composants concernés.
A cet effet, la classe UIComponent qui est la classe ancêtre de tout composant JSF dispose de la méthode processDecodes(). Cette méthode est appelée récursivement sur chaque composant de l'arbre. Par polymorphisme, les composants ont la responsabilité de décoder les informations qui les concernent.
Ainsi, chaque information envoyée lors de la
requête est associée à l'objet correspondant dans l'arbre de vue. Plus
précisément, les attributs de cet objet sont remis à jour par les
nouvelles valeurs envoyées. Ainsi, si nous avons par exemple côté
client cette ligne là :
<h:inputText
value="5"/>
vous retrouver alors cette valeur 5 sur
l'objet HtmlInputText correspondant qui est
mis à jour par la propriété équivalente value,
et ceci par l'intermédiaire de la méthode setValue().
Certaines balises JSF sont parfois liés à des JavaBeans au moyen des expressions EL JSF. Les JavaBeans représentent la logique métier. Il est alors impératif de valider et ou de convertir les valeurs qui proviennent du client avant de les injecter dans le JavaBean correspondant. En effet, par exemple, si l'utilisateur saisie la chaîne suivante "345red" en lieu et place d'un nombre entier (valeur attendue), alors cela provoquera une erreur.
Vous allez le découvrir dans les chapitres qui suivent, JSF offre les concepts de validation (Validator) et de conversion (Converter) pour effectuer les traitements sur les valeurs soumises par l'utilisateur avant leurs injections dans les JavaBeans.
Un composant peut donc éventuellement posséder des Validator et un Converter qui sont invoqués lors de l'appel de la méthode processValidators() de la classe ancêtre UIComponent. Cette méthode est appelée récursivement sur l'arbre de vue. JSF procède d'abord à la conversion de la valeur extraite de la requête HTTP (String) puis valide cette valeur en utilisant les Validator.
Si une erreur survient durant cette phase, à cause d'un problème de validation, alors JSF arrête le processus normal et saute directement vers la dernière phase qui correspond au rendu de la réponse.
Lorsque la requête arrive à cette phase de traitement, les objets sont dans un état qui correspond à la vue du client. Les informations de la requête HTTP qui sont destinées à mettre à jour les données métiers ont été validées. La méthode processUpdates() de UIComponent est appelée récursivement sur l'arbre des composants. Sa responsabilité consiste à mettre à jour, cette fois-ci, les Javabeans correspondant à la logique métier.
Egalement durant cette phase, si une erreur
survient, alors JSF arrête le processus normal et saute directement à
la phase du rendu de la réponse.
.
Vous allez le découvrir dans les chapitres qui suivent, JSF offre un modèle événementiel qui permet de détecter les changements du modèle. A ce niveau, l'appel de la méthode processUpdates() peut donc éventuellement poster des événements.
Lorsque cette phase est atteinte, le modèle métier est mis à jour, des événements sont en attentes dans la queue des événements. Ces événements sont de deux natures. Soit ils sont issus, de façon classique de l'attribut action des balises <h:commandButton> ou <h:commandLink>. Soit ils sont plutôt issus de l'attribut actionListener qui met donc en oeuvre un système d'écouteur supplémentaire (ActionListener). -- peut-être à revoir --
La méthode processApplication() de UIComponent est appelée récursivement sur l'arbre de vue. Sa responsabilité consiste à diffuser (Broadcast) certains événements de la queue vers les écouteurs d'événements associés (listeners).
C'est la dernière phase du traitement d'une requête JSF. Sa première responsabilité consiste à encoder l'arbre de vue courant dans un langage compréhensible par le client, ici le HTML. JSF peut encoder le HTML, le WML, le XML, etc. Ici donc, nous partons de l'arbre de vue et chaque objet s'occupe de créer la balise correspondante dans le format standard requis.
Pour ce faire, le UIComponent dispose d'un jeu de méthodes dont la signature commence par encodeXXX(). ces méthodes sont particulièrement adaptées à l'encodage dans des langages à balises. Ces méthodes sont au nombre de trois :
Ces méthodes correspondent grossièrement aux
étapes d'encodage d'une balise : balise ouvrante, balises filles,
balise fermante (sujet traité ultérieurement).
.
Sa deuxième responsabilité consiste à sauvegarder l'état des objets de l'arbre de vue. Pour ce faire une deuxième méthode saveState() est également appelée récursivement. Cette méthode répond à la méthode restoreState() que nous avons découvert durant la première phase Création de l'arbre de vue. Ce comportement est donné par l'interface StateHolder implémentée par UIComponent.
Une application web typique contient plusieurs pages partageant toutes le même aspect, un en-tête, un pied de page, un menu, etc. Facelets permet de définir une disposition de page dans un fichier modèle qui pourra être utilisé par toutes les autres pages.
Ce fichier définit les zones (à l'aide du marqueur <ui:inset>) dont le contenu sera remplacé grâce aux marqueurs <ui:component>, <ui:composition>, <ui:fragment> ou <ui:decorate> des pages clientes.
Balises | Description |
---|---|
<ui:include> | Intègre un contenu supplémentaire issu d'un autre document. |
<ui:composition> | Définit une composition dans la page réelle utilisant un modèle de page. Le même modèle peut être utilisé par plusieurs compositions. Un modèle de page est une page JSF normale qui comporte toute l'ossature figée que nous retrouverons systématiquement dans l'ensemble des pages, et des parties variables (spécifiées par la balise <ui:insert>) dont le contenu sera spécifique à chacune des pages web et qui pourront être donc complétées au moyen de cette balise <ui:composition>. |
<ui:decorate> | Permet de décorer le contenu d'une page. Nous utilisons cette balise à la place de la précédente lorsque le site est relativement modeste. |
<ui:define> | Définit un contenu qui sera inséré à l'endroit spécifique de la page, désigné par la balise <ui:insert> du modèle. |
<ui:insert> | Spécifie un point d'insertion dans le modèle dans lequel nous pourrons éventuellement par la suite insérer un contenu au moyen de la balise <ui:define> de la page réelle qui se sert de ce modèle. |
<ui:param> | Spécifie un paramètre qui est passé dans un fichier inclu ou à un modèle de page. |
<ui:component> | Ce marqueur est similaire à <ui:composition>, mis à part qu'il crée un nouveau composant qui sera rajouté à l'arbre des composants de la page en cours. |
<ui:fragment> | Ajoute un fragment à une page. Ce marqueur est similaire à <ui:decorate>, mis à part qu'il crée un composant qui est rajouté à l'arbre des composants de la page en cours. |
<ui:debug> | Cette balise permet à l'utilisateur d'afficher automatiquement un nouvel onglet dans le navigateur, à l'aide d'un raccourci clavier, qui nous montre à la fois la hiérarchie des composants constituant la page en cours avec également l'ensemble des variables actuellement accessibles dans cette application web. |
<ui:remove> | Lors de phase de test, vous pouvez auculter momentanément une partie de la page web (ce qui evite les mises en commentaires). Tout ce qui sera introduit à l'intérieur de ce marqueur ne sera pas interprété par JSF. |
<ui:repeat> | Itération à partir d'une liste, d'un tableau, d'un ensemble de valeurs, ou même d'un simple objet. |
Afin d'illustrer les modèles de page avec l'ensemble des balises qui nous concerne, je vous propose de reprendre l'application web sur le jeu de tirage de nombre aléatoite que nous avons mis en oeuvre lors de l'étude précédente dans laquelle nous allons rajouter un modèle de page.
Le bean géré nombre reste identique à la version précédente. Je rappelle que ce bean est le modèle qui réalise tous les traitements nécessaires en coulisse.
package bean; import javax.inject.*; import javax.enterprise.context.*; import java.io.Serializable; @Named @SessionScoped public class Nombre implements Serializable { private int valeurMax = 20; private int tentativeMax = 4; private int valeur; private int alea; private int tentative; private boolean test; public int getTentativeMax() { return tentativeMax; } public void setTentativeMax(int tentativeMax) { this.tentativeMax = tentativeMax; } public int getValeurMax() { return valeurMax; } public void setValeurMax(int valeurMax) { this.valeurMax = valeurMax; } public int getValeur() { return valeur; } public void setValeur(int valeur) { this.valeur = valeur; } public int getTentative() { return tentative; } public String recommencer() { alea = (int)(Math.random()*valeurMax)+1; tentative = 0; valeur = 0; test = false; return ; } public void calcul() { if (tentative<tentativeMax) { tentative++; test = alea == valeur; } } public String getRésultat() { if (test) return ; if (tentative==tentativeMax) return ; if (tentative==0) return ; else return ; } public String getProgression() { if (tentative==0 || test) return ; return +(alea>valeur ? : ); } public int getAlea() { return alea; } public boolean isFin() { return test || tentative == tentativeMax; } }
Nous conservons également la feuille de style qui, je le rappelle se situe directement dans le répertoire de ressources (nommé resources), sans prise en compte de nom de bibliothèque :
root { display: block; } body { background-color: orange; font-weight: bold; color: maroon; } .saisie { text-align:right; background-color:yellow; font-weight:bold; color:green; width: 30px; display: block; } .normal { font-weight:bold; } h2, h3 { background-color: yellow; padding: 5px; border: groove; } .progression { color: blue; } .resultat { border: groove; padding-right: 3px; }
Cette application web est constituée de deux pages web (deux vues). Je vous propose donc de réaliser un modèle commun à ces deux pages que je vais également placé dans le répertoire des ressources (ce n'est pas une obligation) :
<?xml version="1.0" encoding="UTF-8"?> <html xmlns= xmlns:h= xmlns:ui="http://java.sun.com/jsf/facelets"> <h:head> <title>Recherche d'un nombre aléatoire</title> </h:head> <h:outputStylesheet name= /> <h:body> <h:form> <ui:include src="/resources/limites.xhtml" /> <hr /> <ui:insert name="bouton" /> </h:form> <ui:insert name="contenu"><hr /></ui:insert> <ui:debug /> </h:body> </html>
<?xml version="1.0" encoding="UTF-8"?> <html xmlns= xmlns:h=> <h:panelGrid columns=> <h:outputText value= styleClass=normal /> <h:inputText value=#{nombre.valeurMax} styleClass=saisie/> <h:outputText value= styleClass=normal/> <h:inputText value=#{nombre.tentativeMax} styleClass=saisie/> </h:panelGrid> </html>
Il est possible de changer le raccourci clavier au moment de la définition de cette balise au moyen de l'attribut hotkey. Par exemple, si vous désirez activer la fenêtre de debug avec le raccourci CTRL+SHIFT+i, voici ce que vous devez écrire : <ui:debug hotkey="i" />.
Notre modèle est finalement pourvu d'un certain nombre d'informations. Nous disposons de toute l'ossature de la page web par défaut avec en plus, systématiquement pour toutes les pages, l'affichage des valeurs maximales choisies choisi par l'utilisateur.
Dans ce projet, je rappelle que nous disposons de deux vues qui vont exploiter chacune le modèle de page que nous venons de mettre en oeuvre :
<?xml version="1.0" encoding="UTF-8"?> <html xmlns= xmlns:h= xmlns:ui="http://java.sun.com/jsf/facelets"> <ui:composition template="resources/modèle.xhtml"> <ui:define name="bouton"> <h:commandButton value= action=#{nombre.recommencer}" /> </ui:define> </ui:composition> </html>
Vu que dans cette page nous n'utilisons pas l'insertion <ui:insert name="contenu">, une balise <hr /> est bien rajoutée automatiquement.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f= xmlns:ui="http://java.sun.com/jsf/facelets"> <ui:composition template="resources/modèle.xhtml"> <ui:define name="contenu"> <h:form> <h:panelGrid columns=> <h:outputText value= /> <h:inputText value=#{nombre.valeur} styleClass=saisie /> <h:outputText value=#{nombre.progression} styleClass=progression /> <h:outputFormat value=> <f:param value=#{nombre.tentative} /> </h:outputFormat> <h:inputText value=#{nombre.tentative} styleClass=saisie disabled= /> </h:panelGrid> <hr /> <h:commandButton value= action=#{nombre.calcul} disabled=#{nombre.fin} /> <h:commandButton value= action=#{nombre.recommencer} /> <h:commandButton value= action=configurer.xhtml /> <hr /> <h2> <h:outputText value=#{nombre.résultat} /> <h:outputText value=#{nombre.alea} styleClass=saisie rendered=#{nombre.fin} /> </h2> </h:form> </ui:define> </ui:composition> </html>
Dans la partie fixe de votre modèle, il est possible d'assouplir cette partie normalement figée et de prévoir ainsi un petit réglage annexe qui peut s'avérer souvent très utile. Nous rendons ainsi notre modèle paramétrable.
Pour cela, nous utilisons la balise <ui:param> qui nous permet d'être en relation avec un paramètre défini dans le modèle au travers d'une expression EL.
<?xml version="1.0" encoding="UTF-8"?> <html xmlns= xmlns:h=> <h:panelGrid columns=> <h:outputText value= styleClass=normal /> <h:inputText value=#{nombre.valeurMax} styleClass=saisie readonly="#{lectureSeule}"/> <h:outputText value= styleClass=normal/> <h:inputText value=#{nombre.tentativeMax} styleClass=saisie#{lectureSeule}/> </h:panelGrid> </html>
Pour cela, je rajoute dans la page qui représente les valeurs limites, l'attribut readonly dans les balises <h:input> et au moyen d'une expression EL. Je précise le nom du paramètre que je désire prendre en compte dans mon modèle, ici lectureSeule.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f= xmlns:ui="http://java.sun.com/jsf/facelets"> <ui:composition template="resources/modèle.xhtml"> <ui:param name="lectureSeule" value="true"> <--- définition de la valeur à ajouter au paramètre lectureSeule <ui:define name="contenu"><h:form> <h:panelGrid columns=> <h:outputText value= /> <h:inputText value=#{nombre.valeur} styleClass=saisie /> <h:outputText value=#{nombre.progression} styleClass=progression /> <h:outputFormat value=> <f:param value=#{nombre.tentative} /> </h:outputFormat> <h:inputText value=#{nombre.tentative} styleClass=saisie disabled= /> </h:panelGrid> <hr /> <h:commandButton value= action=#{nombre.calcul} disabled=#{nombre.fin} /> <h:commandButton value= action=#{nombre.recommencer} /> <h:commandButton value= action=configurer.xhtml /> <hr /> <h2> <h:outputText value=#{nombre.résultat} /> <h:outputText value=#{nombre.alea} styleClass=saisie rendered=#{nombre.fin} /> </h2> </h:form> </ui:define> </ui:composition> </html>
Dans la page web concernée, j'utilise maintenant la balise <ui:param name="lectureSeule" value="true"> afin de spécifier la valeur que je désire pour le paramètre en question. Et le tour est joué.
Quelquefois, lors de l'élaboration d'un projet, pour découvrir l'endroit qui cause un disfonctionnement, vous pouvez placer des commentaires sur une partie de code à l'aide de l'écriture suivante :
<!-- votre code -->
Le problème, c'est que si vous avez des expressions EL dans votre code, JSF les évaluent quand même, malgré les commentaires. Si, dans ces expressions, vous faites appel à des méthodes du bean géré, ces dernières risquent de lancer une exception voyant qu'aucune valeur normale n'est proposée.
Pour résoudre cette difficultés, vous pouvez utiliser la balise <ui:remove> et entourer ainsi la partie de code que vous ne désirez pas prendre en compte momentanément pour la visualisation de la page.
... <ui:composition template="resources/modèle.xhtml"> <ui:remove> <ui:param name="lectureSeule" value="true"> <--- définition de la valeur à ajouter au paramètre lectureSeule </ui:remove> <ui:define name="contenu"><h:form> ... </ui:define> </ui:composition> </html>
Par exemple, dans ce code, j'empêche la mise en place de la lecture seule sur les zones de saisie des valeurs limites afin de bien visualiser ce fonctionnement là.
La question qui se pose souvent est : Que se passe-t-il si l'opérateur fait une mauvaise saisie ? La première démarche consisterait à prendre en compte ce problème au niveau du modèle métier, c'est-à-dire à l'intérieur du bean géré qui traite l'information. Dans ce cas de figure, le code du bean géré serait considérablement alourdi. Par ailleurs, nous retrouverions du traitement qui ne correspondrait plus à la logique métier. Cette démarche n'est pas souhaitable. Il est préférable de séparer les problèmes.
JSF fournit un mécanisme standard de conversion et de validation permettant de traiter les saisies de utilisateurs afin d'assurer l'intégrité des données. Lorsque vous invoquez des méthodes métiers, vous pouvez donc vous fier à des données valides : la conversion et la validation permettent aux développeur de ce concentrer sur la logique métier au lieu de passer du temps à vérifier que les données saisies ne sont pas null, qu'elles appartiennent bien à un intervalle précis, etc.
Il est vraiment très important que les valeurs saisies soient d'abord adaptées au traitement souhaité, correspondant ainsi au format prédéfini et que les valeurs proposées correspondent également à la fourchette des valeurs requises.
La conversion a lieu lorsque les données saisies par l'utilisateur doivent être transformées de String en un objet et vice versa (le protocole HTTP ne manipule que les chaînes de caractères). Elle garantit que les informations sont du bon type - en convertissant, par exemple, un String en java.util.Date, un String en Integer ou des dollars en euros. Comme pour la validation, elle garantit que les données contiennent ce qui est attendu (une date au format jj/mm/aaaa, un réel compris entre 3.14 et 3.15, etc.).
Je fais juste un petit rappel sur le cycle de traitement des requêtes JSF afin de voir où se situe les phases de conversions et de validations.
JSF fournit un ensemble de convertisseurs et de validateurs standard et vous permet également de créer les vôtres très facilement.
.
Lorsqu'un formulaire est affiché par un navigateur, l'utilisateur remplit les champs et appuie sur un bouton de soumission, ayant pour effet de transporter les données vers le serveur dans une requête HTTP formée de chaînes. Avant de mettre à jour le modèle du bean géré, ces données textuelles doivent être converties dans les objets cibles (Float, Integer, BigDecimal, etc.). L'opération inverse aura lieu lorsque les données seront renvoyées au client dans la réponse pour être affichées par le navigateur.
Il faut se souvenir que les valeurs qui sont soumises aux travers des requêtes ne sont, en réalité, que des chaînes de caractères puisque le proptocole HTTP ne connaît que ce type de données. Par contre, le modèle métier a besoin de travailler avec des données d'autres types. Par ailleurs, la représentation des valeurs sur la page Web doit être généralement affinée afin que l'utilisateur voit bien de quoi il s'agit. Il faut que l'interface utilisateur soit la plus ergonomique possible en proposant une information adaptée et bien présentée. Pour toutes ces raisons, nous avons besoin de convertir les données en pensant bien qu'il s'agit en réalité bien souvent d'un formatage sous forme de texte côté utilisateur.
JSF fournit des convertisseurs pour les types classiques comme les dates et les nombres. Si une propriété du bean géré est de type primitif (Integer, int, Float, float, etc.), JSF convertira automatiquement la valeur du composant d'interface dans le type adéquat et inversement. Si elle est d'un autre type, vous devrez fournir votre propre convertisseur.
JSF convertira automatiquement les valeurs saisies en nombre lorsque la propriété du bean géré est d'un type numérique primitif et en date ou en heure lorsque la propriété est d'un type date. Si ces conversions automatiques ne conviennent pas, afin d'avoir un aspect visuel plus confortable par exemple, vous pouvez les contrôler explicitement via les marqueurs standard <f:convertNumber> et <f:convertDateTime>. Pour ce faire, vous devez imbriquer le convertisseur dans un marqueur d'entrée ou de sortie.
<h:inputText value=#{conv.franc} readonly=> <f:convertNumber type= currencySymbol=/> </h:inputText>
<h:outputText value=#{test.aujourdhui}> <f:convertDateTime dateStyle=/> </h:outputText> <h:inputText value=#{test.expiration}> <f:convertDateTime pattern=/> </h:inputText>
Il ne faut pas oublier que JSF est déjà très compétent puisqu'il propose des conversions implicites entre les types primitifs : int, double, long, etc. et les chaîne de caractères String prévues par le protocole HTTP. Automatiquement, le modèle métier retrouve les valeurs dans le type désiré sans qu'il soit nécessaire d'écrire quoique se soit sur la page JSF.
En effet, lorsque vous désirez représenter des nombres réels sans présentation particulière, comme les pourcentages et les valeurs monétaires, vous pouvez donc vous passer de mettre en place le mécanisme de conversion explicite.
Ceci dit, même si cela n'est pas écrit, JSF utilise quand même un mécanisme de conversion qui permet de mettre en relation des classes spécialisées dans le type de conversion à réaliser avec les classes enveloppes des types primitifs.
Classes enveloppes | Convertisseur |
---|---|
java.math.BigDecimal | javax.faces.convert.BigDecimalConverter |
java.math.BigInteger | javax.faces.convert.BigIntegerConverter |
java.lang.Boolean | javax.faces.convert.BooleanConverter |
java.lang.Byte | javax.faces.convert.ByteConverter |
java.lang.Character | javax.faces.convert.CharacterConverter |
java.util.Date | javax.faces.convert.DateTimeConverter |
java.lang.Double | javax.faces.convert.DoubleConverter |
java.lang.enum | javax.faces.convert.EnumConverter |
java.lang.Float | javax.faces.convert.FloatConverter |
java.lang.Integer | javax.faces.convert.IntegerConverter |
java.lang.Long | javax.faces.convert.LongConverter |
java.lang.Number | javax.faces.convert.NumberConverter |
java.lang.Short | javax.faces.convert.ShortConverter |
Le choix du formatage et de la conversion standard s'exprime toujours côté page Web. Nous allons donc tout de suite nous pencher sur l'ensemble des attributs qui composent la balise <f:convertNumber> qui prévoit le formatage et la conversion des valeurs numériques :
Attributs | Type | Description |
---|---|---|
type | String | number (par défaut) pour des nombres réels classiques, currency pour les valeurs monétaires, ou percent pour les valeurs exprimées en poucentage. La représentation graphique de ces valeurs numériques tient compte de la séparation des milliers et de la séparation entre la partie entière et la partie décimale en respectant la région utilisée. Pour la France, nous aurons donc un espace pour la sépartion des milliers, et une virgule pour séparer la partie entière de la partie décimale. |
pattern | String | Permet de proposer un motif de présentation personnalisé qui utilise derrière les compétences de la classe concrète java.text.DecimalFormat. A titre d'exemple, voici le motif correspondant à l'expression d'une valeur monétaire en Franc avec uniquement deux chiffres décimaux : "#,##0.00 F". |
maxFractionDigits | int | Impose une valeur limite sur le nombre de chiffres décimaux à représenter. |
minFractionDigits | int | Impose un nombre de chiffres décimaux à représenter. Si le nombre ne possède pas de valeurs décimales, alors les chiffres 0 sont proposés. |
maxIntegerDigits | int | Impose une valeur limite sur le nombre de chiffres sur la partie entière de la valeur réelle à représenter. |
minIntegerDigits | int | Impose un nombre de chiffres sur la partie entière à représenter. |
integerOnly | boolean | Lorsque cet attribut est validé, le système vérifie que seule la partie entière est soumise (false par défaut). |
groupingUsed | boolean | Permet de prendre en compte ou pas la séparation des milliers (true par défaut). |
locale | java.util.Locale ou String | Choix du paramètre régional à utiliser. |
currencyCode | String | Spécification de la monnaie à prendre en compte comme "EUR" par exemple. |
currencySymbol | String | Choix d'une autre monnaie non prévue par le standard et permet ainsi de personnaliser l'affichage. |
De la même façon, nous allons consulter l'ensemble des attributs de la balise <f:convertDateTime> qui prévoit le formatage et la conversion de la date et ou de l'heure associées à un objet de type Date.
Attributs | Type | Description |
---|---|---|
type | String | date (par défaut) pour prendre en compte la date uniquement, time pour prendre en compte l'heure uniquement, ou both pour prendre en compte à la fois la date et l'heure. |
dateStyle | String | default, short, medium, long ou full. Affiche la date avec plus ou moins de renseignements. |
timeStyle | String | default, short, medium, long ou full. Affiche l'heure avec plus ou moins de renseignements. |
pattern | String | Permet de proposer un motif de présentation personnalisé qui utilise derrière les compétences de la classe concrète java.text.SimpleDateFormat. |
locale | java.util.Locale ou String | Choix du paramètre régional à utiliser. |
timeZone | java.util.TimeZone | Tient compte du fuseau horaire. |
Nous allons reprendre le projet conversion monétaire entre les €uros et les francs que nous avons déjà abordé lors de l'étude précédente. Nous allons juste changer son aspect en prévoyant une autre feuille de style et nous allons rajouter l'affichage de la date et de l'heure en cours comme cela vous est présenté ci-dessous :
root { display: block; } body { background-color: green; color: yellow; font-weight: bold; } .cadre { background: darkgreen; border-radius: 5px; padding: 7px; box-shadow: -1px -1px 3px lightgreen, 2px 2px 5px black; } .saisie { background: yellow; color: darkgreen; font-weight: bold; text-align: right; padding-right: 7px; padding-left: 7px; margin-right: 15px; margin-left: 15px; width: 170px; } .entete { padding-bottom: 10px; } .titre { color: green; text-shadow: -1px -1px 1px black, 1px 1px 1px lightgreen; margin: 10px; } .jour, .erreur { border-radius: 3px; box-shadow: 1px 1px 3px black inset, 1px 1px 1px lightgreen; padding-left: 15px; padding-right: 15px; text-align: center; margin: 5px; } .erreur { width: 460px; display: block; margin-left: 27px; margin-top: 10px; margin-bottom: 5px; padding: 3px; }
package bean; import javax.faces.bean.*; @ManagedBean(name= ) @ViewScoped public class Conversion { private final double TAUX = 6.55957; private double euro; private double franc; public double getEuro() { return euro; } public void setEuro(double euro) { this.euro = euro; } public double getFranc() { return franc; } public void setFranc(double franc) { this.franc = euro * TAUX; } public Date getDate() { return new Date(); } public void euroFranc() { franc = euro * TAUX; } }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet library= name= /> <h:body> <h:form> <h:panelGrid styleClass=cadre headerClass= columns=> <f:facet name=> <h:outputText value= styleClass=titre /> <h:outputText value=#{conv.date} styleClass=jour> <f:convertDateTime pattern= /> </h:outputText> <h:outputText value=#{conv.date} styleClass=jour> <f:convertDateTime type= timeStyle= timeZone=/> </h:outputText> </f:facet> <h:inputText value=#{conv.euro} styleClass=saisie> <f:convertNumber type= currencySymbol=/> </h:inputText> <h:commandButton value= action=#{conv.euroFranc} /> <h:inputText value=#{conv.franc} readonly= styleClass=saisie> <f:convertNumber type= currencySymbol=/> </h:inputText> </h:panelGrid> </h:form> </h:body> </html>
Que se passe-t-il lorsque nous proposons une mauvaise valeur dans la zone de saisie ? Qu'est-ce que nous entendons d'ailleurs par "mauvaise valeur" ? Imaginons que nous tapions une chaîne de caractères à la place de la valeur réelle attendue, et que nous cliquions sur l'un des boutons de traitement. Nous avons l'impression que rien ne se passe. En tout cas le traitement demandé n'est pas exécuté. En réalité, le système se protège de toute erreur de conversion. Si la conversion ne peut être réalisée convenablement, la logique métier (le bean géré) n'est pas sollicité.
Lorsqu'une erreur de conversion se produit, les actions suivantes sont demandées :
Le problème, dans cette situation, c'est que l'utilisateur n'est pas tellement au courant de ce qui se passe. Il a l'impression que le système ne fonctionne pas. Il serait souhaitable qu'il soit averti de sa mauvaise saisie. Pour cela, nous devons rajouter une balise <h:message> qui éventuellement récupère le message qui a été posté par le composant provoquant l'erreur. Rappelez-vous que nous devons choisir un identificateur afin d'établir la relation entre la balise <h:inputText> correspondant à la zone de saisie (au travers de l'attribut id) avec la balise de message d'erreur <h:message> (par l'intermédiaire de l'attribut for).
Typiquement, pour les erreurs de saisie, les messages sont associés avec un composant particulier et précise qu'une erreur de conversion ou de validation est survenue. Bien que nous utilisons les messages souvent de la même façon, il existe en réalité quatre variétés de message : Information, Avertissement, Erreur et Fatal.
Tous les types de message comporte une description sommaire et une description détaillée. Par exemple, de façon sommaire nous pourrions dire que l'entrée est invalide, alors que dans le détail nous pourrions indiquer que le nombre saisi est plus grand que la valeur maximal prévue. Les applications JSF utilise deux balises pour représenter ces messages : <h:messages> et <h:message>.
Attributs | Description |
---|---|
errorClass, errorStyle, fatalClass, fatalStyle, infoClass, infoStyle, warnClass, warnStyle | Styles CSS associés aux quatre types de message d'erreur. |
for | Identifiant désignant le composant à prendre en compte pour afficher un éventuel message d'erreur le concernant. (<h:message> uniquement). |
globalOnly | Instruction imposant d'afficher uniquement les messages d'erreur globaux, les messages particuliers étant alors désactivés. (<h:messages> uniquement). |
layout | Permet de spécifier l'orientation de l'ensemble des messages d'erreur survenant : "table" ou "list". (<h:messages> uniquement). |
showDetail | Booléen qui détermine si le message d'erreur doit être affiché sous forme détaillé. Par défaut, les valeurs sont false pour <h:messages> et true pour <h:message>. |
showSummary | Booléen qui détermine si le message d'erreur doit être affiché sous forme simplifiée. Par défaut, les valeurs sont true pour <h:messages> et false pour <h:message>. |
tooltip | Booléen qui détermine si un message détaillé doit être rendu visible dans une petite bulle d'avertissement. Cet avertissement ne sera toutefois visible uniquement à la condition que les attributs showDetail et showSummary soient validés. |
Les messages d'erreurs proposées sont automatiquement traduite dans la langue locale. Ils sont relativement adaptés à la situation. Il est également possible de proposer un message personnalisé sur chaque balise de saisie au moyen des attributs converterMessage, validatorMessage et requiredMessage.
<h:body> <h:form> <h:panelGrid styleClass=cadre headerClass= columns=> <f:facet name=> <h:outputText value= styleClass=titre /> <h:outputText value=#{conv.date} styleClass=jour> <f:convertDateTime pattern= /> </h:outputText> <h:outputText value=#{conv.date} styleClass=jour> <f:convertDateTime type= timeStyle= timeZone=/> </h:outputText> </f:facet> <h:inputText value=#{conv.euro} styleClass=saisie id=euro> <f:convertNumber type= currencySymbol=/> </h:inputText> <h:commandButton value= action=#{conv.euroFranc} /> <h:inputText value=#{conv.franc} readonly= styleClass=saisie> <f:convertNumber type= currencySymbol=/> </h:inputText> <f:facet name=> <h:message for= styleClass=erreur/> </f:facet> </h:panelGrid> </h:form> </h:body>
Deux choses me perturbent dans cet affichage par défaut, d'une part l'intitulé j_idt7:euro, et d'autre part la proposition de l'exemple Exemple : {1} qui ne me semble pas adapté pour ce cas de figure.
<h:inputText value=#{conv.euro} styleClass=saisie label="Saisie des euros" id=euro>
<h:message for= styleClass=erreur showSummary= showDetail=/>
<h:inputText value=#{conv.euro} styleClass=saisie id=euro converterMessage=> <f:convertNumber type= currencySymbol=/> </h:inputText>
Contrairement aux convertisseurs, les validations concernent uniquement les composants qui permettent la saisie des valeurs. La validation permet de s'assurer que le modèle métier ne se trouve pas dans une situation incohérente. En effet, la validation standard JSF contrôle la donnée saisie et vérifie qu'elle correspond bien au canevas souhaité. Si ce n'est pas le cas, le modèle métier n'est pas sollicité, et nous passons directement à la phase du rendu de la réponse.
Le modèle métier est ainsi protégé de toutes mauvaises valeurs non prévues. Du coup, le code du bean géré se trouve allégé et ne dispose que des opérations nécessaires correspondantes uniquement à la logique métier sans se préoccuper des cas ambigüs. C'est la vue qui s'occupe de faire en sorte que les bonnes valeurs soient bien introduites. Ainsi, chacun s'occupe de son propre travail avec une nette séparation entre la récupération des données d'une part, et le traitement de ces dernières d'autre part.
JSF simplifie la validation des données en fournissant des validateurs standard et en permettant d'en créer de nouveaux, adaptés à vos besoin. Les validateurs agissent comme des contrôles de premier niveau en validant les valeurs des composants de l'interface utilisateur avant qu'elles ne soient traitées par le bean géré.
Dans JSF, les composants d'interface mettent en oeuvre des validations simples qui permettent de :
Balises | Classe de validation | Attributs | Valeur |
---|---|---|---|
<f:validateDoubleRange> | DoubleRangeValidator | minimum maximum |
Compare la valeur du composant aux valeurs minimales et maximales indiquées (de type double). |
<f:validateLongRange> | LongRangeValidator | minimum maximum |
Compare la valeur du composant aux valeurs minimales et maximales indiquées (de type long). |
<f:validateLength> | LenghtValidator | minimum maximum |
Teste le nombre de caractères de la valeur textuelle du composant. |
<f:validateRequired> | RequiredValidator | Vérife qu'une valeur est bien saisie dans le composant (donnée obligatoire). | |
<f:validateRegex> | RegexValidator | motif | Compare la valeur du composant à une expression régulière. |
<f:validateBean> | BeanValidator | Groupe de validations | Vérifie qu'un groupe de validations est bien respecté au travers d'un composant spécifique dont les propriétés sont déjà préétablies. |
<h:inputText value="#{commande.codePostal}"> <f:validateLength minimum="5" maximum="5" /> <f:validateLongRange minimum="1000" maximum="98900" /> </h:inputText>
<h:inputText value="#{commande.codePostal}"> <f:validateRequired /> <f:validateLength minimum="5" maximum="5" /> <f:validateLongRange minimum="1000" maximum="98900" /> </h:inputText>
<h:inputText value="#{commande.codePostal}" required="true"> <f:validateLength minimum="5" maximum="5" /> <f:validateLongRange minimum="1000" maximum="98900" /> </h:inputText>
<h:inputText value="#{commande.codePostal}" required="true"> <f:validator validatorId="javax.faces.validator.LengthValidator"> <f:attribute name="minimum" value="5" /> <f:attribute name="maximum" value="5" /> </f:validator> <f:validator validatorId="javax.faces.validator.LongRangeValidator"> <f:attribute name="minimum" value="1000" /> <f:attribute name="maximum" value="98900" /> </f:validator> </h:inputText>
<h:form> <h:outputText value= /> <h:inputText value=#{test.mail} id="erreur" validatorMessage=> <f:validateRegex pattern= /> </h:inputText> <h:message for="erreur" /> </h:form>
A l'image de la conversion, nous pouvons proposer des messages associés à des problèmes de validation. L'idéal est de proposer des messages personnalisés à l'aide des attributs spécifiques validatorMessage et requiredMessage, qui sont présent notamment dans les zones de saisie. En reprenant l'exemple de la conversion monétaire, voici ce que nous pouvons faire :
<h:body> <h:form> <h:inputText value=#{conv.euro} styleClass="saisie" id="euro" required= converterMessage= validatorMessage=requiredMessage=> <f:convertNumber type= currencySymbol=/> <f:validateDoubleRange minimum= /> </h:inputText> <h:commandButton value= action=#{conv.euroFranc} /> <h:inputText value=#{conv.franc} readonly= styleClass="resultat"> <f:convertNumber type= currencySymbol=/> </h:inputText> <p><h:message for="euro" styleClass="erreur"/></p> </h:form> </h:body>
Grâce à ce système de validations, notre application Web devient très performante puisque nous contrôlons parfaitement toute les saisies réalisées par l'opérateur. Il existe quand même une situation où ce système de validations s'avère génant. Imaginons que l'utilisateur, après avoir afficher une page web correspondant à une commande en ligne, désire sortir sans finalement passer sa commande. Pour cela, il clique sur un bouton "Annuler". Voici ce qu'il se produit alors :
Le problème, c'est que pour passer vers une autre page Web, nous sollicitons systématiquement ce système de validations. Or l'utilisateur n'a saisie aucune valeur puisqu'il désirait ressortir tout de suite. Du coup, nous sommes dans une impasse. Vous êtes obliger de saisir les valeurs pour pouvoir ressortir. Ce qui est totalement incohérent.
Pour sortir de cette impasse, il existe une solution très simple qui a été justement mis en place pour prévenir ce cas d'utilisation. Si vous désirez inhiber le système de validation, vous devez activer l'attribut immediate (avec la valeur true) des balises correspondant à la navigation, comme le sont les balises <h:commandButton>, <h:button>, <h:commandLink> ou <h:link>. Voici ce que nous pourrions écrire dans le code correspondant à la vue ci-dessus :
<h:form> <h:panelGrid columns="3" styleClass="général" rowClasses="un, deux" cellpadding="3"> <h:outputText value="Article : " /> <h:inputText value="#{commande.article}" id="article" required="true" requiredMessage="Précisez votre article."/> <h:message for="article" errorClass="erreur"/> <h:outputText value="Prix : " /> <h:inputText value="#{commande.prix}" id="prix" required="true" validatorMessage="Le prix doit être compris entre 10 et 10000."> <f:convertNumber minFractionDigits="2" maxFractionDigits="2"/> <f:validateDoubleRange minimum="10" maximum="10000" /> </h:inputText> <h:message for="prix" errorClass="erreur" /> <h:outputText value="Quantité : " /> <h:inputText value="#{commande.quantité}" size="1" id="quantité" validatorMessage="La quantité n'est pas bonne !" > <f:validateLongRange minimum="1" /> </h:inputText> <h:message for="quantité" errorClass="erreur" /> <h:outputText value="Adresse : " /> <h:inputText value="#{commande.adresse}" id="adresse" required="true"
requiredMessage="Donnez votre adresse."/> <h:message for="adresse" errorClass="erreur" /> <h:outputText value="Code postal : " /> <h:inputText value="#{commande.codePostal}" size="3" maxlength="5" id="codePostal" required="true" validatorMessage="Votre code postal n'est pas bon !"> <f:validateLength minimum="5" maximum="5" /> <f:validateLongRange minimum="1000" maximum="98900" /> </h:inputText> <h:message for="codePostal" errorClass="erreur" /> <h:outputText value="Ville : " /> <h:inputText value="#{commande.ville}" id="ville" required="true"
requiredMessage="Précisez le nom de la ville."/> <h:message for="ville" errorClass="erreur" /> <h:outputText value="Adresse Mail : " /> <h:inputText value="#{commande.adresseMail}" id="mail" required="true" requiredMessage="Donnez votre adresse mail."/> <h:message for="mail" errorClass="erreur"/> <h:outputText value="Carte de crédit : " /> <h:inputText value="#{commande.carte}" id="carte" required="true" requiredMessage="Le code de la carte n'est pas valide !" validatorMessage="Le code de la carte n'est pas valide !"> <f:validateLength minimum="13" /> </h:inputText> <h:message for="carte" errorClass="erreur" /> <h:outputText value="Expiration (ex: 02/2007) : " /> <h:inputText value="#{commande.expiration}" size="7" id="expiration" required="true" requiredMessage="Précisez la date de validation."> <f:convertDateTime pattern="MM/yyyy" /> </h:inputText> <h:message for="expiration" errorClass="erreur" /> </h:panelGrid> <hr /> <h:commandButton action="Commander" value="Commander" /> <h:commandButton action="Annuler" value="Annuler" immediate="true" /> </h:form>
Depuis la version JSF 2.0, il existe une autre façon d'utiliser les validations standard. Plutôt que de les évoquer au niveau de la vue, il est possible maintenant de les spécifier directement dans le bean géré sur chacun des attributs où vous désirez placer une contrainte particulière. Cela se fait au travers d'annotation spécifiques dont voici la liste :
Annotation | Attributs | Description |
---|---|---|
@Null, @NotNull | Aucun | Vérifie que l'objet existe ou pas. |
@Min, @Max | Valeurs limites (long) | Vérifie que la valeur proposée n'atteint pas les limites spécifiées. S'applique pour les entiers (int, long, short, byte, BigInteger et BigDecimal). Les réels ne sont pas pris en compte (double, float). |
@DecimalMin, @DecimalMax | Valeurs limites (String) | Même chose, mais peut s'appliquer aussi pour les chaînes de caractères. |
@Digits | integer, fraction | Vérife qu'une valeur ne dépasse pas le nombre de chiffres spécifier par la partie entière (avant la virgule) et la partie décimale (après la virgule). S'applique pour les entiers et les chaînes de caractères (int, long, short, byte, BigInteger, BigDecimal et String). |
@AssertTrue, @AssertFalse | Aucun | Vérifie que la valeur saisie est bien un booléen avec soit la valeur true, soit la valeur false. |
@Past, @Future | Aucun | Vérifie que la date proposée est antérieure ou postérieure à la date actuelle. |
@Size | min, max | Vérifie que la taille de la chaîne, du tableau, d'une collection ou d'une carte respecte bien les limites imposées. |
@Pattern | regexp, flags | Vérifie que la valeur proposée respecte bien l'expression régulière ou les options de compilation. |
Toutes ces validations possèdent systématiquement deux attributs supplémentaires, message et groups. A l'aide de l'attribut message, vous pouvez préciser l'intitulé de l'alerte qui sera affiché ensuite par la balise <h:message>. Nous verrons l'intérêt de l'attribut groups un peu plus loin dans ce chapitre.
La validation proposée directement par le bean géré possède un énorme avantage. Supposez que vous ayez une application web qui intègre plusieurs pages pour un même bean géré. Vous n'êtes pas obligé de rajouter les règles de validation sur chacune des pages, ce qui est spécifié par le bean géré se répercute sur chacune de ces pages. Par ailleurs, si un même attribut possède plusieurs validations, vous pouvez proposer, grâce à cette technique, un message d'erreur circonstancié.
Chacune de ses coordonnées sont vérifiées et dans la vue que je vous montre, sur la partie droite, se trouve les messages d'erreur circonstanciés.
package bean; import java.util.Date; import javax.faces.bean.*; import javax.validation.constraints.*; @ManagedBean @SessionScoped public class Personne { @Size(min=2, message= ) private String nom; @NotNull @Past private Date naissance; @Pattern(regexp=\\\\\\\\\\ , message= ) private String mail; @Min(value=1000, message= ) @Max(value=98900, message= ) private int codePostal; public String getNom() { return nom; } public void setNom(String identité) { this.nom = identité; } public Date getNaissance() { return naissance; } public void setNaissance(Date naissance) { this.naissance = naissance; } public String getMail() { return mail; } public void setMail(String mail) { this.mail = mail; } public int getCodePostal() { return codePostal; } public void setCodePostal(int codePostal) { this.codePostal = codePostal; } }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <style type=> body { background: green; color: yellow; font-weight: bold; text-shadow: 3px 3px 10px black; } .panneau { background: darkgreen; border-radius: 5px; box-shadow: 1px 1px 3px black; } .titre { font-size: 20px; } </style> <h:body> <h:form> <h:panelGrid columns= styleClass=panneau headerClass=titre cellspacing=> <f:facet name=>Vos coordonnées<hr /></f:facet> <h:outputText value=/> <h:inputText value=#{personne.nom} id=nom /> <h:message for= /> <h:outputText value= /> <h:inputText value=#{personne.naissance} id=naissance validatorMessage=> <f:convertDateTime pattern= /> </h:inputText> <h:message for= /> <h:outputText value= /> <h:inputText value=#{personne.mail} id=mail /> <h:message for= /> <h:outputText value= /> <h:inputText value=#{personne.codePostal} id=codePostal /> <h:message for= /> <f:facet name=><hr /><h:commandButton value= /></f:facet> </h:panelGrid> </h:form> </h:body> </html>
Globalement, je trouve que cette approche est plus lisible et plus directe.
Revenons à notre souci de convertir les valeurs afin qu'elles soient mises en forme, en étant adaptées aux propriétés prévues par le modèle. Les convertisseurs standard proposent déjà des conversions qui, dans 90% des cas, nous permettront de résoudre la plupart des problèmes rencontrés. Toutefois, il peut arriver dans certaines situations, que ces conversions standard ne résolvent pas notre problème particulier. Il faut alors proposer une conversion personnalisée adaptée à la situation présente.
J'aimerais, par exemple, pouvoir réaliser du traitement logique sur des valeurs
binaires. Les calculs à entreprendre sont le OU logique, le ET logique
et le OU Exclusif, sur deux valeurs binaires de 16 bits. Nous
souhaitons, par ailleurs, représenter et saisir les nombres binaires
par quartets (paquets de 4 bits). Par défaut, les valeurs binaires ne sont pas interprétées directement par le système, nous devons donc prévoir une conversion personnalisée.
Notre modèle MVC possède donc une seule page Web index.xhtml avec son modèle associé bean.Calcul, qui s'occupe de la logique métier. Par contre, vous découvrez entre les deux, une classe intermédiaire conversion.Binaire (passage obligatoire) qui s'occupe de la conversion entre une valeur binaire écrite sous forme de chaîne de caractères et une valeur décimale entière.
Souvenez-vous, qu'avant de soumettre la valeur au propriétés du modèle, le cycle de traitement des requêtes JSF nous impose de passer d'abord par une conversion éventuelle. Dans le même type de raisonnement, l'affichage de la valeur, proposée par le modèle passe également par une phase de conversion avant d'avoir le rendu effectif. Pour résumer, nous devons systématiquement passer par la conversion pour échanger des valeurs entre la page XHTML et le bean géré correspondant.
D'une façon générale, un convertisseur est une classe qui assure une conversion entre une chaîne de caractères (type String) et un objet (type Object). Effectivement, avec le protocole HTTP, les requêtes qui sont envoyées, sont toujours des chaînes de caractères qu'il faut ensuite transformer dans le type requis. Pour assurer la conversion vers n'importe quel type, l'idéal est donc de prendre plutôt un type générique comme la classe Object.
Lorsque vous désirez créer votre propre convertisseur, vous devez mettre en oeuvre une classe qui implémente l'interface Converter qui possède deux méthodes que vous devez donc impérativement redéfinir.
interface Converter {
public Object getAsObject(FacesContext context, UIComponent component, String value) throws ConvertException;
public String getAsString(FacesContext context, UIComponent component, Object value) throws ConvertException;
}
Pour utiliser un convertisseur dans une application web, il doit être enregistré. Cela consiste simplement à placer l'annotation spécifique @FacesConverter directement sur la classe qui implémente la conversion avec le nom d'objet que vous désirez.
package bean; import javax.faces.bean.*; @ManagedBean @ViewScoped public class Calcul { private int premier; private int deuxième; private int résultat; public int getPremier() { return premier; } public void setPremier(int premier) { this.premier = premier; } public int getDeuxième() { return deuxième; } public void setDeuxième(int deuxième) { this.deuxième = deuxième; } public int getRésultat() { return résultat; } public void ouLogique() { résultat = premier | deuxième; } public void etLogique() { résultat = premier & deuxième; } public void ouExclusif() { résultat = premier ^ deuxième; } }
Ce bean, finalement, est très simple, pour ne pas dire rudimentaire. Nous disposons de trois propriétés : premier, deuxième et résultat qui sont des entiers en base 10. Le bean possède trois méthodes supplémentaires : ouLogique(), etLogique() et ouExclusif() qui s'occupent d'effectuer le calcul demandé et seront donc sollicitées par les boutons correspondant prévues par la page xhtml.
package convertisseur; import java.text.*; import javax.faces.component.*; import javax.faces.context.FacesContext; import javax.faces.convert.*; @FacesConverter( ) public class Binaire implements Converter { @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { StringBuilder chaîne = new StringBuilder(value); for (int i=0 ; i<chaîne.length(); ) if (Character.isDigit(chaîne.charAt(i))) i++; else chaîne.deleteCharAt(i); return Integer.parseInt(chaîne.toString(), 2); } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { long décimal = Long.parseLong(Integer.toBinaryString((Integer)value)); DecimalFormat binaire = new DecimalFormat( ); return binaire.format(décimal); } }
Revenons à notre sujet principal. La classe Binaire permet donc, au travers de la méthode getAsObject(), de récupérer une valeur binaire saisie par l'opérateur avec éventuellement des espaces entre les quartets. Cette valeur binaire est ensuite tranformée dans la valeur décimale équivalente. La méthode getAsString() prend la valeur décimale et fabrique une chaîne de caractères qui représente la valeur binaire correspondante et la formate pour qu'elle affiche tous les chiffres binaires (les 16 bits) par groupe de 4, avec comme séparateur, le séparateur espace.
Grâce à l'annotation @FacesConverter, nous déclarons un nouveau convertisseur qui se nomme binaire qui pourra par la suite être directement utilisé par la ou les pages web. Il existe une deuxième notation possible pour déclarer ce convertisseur :
@FacesConverter(value= ) public class Binaire implements Converter { @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { ... } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { ... } }
root { display: block; } body { background: green; color: yellow; font-weight: bold; text-shadow: 0px 0px 10px yellow; text-align: center; } .panneau { background: darkgreen; border-radius: 5px; box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen; } .titre { font-size: 20px; } input[type= ] { text-align: center; border-radius: 5px; box-shadow: 2px 2px 5px black inset; }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet name= /> <h:body> <h:form> <h:panelGrid columns= styleClass="panneau" headerClass="titre"> <f:facet name=>Calcul binaire<hr /></f:facet> <h:inputText value=#{calcul.premier} converter="binaire" /> <h:inputText value=#{calcul.deuxième} converter="binaire" /> <h:panelGroup> <h:commandButton value= action=#{calcul.ouLogique} /> <h:commandButton value= action=#{calcul.etLogique} /> <h:commandButton value= action=#{calcul.ouExclusif} /> </h:panelGroup> <h:inputText value=#{calcul.résultat} converter="binaire" readonly=/> </h:panelGrid> </h:form> </h:body> </html>
Pour utiliser ce convertisseur binaire, la solution la plus simple consiste à utiliser l'attribut converter de la balise <h:inputText>. Vous spécifiez alors le nom du convertisseur défini pa l'annotation @FacesConverter, ici binaire.
Une autre alternative, consiste à utiliser la balise <f:converter> à l'intérieur de la balise <h:inputText> et de spécifier le nom du convertisseur au travers de l'attribut converterId.
<h:panelGrid columns= styleClass="panneau" headerClass="titre"> <f:facet name=>Calcul binaire<hr /></f:facet> <h:inputText value=#{calcul.premier}> <f:converter converterId="binaire"> </h:inputText> <h:inputText value=#{calcul.deuxième}> <f:converter converterId="binaire"> </h:inputText> <h:panelGroup> <h:commandButton value= action=#{calcul.ouLogique} /> <h:commandButton value= action=#{calcul.etLogique} /> <h:commandButton value= action=#{calcul.ouExclusif} /> </h:panelGroup> <h:inputText value=#{calcul.résultat} readonly=> <f:converter converterId="binaire"> </h:inputText> </h:panelGrid>
Attention, ce que nous venons de proposer n'est pas complet. Si jamais un utilisateur réalise une mauvaise saisie, vous allez voir apparaître une page d'erreur qui montre l'exception qui est levée. Ce type de message est désastreux, puisque les utilisateurs ne savent pas du tout de quoi il s'agit. Ils ont juste l'impression que le système est "planté". Il faut à tout prix gérer la mauvaise saisie de l'opérateur en l'avertissant du problème rencontré.
Si la saisie est mauvaise, la
conversion ne peut pas se faire correctement. C'est donc au niveau de
la conversion qu'il est nécessaire d'effectuer un contrôle sur la
saisie réalisée. Si cette saisie est incorrecte, il faut lever une
exception correspondant au problème de conversion. Ce type d'exception
existe, il s'agit de la classe ConverterException.
Si une telle exception est effectivement envoyée, la page reste en
l'état. Si vous désirez en plus avertir l'utilisateur du problème
rencontré, vous pouvez alors passer, en paramètre de l'objet ConverterException, un objet de type FacesMessage qui spécifie le message
d'erreur.
package convertisseur; import java.text.*; import javax.faces.component.*; import javax.faces.context.FacesContext; import javax.faces.convert.*; @FacesConverter( ) public class Binaire implements Converter { @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { StringBuilder chaîne = new StringBuilder(value); for (int i=0 ; i<chaîne.length(); ) if (Character.isDigit(chaîne.charAt(i))) i++; else chaîne.deleteCharAt(i); String chaîneBinaire = chaîne.toString(); if (!chaîneBinaire.matches( )) { FacesMessage erreur = new FacesMessage( , ); throw new ConverterException(erreur); } return Integer.parseInt(chaîneBinaire, 2); } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { long décimal = Long.parseLong(Integer.toBinaryString((Integer)value)); DecimalFormat binaire = new DecimalFormat( ); return binaire.format(décimal); } }
Je rappelle que par défaut la balise <h:messages> diffuse les messages d'erreur sommaire, alors que par défaut la balise <h:message> affiche le détail de ces mêmes messages d'erreur. Il est toutefois possible de changer le comportement par défaut de ces balises au travers des attributs : showDetail et showSummary. Il est ainsi possible d'afficher pour un même message d'erreur à la fois le sommaire et le détail.
Il existe différents types d'erreur :
Par défaut le type d'erreur proposé est de type informatif (SEVERITY_INFO). Il est toutefois possible de proposer un type d'erreur particulier au travers de la méthode setSeverity() de la classe FacesMessage. Vous avez une autre alternative qui consiste à prendre le constructeur de la classe FacesMessage avec trois paramètres, dont le premier est prévu pour le type d'erreur souhaité.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet name= /> <h:body> <h:form> <h:panelGrid columns= styleClass="panneau" headerClass="titre" cellspacing= > <f:facet name=>Calcul binaire<hr /></f:facet> <h:inputText value=#{calcul.premier} converter="binaire" id="premier" /> <h:message for="premier" /> <h:inputText value=#{calcul.deuxième} converter="binaire" id="deuxieme" /><h:message for="deuxieme" /> <h:panelGroup> <h:commandButton value= action=#{calcul.ouLogique} /> <h:commandButton value= action=#{calcul.etLogique} /> <h:commandButton value= action=#{calcul.ouExclusif} /> </h:panelGroup> <h:inputText value=#{calcul.résultat} converter="binaire" readonly=/> </h:panelGrid> </h:form> </h:body> </html>
Dans la vue, il suffit de rajouter les balises <h:message> et d'adapter le panneau en conséquence.
.
Comme pour les convertisseurs, il est possible de définir ces propres validations. Si vous le souhaitez, vous pouvez rajouter une validation personnalisée avec un convertisseur personnalisé. Rien ne l'empêche. Par contre, suivant le cycle de traitement des requêtes JSF, la validation ne s'effectue qu'après la conversion. Effectivement, la valeur doit d'abord être mise en forme avant de pouvoir être évaluée. Ce n'est qu'après cette nouvelle phase que la valeur est éventuellement injectée dans le bean métier suivant le résultat du test.
A titre d'exemple, nous allons
mettre en oeuvre une validation, qui n'existe pas en standard, qui va
contrôler si la valeur saisie est un nombre premier. Nous prenons comme support l'application Web précédente.
Attention, contrairement aux convertisseurs personnalisés, la validation ne s'intéresse qu'à une valeur saisie par l'opérateur. Elle n'est utilisée que dans ce sens là. Elle ne s'intéresse pas du tout au rendu d'une réponse venant de la logique métier, puisque la valeur qui se trouve dans le bean géré correspondant est nécessairement correcte.
Comme les convertisseurs, un validateur est une classe qui doit implémenter une interface et redéfinir une méthode. Dans le cas des validateurs, cette interface est javax.faces.validator.Validator, qui n'expose que la méthode validate(). Voici d'ailleurs la déclaration complète de cette interface :
interface Validator {
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException;
}
Cette méthode validate() comporte les mêmes paramètres que les méthodes issues de l'interface Converter. Toutefois, validate() ne renvoie pas de valeur. Effectivement, cette méthode teste uniquement si la valeur récupérée par le paramètre value correspond au critère de validation choisi. Si la valeur est correcte, rien ne se passe en particulier, si ce n'est que le cycle continu et propose donc cette valeur au modèle métier. Dans le cas contraire, une exception de type ValidatorException est levée avec un message d'erreur adaptée à la situation. Dans ce cas là, le cycle normal est interrompu, et le rendu de la page xhtml est alors proposé. La page JSF peut alors afficher le message d'erreur proposé.
Comme pour le convertisseur, un validateur doit être enregistré afin de pouvoir être utilisé dans la page web. Cela consiste simplement à placer cette fois-ci l'annotation spécifique @FacesValidator directement sur la classe qui implémente la validation avec le nom d'objet que vous désirez.
package validateur; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.validator.*; @FacesValidator( ) public class NombrePremier implements Validator { @Override public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { int nombre = (Integer)value; for (int i=2; i<=Math.sqrt(nombre); i++) { if (nombre%i == 0) { String sommaire = context.getMessages().hasNext() ? : ; FacesMessage erreur = new FacesMessage(sommaire, ); throw new ValidatorException(erreur); } } } }
La classe NombrePremier implémente le validateur personnalisé en redéfinissant la méthode validate(). Grâce à l'annotation @FacesValidator, nous déclarons un nouveau validateur qui se nomme nombrePremier qui pourra par la suite être directement utilisé par la ou les pages web.
package bean; import javax.faces.application.FacesMessage; import javax.faces.bean.*; import javax.faces.context.FacesContext; @ManagedBean @ViewScoped public class Calcul { private int premier; private int deuxième; private int résultat; public int getPremier() { return premier; } public void setPremier(int premier) { this.premier = premier; } public int getDeuxième() { return deuxième; } public void setDeuxième(int deuxième) { this.deuxième = deuxième; } public int getRésultat() { return résultat; } public void ouLogique() { résultat = premier | deuxième; FacesContext context = FacesContext.getCurrentInstance(); context.addMessage("messages", new FacesMessage( )); } public void etLogique() { résultat = premier & deuxième; FacesContext context = FacesContext.getCurrentInstance(); context.addMessage("messages", new FacesMessage( )); } public void ouExclusif() { résultat = premier ^ deuxième; FacesContext context = FacesContext.getCurrentInstance(); context.addMessage("messages", new FacesMessage( )); } }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet name= /> <h:body> <h:form> <h:panelGrid columns= styleClass="panneau" headerClass="titre" cellspacing=> <f:facet name=>Calcul binaire<hr /></f:facet> <h:inputText value=#{calcul.premier} converter="binaire" validator="nombrePremier" id="premier" /> <h:message for="premier" /> <h:inputText value=#{calcul.deuxième} converter="binaire" validator="nombrePremier" id="deuxieme" /> <h:message for="deuxieme" /> <h:panelGroup> <h:commandButton value= action=#{calcul.ouLogique} /> <h:commandButton value= action=#{calcul.etLogique} /> <h:commandButton value= action=#{calcul.ouExclusif} /> </h:panelGroup> <h:inputText value=#{calcul.résultat} converter="binaire" readonly=/> <h:messages layout= id="messages" /> </h:panelGrid> </h:form> </h:body> </html>
Pour utiliser ce validateur, la solution la plus simple consiste à utiliser l'attribut validator de la balise <h:inputText>. Vous spécifiez alors le nom du validateur défini pa l'annotation @FacesValidator, ici nombrePremier.
Une autre alternative, consiste à utiliser la balise <f:validator> à l'intérieur de la balise <h:inputText> et de spécifier le nom du validateur au travers de l'attribut validatorId.
<h:panelGrid columns= styleClass="panneau" headerClass="titre"> <f:facet name=>Calcul binaire<hr /></f:facet> <h:inputText value=#{calcul.premier} id="premier"> <f:converter converterId="binaire"> <f:validator validatorId="nombrePremier"> </h:inputText> <h:message for="premier" /> <h:inputText value=#{calcul.deuxième} id="deuxieme"> <f:converter converterId="binaire"> <f:validator validatorId="nombrePremier"> </h:inputText> <h:message for="deuxieme" /> <h:panelGroup> <h:commandButton value= action=#{calcul.ouLogique} /> <h:commandButton value= action=#{calcul.etLogique} /> <h:commandButton value= action=#{calcul.ouExclusif} /> </h:panelGroup> <h:inputText value=#{calcul.résultat} readonly=> <f:converter converterId="binaire"> </h:inputText> <h:messages layout= id="messages" /> </h:panelGrid>
Attention, ce que nous venons de proposer n'est pas complet. Si jamais un utilisateur réalise une mauvaise saisie, vous allez voir apparaître une page d'erreur qui montre l'exception qui est levée. Ce type de message est désastreux, puisque les utilisateurs ne savent pas du tout de quoi il s'agit. Ils ont juste l'impression que le système est "planté". Il faut à tout prix gérer la mauvaise saisie de l'opérateur en l'avertissant du problème rencontré.
Si la saisie est mauvaise, la
conversion ne peut pas se faire correctement. C'est donc au niveau de
la conversion qu'il est nécessaire d'effectuer un contrôle sur la
saisie réalisée. Si cette saisie est incorrecte, il faut lever une
exception correspondant au problème de conversion. Ce type d'exception
existe, il s'agit de la classe ConverterException.
Si une telle exception est effectivement envoyée, la page reste en
l'état. Si vous désirez en plus avertir l'utilisateur du problème
rencontré, vous pouvez alors passer, en paramètre de l'objet ConverterException, un objet de type FacesMessage qui spécifie le message
d'erreur.
package convertisseur; import java.text.*; import javax.faces.component.*; import javax.faces.context.FacesContext; import javax.faces.convert.*; @FacesConverter( ) public class Binaire implements Converter { @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { StringBuilder chaîne = new StringBuilder(value); for (int i=0 ; i<chaîne.length(); ) if (Character.isDigit(chaîne.charAt(i))) i++; else chaîne.deleteCharAt(i); String chaîneBinaire = chaîne.toString(); if (!chaîneBinaire.matches( )) { String sommaire = context.getMessages().hasNext() ? : ; FacesMessage erreur = new FacesMessage(sommaire, ); throw new ConverterException(erreur); } return Integer.parseInt(chaîneBinaire, 2); } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { long décimal = Long.parseLong(Integer.toBinaryString((Integer)value)); DecimalFormat binaire = new DecimalFormat( ); return binaire.format(décimal); } }
J'en profite pour changer également le code du convertisseur pour qu'il gère les messages de la même façon que la validateur. Voici d'ailleurs un exemple d'erreurs, à la fois de convertion et de validation :
Nous venons de voir comment comment implémenter un validateur au travers d'une classe spécifique, ce qui, la plupart du temps est préférable puisque nous séparons bien chacune des fonctionnalités.
Il est toutefois envisageable d'intégrer cette validation directement dans le bean géré au travers d'une méthode spécifique, dont le nom est à votre libre initiative, à la condition toutefois que cette méthode possède exactement la même signature que la méthode validate() de l'interface Validator. Voici, par exemple, la méthode nombrePremier() qui réalise la même validation que précédemment :
package bean; import javax.faces.application.FacesMessage; import javax.faces.bean.*; import javax.faces.context.FacesContext; @ManagedBean @ViewScoped public class Calcul { private int premier; private int deuxième; private int résultat; public int getPremier() { return premier; } public void setPremier(int premier) { this.premier = premier; } public int getDeuxième() { return deuxième; } public void setDeuxième(int deuxième) { this.deuxième = deuxième; } public int getRésultat() { return résultat; } public void ouLogique() { résultat = premier | deuxième; FacesContext context = FacesContext.getCurrentInstance(); context.addMessage("messages", new FacesMessage( )); } public void etLogique() { résultat = premier & deuxième; FacesContext context = FacesContext.getCurrentInstance(); context.addMessage("messages", new FacesMessage( )); } public void ouExclusif() { résultat = premier ^ deuxième; FacesContext context = FacesContext.getCurrentInstance(); context.addMessage("messages", new FacesMessage( )); } public void nombrePremier(FacesContext context, UIComponent component, Object value) throws ValidatorException { int nombre = (Integer)value; for (int i=2; i<=Math.sqrt(nombre); i++) { if (nombre%i == 0) { String sommaire = context.getMessages().hasNext() ? : ; FacesMessage erreur = new FacesMessage(sommaire, ); throw new ValidatorException(erreur); } } } }
Bien entendu, dans la vue, vous devez appeler votre validateur de façon différente, au moyen d'une expression EL, en faisant tout simplement référence à cette méthode spécifique.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet name= /> <h:body> <h:form> <h:panelGrid columns= styleClass="panneau" headerClass="titre" cellspacing=> <f:facet name=>Calcul binaire<hr /></f:facet> <h:inputText value=#{calcul.premier} converter="binaire" validator=#{calcul.nombrePremier} id="premier" /> <h:message for="premier" /> <h:inputText value=#{calcul.deuxième} converter="binaire" validator=#{calcul.nombrePremier} id="deuxieme" /> <h:message for="deuxieme" /> <h:panelGroup> <h:commandButton value= action=#{calcul.ouLogique} /> <h:commandButton value= action=#{calcul.etLogique} /> <h:commandButton value= action=#{calcul.ouExclusif} /> </h:panelGroup> <h:inputText value=#{calcul.résultat} converter="binaire" readonly=/> <h:messages layout= id="messages" /> </h:panelGrid> </h:form> </h:body> </html>
Avec JSF, nous avons une structure de type MVC où les pages xhtml s'occupent uniquement de l'affichage alors que les beans gérés s'occupent, de leurs côtés, de tout le traitement. Par ailleurs, le contrôle de l'ensemble des requêtes est assuré par une seule servlet FacesServlet qui travaille ici plutôt en tâche de fond. Du moins, avec JSF, nous nous en préoccupons pas.
Dans certaines situations, ce canevas peut être légèrement modifié afin d'intégrer des servlets supplémentaires qui prennent momentanément le contrôle d'une partie de la page Web, notamment pour gérer les flux binaires, comme des images créées de toute pièce que nous devons déployer dans un flux connexe de la page web principale.
Afin de démontrer l'utilité de ces servlets, nous allons mettre en oeuvre une application Web qui permet de délivrer l'histogramme d'une des photos présente sur le serveur. Tous les traitements nécessaires à cette application sont exécutés côté serveur Web.
L'avantage de ce système, c'est que le poste client n'a pas besoin de disposer d'un quelconque programme installé où même d'une machine virtuelle. Le client a juste besoin de disposer d'un navigateur. Ainsi chaque client peut consulter l'ensemble des photos stockés sur le serveur qui sert alors de serveur de photos.
Avec le protocole HTTP, nous manipulons essentiellement du texte. L'affichage des images dans une page Web est toutefois particulier. En effet, n'oubliez pas que, bien que cela ne se voit pas, les images sont des fichiers séparés de la page Web elle-même. Ainsi, pour afficher une image dans une page Web, nous utilisons la balise <img> du HTML et vous devez spécifier ensuite le nom du fichier image (le nom lui-même est un texte) au travers de l'attribut src. Enfin, le navigateur, à l'aide de ce nom, récupère l'image qui est finalement un flux binaire, pour l'afficher à l'endroit où se situe la balise. Dans le cas de JSF, nous avons le même comportement. Toutefois, le nom de la balise est <h:graphicImage> et l'attribut est value.
Attention, cet attribut value de cette balise <h:graphicImage> attend uniquement une entité de type texte et surtout pas un flux binaire. Le problème pour l'application que nous devons réaliser, c'est que les images à afficher ne sont pas des fichiers. En effet, dans un premier temps, nous devons retailler l'image originale afin de proposer une vignette d'une largeur de 300 pixels et permettre ainsi un téléchargement réduit en temps. Par ailleurs, l'histogramme correspondant à cette vignette, est également une image entièrement construite de toute pièce par l'application Web. La difficulté ici, c'est de mettre en relation la page Web avec ces deux images binaires construites par l'application Web.
La solution, au moment de l'utilisation de la balise <h:graphicImage>, est d'utiliser le protocole HTTP en choisissant le bon type MIME. Pour que le protocole soit activé, vous devez donc solliciter soit une nouvelle page xhtml qui va réaliser le traitement souhaité, soit une servlet. Lorsque vous devez présenter un élément qui n'est pas une page web, l'idéal est de prendre une servlet qui reste spécialisée dans le protocole HTTP, qui sait donc reconnaître les différentes requêtes proposée, mais qui est également capable de produire des documents de n'importe quel nature, comme les flux binaires représentant les images.
Il faut noter que les servlets sont bien adaptées à ce genre de situation puisqu'elles disposent de toute l'ossature nécessaire pour gérer convenablement les flux prévus par le protocole HTTP, notamment avec les objets request et response directement intégrés aux méthodes d'appel doGet() et doPost() (voir plus loin).
Le bean géré doit quelquefois être en rapport direct avec la vue actuelle, comme c'est le cas ici, par exemple, lorsque nous proposons de télécharger le fichier image de la photo actuellement affichée.
JSF définit la classe abstraite javax.faces.context.FacesContext pour représenter les informations contextuelles associées au traitement d'une requête et à la production de la réponse correspondante. Cette classe permet d'interagir avec l'interface utilisateur et le reste de l'environnement JSF.
Pour y accéder, vous devez soit utiliser l'objet implicite facesContext directement dans vos pages xhtml, soit obtenir une référence dans vos bean gérés à l'aide de la méthode statique getCurrentInstance() qui renvoie alors l'instance du thread courant (contexte actuel) et vous pourrez alors invoquer une des méthodes prévues dans le tableau ci-dessous :
FacesContext contexte = FacesContext.getCurrentInstance();
Ici, l'objet contexte représente donc la page Web qui est en communication avec notre bean géré. Nous pouvons, dès lors l'utiliser pour effectuer un certain nombre d'opérations intéressantes par l'intermédiaire de méthodes adaptées à la situation. Voici juste quelques unes de ses méthodes.
Méthode | Description |
---|---|
addMessage() | Ajoute un message d'erreur et permet ainsi d'envoyer un message d'alerte ou d'erreur directement sur le composant client qui a provoqué l'erreur. |
getApplication() | Retourne l'objet représentant l'ensemble de l'application Web |
getAttributes() | Renvoie un objet Map représentant l'ensemble des attributs associés à l'instance FacesContext. |
getCurrentInstance() | Renvoie l'instance FacesContext pour la requête traitée par la thread courant. |
getExternalContext() | Retourne un objet relatif à tous les éléments externes à la page courante. Cet objet est d'une grande utilité. Il permet de récupérer toutes les informations issues de la requête, donc d'être en relation avec l'objet request que nous avons déjà utilisé avec les servlets. Nous pouvons également connaître l'objet response, l'objet session, etc. |
getMaximumSeverity() | Renvoie le niveau d'importance maximal pour tout FacesMessage mis en file d'attente. |
getMessages() | Renvoie une collection de FacesMessage. |
getViewRoot() | Renvoie le composant racine associé à la requête. |
release() | Libère les ressources associées à cette instance de FacesContext. |
renderResponse() | Signale à l'implémentation JSF que le contrôle devra être transmis à la phase de rendu de la réponse dès la fin de l'étape de traitement courante de la requête, en ignorant les étapes qui n'ont pas encore été exécutées. |
responseComplete() | Signale à l'implémentation JSF que la réponse HTTP de cette requête a déjà été produite et que le cycle de vie du traitement de la requête doit se terminer dès la fin de l'étape en cours. |
La classe javax.faces.context.ExternalContext permet d'être en relation avec tous les éléments qui sont en relation
avec notre page Web. Ainsi, il est possible de connaître tout ce qui
concerne la requête, la réponse, la session en cours, etc.
FacesContext contexte = FacesContext.getCurrentInstance();
ExternalContext externe = contexte.getExternalContext();
Méthode | Description |
---|---|
addApplicationMap() | Retourne les objets (les attributs) stockés dans l'application Web. |
getInitParameter() | Retourne le paramètre initialisé dans le contexte de l'application Web spécifié par le descripteur de déploiement. |
getInitParameterMap() | Délivre l'ensemble des paramètres d'initialisation de l'application Web. |
getRemoteUser() | Procure le nom de loggin de l'utilisateur fabriqué dans la requête courante. |
getRequestCookieMap() | Récupère l'ensemble des cookies de la requête. |
getRequest() | Récupère l'objet request représentant la requête envoyée. |
getRequestContextPath() | Retourne la portion d'URL qui correspond à l'emplacement de l'application Web. |
getRequestHeaderMap() | Retourne l'ensemble de l'en-tête de la requête. |
getRequestHeaderValuesMap() | Retourne l'ensemble de l'en-tête de la requête. Les valeurs sont cette fois-ci sous forme de tableaux de chaînes pour certains éléments d'en-tête. |
getRequestMap() | Retourne les attributs de l'application courante. |
getRequestParameterMap() | Retourne les paramètres de la requête. |
getRequestParameterNames() | Retourne les paramètres de la requête en passant par un itérateur. |
getRequestParameterValuesMap() | Retourne les paramètres de la requête en prenant en compte plutôt les tableaux de chaînes. |
getRequestPathInfo() | Retourne uniquement la portion de l'URL qui suit le nom de l'application Web. |
getRessource() | Permet de récupérer l'URL d'une ressource. |
getRessourceAsStream() | Récupère une ressource sous forme de flux d'entrée. |
getResponse() | Récupère l'objet response représentant le flux sortant en connexion avec le navigateur. |
getSession() | Mise en oeuvre ou consultation d'une session. |
getSessionMap() | Récupère les attributs créés dans la session. |
log(String message) | Envoie un message dans le journal de l'application Web. |
Pour comprendre le traitement relatif aux images, comme retailler une image ou le calcul d'un histogramme, revoyez les cours sur le traitement d'images.
package bean; import java.awt.image.*; import java.io.*; import javax.annotation.PostConstruct; import javax.faces.bean.*; import javax.faces.context.*; import javax.servlet.http.*; @ManagedBean @SessionScoped public class Traitement { private String nomFichier; private BufferedImage vignette; private int largeurVignette = 300; private final String répertoire = ; private String[] fichiers; @PostConstruct void init() { File photos = new File(répertoire); fichiers = photos.list(); if (fichiers.length!=0) nomFichier = fichiers[0]; } public String[] getFichiers() { return fichiers; } public int getLargeurVignette() { return largeurVignette; } public long getPoids() { return new File(répertoire+nomFichier).length(); } public BufferedImage getVignette() { return vignette; } public void setVignette(BufferedImage vignette) { this.vignette = vignette; } public String getNomFichier() { return nomFichier; } public void setNomFichier(String nomFichier) { this.nomFichier = nomFichier; } public String getPath() { return répertoire+nomFichier; } public void télécharger() throws IOException { ExternalContext externe = FacesContext.getCurrentInstance().getExternalContext(); HttpServletResponse réponse = (HttpServletResponse) externe.getResponse(); File fichier = new File(répertoire+nomFichier); réponse.setContentType( ); réponse.setContentLength((int)fichier.length()); réponse.setHeader( , \" +nomFichier+\" ); FileInputStream lectureOctets = new FileInputStream(fichier); byte[] octets = new byte[(int) fichier.length()]; lectureOctets.read(octets); OutputStream out = new BufferedOutputStream(réponse.getOutputStream()); out.write(octets); out.close(); } }
package servlet; import java.awt.geom.AffineTransform; import java.awt.image.*; import java.io.*; import java.net.URL; import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet( ) public class LireImage extends HttpServlet { private BufferedImage image; private BufferedImage vignette; private bean.Traitement traitement; private void créerVignette() { double ratio = (double)traitement.getLargeurVignette()/image.getWidth(); vignette = new BufferedImage((int)(image.getWidth()*ratio), (int)(image.getHeight()*ratio), image.getType()); AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio); int interpolation = AffineTransformOp.TYPE_NEAREST_NEIGHBOR; AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation); retaillerImage.filter(image, vignette); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType( ); OutputStream out = response.getOutputStream(); traitement = (bean.Traitement) request.getSession().getAttribute( ); image = ImageIO.read(new URL( +traitement.getPath())); créerVignette(); traitement.setVignette(vignette); ImageIO.write(vignette, , out); out.close(); } }
package servlet; import java.awt.*; import java.awt.geom.Rectangle2D; import java.awt.image.*; import java.io.*; import javax.imageio.ImageIO; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet( ) public class Histogramme extends HttpServlet { private BufferedImage vignette; private BufferedImage histogramme; private bean.Traitement traitement; private int[] rouge; private int[] vert; private int[] bleu; private int largeur = 256; private int hauteur; private Graphics2D dessin; private void calculerHistogramme() { hauteur = vignette.getHeight(); rouge = new int[256]; vert = new int[256]; bleu = new int[256]; récupérerRVB(); tracerHistogrammes(); } private void récupérerRVB() { Raster trame = vignette.getRaster(); ColorModel modèle = vignette.getColorModel(); for (int y=0; y<vignette.getHeight(); y++) for (int x=0; x<vignette.getWidth(); x++) { Object données = trame.getDataElements(x, y, null); rouge[modèle.getRed(données)]++; vert[modèle.getGreen(données)]++; bleu[modèle.getBlue(données)]++; } } private void tracerHistogrammes() { histogramme = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB); dessin = histogramme.createGraphics(); Rectangle2D rectangle = new Rectangle2D.Double(0, 0, largeur-1, hauteur-1); dessin.setPaint(Color.black); dessin.fill(rectangle); changerAxes(); dessin.setPaint(new Color(1F, 0F, 0F, 0.7F)); tracerHistogramme(rouge); dessin.setPaint(new Color(0F, 1F, 0F, 0.7F)); tracerHistogramme(vert); dessin.setPaint(new Color(0F, 0F, 1F, 0.7F)); tracerHistogramme(bleu); } private void changerAxes() { dessin.translate(0, hauteur); double surfaceImage = vignette.getWidth()*vignette.getHeight(); double surfaceHistogramme = histogramme.getWidth()*histogramme.getHeight(); dessin.scale(1, -surfaceHistogramme/surfaceImage/3); } private void tracerHistogramme(int[] couleur) { for (int i=0; i<255; i++) dessin.drawLine(i, 0, i, couleur[i]); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType( ); OutputStream out = response.getOutputStream(); traitement = (bean.Traitement) request.getSession().getAttribute( ); vignette = traitement.getVignette(); calculerHistogramme(); ImageIO.write(histogramme, , out); out.close(); } }
Nous retrouvons la même architecture que la servlet précédente. Toutefois, le traitement étant beaucoup plus conséquent, la servlet dispose donc de pas mal de méthodes privées.
Nous avons bien séparé les différents traitements de fond à réaliser : permettre l'interaction entre les différents composants et le téléchargement de la photo complète au moyen du bean géré traitement, la génération de la vignette correspondante au moyen de la servlet LireImage, et enfin la génération de l'histogramme au moyen de la servlet Histogramme.
ATTENTION : Lorsque nous utilisons plusieurs servlets, ces dernières sont exécutées normalement dans des threads séparées, ce qui est la plus part du temps un énorme avantage puisque nous faisons du traitement parallèle. Dans notre cas, cela peut être problématique puisque la servlet de l'histogramme doit s'exécuter après la génération de la vignette (l'histogramme est déduit de cette dernière).
Dans un cas comme celui-ci, il est possible de réaliser l'ensemble des traitements uniquement à l'intérieur du bean géré, les servlets ne seront utilisées que pour envoyer les flux binaires séparés utiles pour la page web dans son ensemble.
Nous ne pouvons pas nous passer des servlets, parce que si nous envisagions d'utiliser une méthode du bean géré, à l'image de la méthode télécharcher(), cela aurait comme conséquence d'ouvrir de nouvelles page web pour ces deux images générées, et non de les intégrer à l'intérieur d'une même page web commune (un fichier image est bien indépendant de la page web qui le visualise).
root { display: block; } body { background: green; color: yellow; font-weight: bold; text-shadow: 0px 0px 10px yellow; text-align: center; } .panneau { background: darkgreen; border-radius: 5px; box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen; } .titre { font-size: 20px; } img { border-radius: 10px; box-shadow: 2px 2px 5px black; }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet library= name= /> <h:body> <h:form> <h:panelGrid columns= cellpadding= styleClass=panneau> <f:facet name=> <h:outputText value= /> <h:selectOneMenu value=#{traitement.nomFichier} onchange=submit()> <f:selectItems value=#{traitement.fichiers} /> </h:selectOneMenu> </f:facet> <h:graphicImage url="LireImage" /> <h:graphicImage url="Histogramme" /> <h:outputText value=#{traitement.poids}> <f:convertNumber pattern= /> </h:outputText> <h:commandButton value= action=#{traitement.télécharger} /> </h:panelGrid> </h:form> </h:body> </html>
Cette fois-ci, dans la balise <h:graphicImage>, nous utilisons plutôt l'attribut url en lieu et place de l'attribut value. C'est au travers de cet attribut url que nous faisons appel à la servlet correspondante. Vu que nous utilisons cet attribut, nous comprenons bien que le la requête soumise au serveur est de type GET.
Nous avons bien séparé les différents traitements de fond à réaliser : permettre l'interaction entre les différents composants et le téléchargement de la photo complète au moyen du bean géré traitement, la génération de la vignette correspondante au moyen de la servlet LireImage, et enfin la génération de l'histogramme au moyen de la servlet Histogramme.
Dans un cas comme celui-ci, il est possible de réaliser l'ensemble des traitements uniquement à l'intérieur du bean géré, les servlets ne seront utilisées que pour envoyer les flux binaires séparés utiles pour la page web dans son ensemble.
package bean; import java.awt.image.*; import java.io.*; import javax.annotation.PostConstruct; import javax.faces.bean.*; import javax.faces.context.*; import javax.servlet.http.*; @ManagedBean @SessionScoped public class Traitement { private BufferedImage image; private BufferedImage vignette; private BufferedImage histogramme; private int largeurVignette = 300; private int[] rouge; private int[] vert; private int[] bleu; private int largeur = 256; private int hauteur; private Graphics2D dessin; private String nomFichier; private final String répertoire = ; private String[] fichiers; @PostConstruct void init() throws IOException { File photos = new File(répertoire); fichiers = photos.list(); if (fichiers.length!=0) setNomFichier(fichiers[0]); } public BufferedImage getVignette() { return vignette; } public BufferedImage getHistogramme() { return histogramme; } public String getNomFichier() { return nomFichier; } public String getPath() { return répertoire+nomFichier; } public long getPoids() { return new File(répertoire+nomFichier).length(); } public void setNomFichier(String nomFichier) throws IOException { this.nomFichier = nomFichier; image = ImageIO.read(new URL( +répertoire+nomFichier)); créerVignette(); calculerHistogramme(); } public void télécharger() throws IOException { ExternalContext externe = FacesContext.getCurrentInstance().getExternalContext(); HttpServletResponse réponse = (HttpServletResponse) externe.getResponse(); File fichier = new File(répertoire+nomFichier); réponse.setContentType( ); réponse.setContentLength((int)fichier.length()); réponse.setHeader( , \" +nomFichier+\" ); FileInputStream lectureOctets = new FileInputStream(fichier); byte[] octets = new byte[(int) fichier.length()]; lectureOctets.read(octets); OutputStream out = new BufferedOutputStream(réponse.getOutputStream()); out.write(octets); out.close(); } private void créerVignette() { double ratio = (double)traitement.getLargeurVignette()/image.getWidth(); vignette = new BufferedImage((int)(image.getWidth()*ratio), (int)(image.getHeight()*ratio), image.getType()); AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio); int interpolation = AffineTransformOp.TYPE_NEAREST_NEIGHBOR; AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation); retaillerImage.filter(image, vignette); } private void calculerHistogramme() { hauteur = vignette.getHeight(); rouge = new int[256]; vert = new int[256]; bleu = new int[256]; récupérerRVB(); tracerHistogrammes(); } private void récupérerRVB() { Raster trame = vignette.getRaster(); ColorModel modèle = vignette.getColorModel(); for (int y=0; y<vignette.getHeight(); y++) for (int x=0; x<vignette.getWidth(); x++) { Object données = trame.getDataElements(x, y, null); rouge[modèle.getRed(données)]++; vert[modèle.getGreen(données)]++; bleu[modèle.getBlue(données)]++; } } private void tracerHistogrammes() { histogramme = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB); dessin = histogramme.createGraphics(); Rectangle2D rectangle = new Rectangle2D.Double(0, 0, largeur-1, hauteur-1); dessin.setPaint(Color.black); dessin.fill(rectangle); changerAxes(); dessin.setPaint(new Color(1F, 0F, 0F, 0.7F)); tracerHistogramme(rouge); dessin.setPaint(new Color(0F, 1F, 0F, 0.7F)); tracerHistogramme(vert); dessin.setPaint(new Color(0F, 0F, 1F, 0.7F)); tracerHistogramme(bleu); } private void changerAxes() { dessin.translate(0, hauteur); double surfaceImage = vignette.getWidth()*vignette.getHeight(); double surfaceHistogramme = histogramme.getWidth()*histogramme.getHeight(); dessin.scale(1, -surfaceHistogramme/surfaceImage/3); } private void tracerHistogramme(int[] couleur) { for (int i=0; i<255; i++) dessin.drawLine(i, 0, i, couleur[i]); } }
package servlet; import java.io.*; import java.net.URL; import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet( ) public class LireImage extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType( ); OutputStream out = response.getOutputStream(); bean.Traitement traitement = (bean.Traitement) request.getSession().getAttribute( ); ImageIO.write(traitement.getVignette(), , out); out.close(); } }
package servlet; import java.io.*; import javax.imageio.ImageIO; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet( ) public class Histogramme extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType( ); OutputStream out = response.getOutputStream(); bean.Traitement traitement = (bean.Traitement) request.getSession().getAttribute( ); ImageIO.write(traitement.getHistogramme(), , out); out.close(); } }
Ces deux servlets sont très similaires. L'idée serait d'en proposer qu'une seule qui se préoccupe uniquement d'envoyer un flux binaire représentant une image au format PNG, quelque soit cette image. C'est la page web qui décidera de quelle image il s'agit, soit la vignette, soit l'histogramme.
Vu que nous utilisons une requête de type GET dans le protocole HTTP, il est alors facile de proposer un paramètre supplémentaire, que nous appelons image à l'intérieur duquel nous précisons le type d'image que nous souhaitons visualiser. Voici ainsi la modification que nous apportons sur la vue.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet library= name= /> <h:body> <h:form> <h:panelGrid columns= cellpadding= styleClass=panneau> <f:facet name=> <h:outputText value= /> <h:selectOneMenu value=#{traitement.nomFichier} onchange=submit()> <f:selectItems value=#{traitement.fichiers} /> </h:selectOneMenu> </f:facet> <h:graphicImage url="LireImage?image=vignette" /> <h:graphicImage url="LireImage?image=histogramme" /> <h:outputText value=#{traitement.poids}> <f:convertNumber pattern= /> </h:outputText> <h:commandButton value= action=#{traitement.télécharger} /> </h:panelGrid> </h:form> </h:body> </html>
Nous n'utilisons donc que la première servlet LireImage à l'intérieur de laquelle nous récupérons la valeur du paramètre soumis par la vue et nous prenons l'image désirée à l'aide du bean géré. Voici la modification de cette servlet en conséquence :
package servlet; import java.awt.image.BufferedImage; import java.io.*; import java.net.URL; import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet( ) public class LireImage extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType( ); OutputStream out = response.getOutputStream(); bean.Traitement traitement = (bean.Traitement) request.getSession().getAttribute( ); boolean isVignette = request.getParameter( ).equals( ); BufferedImage image = isVignette ? traitement.getVignette() : traitement.getHistogramme(); ImageIO.write(image, , out); out.close(); } }
Dans ce chapitre, nous allons découvrir un des gros avantages de JSF qui permet la gestion des événements côté client. Lorsque l'utilisateur fait un choix, valide une information ou clique sur une partie d'une image par exemple, ces types d'événement peuvent alors être pris en compte en temps réel et ainsi soumettre ces choix instantanément au serveur afin qu'il réalise l'opération souhaitée et délivre finalement le résultat en reconstituant la page Web en cours.
Avec cette gestion d'événements, nous ne sommes plus obligé de proposer un bouton de soumission par type de sélection, comme les menus déroulant, les boutons radios, les cases à cocher, etc. Dès qu'une sélection est faite, elle est soumise instantanément au serveur, afin d'avoir un affichage adapté à la nouvelle situation et ainsi de proposer de nouveaux choix à l'utilisateur relatif au premiers déjà réalisés. L'évolution de la page est ainsi contextuelle et suit, à la volée, la saisie de l'opérateur.
Il est à noter que la gestion d'événements concerne systématiquement la même page Web. Il faut bien comprendre que la page évolue pour faire en sorte que la saisie des valeurs soit le plus rapide possible en proposant des choix adaptés suivant l'évolution de la saisie déjà réalisés par l'opérateur. Une fois que le formulaire est rempli, nous pouvons passer à une autre étape, souvent avec l'affichage d'une nouvelle page Web, en cliquant cette fois-ci sur l'unique bouton de soumission.
Le modèle d'événements proposé par JSF est très proche de celui utilisé par Java SE avec ses applications SWING. Les développeurs Web ne sont généralement pas familiarisés avec ce modèle de développemnt utilisant MVC en natif, considérablement différent du modèle utilisé par le protocole HTTP.
Il existe en effet deux concepts dans ce mécanisme : les événements eux mêmes (events) et les écouteurs (listeners). Un événement est généré, par exemple, lorsque la valeur d'une zone de saisie est modifiée. L'écouteur ou listener est, de son côté, responsable de l'implémentation du code dans l'application lorsque l'événement est déclenché.
Par exemple, lorsque l'utilisateur saisie un texte ou change sa valeur, nous souhaitons mettre à jour automatiquement un message assosié. Pour cela le framework déclanche un événement lorsque la valeur change dans le champ. Le développeur doit donc associer un écouteur (listener) pour cet événement (event). Autre exemple, lorsque nous utilisons un bouton et que nous cliquons dessus, celui-ci va envoyer un événement pour indiquer son changement d'état et son listener associé va répondre à l'événement.
En fait, avec JSF, lorsque nous cliquons sur le bouton, rien ne se passe réellement sur le client, les changements sont évalués par le cycle de vie des requêtes. JSF propose ainsi quatre types d'événements avec la possibilité de les écouter par l'intermédiaire de listeners :
Avant de traiter ces différents types d'événement, il est bon de revenir sur le cycle de vie afin de connaître à quel moment sont pris en compte les deux principaux type d'événements dans le cycle de traitement des requêtes.
Nous allons désormais rentrer dans le vif du sujet en étudiant en profondeur ce qui se passe réellement lorsqu'un événement de type "Changement de valeur" se produit. Typiquement, ce type d'événement est extrêmement utilisé lorqu'un composant dépend d'un autre dans la même page web.
Par exemple, j'aimerai avoir automatiquement la liste des prénoms associés à un nom que je sélectionne au préalable depuis une autre liste, afin de renseigner le plus rapidement possible le formulaire d'une page. Dans le même ordre d'idée, il pourrait être sympatique de connaître l'ensemble de l'identité de la personne choisi en sélectionnant cette fois-ci le prénom parmi la liste proposée automatiquement.
Avec JSF, du côté de la vue, un changement de valeur est toujours associé à une méthode JavaScript de type onchange ou onclick. JSF propose alors pour cela l'attribut valueChangeListener à l'intérieur duquel vous spécifier la méthode du bean qui prend en compte ce type d'événement.
<h:selectOneMenu value=#{traitement.nomFichier} onchange=submit()> <f:selectItems value=#{traitement.fichiers} /> </h:selectOneMenu>
A ce moment là, nous n'avons jamais évoqué cette notion d'événement (nous nous servions tout simplement de l'attribut value) et nous pourrions à juste titre nous demander de l'opportunité d'une telle gestion (en prenant en compte dans ce cas là l'attribut valueChangeListener).
Il est à noter que la gestion d'événements concerne systématiquement la même page Web. Il faut bien comprendre que la page évolue pour faire en sorte que la saisie des valeurs soit le plus rapide possible en proposant des choix adaptés suivant l'évolution de la saisie déjà réalisés par l'opérateur. Une fois que le formulaire est rempli, nous pouvons, en finalité, passer à une autre étape, souvent avec l'affichage d'une nouvelle page Web, en cliquant cette fois-ci sur l'unique bouton de soumission.
Quand appliquer une gestion événementielle ? Lorsque vous disposez de plusieurs zones de sélection dont les choix sont figés, il s'agit d'une soumission classique. Lorsque par contre, des sélections évoluent en fonctions des autres, il est nécessaire de prévoir une gestion événementielle qui permet de prendre en compte les changements proposés sur la même page avant d'aboutir à la requête définitive.
Lorsque vous désirez prendre en compte cet événement, vous devez alors spécifier la méthode du bean qui va le traiter à l'aide de l'attribut valueChangeListener de la balise concernée. Par ailleurs, vous devez demander à soumettre immédiatement votre requête en sollicitant la fonction JavaScript submit(). Pour une zone de saisie, voici ce qu'il faut donc écrire d'un point de vue général :
<h:inputText value="#{bean.propriété}" valueChangeListener="#{bean.méthodeEvénement}" onchange="submit()" />
Ensuite, dans votre JavaBean, vous devez donc implémenter cette méthode particulière qui doit alors posséder un attribut de type ValueChangeEvent :
public class UnJavaBeanGéré { private int propriété; public int getPropriété() { return propriété; } public void setPropriété(int propriété) { this.propriété = propriété; } public void méthodeEvénement(ValueChangeEvent evt) { propriété = (Integer)evt.getNewValue(); ... } }
Il est très facile de gérer les événements. Ici, l'objet evt de type ValueChangeEvent possède un certain nombre de méthodes. La plus importante est certainement la méthode getNewValue() qui permet de récupérer instantanément la nouvelle valeur saisie dans la zone d'entrée.
Nous pourrions nous poser la question de savoir pourquoi récupérer cette valeur saisie au travers de cette méthode getNewValue(), alors que normalement la propriété correspondante doit s'occuper elle-même de la récupérer. Nous l'avons déjà évoqué lors du cycle de traitement des requêtes, la propriété ne l'a pas encore récupérée puisque cette phase de récupération se situe après.
A ce sujet, dans bien des cas, nous n'avons même pas besoin de créer une propriété spécifique puisque nous avons la possibilité de récupérer la valeur souhaité au travers de cette méthode getNewValue(). Ceci est vrai pour tout ce qui est sélections, par contre, pour les zones de saisie, vous êtes obligé de conserver la propriété. Ainsi, en reprenant le canevas général ci-dessus nous pouvons nous passer de l'attribut value et garder uniquement l'attribut valueChangeListener :
<h:selectOneMenu valueChangeListener="#{bean.méthodeEvénement}" onchange="submit()">
public class UnJavaBeanGéré { private int attribut; public void méthodeEvénement(ValueChangeEvent evt) { attribut = (Integer)evt.getNewValue(); ... } }
Méthode | Retour | Description de la classe javax.faces.event.ValueChangeEvent |
---|---|---|
addComponent() | UIComponent | Retourne le composant qui a sollicité l'événement. |
getNewValue() | Object | Retourne la valeur (ou le choix) proposée par le composant représentant l'entrée qui a sollicité l'événement, après toutefois être passé par la phase de conversion et de validation. |
getOldValue() | Object | Retourne l'ancienne valeur (dans le cas où vous n'avez pas prévu d'attribut pour cette propriété). |
Afin de valider cette partie, nous allons mettre en oeuvre, sous forme très réduite, une application web qui permet de faire la gestion du personnel dont je rappelle l'aperçu :
package entité; import java.io.Serializable; import javax.persistence.*; @Entity @NamedQueries({ @NamedQuery(name= , query= ), @NamedQuery(name= , query= ), @NamedQuery(name= , query= ) }) public class Personne implements Serializable { @Id @GeneratedValue(strategy= GenerationType.IDENTITY) private long id; private String nom; private String prenom; private int age; public long getId() { return id; } public String getNom() { return nom; } public String getPrenom() { return prenom; } public int getAge() { return age; } public void setNom(String nom) { this.nom = nom.toUpperCase(); } public void setPrenom(String prénom) { if (prénom.isEmpty()) return; StringBuilder chaine = new StringBuilder(prénom.toLowerCase()); chaine.setCharAt(0, Character.toUpperCase(chaine.charAt(0))); this.prenom = chaine.toString(); } public void setAge(int âge) { this.age = âge; } }
package bean; import entité.Personne; import java.util.List; import javax.annotation.PostConstruct; import javax.inject.Named; import javax.enterprise.context.SessionScoped; import javax.ejb.Stateful; import javax.faces.event.ValueChangeEvent; import javax.persistence.*; @Named @SessionScoped @Stateful public class Gestion { @PersistenceContext private EntityManager bd; private Personne personne; private enum Enregistrement {Nouveau, Enregistré, ExisteDéjà}; private Enregistrement etat; private String nom; private String prénom; private List<String> noms; private List<String> prénoms; public Personne getPersonne() { return personne; } public void setPersonne(Personne personne) { this.personne = personne; } public String getEtat() { return etat.toString(); } public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } public List<String> getNoms() { return noms; } public List<String> getPrénoms() { return prénoms; } @PostConstruct private void init() { rechercheNoms(); if (noms!=null && !noms.isEmpty()) { nom = noms.get(0); miseAJour(); } else { personne = new Personne(); etat = Enregistrement.Nouveau; } } private void miseAJour() { recherchePrénoms(); prénom = prénoms.get(0); personne = recherchePersonne(nom, prénom); etat = Enregistrement.Enregistré; } public void nouveau() { personne = new Personne(); etat = Enregistrement.Nouveau; } public void enregistrer() { try { recherchePersonne(personne.getNom(), personne.getPrenom()); etat = Enregistrement.ExisteDéjà; } catch (Exception ex) { bd.persist(personne); etat = Enregistrement.Enregistré; rechercheNoms(); recherchePrénoms(); } } public void supprimer() { bd.remove(bd.find(Personne.class, personne.getId())); init(); } public void changeNom(ValueChangeEvent evt) { nom = (String) evt.getNewValue(); miseAJour(); } public void changePrénom(ValueChangeEvent evt) { prénom = (String) evt.getNewValue(); personne = recherchePersonne(nom, prénom); etat = Enregistrement.Enregistré; } private void rechercheNoms() { Query requête = bd.createNamedQuery( ); noms = requête.getResultList(); } private void recherchePrénoms() { Query requête = bd.createNamedQuery( ); requête.setParameter( , nom); prénoms = requête.getResultList(); } private Personne recherchePersonne(String nom, String prénom) { Query requête = bd.createNamedQuery( ); requête.setParameter( , nom); requête.setParameter( , prénom); return (Personne) requête.getSingleResult(); } }
Remarquez l'absence de la propriété prénom (seul l'attribut existe) qui n'est pas particulièrement utile puisque c'est l'événement associé à la sélection du prénom qui enregistre le choix effectué.
root { display: block; } body { background: green; color: yellow; font-weight: bold; text-shadow: 0px 0px 10px yellow; } .panneau { background: darkgreen; border-radius: 5px; box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen; padding: 5px; } .entete { font-size: 20px; padding-bottom: 7px; } .pied { padding-top: 7px; } input[type= ] { background: yellow; color: darkgreen; padding-right: 7px; padding-left: 7px; border-radius: 3px; }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet library= name= /> <h:body> <h:form> <h:panelGrid columns= styleClass=panneau headerClass=entete footerClass=pied> <f:facet name=> <h:outputText value= /> </f:facet> <h:outputText value= /> <h:inputText value=#{gestion.personne.prenom}/> <h:outputText value= /> <h:inputText value=#{gestion.personne.nom} /> <h:outputText value= /> <h:inputText value=#{gestion.personne.age} /> <h:outputText value=#{gestion.etat} style=color: aquamarine /> <f:facet name=> <h:commandButton value= action=#{gestion.nouveau}/> <h:commandButton value= action=#{gestion.enregistrer} /> <h:commandButton value= action=#{gestion.supprimer} /> </f:facet> </h:panelGrid> </h:form> <br /> <h:panelGrid columns= styleClass=panneau headerClass=entete> <f:facet name=> <h:outputText value= /> </f:facet> <h:form> <h:selectOneMenu value=#{gestion.nom} valueChangeListener=#{gestion.changeNom} onchange=submit() ><f:selectItems value=#{gestion.noms} /> </h:selectOneMenu> </h:form> <h:form> <h:selectOneMenu valueChangeListener=#{gestion.changePrénom}submit()> <f:selectItems value=#{gestion.prénoms} /> </h:selectOneMenu> </h:form> </h:panelGrid> </h:body> </html>
Pour cet événement, vous devez spécifier la méthode du bean qui va traiter votre événement au travers de l'attribut actionListener. Par contre, il n'est plus nécessaire de solliciter la fonction JavaScript submit() puisque les balises concernées <h:commandButton> et <h:commandLink> sont déjà prévues pour émettre une soumission à une requête. Pour un bouton, voici ce qu'il faut donc écrire d'un point de vue général :
<h:commandButton action="#{bean.changerVue}" actionListener="#{bean.méthodeEvénement}" />
Ensuite, dans votre bean géré, vous devez donc implémenter cette méthode particulière qui doit alors posséder un attribut de type ActionEvent :
public class UnBeanGéré { public String changerVue() { ... return "Passer à la page Web suivante"; } public void méthodeEvénement(ActionEvent evt) { ... } }
Méthode | Retour | Description de la classe javax.faces.event.ActionEvent |
---|---|---|
getComponent() | UIComponent | Retourne le composant qui a sollicité l'événement. |
La classe ActionEvent possède très peu de méthodes. Il faut dire que dans la plupart du temps il s'agit juste de savoir si une validation a été effectué. Toutefois, il peut être nécessaire de connaitre, par exemple, les coordonnées de la souris au moment du clic. Voici la procédure à suivre dans ce cas là :
public void méthodeEvénement(ActionEvent evt) { FacesContext page = FacesContext.getCurrentInstance(); String client = evt.getComponent().getClientId(page); Map requête = page.getExternalContext().getRequestParameterMap(); int x = Integer.parseInt((String)requête.get(client+".x")); int y = Integer.parseInt((String)requête.get(client+".y")); ... }
Nous pourrions légitimement nous poser la question de l'utilité de l'attribut actionListener alors qu'il existe déjà l'attribut action. En réalité chacun joue son propre rôle. actionListener permet de valider un certain nombre de choses avant de passer éventuellement à une autre page. Elle devient impérative si vous désirez gérer les coordonnées de la souris. L'attribut action, de son côté, est prévu pour la navigation entre les pages, c'est ce qui permet d'évoluer dans le site.
L'événement de type action est déclenché durant la phase d'invocation de la logique applicative. Cependant, il est parfois nécessaire d'exécuter une méthode avant la phase de validation. Par exemple, un bouton de type Annuler permet d'initialiser le formulaire en vidant les champs et doit appeler une méthodes avant la validation des saisies.
package bean; import java.awt.*; import java.awt.geom.*; import java.awt.image.*; import java.io.*; import java.net.URL; import java.util.Map; import javax.annotation.PostConstruct; import javax.faces.bean.*; import javax.faces.context.*; import javax.faces.event.*; import javax.imageio.ImageIO; import javax.servlet.http.*; @ManagedBean @SessionScoped public class Traitement { private String nomFichier; private BufferedImage image; private BufferedImage imageRetouchée; private BufferedImage vignette; private BufferedImage retouche; private int largeur = 300; private int hauteur; private int centreZoomX, centreZoomY; private int intensité; private int contraste; private Graphics2D dessin; private final String répertoire = ; private String[] fichiers; @PostConstruct void init() throws IOException { File photos = new File(répertoire); fichiers = photos.list(); if (fichiers.length!=0) setNomFichier(fichiers[0]); } public String[] getFichiers() { return fichiers; } public BufferedImage getVignette() { return vignette; } public BufferedImage getRetouche() { return retouche; } public BufferedImage getZoom() { return imageRetouchée.getSubimage(centreZoomX-largeur/2, centreZoomY-hauteur/2, largeur, hauteur); } public String getNomFichier() { return nomFichier; } public String getPath() { return répertoire+nomFichier; } public long getPoids() { return new File(répertoire+nomFichier).length(); } public void setNomFichier(String nomFichier) throws IOException { this.nomFichier = nomFichier; image = ImageIO.read(new URL( +répertoire+nomFichier)); imageRetouchée = new BufferedImage(image.getWidth(), image.getHeight(), image.getType()); créerVignette(); centreZoomX = image.getWidth()/2; centreZoomY = image.getHeight()/2; intensité = contraste = 0; retouche(); } public int getLargeur() { return largeur; } public void setLargeur(int largeur) { this.largeur = largeur; } public int getContraste() { return contraste; } public void setContraste(int contraste) { this.contraste = contraste; } public int getIntensité() { return intensité; } public void setIntensité(int intensité) { this.intensité = intensité; } public void télécharger() throws IOException { ExternalContext externe = FacesContext.getCurrentInstance().getExternalContext(); HttpServletResponse réponse = (HttpServletResponse) externe.getResponse(); réponse.setContentType( ); réponse.setHeader( , \" +nomFichier+\" ); OutputStream out = réponse.getOutputStream(); ImageIO.write(imageRetouchée, , out); out.close(); } private void créerVignette() { double ratio = (double)largeur/image.getWidth(); vignette = new BufferedImage((int)(image.getWidth()*ratio), (int)(image.getHeight()*ratio), image.getType()); hauteur = vignette.getHeight(); AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio); int interpolation = AffineTransformOp.TYPE_BICUBIC; AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation); retaillerImage.filter(image, vignette); } private void retouche() { int[] courbeInitiale = new int[256]; byte[] courbe = new byte[256]; for (int i=0; i<256; i++) { courbeInitiale[i] = (int) (i+intensité*Math.sin(i*Math.PI/255)-contraste*Math.sin(i*2*Math.PI/255)); if (courbeInitiale[i]<0) courbe[i] = (byte)0; else if (courbeInitiale[i]>255) courbe[i] = (byte)255; else courbe[i] = (byte)courbeInitiale[i]; } retouche = new BufferedImage(vignette.getWidth(), vignette.getHeight(), vignette.getType()); ByteLookupTable table = new ByteLookupTable(0, courbe); LookupOp opération = new LookupOp(table, null); opération.filter(vignette, retouche); opération.filter(image, imageRetouchée); } public void changeLargeur(ValueChangeEvent evt) { largeur = (Integer)evt.getNewValue(); créerVignette(); retouche(); } public void changerCentreZoom(ActionEvent evt) { FacesContext ctx = FacesContext.getCurrentInstance(); String clientId = evt.getComponent().getClientId(ctx); Map requête = ctx.getExternalContext().getRequestParameterMap(); int x = Integer.parseInt((String)requête.get(clientId+ )); int y = Integer.parseInt((String)requête.get(clientId+ )); centreZoomX = image.getWidth()*x/largeur; centreZoomY = image.getHeight()*y/hauteur; } public void changeIntensité(ValueChangeEvent evt) { intensité = (Integer) evt.getNewValue(); retouche(); } public void changeContraste(ValueChangeEvent evt) { contraste = (Integer) evt.getNewValue(); retouche(); } }
root { display: block; } body { background: green; color: yellow; font-weight: bold; text-shadow: 0px 0px 10px yellow; } .panneau { background: darkgreen; border-radius: 5px; box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen; padding: 5px; } .entete { font-size: 18px; padding-bottom: 5px; } img, input[type= ] { border-radius: 10px; box-shadow: 2px 2px 5px black; } form { float: right; }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet library= name= /> <h:body> <h:panelGrid columns= cellpadding= styleClass=panneau headerClass=entete> <f:facet name=> <h:form> <h:outputText value= /> <h:selectOneMenu value=#{traitement.nomFichier} onchange=submit()> <f:selectItems value=#{traitement.fichiers} /> </h:selectOneMenu> </h:form> </f:facet> <h:form> <h:commandButton image= actionListener=#{traitement.changerCentreZoom}/> </h:form> <h:graphicImage url= /> <h:graphicImage url= /> <h:panelGrid columns= styleClass=panneau style=float: right> <h:outputText value= /> <h:form> <h:inputText value=#{traitement.largeur} valueChangeListener=#{traitement.changeLargeur} onchange=submit() size= /> </h:form> <h:outputText value= /> <h:form> <h:inputText size= value=#{traitement.intensité} valueChangeListener=#{traitement.changeIntensité} onchange=submit()> <f:validateLongRange minimum= maximum= /> </h:inputText> </h:form> <h:message for= /> <h:outputText value= /> <h:form> <h:inputText size= value=#{traitement.contraste} valueChangeListener=#{traitement.changeContraste} onchange=submit() validatorMessage= id=contraste > <f:validateLongRange minimum= maximum= /> </h:inputText> </h:form> <h:message for= /> <h:outputText value=#{traitement.poids}> <f:convertNumber pattern= /> </h:outputText> <h:form> <h:commandButton value= action=#{traitement.télécharger} /> </h:form> </h:panelGrid> </h:panelGrid> </h:body> </html>
package servlet; import java.awt.image.BufferedImage; import java.io.*; import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet( ) public class LireImage extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType( ); OutputStream out = response.getOutputStream(); bean.Traitement traitement = (bean.Traitement)request.getSession().getAttribute( ); String typeImage = request.getParameter( ); BufferedImage image = null; if (typeImage.equals( )) image = traitement.getVignette(); if (typeImage.equals( )) image = traitement.getRetouche(); if (typeImage.equals( )) image = traitement.getZoom(); ImageIO.write(image, , out); out.close(); } }
Revenons sur le cycle de traitement des événements : Vous remarquez que la gestion des événements, quelque soit le type, est toujours réalisée après la phase de conversion et de validation.
Il est possible, pour une entrée particulière, de ne pas passer par tout le cycle de traitement des requêtes avec la prise en compte des différentes phases. Il est effectivement possible de traiter uniquement la gestion des événements, sans passer donc par la phase 3 de conversion et de validation, et de passer ensuite, dès que le traitement de l'événement est réalisé, directement en phase 6 correspondant au rendu de la page Web.
Cette fois-ci, c'est tout de suite à l'issue de la phase 2, qui s'occupe de récupérer la valeur des requêtes, que la gestion des événements est pris en compte.
Notez toutefois, que si vous désirez prendre en compte un changement de valeur, il est nécessaire que cette nouvelle valeur subisse éventuellement une conversion et une validation afin qu'elle soit correcte et puisse donc être interprétée de façon convenable.
Dans le projet que nous venons de construire, si je désire avoir une gestion immédiate du clic sur la vignette pour proposer une loupe adaptée, voici la modification à apporter sur la vue.
<h:form> <h:commandButton image= actionListener=#{traitement.changerCentreZoom} immediat=true /> </h:form>
Ensuite, dans la méthode qui réalise le traitement de l'événement, après avoir géré le problème demandé, il est préférable d'atteindre directement la phase de rendu de la réponse, et ainsi de court-circuiter tous les autres phases intermédiaires, au moyen de la méthode renderResponse() de la classe FacesContext. Toujours pour notre exemple, voici donc le code correspondant dans le bean traitement.
public void changerCentreZoom(ActionEvent evt) { FacesContext ctx = FacesContext.getCurrentInstance(); String clientId = evt.getComponent().getClientId(ctx); Map requête = ctx.getExternalContext().getRequestParameterMap(); int x = Integer.parseInt((String)requête.get(clientId+ )); int y = Integer.parseInt((String)requête.get(clientId+ )); centreZoomX = image.getWidth()*x/largeur; centreZoomY = image.getHeight()*y/hauteur; ctx.renderResponse(); // ligne à rajouter }
JSF 2.0 introduit une nouvelle gestion d'événements qui est capable de prendre en compte les notifications globales d'une application web, ce que nous nommons événements système. Nous pouvons ainsi prendre en compte certaines notifications dans des phases bien particulières qui sont recensées dans le tableau ci-dessous :
Classe Evenements | Source de l'événement | Description |
---|---|---|
PostConstructApplicationEvent PreDestroyApplicationEvent |
Application | Après le démarrage de l'application web. Avant qu'elle se termine définitivement. |
PostAddToViewEvent PreRemoveFromViewEvent |
UIComponent | Après l'ajout du composant dans l'arbre de vue. Avant qu'il soit enlever de l'arbre de vue. |
PostRestoreStateEvent | UIComponent | Après la restauration du composant. |
PreValidateEvent PostValidateEvent |
UIComponent | Avant et après la validation d'un composant. |
PreRenderViewEvent | UIViewRoot | Avant que le rendu de l'élément racine de la page ne soit effectué. |
PreRenderComponentEvent | UIComponent | Avant que le rendu du composant ne soit effectué. |
PostConstructViewMapEvent PreDestroyViewMapEvent |
UIViewRoot | Après que l'élément racine de la page ne soit créé Avant sa destruction. |
PostConstructCustomScopeEvent PreDestroyCustomScopeEvent |
ScopeContext | Après que l'élément personnalisé de la page ne soit créé Avant sa destruction. |
ExceptionQueuedEvent | ExceptionQueueEventContext | Après le lancement d'une exception. |
<h:inputText value=#{bean.propriété}> <f:event name=postValidate listener=#{bean.méthodeEvénement} /> </h:inputText>
Ensuite, dans votre bean géré, vous devez donc implémenter une méthode spécifique qui doit alors posséder un attribut de type ComponentSystemEvent :
public class UnJavaBeanGéré { private int propriété; public int getPropriété() { return propriété; } public void setPropriété(int propriété) { this.propriété = propriété; } public void méthodeEvénement(ComponentSystemEvent evt) { ... } }
Il est possible de passer par une classe spécifique qui implémente l'action associée à l'événement souhaité. Cette classe doit alors être annotée avec @ListenerFor.
@ListenerFor(systemEventClass=PreRenderViewEvent.class) public class ClasseEvénement extends Rendered { ... }
Il existe des situations où vous avez besoin de recenser un ensemble d'informations venant de zones de saisie différentes et de contrôler la cohérence des unes envers les autres. C'est le cas notamment de la saisie d'une date. Certes, nous pouvons prévoir une seule zone de saisie où l'opérateur doit entrer la date en entier. Il est toutefois plus facile de prévoir une zone de saisie par élément, comme le jour du mois, le mois et l'année. Ainsi, la date est répartie. Par contre, il faut tout de même vérifier que la saisie est correcte en contrôlant, par exemple, que l'opérateur ne propose pas le 31 avril.
package bean; import java.text.SimpleDateFormat; import java.util.*; import javax.annotation.PostConstruct; import javax.faces.application.FacesMessage; import javax.faces.bean.*; import javax.faces.component.*; import javax.faces.context.FacesContext; import javax.faces.event.ComponentSystemEvent; @ManagedBean(name= ) @ViewScoped public class JourSemaine { private int jour; private enum Mois {Janvier, Février, Mars, Avril, Mai, Juin, Juillet, Août, Septembre, Octobre, Novembre, Décembre}; private Mois mois; private int année; private String erreur; public int getAnnée() { return année; } public void setAnnée(int année) { this.année = année; } public int getJour() { return jour; } public void setJour(int jour) { this.jour = jour; } public Mois getMois() { return mois; } public void setMois(Mois mois) { this.mois = mois; } public Mois[] getListeMois() { return Mois.values(); } @PostConstruct private void init() { Calendar date = Calendar.getInstance(); jour = date.get(Calendar.DAY_OF_MONTH); mois = Mois.values()[date.get(Calendar.MONTH)]; année = date.get(Calendar.YEAR); } public void recherche() { Calendar date = new GregorianCalendar(année, mois.ordinal(), jour); SimpleDateFormat jourSemaine = new SimpleDateFormat( ); FacesContext ctx = FacesContext.getCurrentInstance(); ctx.addMessage(null, new FacesMessage(jourSemaine.format(date.getTime()).toUpperCase())); } public void validationGlobale(ComponentSystemEvent evt) { UIComponent source = evt.getComponent(); UIInput saisieJour = (UIInput) source.findComponent( ); UIInput saisieMois = (UIInput) source.findComponent( ); UIInput saisieAnnée = (UIInput) source.findComponent( ); int jour = (Integer) saisieJour.getLocalValue(); Mois mois = (Mois) saisieMois.getLocalValue(); int année = (Integer) saisieAnnée.getLocalValue(); if (!dateValide(jour, mois.ordinal(), année)) { FacesContext ctx = FacesContext.getCurrentInstance(); ctx.addMessage(null, new FacesMessage(erreur)); ctx.renderResponse(); } } private boolean dateValide(int jour, int mois, int année) { if (jour<1) { erreur = ; return false; } erreur = ; if (mois==1) { boolean bissextile = année%4==0 && (année%400==0 || année%100!=0); erreur += bissextile ? : ; return bissextile ? jour<=29 : jour<=28; } boolean trente = mois==3 || mois==5 || mois==8 || mois==10; erreur += trente ? : ; return trente ? jour<=30 : jour<=31; } }
root { display: block; } body { background: green; color: yellow; font-weight: bold; text-shadow: 0px 0px 10px yellow; } .panneau { background: darkgreen; border-radius: 5px; box-shadow: 2px 2px 5px black, -2px -2px 5px lightgreen; padding: 5px; } .entete { font-size: 18px; padding-bottom: 7px; color: aquamarine; } .pied { padding-top: 7px; } input[type= ] { background: yellow; padding-right: 7px; padding-left: 7px; border-radius: 3px; width: 78px; } select { background: yellow; } .resultat { float: right; color: aquamarine; }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet name= library= /> <h:body> <h:form> <h:panelGrid columns= styleClass=panneau headerClass=entete footerClass=pied> <f:event type= listener=#{semaine.validationGlobale} /> <f:facet name=> <h:outputText value= /> </f:facet> <h:outputText value= /> <h:inputText value=#{semaine.jour} id=jour /> <h:outputText value= /> <h:selectOneMenu value=#{semaine.mois} id=mois > <f:selectItems value=#{semaine.listeMois} /> </h:selectOneMenu> <h:outputText value= /> <h:inputText value=#{semaine.année} id=annee /> <f:facet name=> <h:commandButton value= action=#{semaine.recherche}/> <h:messages layout= styleClass=resultat /> </f:facet> </h:panelGrid> </h:form> </h:body> </html>
L'élément important ici est la balise <f:event> qui permet de choisir le type d'événement que vous désirez prendre en compte. Le choix se fait justement au travers de l'attribut type, ici donc l'événement postValidate. Il suffit ensuite de spécifier la méthode qui implémente les fonctionnalités attendues en relation avec cet événement spécifique au travers de l'attribut listener. Remarquez que le placement de cette balise se situe juste au niveau du panneau puisque nous désirons globaliser plusieurs valeurs de composants répartis dans ce dernier.
Quelquefois, vous désirez prendre des décisions particulières avant d'afficher définitivement une page et de rentrer donc dans la phase du rendu, par exemple pour télécharger des données, réaliser des changement sur un composant de la page, ou tout simplement de rendre une navigation conditionnelle vers une autre page.
Par exemple, sur le projet précédent, nous pourrions aboutir à notre page de recherche du jour de la semaine à la condition de s'être déjà authentifier. Par contre, si l'authentification a déjà eu lieu, il ne doit pas être nécessaire de s'authentifier de nouveau et d'accéder ainsi directement à la page de recherche.
Nous allons modifier notre site en conséquence. La page de recherche ne s'appelle plus index.xhtml mais semaine.xhtml. La page index.xhtml servira maintenant à l'authentification. Nous allons également rajouter un bean géré Login qui s'occupera du traitement spécifique de l'authentification.
package bean; import javax.faces.application.ConfigurableNavigationHandler; import javax.faces.bean.*; import javax.faces.context.FacesContext; import javax.faces.event.*; @ManagedBean @SessionScoped public class Login { private String utilisateur = ; private String motDePasse = ; private boolean valide; public String getMotDePasse() { return motDePasse; } public void setMotDePasse(String motDePasse) { this.motDePasse = motDePasse; } public String getUtilisateur() { return utilisateur; } public void setUtilisateur(String utilisateur) { this.utilisateur = utilisateur; } public String connexion() { valide = utilisateur.equalsIgnoreCase( ) && motDePasse.equals( ); return valide ? : ; } public void test(ComponentSystemEvent evt) { if (valide) { FacesContext ctx = FacesContext.getCurrentInstance(); ConfigurableNavigationHandler nav =(ConfigurableNavigationHandler) ctx.getApplication().getNavigationHandler(); nav.performNavigation( ); } } }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <f:view> <f:event type= listener=#{login.test} /> <h:head /> <h:outputStylesheet name= library= /> <h:body> <h:form> <h:panelGrid columns= styleClass=panneau headerClass=entete footerClass=pied> <f:facet name=> <h:outputText value= /> </f:facet> <h:outputText value= /> <h:inputText value=#{login.utilisateur} /> <h:outputText value= /> <h:inputSecret value=#{login.motDePasse} /> <f:facet name=> <h:commandButton value= action=#{login.connexion}/> </f:facet> </h:panelGrid> </h:form> </h:body> </f:view> </html>
Encore une fois, l'élément important est la balise <f:event> qui me permet de choisir cette fois-ci l'événement preRenderView, c'est-à-dire juste le moment avant le rendu de cette page d'accueil. C'est la méthode test() qui est alors lancée automatiquement pour savoir si la page doit effectivement s'afficher ou pas.
Dans ce cas particulier, vous remarquez la présence de la balise <f:view> qui d'habitude n'a aucun intérêt alors qu'ici elle justifie largement sa présence. Elle est effet indispensable puisque le test se fait sur le rendu de la page et cette balise représente l'objet racine UIViewRoot sur lequel repose tous les autres objets graphiques de la page.
Vous devez toujours placer la balise <f:event> juste à l'intérieur de la balise qui sert de référence, ici donc <f:view>. Dans l'exemple précédent, j'avais placer la balise <f:event> juste à l'intérieur <h:panelGrid>.
Tous les composants que nous avons présentés lors de l'étude précédente font partis de JSF et sont disponibles dans toutes les implémentations qui respectent la spécification. En outre, comme elle repose sur des composants réutilisables, JSF fournit le moyen de créer et d'intégrer aisément dans les applications ses propres composants ou de composants provenant de tierces parties (comme nous le verrons à la fin de cette étude).
Nous avons déjà mentionné le fait que tous les composants héritaient, directement ou indirectement, de la classe javax.faces.component.UIComponent. Avant JSF 2.0, pour créer son propre composant, il fallait étendre la classe la plus proche du nouveau composant (UICommand, UIGraphics, UIOutput, etc.), la déclarer dans le fichier faces-config.xml et fournir un descripteur de marqueur et une représentation. Ces étapes étaient complexe.
Le but des composants composites est de permettre aux développeurs de créer de vrais composants graphiques réutilisables sans avoir besoin d'écrire du code Java ou de mettre en place une configuration XML.
Cette nouvelle approche consiste à créer une page XHTML contenant les composants, puis de l'utiliser comme composant élémentaire dans d'autres pages. Cette page XHTML est alors vue comme un véritable composant supportant des validateurs, des convertisseurs et des écouteurs. Les composants composites peuvent contenir n'importe quel marqueur valide et utiliser les modèles. Ils sont traités comme des ressources et doivent donc impérativement se trouver dans les nouveaux répertoires standard des ressources.
Balises | Description |
---|---|
<composite:interface> | Déclare le contrat d'un composant, avec éventuellement : des attributs, des actions, des valeurs par défaut, possibilité de le rendre éditable ou pas, ou avec des facettes. |
<composite:implementation> | Définit l'implémentation d'un composant qui contient un certain nombre d'autres balises JSF usuelles. |
<composite:attribute> | Déclare un attribut pouvant être fourni à une instance du composant. Un marqueur <composite:interface> peut en contenir plusieurs. |
<composite:facet> | Déclare que ce composant supporte une facette avec un nom spécifique. |
<composite:insertFacet> | Insère une facette définie par la page qui fait appel à ce composant. La facette insérée sera alors représentée dans le composant. |
<composite:renderFacet> | Visualise la facette spécifiée par la page qui utilise le composant comme un composant fils. |
<composite:insertChildren> | Tous les composants fils (imbriqués) seront tout simplement insérés dans la représentation de ce composant à l'endroit de cette balise. |
<composite:valueHolder> | Expose une implémentation de ValueHolder. |
<composite:editableValueHolder> | Expose une implémentation de EditableValueHolder. |
<composite:actionSource> | Expose le composant pour une action ou un événement à l'image des boutons et des liens. |
<composite:extension> | La page qui fait appel à ce composant peut placer cette balise dans une interface. La balise <composite:extension> peut contenir des données XML. |
L'écriture d'un composant avec JSF 2.0 est relativement proche de celle que nous utilisons pour Java :
Par exemple, nous pourrions construire un composant d'authentification (sujet relativement fréquent) qui implémente un formulaire avec un nom de login, un mot de passe et un bouton de validation. Nous devons donc implémenter ce composant d'authentification à l'aide des composants JSF <h:form>, <h:inputText>, <h:inputSecret>, <h:commandButton>, etc.
Afin d'être réutilisable, les composants composites ont besoin quelquefois de plus qu'une simple implémentation, et en même temps elles doivent être le plus configurable possible. A titre d'exemple, en reprenant notre composant d'authentification, pour qu'il soit facile à utiliser, avec un maximum de souplesse, il serait intéressant de personnalisé les intitulés associés au nom du login et du mot de passe. Parallèlement, nous pouvons rattâcher un validateur sur l'ensemble des deux champs, rattâcher également un événement de type action sur le bouton de soumission. Vous devez préciser tous ces critères lors de la déclaration de l'interface du composant.
Nous allons tout de suite voir comment créer notre premier composant composite. Pour le premier nous n'allons pas proposer d'attribut. Il s'appelle <comp:debug>. Comme son nom l'indique, il va servir au mode débugage afin de visualiser automatiquement le listing complet de la partie en-tête d'une requête HTTP suivi de ses paramètres, comme cela vous est présenté ci-dessous :
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:cc= xmlns:h= xmlns:c= xmlns:f=> <!-- INTERFACE --> <cc:interface /> <!-- IMPLEMENTATION --> <cc:implementation> <h:panelGrid columns= styleClass=panneau > <f:facet name=> <h:outputText value= /> </f:facet> <c:forEach items=#{headerValues} var=élément> <h:outputText value=#{élément.key} style=color: white /> <c:forEach var=valeur items=#{élément.value}> <h:outputText value=#{valeur} escape= style=color: black /> <br /> </c:forEach> </c:forEach> <c:forEach items=#{paramValues} var=élément> <h:outputText value=#{élément.key} style=color: white /> <c:forEach var=valeur items=#{élément.value}> <h:outputText value=#{valeur} escape= style=color: black /> <br /> </c:forEach> </c:forEach> </h:panelGrid> </cc:implementation> </html>
Maintenant que notre composant est construit, nous pouvons l'utiliser comme n'importe quelle balise standard JSF. Rappelez-vous que nous avons décidé de l'appeler <comp:debug> :
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <!--nouvelle déclaration --> <f:view> <f:event type= listener=#{login.test} /> <h:head /> <h:outputStylesheet name= library= /> <h:body> <h:form> <h:panelGrid columns= styleClass=panneau headerClass=entete footerClass=pied> <f:facet name=> <h:outputText value= /> </f:facet> <h:outputText value= /> <h:inputText value=#{login.utilisateur} /> <h:outputText value= /> <h:inputSecret value=#{login.motDePasse} /> <f:facet name=> <h:commandButton value= action=#{login.connexion}/> </f:facet> </h:panelGrid> </h:form> <comp:debug /> <!-- nouveau composant --> </h:body> </f:view> </html>
Les composants composites sont très utiles puisqu'ils sont par nature réutilisables, et ils sont généralement réutilisables parce qu'ils peuvent être configurés suivant les circonstances. Pour cela, ces composants doivent être paramètrables. Ceci se réalise tout simplement en prévoyant des attributs sur ces balises personnalisées.
Nous pouvons, par exemple, contruire un nouveau composant icone qui permet de placer dans votre page web une petite image qui va permettre, lors d'un clic sur celle-ci, de lancer une action particulière et de se rediriger éventuellement vers une nouvelle page.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:comp=> <h:body style=background: green; color: yellowgreen> <h2>Gestion des liens au travers d'icônes</h2> <hr /> <h:form> <comp:icone image= action=#{gestion.ouvrir}/> <comp:icone image= action=#{gestion.enregistrer} bulle= /> </h:form> </h:body> </html>
Ce composant personnalisé possède trois attributs : le nom de l'image qui représente l'icône, la méthode qui va se lancer lorsque nous cliquons sur cette icône et éventuellement une petite information sous forme de bulle d'aide qui s'affiche lorsque le curseur de la souris passe dessus.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:cc= xmlns:h=> <!-- INTERFACE --> <cc:interface> <cc:attribute name= /> <cc:attribute name= method-signature=java.lang.StringtraitementDuLien() /> <cc:attribute name= /> </cc:interface> <!-- IMPLEMENTATION --> <cc:implementation> <h:commandLink action=#{cc.attrs.action} title=#{cc.attrs.bulle} > <h:graphicImage library= name=#{cc.attrs.image} style=margin: 5px/> </h:commandLink> </cc:implementation> </html>
<!-- INTERFACE --> <cc:interface> <cc:attribute name="date" type="java.util.Date" /> </cc:interface>
Ce composant <comp:icone /> que nous venons de construire est déjà très intéressant, mais il est possible d'aller plus loin dans notre démarche en le rendant plus sophistiqué. Nous pouvons par exemple :
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:cc= xmlns:h=> <!-- INTERFACE --> <cc:interface> <cc:attribute name= required=/> <cc:attribute name= default= /> <cc:attribute name= default= /> <cc:attribute name= method-signature=java.lang.StringtraitementDuLien() /> <cc:attribute name= /> </cc:interface> <!-- IMPLEMENTATION --> <cc:implementation> <h:commandLink action=#{cc.attrs.action} title=#{cc.attrs.bulle} immediate=#{not cc.attrs.phaseValidation}> <h:graphicImage library= name=#{cc.attrs.image} styleClass=#{cc.attrs.styleClass}/> </h:commandLink> </cc:implementation> </html>
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:comp=> <h:head /> <h:outputStylesheet name= /> <h:body> <h2>Gestion des liens au travers d'icônes</h2> <hr /> <h:form> <comp:icone image= action=#{gestion.ouvrir}/> <comp:icone image= action=#{gestion.enregistrer} bulle= /> </h:form> </h:body> </html>
root { display: block; } body { background: green; color: yellowgreen; } .icone { border-radius: 22px; margin: 5px; padding: 6px; box-shadow: 1px 1px 3px black, -1px -1px 2px white; }
Jusqu'à présent, nous avons construit des composants où seuls les balises de la partie implémentation réalise le traitement souhaité. Il s'agit, dans ce cadre là d'une simple factorisation d'un ensemble de balises pour en avoir une seule qui correspond à une fonctionnalité donnée.
Dans certains cas, nous pouvons aussi avoir besoin de traitement de fond et donc d'avoir, dans la génération de ces nouveaux composants, une interaction avec un bean géré pour réaliser le traitement souhaité.
Nous allons voir comment faire en reprenant le projet sur le calcul du jour de la semaine et proposer un nouveau composant <comp:login />, qui sera d'ailleurs bien utile pour beaucoup d'applications web.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:comp=> <h:head /> <h:outputStylesheet name= library= /> <h:body> <comp:login utilisateur=#{utilisateur} soumettre=#{utilisateur.connexion} /> </h:body> </html>
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:cc= xmlns:h= xmlns:f=> <!-- INTERFACE --> <cc:interface> <cc:attribute name=utilisateur required= /> <cc:attribute name=soumettre method-signature=java.lang.Stringaction() /> </cc:interface> <!-- IMPLEMENTATION --> <cc:implementation> <h:form> <h:panelGrid columns= styleClass= headerClass= footerClass=> <f:facet name=> <h:outputText value= /> </f:facet> <h:outputText value= /> <h:inputText value=#{cc.attrs.utilisateur.nom} /> <h:outputText value= /> <h:inputSecret value=#{cc.attrs.utilisateur.motDePasse} /> <f:facet name=> <h:commandButton value= action=#{cc.attrs.soumettre}/> </f:facet> </h:panelGrid> </h:form> </cc:implementation> </html>
package bean; import javax.faces.bean.*; @ManagedBean @SessionScoped public class Utilisateur { private String nom = ; private String motDePasse = ; public String getMotDePasse() { return motDePasse; } public void setMotDePasse(String motDePasse) { this.motDePasse = motDePasse; } public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } public String connexion() { boolean valide = nom.equalsIgnoreCase( ) && motDePasse.equals( ); return valide ? : ; } }
Afin d'offrir le maximum de souplesse, il est possible de faire en sorte que le composant soit configurable par l'utilisateur, afin de permettre, par exemple, de rattacher un validateur sur les entrées internes du composants. Il est nécessaire, dans ce cas là, d'exposer ces zones de saisie afin de les mettre en relation avec les validateurs souhaités par l'utilisateur.
Afin de permettre cette interaction, il suffit d'utiliser la balise <cc:editableValueHolder /> à l'intérieur de laquelle vous spécifiez, d'une part l'identificateur qui sera exploité par l'utilisateur, au travers de l'attribut name, et d'autre part la ou les cibles associées figurant à l'intérieur du composant, à l'aide de l'attribut targets.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:cc= xmlns:h= xmlns:f=> <!-- INTERFACE --> <cc:interface> <cc:editableValueHolder name="saisieNom" targets="form:nom" /> <cc:editableValueHolder name="saisieMotDePasse" targets="form:motDePasse" /> <cc:editableValueHolder name="lesDeux" targets="form:nom form:motDePasse" /> <cc:attribute name="utilisateur" required= /> <cc:attribute name="soumettre" method-signature=java.lang.Stringaction() /> </cc:interface> <!-- IMPLEMENTATION --> <cc:implementation> <h:form id="form"> <h:panelGrid columns= styleClass=panneau headerClass= footerClass=> <f:facet name=> <h:outputText value= /> </f:facet> <h:outputText value= /> <h:inputText value=#{cc.attrs.utilisateur.nom} id="nom" label= /> <h:message for="nom" /> <h:outputText value= /> <h:inputSecret value=#{cc.attrs.utilisateur.motDePasse} id="motDePasse" label= /> <h:message for="motDePasse" /> <f:facet name=> <h:commandButton value= action=#{cc.attrs.soumettre}/> </f:facet> </h:panelGrid> </h:form> </cc:implementation> </html>
<h:form id="form"> <h:inputText value=#{cc.attrs.utilisateur.nom} id="nom" label= /> </h:form>
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:comp= xmlns:f=> <h:head /> <h:outputStylesheet name= library= /> <h:body> <comp:login utilisateur=#{utilisateur} soumettre=#{utilisateur.connexion}> <f:validateLength maximum= for="lesDeux" /> <f:validateLength minimum= for="saisieNom" /> <f:validateLength minimum= for="saisieMotDePasse" /> </comp:login> </h:body> </html>
Pour exposer ses composants, nous venons de découvrir la balise <cc:editableValueHolder> qui est spécialisée dans les zones de saisie. Il existe deux autres balises qui permettent d'exposer d'autres parties du composant, <cc:valueHolder> et <cc:actionSource>.
A titre d'exemple, nous pouvons exposer le bouton de soumission de la validation de la connexion.
.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:cc= xmlns:h= xmlns:f=> <!-- INTERFACE --> <cc:interface> <cc:actionSource name="bouton" targets="form:actionListener" /> <cc:editableValueHolder name="saisieNom" targets="form:nom" /> <cc:editableValueHolder name="saisieMotDePasse" targets="form:motDePasse" /> <cc:editableValueHolder name="lesDeux" targets="form:nom form:motDePasse" /> <cc:attribute name="utilisateur" required= /> <cc:attribute name="soumettre" method-signature=java.lang.Stringaction() /> </cc:interface> <!-- IMPLEMENTATION --> <cc:implementation> <h:form id="form"> <h:panelGrid columns= styleClass=panneau headerClass= footerClass=> <f:facet name=> <h:outputText value= /> </f:facet> <h:outputText value= /> <h:inputText value=#{cc.attrs.utilisateur.nom} id="nom" label= /> <h:message for="nom" /> <h:outputText value= /> <h:inputSecret value=#{cc.attrs.utilisateur.motDePasse} id="motDePasse" label= /> <h:message for="motDePasse" /> <f:facet name=> <h:commandButton value= action=#{cc.attrs.soumettre}"actionListener"/> </f:facet> </h:panelGrid> </h:form> </cc:implementation> </html>
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:comp= xmlns:f=> <h:head /> <h:outputStylesheet name= library= /> <h:body> <comp:login utilisateur=#{utilisateur} soumettre=#{utilisateur.connexion}> <f:actionListener for="bouton" type="LoginActionListener" /> <f:validateLength maximum= for="lesDeux" /> <f:validateLength minimum= for="saisieNom" /> <f:validateLength minimum= for="saisieMotDePasse" /> </comp:login> </h:body> </html>
Toujours, dans la notion de composant personnalisé configurable, il est également possibles de rajouter des facettes à l'image des panneaux et des tableaux sur lesquelles nous avons déjà travaillé.
Nous pourrions par exemple proposer une invite de l'authentification paramétrable (éventuellement sans invite) sur la partie haute du panneau. Il suffit pour cela, comme nous l'avons déjà pratiqué, de proposer la balise <f:facet>.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:comp= xmlns:f=> <h:head /> <h:outputStylesheet name= library= /> <h:body> <comp:login utilisateur=#{utilisateur} soumettre=#{utilisateur.connexion}> <f:facet name="header">Authentification</f:facet> <f:validateLength maximum= for="lesDeux" /> <f:validateLength minimum= for="saisieNom" /> <f:validateLength minimum= for="saisieMotDePasse" /> </comp:login> </h:body> </html>
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:cc= xmlns:h= xmlns:f=> <!-- INTERFACE --> <cc:interface> <cc:facet name="header" /> <cc:editableValueHolder name="saisieNom" targets="form:nom" /> <cc:editableValueHolder name="saisieMotDePasse" targets="form:motDePasse" /> <cc:editableValueHolder name="lesDeux" targets="form:nom form:motDePasse" /> <cc:attribute name="utilisateur" required= /> <cc:attribute name="soumettre" method-signature=java.lang.Stringaction() /> </cc:interface> <!-- IMPLEMENTATION --> <cc:implementation> <h:form id="form"> <h:panelGrid columns= styleClass=panneau headerClass= footerClass=> <cc:insertFacet name="header" /> <h:outputText value= /> <h:inputText value=#{cc.attrs.utilisateur.nom} id="nom" label= /> <h:message for="nom" /> <h:outputText value= /> <h:inputSecret value=#{cc.attrs.utilisateur.motDePasse} id="motDePasse" label= /> <h:message for="motDePasse" /> <f:facet name=> <h:commandButton value= action=#{cc.attrs.soumettre}"actionListener"/> </f:facet> </h:panelGrid> </h:form> </cc:implementation> </html>
Les composants composites sont représentés par un ensemble de balises. Quelquefois, il serait judicieux de pouvoir proposer éventuellement un contenu supplémentaire à ce composant que nous placerions dans le corps de sa balise.
Par défaut, si vous placez quelque chose à l'intérieur du corps de la balise, JSF n'en tient pas compte. Vous pouvez changer ce comportement en utilisant la balise <cc:insertChildren /> que vous placez à l'endroit voulu dans la partie implémentation de votre définition de composant.
A titre d'exemple, nous pourrions éventuellement placer un lien vers une nouvelle page d'enregistrement à l'intérieur du composant <comp:login> :
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:comp= xmlns:f=> <h:head /> <h:outputStylesheet name= library= /> <h:body> <comp:login utilisateur=#{utilisateur} soumettre=#{utilisateur.connexion}> <f:facet name="header">Authentification</f:facet> <f:validateLength maximum= for="lesDeux" /> <f:validateLength minimum= for="saisieNom" /> <f:validateLength minimum= for="saisieMotDePasse" /> <h:link style="color: white">Nouvel utilisateur...</h:link> </comp:login> </h:body> </html>
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:cc= xmlns:h= xmlns:f=> <!-- INTERFACE --> <cc:interface> <cc:facet name="header" /> <cc:editableValueHolder name="saisieNom" targets="form:nom" /> <cc:editableValueHolder name="saisieMotDePasse" targets="form:motDePasse" /> <cc:editableValueHolder name="lesDeux" targets="form:nom form:motDePasse" /> <cc:attribute name="utilisateur" required= /> <cc:attribute name="soumettre" method-signature=java.lang.Stringaction() /> </cc:interface> <!-- IMPLEMENTATION --> <cc:implementation> <h:form id="form"> <h:panelGrid columns= styleClass=panneau headerClass= footerClass=> <cc:insertFacet name="header" /> <h:outputText value= /> <h:inputText value=#{cc.attrs.utilisateur.nom} id="nom" label= /> <h:message for="nom" /> <h:outputText value= /> <h:inputSecret value=#{cc.attrs.utilisateur.motDePasse} id="motDePasse" label= /> <h:message for="motDePasse" /> <cc:insertChildren /> <f:facet name=> <h:commandButton value= action=#{cc.attrs.soumettre}"actionListener"/> </f:facet> </h:panelGrid> </h:form> </cc:implementation> </html>
A titre de conclusion, je vous propose de fabriquer un nouveau composant composite qui permet de visualiser une vignette d'une photo originale en taille réduite dont la largeur peut être configurable. Pour cela, nous allons nous servir de la servlet que nous avons déjà mis en oeuvre. Je vous donne la liste du code sans commentaire particulier.
package servlet; import java.awt.geom.AffineTransform; import java.awt.image.*; import java.io.*; import java.net.URL; import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet( ) public class Vignette extends HttpServlet { private BufferedImage image; private BufferedImage vignette; private int largeurVignette; private void créerVignette() { double ratio = (double)largeurVignette / image.getWidth(); vignette = new BufferedImage((int)(image.getWidth()*ratio), (int)(image.getHeight()*ratio), image.getType()); AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio); int interpolation = AffineTransformOp.TYPE_NEAREST_NEIGHBOR; AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation); retaillerImage.filter(image, vignette); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType( ); OutputStream out = response.getOutputStream(); largeurVignette = Integer.parseInt(request.getParameter( )); String fichierImage = request.getParameter( ); image = ImageIO.read(new URL( +fichierImage)); créerVignette(); ImageIO.write(vignette, , out); out.close(); } }
.vignette { border-radius: 10px; box-shadow: 2px 2px 5px black; margin: 5px; }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:cc= xmlns:h=> <!-- INTERFACE --> <cc:interface> <cc:attribute name= required= /> <cc:attribute name= default= /> </cc:interface> <!-- IMPLEMENTATION --> <cc:implementation> <h:outputStylesheet library= name= /> <h:graphicImage url=#{cc.attrs.image}#{cc.attrs.largeur} styleClass=vignette /> </cc:implementation> </html>
Les trois fichiers précédents constituent le composant composite dans son ensemble. Afin de bien montrer son utilisation, je vous propose la page d'accueil suivante avec son bean géré correspondant.
package bean; import java.io.File; import javax.annotation.PostConstruct; import javax.faces.bean.*; @ManagedBean(name= ) @ViewScoped public class GaleriePhoto implements java.io.Serializable { private final String répertoire = ; private String[] fichiers; private int largeur = 250; @PostConstruct void init() { fichiers = new File(répertoire).list(); } public String[] getFichiers() { return fichiers; } public int getLargeur() { return largeur; } public void setLargeur(int largeurVignette) { largeur = largeurVignette; } public String getRépertoire() { return répertoire; } }
root { display: block; } body { background: darkgreen; color: yellow; font-weight: bold; text-shadow: 0px 0px 10px yellow; text-align: center; } input[type= ] { text-align: center; border-radius: 5px; box-shadow: 2px 2px 5px black inset; }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:comp= xmlns:c=> <h:head /> <h:outputStylesheet library= name= /> <h:body> <h:form> <h2>Galerie de photos</h2> Largeur : <h:inputText value=#{galerie.largeur} size=/> <hr /> <c:forEach items=#{galerie.fichiers} var=> <comp:vignette image=#{galerie.répertoire}#{fichierImage} largeur=#{galerie.largeur} /> </c:forEach> </h:form> </h:body> </html>
Une application web doit fournir une interface riche et rapide. Cette réactivité peut être obtenue en ne modifiant que de petites parties de la page de façon asynchrone, sans que la page ne soit rechargée entièrement, et c'est exactement pour cela qu'Ajax a été conçu.
Par exemple, sur notre application web de conversion que nous avons traité lors de l'étude précédente, nous pourrions souhaiter que seule la zone des francs soit changée lors de la soumission de la requête de conversion à partir des €uros :
Voici le code correspondant :
Je rappelle que le bean géré est la classe Java annotée par @ManagedBean qui possède deux propriétés, euro et franc, qui sont synchronisées avec les valeurs des composants de la page. La méthode euroFranc() de ce bean managé réalise le traitement de la conversion.
Conceptuellement, Ajax est simple. Les requêtes Ajax diffèrent des requêtes standard HTTP sur seulement deux points essentiels :
Lorsque vous désirez soumettre un tout petit traitement en relation avec un ou plusieurs composants de votre page web, sans prendre en compte la globalité du formulaire, vous devez tenir compte des critères suivants :
Nous pourrions, à titre d'exemple très simple pour valider ce principe, réaliser une toute petite application web qui permet de faire écho à toute ce que l'utilisateur saisie. A chaque fois que l'utilisateur tape une nouvelle lettre, nous la voyons intantanément réécrite sur la partie droite du panneau.
Il est également possible d'avoir plusieurs exécutions et plusieurs rendus possibles en même temps. Dans le cas d'école suivant, lorsque nous quittons la zone de saisie (blur), cette dernière est analysée avec la zone du mot de passe. A l'issue du traitement Ajax demandé, les deux sorties identifiées par erreur et erreurPassword sont remises à jour.
<h:inputText > <f:ajax event= execute="@this motDePasse" render="erreur erreurPassword" /> </h:inputText> <h:outputText id="erreur" /> <h:inputSecret id="motDePasse" /> <h:outputText id="erreurPassword" />
Ceci dit, dans la plupart des cas, vous exécutez uniquement se qui est géré par la zone de saisie qui englobe la balise <f:ajax>. Comme ce cas de figure est très fréquent, JSF exécute @this par défaut. Il est ainsi possible d'omettre l'écriture execute="@this.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet name= library= /> <h:body> <h:form> <h:panelGrid columns= styleClass="panneau" cellpadding=> <h:outputText value= /> <h:inputText value=#{utilisateur.nom}> <f:ajax event="keyup" render="echo" /> </h:inputText> <h:outputText value=#{utilisateur.nom} id="echo" /> </h:panelGrid> </h:form> </h:body> </html>
Au dela de @this, vous pouvez également utiliser d'autres valeurs prédéfinies comme @form, @all ou @none dans l'attribut execute de la balise <f:ajax>. Dans le tableau ci-dessous, j'en profite pour recenser les attributs intégrés dans cette balise.
Attribut | Description |
---|---|
disabled | Désactive la balise lorsque vous spécifiez la valeur true, comme pour les autres balises qui possèdent cet attribut. |
event | Evénement qui lance la requête Ajax. Vous pouvez choisir tous les événements JavaScript que nous avons abordé lors de l'étude précédente, sans toutefois le prefixe on. Il est également possible de choisir les événements action et valueChange que nous connaissons déjà. |
execute | Liste de composants, séparé par un espace, que JSF prend en compte lors d'une soumission à une requête Ajax. Les mots clés valides sont @this @form @all @none. Si vous ne spécifiez pas cet attribut, JSF utilise la mot clé @this comme valeur par défaut. |
immediate | Lorsque cet attribut est validé avec la valeur true, la validation est occultée. |
listener | JSF invoque la méthode méthode(AjaxBehaviorEvent evt) à chaque requête Ajax durant la phase "Appel de l'application". C'est intéressant que cela soit durant cette phase, puisque dans ce cas là, les propriétés du bean géré sont déjà validées et introduites dans le bean avant l'exécution de cette méthode. |
render | Liste d'identifiants, séparés par un espace, associés aux différents composants qui vont être remis à jour à l'issue de la requête Ajax. Vous avez le droit de prendre également les mots clés valides utilisés par l'attribut execute, savoir @this @form @all @none. Si vous ne spécifiez pas cet attribut render, la valeur par défaut est @none, ce qui aura pour conséquence de ne proposer aucun rendu après la requête complète Ajax. |
Un des grands intérêt d'utiliser Ajax est de vérifier "à la volée" les valeurs saisies par l'utilisateur. Grâce à ce système, nous ne sommes plus obligé d'attendre la soumisssion de la requête entière, en cliquant sur le bouton prévu à cet effet, pour vérifier la validité des données introduites.
A titre d'exemple, je vous propose de reprendre la partie correspondant à l'identification d'un utilisateur. Le nom de l'identifiant doit avoir au moins 2 caractères, uniquement alphabétiques. Le contrôle s'effectue après chaque caractère saisie. Le mot de passe doit lui-même posséder au moins quatre caractères. Cette fois-ci, le contrôle s'effectue à la perte du focus. Pour terminer, lorsque le curseur de la souris passe au-dessus du bouton de soumission, les deux zones de saisie sont évaluées.
package bean; import javax.faces.bean.*; import javax.validation.constraints.*; @ManagedBean @SessionScoped public class Login { @Pattern(regexp= , message= ) @Size(min=2, message= ) private String utilisateur; @Size(min=4, message= ) private String motDePasse; public String getMotDePasse() { return motDePasse; } public void setMotDePasse(String motDePasse) { this.motDePasse = motDePasse; } public String getUtilisateur() { return utilisateur; } public void setUtilisateur(String utilisateur) { this.utilisateur = utilisateur; } public String connexion() { boolean valide = utilisateur.equalsIgnoreCase( ) && motDePasse.equals( ); return valide ? : ; } }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet name="principal.css" library="css" /> <h:body> <h:form> <h:panelGrid columns= styleClass="panneau" headerClass="entete" footerClass="pied"> <f:facet name=> <h:outputText value= /> </f:facet> <h:outputText value= /> <h:inputText value=#{login.utilisateur} id="nom"> <f:ajax event="keyup" render="erreurNom" /> </h:inputText> <h:message for= id="erreurNom" /> <h:outputText value= /> <h:inputSecret value=#{login.motDePasse} id="passe"> <f:ajax event="blur" render="erreurPasse" /> </h:inputSecret> <h:message for= id="erreurPasse" /> <f:facet name=> <h:commandButton value= action=#{login.connexion}> <f:ajax event="mouseover" execute="nom passe" render="erreurNom erreurPasse" /> </h:commandButton> </f:facet> </h:panelGrid> </h:form> </h:body> </html>
Dans tout ce que nous venons de voir, nous plaçons systématiquement la balise <f:ajax> à l'intérieur d'autres composants qui servent alors de soumission à une requête Ajax associée. JSF permet également d'associer une requête Ajax cette fois-ci sur un groupe de composants.
En reprenant l'exemple précédent, la balise <f:ajax> peut englober les deux zones de saisies afin de les prendre en compte automatiquement. Il reste juste à définir le rendu que vous désirez prendre en compte.
Par défaut, ce sont les événements de type action (pour les boutons et les liens) et les événement de type valueChange (pour les composants de sortie, d'entrée et de sélection) qui sont automatiquement évalués.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet name="principal.css" library="css" /> <h:body> <h:form> <h:panelGrid columns= styleClass="panneau" headerClass="entete" footerClass="pied"> <f:facet name=> <h:outputText value= /> </f:facet><h:outputText value= /> <h:inputText value=#{login.utilisateur} id="nom" /> <h:message for= id="erreurNom" /> <h:outputText value= /> <h:inputSecret value=#{login.motDePasse} id="passe" /> <h:message for= id="erreurPasse" /> </f:ajax> <f:facet name=> <h:commandButton value= action=#{login.connexion}> <f:ajax event="mouseover" execute="nom passe" render="erreurNom erreurPasse" /> </h:commandButton> </f:facet> </h:panelGrid> </h:form> </h:body> </html>
Dans cet exemple, c'est lors d'un changement de valeur sur une des zones de saisie que la validation est vérifiée. Si vous souhaitez prendre un autre type d'événement, et donc ne pas prendre ceux proposer par défaut, il suffit, comme précédemment, d'utiliser l'attribut event et de spécifier celui qui vous intéresse. Par exemple, si vous désirez soumettre la validation après chaque changement de focus, voilà ce qu'il faudrait changer :
<f:ajax event="blur" render="erreurNom erreurPasse">.
Pour terminer ce chapitre, je vous propose de traiter un dernier exemple. Nous allons reprendre notre application sur le calcul du jour de la semaine suivant la date que nous fixons.
Cette fois-ci, toutefois, nous proposons une liste des jours du mois en correspondance du mois et de l'année en cours. Ainsi, nous n'aurons plus de problème de validation. Par ailleurs, dès qu'un changement est effectué, la liste des jours doit être recalculée et le résultat doit être affichée instantanément.
package bean; import java.text.SimpleDateFormat; import java.util.*; import javax.annotation.PostConstruct; import javax.faces.bean.*; import javax.faces.event.AjaxBehaviorEvent; @ManagedBean(name= ) @ViewScoped public class JourSemaine { private int jour; private int[] jours; private enum Mois {Janvier, Février, Mars, Avril, Mai, Juin, Juillet, Août, Septembre, Octobre, Novembre, Décembre}; private Mois mois; private int année; private String résultat; public int getJour() { return jour; } public void setJour(int jour) { this.jour = jour; } public Mois getMois() { return mois; } public void setMois(Mois mois) { this.mois = mois; listeJours(); } public int getAnnée() { return année; } public void setAnnée(int année) { this.année = année; listeJours(); } public Mois[] getListeMois() { return Mois.values(); } public int[] getJours() { return jours; } public String getRésultat() { return résultat; } @PostConstruct private void init() { Calendar date = Calendar.getInstance(); jour = date.get(Calendar.DAY_OF_MONTH); année = date.get(Calendar.YEAR); setMois(Mois.values()[date.get(Calendar.MONTH)]); SimpleDateFormat jourSemaine = new SimpleDateFormat( ); résultat = jourSemaine.format(date.getTime()).toUpperCase(); } public void recherche(AjaxBehaviorEvent evt) { Calendar date = new GregorianCalendar(année, mois.ordinal(), jour); SimpleDateFormat jourSemaine = new SimpleDateFormat( ); résultat = jourSemaine.format(date.getTime()).toUpperCase(); } private void listeJours() { int nombreJourMaxi; int intMois = mois.ordinal(); if (intMois==1) { boolean bissextile = année%4==0 && (année%400==0 || année%100!=0); nombreJourMaxi = bissextile ? 29 : 28; } else { boolean trente = intMois==3 || intMois==5 || intMois==8 || intMois==10; nombreJourMaxi = trente ? 30 : 31; } jours = new int[nombreJourMaxi]; for (int i=0; i<nombreJourMaxi; i++) jours[i] = i+1; } }
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns= xmlns:h= xmlns:f=> <h:head /> <h:outputStylesheet name="principal.css" library="css" /> <h:body> <h:form> <h:panelGrid columns= styleClass="panneau" headerClass="entete" footerClass="pied"> <f:facet name=> <h:outputText value= /> </f:facet> <f:ajax render="jour resultat" listener=#{semaine.recherche}> <h:outputText value= /> <h:selectOneMenu value=#{semaine.jour} id="jour"> <f:selectItems value=#{semaine.jours} /> </h:selectOneMenu> <h:outputText value= /> <h:selectOneMenu value=#{semaine.mois}> <f:selectItems value=#{semaine.listeMois} /> </h:selectOneMenu> <h:outputText value= /> <h:inputText value=#{semaine.année} /> </f:ajax> <f:facet name=> <h:outputText value=#{semaine.résultat} styleClass="resultat" id="resultat" /> </f:facet> </h:panelGrid> </h:form> </h:body> </html>
La balise <f:ajax> entoute toutes les zones de saisie. Dès qu'un changement est effectuée, la liste de la sélection du jour est remise à jour systématiquement et le calcul du jour de la semaine est automatiquement redemandé grâce à l'écouteur placée sur cette balise.
Avant de conclure sur cette étude, je désire juste vous évoquer qu'il existe une librairie très riche et très performante, avec des composants JSF de très haut niveau. Cette librairie s'appelle PrimeFaces, et elle peut être automatiquement introduite dans Netbeans.
Afin de connaître toutes les possibilités qui vous sont offertes dans cette librairie, je vous invite à rejoindre le site correspondant.
.