Navigation

Navigation avec Seam

Un article de ODcWiki.

Jump to: navigation, search
Image:Auteur.png Auteur : hdarmet.


Seam est un framework proposé par le groupe JBoss et dont la conception est dirigée par Gavin King, le « père » d’hibernate. Rappelons que ce framework permet d’abord d’intégrer aisément JavaServer Faces (JSF) avec les EJB 3.0 (sessions et entités).

Cet article présente un principe de navigation entre pages d’une application écrite avec Seam (et donc avec JSF et les EJB 3.0). Trois points – correspondant aux besoins les plus courants – sont abordés :

  • Comment ouvrir une page et permettre de revenir ensuite à la page initiale à l’aide d’un bouton « retour ».
  • Comment passer un paramètre d’une page vers un autre, pour, par exemple, passer d’une liste vers le détail de l’élément sélectionné.
  • Comment permettre la sélection d’une entité à partir d’une page liste et l’affecter à un lien édité par une page formulaire.

Cet article est assez technique. Il demande d’avoir un certain niveau de connaissance sur les technologies Seam, EJB 3.0 et JSF.

Sommaire

Quelques rudiments de navigation avec JSF et Seam

La navigation entre pages de Seam repose, pour l’essentiel, sur celle proposée par JSF. Pour naviguer avec JSF il faut insérer dans une page, un composant d’action (bouton, hyper lien) et préciser dans l’attribut « action » de ce composant, une directive de navigation :

<h:commandButton value="Details" action="pagedetail" />

Cette snippet tirée d’une JSP (ou d’une facelet), intègre dans la page, un bouton nommé « Details ». L’activation de ce bouton provoque une navigation vers la page étiquetée par « pagedetail ».

Comment cela fonctionne-t-il ? L’activation du bouton provoque la soumission de la page. JSF récupère la valeur de l’action du bouton (« pagedetail » dans notre cas). Cette valeur est passée (avec le nom de la page courante) à un composant du moteur de JSF : Le NavigationHandler. Celui ci consulte la configuration de JSF. Cette configuration contient des directives de navigation qui ressemblent à celles-ci :

<navigation-rule>
        <from-view-id>/maliste.jsp</from-view-id>
        
        <navigation-case>
            <from-outcome>pagedetail</from-outcome>
            <to-view-id>/mondetail.jsp</to-view-id>
            <redirect/>
        </navigation-case>
        
        <navigation-case>
            <from-outcome>retour</from-outcome>
            <to-view-id>/menu.jsp</to-view-id>
            <redirect/>
        </navigation-case>
            
    </navigation-rule>

Si le bouton était membre de la page « maliste.jsp », la règle de navigation présentée ci-dessus s’applique. Il recherche le « cas de navigation » idoine, qui est donc celui indiquant que la page cible est « mondetail.jsp ». Le NavigationHandler demande alors l’ouverture de cette nouvelle page.

Une navigation aussi rudimentaire et statique est rarement utilisable. C’est pourquoi JSF permet de remplacer le contenu de l’attribut « action » par la référence vers une méthode d’un JavaBean qui n’accepte pas de paramètre et qui retourne une string :

public String ouvrirDetail() {
		return "pagedetail";
	}

La référence à cette méthode, à partir d’un bouton de la JSP, est matérialisée par une expression JSF EL :

<h:commandButton value="Details" action="#{monbean.ouvrirDetail}" />

Généralement, le contenu d’une telle méthode est plus complexe : il effectue un traitement qui lui permet de choisir une étiquette parmi plusieurs possibilités. Cette méthode peut aussi renvoyer null, ce qui indique qu’aucune navigation ne doit être effectuée.

Voila pour ce qui est strictement du JSF. Ce que Seam apporte de nouveau à ce mécanisme est double :

  • Il enrichit le langage d’expression JSF EL permettant d’appeler des méthodes avec paramètres .
  • Il permet de passer directement le nom de la page à ouvrir, sans passer par le truchement d’une étiquette.

Voici un exemple qui illustre ces deux spécificités de JSF. Commençons par les méthodes de notre bean :

public DomainObject getCurrent() {
		return currentObject;
	}
 
        public String ouvrirDetail(DomainObject ett) {
		return "/detail"+ett.getTypeName()+".jsp";
	}

Voici la déclaration du bouton dans la JSP :

<h:commandButton value="Details" 
      action="#{monbean.ouvrirDetail(monbean.current)}" />

Désormais, la réponse de la méthode « ouvrirDetail » va dépendre du type de l’objet courant édité par le bean. Cet objet a été passé en paramètre ; il provient de la réponse d’une autre méthode (un getter) du bean.

On remarque que ouvrirDetail donne directement le nom de la JSP à ouvrir. Dans ce cas, la consultation du fichier de configuration de JSF n’est pas nécessaire. Notons que Seam n’interdit pas d’utiliser une étiquette, il permet simplement de s’en passer. Je n’épiloguerais pas sur l’intérêt ou non de passer par ce mécanisme d’indirection proposé par JSF : tout dépend des cas. Dans la suite de cet article nous allons retourner directement le nom des JSP à atteindre afin de simplifier les exemples de code. Tout ce qui est expliqué reste applicable si des étiquettes sont utilisées.

Implémenter une fonction retour rudimentaire

L’idée de la fonction retour est que si on navigue de la page A.jsp vers la page B.jsp, puis que l’on demande un retour à partir de B.jsp, on se retrouve sur A.jsp. Mais si la page B.jsp a été ouverte à partir de C.jsp, c’est C.jsp qui doit réapparaître …

Pour implémenter une telle fonctionnalité de manière maintenable, il est indispensable que le nom de la JSP vers lequel retourner ne soit pas indiqué en dur dans B.jsp.

Une première solution consiste à passer la page de retour en paramètre de la page A.jsp ou C.jsp (ou autre ..) vers B.jsp. Cela peut se faire facilement en utilisant le mécanisme d’injection/outjection proposé par Seam :

Dans BeanA.jsp, le backing bean de A.jsp, on écrira :

@Out(value="pageretour", required=false)
	String retour;
 
	public String ouvrirB() {
		retour = "/A.jsp"; 
		return "/B.jsp";
	}

Dans BeanB.jsp, le backing bean de B.jsp on écrira :

@In("pageretour")
	String retour;
 
	public String back() {
		return retour;
	}

Comment cela fonctionne t’il ?

La méthode ouvrirB, activée par le bouton de la page A.jsp modifie la valeur d’un attribut du bean qui est « out-jecté », avant d’indiquer que la page B.jsp doit être ouverte. La valeur outjectée, est placée automatiquement dans le contexte de Seam sous l’étiquette « pageretour », au moment où ouvrirB se termine.

La page B.jsp s’ouvre. Dès qu’une méthode de BeanB est appelée, Seam injecte tous les attributs marqués par l’annotation @In. Il récupère donc la valeur outjectée par BeanA dans son propre attribut nommé « retour ». Cette valeur est utilisée par la méthode back() qui renvoie la navigation vers A.jsp.

Noter que la valeur outjectée par BeanA et injectée par BeanB est marquée comme facultative dans BeanA et requise (le défaut) dans BeanB. En effet, au moment ou A.jsp est affiché, cette valeur n’est pas définie. Ce n’est que lorsque A.jsp passe la main à B.jsp que cette valeur est renseignée. En revanche, dans B.jsp, il est nécessaire que cette valeur soit présente (est elle l’est obligatoirement si l’utilisateur est passé par une page comme A.jsp implémentant le mécanisme de « retour » que nous venons de décrire).

Améliorer la fonction retour

La limite du mécanisme qui vient d’être présenté est qu’il résiste mal à un enchaînement de pages qui peuvent nécessiter des retours multiples : si A.jsp permet d’ouvrir B.jsp qui permet à son tour d’ouvrir C.jsp, les valeurs de retours vont s’écraser : seul le premier retour sera correctement effectué.

Il est possible d’outjecter/injecter de multiples valeurs de retour sous des étiquettes différentes « retourB », « retourC » etc. Mais cela risque d’apporter pas mal de confusion, d’encombrer le contexte de Seam, et de toute façon, cela ne permet pas de gérer une même page qui serait ouvertes plusieurs fois : A.jsp -> B.jsp -> C.jsp -> B.jsp.

Tous nos problèmes peuvent être aisément résolus en utilisant comme valeur outjectée/injectée, non pas une chaîne de caractères, mais une pile de strings. Une page qui veut en ouvrir une autre commence par empiler son propre nom. Une page qui doit effectuer un retour, dépile le nom de la page vers laquelle elle doit naviguer :

BeanA.java contient le code suivant, qui initialise la pile :

@Out(value="Navigation", required=false)
	Stack<String> navigationStack;
 
	public String ouvrirB() {
		navigationStack = new Stack<String>();
		navigationStack.push("/A.jsp");
		return "/B.jsp";
	}

BeanB.java contient le code permettant de récupérer la pile et de l’exploiter :

@In("Navigation")
	Stack<String> navigationStack;
 
	public String ouvrirC() {
		navigationStack.push("/B.jsp");
		return "/C.jsp";
	}
 
	public String back() {
		return navigationStack.pop();
	}

Le code de la méthode BeanB.ouvrirC n’est pas totalement similaire à celui de la méthode BeanA.ouvrirB, car BeanB doit récupérer une pile existante et non en créer une nouvelle. On réservera donc le code donné pour BeanB.ouvrirC pour les pages d’entrée de l’application (menu général).

Notons que JSF propose une méthode permettant de retrouver automatiquement le nom de la page courante :

String viewId = 
               FacesContext.getCurrentInstance().getViewRoot().getViewId();

Ceci nous permet d’écrire une méthode d’ouverture de page générique :

public String openPage(String newViewId) {
                String viewId = FacesContext.getCurrentInstance().
                      getViewRoot().getViewId();
		navigationStack.push(viewId);
		return newViewId;
	}

Que nous appellerons dans nos pages de la façon suivante :

<h:commandButton value="pageC" 
    action="#{beanB.openPage('/C.jsp')}" />

Remarquez que nous avons utilisé la faculté – offerte par Seam (ou JSF 1.2) – de passer des paramètres à une méthode invoquée à l’aide de JSF EL.

Passer un paramètre à l’ouverture d’une page

Il est souvent nécessaire de passer des informations d’une page vers une autre. Le cas très classique consiste à proposer à partir d’une page liste d’ouvrir une page d’édition de la ligne sélectionnée. La page liste doit passer à la page détail, l’objet présenté par la ligne sélectionnée.

Nous avons déjà vu tous les éléments nécessaires pour passer des données d’une page à l’autre en utilisant l’outjection/injection. Le code nécessaire, dans la page liste.jsp sera le suivant :

<h:dataTable value="#{listBean.entities}" var="row">
		<h:column>
			<f:facet name="header">
				<h:outputText value="title" />
			</f:facet>
			<h:commandLink
                             action="#{ listBean.selectEntity(row)}">
				<h:outputText value="#{row.title}"/>
			</h:commandLink>
		</h:column>
		<h:column></h:column></h:dataTable>

Dans cette liste, chaque ligne présente une entité. Le nom de l’entité, donnée dans la première colonne est inclus dans un hyper lien. S’il est activé, cet hyper lien déclenche l’exécution de la méthode selectEntity du backing bean de liste. Cette méthode reçoit en paramètre l’entité sélectionnée.

Le code de cette méthode est le suivant :

@Out(value="SelectedEntity", required=false)
	DomainObject selectedEtt;
 
	public String selectEntity(DomainObject ett) {
		selectedEtt = ett;
                String viewId = FacesContext.getCurrentInstance().
                      getViewRoot().getViewId();
		navigationStack.push(viewId);
		return "/detail.jsp";
	}

Cette méthode initialise l’attribut outjecté du backing bean. Cet attribut définit l’entité sélectionnée. La méthode demande ensuite l’ouverture de la page détail (en armant le mécanisme de retour tel que définit plus haut).

Le backing bean de la page détail récupère cet objet de la manière la plus simple qui soit en définissant une propriété annotée :

@In("SelectedEntity")
	DomainObject selectedEtt;
 
	public DomainObject getEntity() {
		return selectedEtt;
	}

Déléguer la mise à jour d’un attribut à une autre page

Il s’agit d’une particularité du besoin précédant. Il faut cette fois demander à une autre page de mettre à jour un attribut d’un objet édité par une première page. Le cas le plus classique consiste à éditer une entité dans une page formulaire. Cette entité contient une référence vers une autre entité qu’il est possible de choisir parmi une liste. On peut par exemple, choisir une ville et l’affecter à l’attribut « city » d’une entité adresse.

Nous avons donc une page formulaire qui va passer la main à une page liste permettant de sélectionner une entité. Sur sélection de cette entité (City), l’attribut (city) de l’entité présentée (Address) par la page formulaire est mis à jour et la page formulaire est réaffichée.

La question subsidiaire est : quelle page a la responsabilité d’effectuer la mise à jour ? Est-ce la page liste dès qu’une ligne est sélectionnée ? où est-ce la page formulaire au moment ou elle est réaffichée ?

La meilleure solution est la première : il est en effet assez difficile – quoique possible – pour la page formulaire de détecter que la page liste vient de retourner. Sans parler du fait qu’il reste à définir quelle page liste, pour quel attribut, vient de retourner (une entité complexe peut définir de multiples liens vers d’autre entités) et de s’assurer qu’une entité à réellement été sélectionnée (l’utilisateur n’a pas simplement appuyé sur « back »).

La solution est toute simple : il suffit à la page formulaire de passer l’entité sélectionnée par injection/outjection. La page liste définit la fonction de sélection d’une ligne de la manière suivante :

@In("EditedAddress")
	Address address;
 
	public String selectCity(City city) {
		address.setCity(city);
		return navigationStack.pop();
	}

Simple non ? Trop simple en fait. Cela nous oblige à créer une page de sélection de ville pour chaque lien de notre application qui cible une ville : Address.city, Travel.startCity, Travel.targetCity, … Il nous faudrait un mécanisme plus générique afin que la même page de sélection puisse être utilisée dans des contextes différents.

La solution consiste à passer, non pas une entité d’un type connu, mais un objet quelconque, avec la référence d’une méthode (un setter) permettant de mettre à jour l’attribut édité. Ces deux informations sont réunies dans un tableau :

  • Le premier élément contient l’entité éditée
  • Le second élément contient le nom de l’attribut à modifier.

Dans le bean du formulaire on trouve le code suivant :

@Out(value="EditedAttr", required=false)
	Object[] editedAttr = null;
 
	public String chooseCity(Address address) {
		editedAttr = new Object[] {address, "City"};
		String viewId = FacesContext.getCurrentInstance().
                      getViewRoot().getViewId();
		navigationStack.push(viewId);
		return "/listCities.jsp";
	}

Pour exploiter ces informations, nous allons, dans le bean de la liste, utiliser un peu d’introspection :

@In("EditedAttr")
	Object[] editedAttr;
 
	public String selectCity(City city) {
		try {
                        Method selMethod = editedAttr[0].getClass().getMethod(
                            "set"+editedAttr[1], new Class[] {City.class});
			selMethod.invoke(editedAttr[0], new Object[] {city});
			} catch (SecurityException e) {
				throw new RuntimeException(e);
			} catch (NoSuchMethodException e) {
				throw new RuntimeException(e);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			} catch (InvocationTargetException e) {
				throw new RuntimeException(e);
			}
			return navigationStack.pop();
		}
	}
Boîte à outils
Dernière modification de cette page le 21 mai 2007 à 07:24