Kuchen-Zutat-Beispiel mit Java Server Faces
Inhalt:
JSF 1.2_07
Überblick
faces-config.xml
EJB-Injection
Parameterübergabe
Phase Event
Zugriff auf Request aus ManagedBean-Action-Methode
Validierung
Weitere Design-Tips
Dieses Beispiel stellt das KuchenZutat-Beispiel komplett auf JSF um und kämpft dabei mit einer Reihe von
Problemen.
Hier gibt es das Projekt als EAR-Export-Datei: KuchenZutatJSF.ear.
ACHTUNG: wenn man dieses Projekt mit dem JSF 1.2_07-Workaround als EAR-Datei exportiert, werden die JSF-Libraries leider mit exportiert
und landen in WEB-INF\lib. Deshalb sollte man sie entweder nach einem Export aus der WAR-Datei in der EAR-Datei löschen, oder nach einem Re-Import
aus dem Projekt löschen !
Zum Einstieg ein Link zum JSF-Lifecycle: http://www.fh-wedel.de/~si/seminare/ws06/Ausarbeitung/10.JavaServerFaces/jsf2.html#jsfl
JSF 1.2_07
Der JBoss 4.2.2 bringt JSF 1.2_04 mit. Unter dieser Version läuft dieses Beispiel nicht, nach dem ersten Zufügen eines Kuchens erhält man diese
Fehlermeldung:
exception
javax.servlet.ServletException
javax.faces.webapp.FacesServlet.service(FacesServlet.java:256)
org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
root cause
java.lang.NullPointerException
com.sun.faces.renderkit.RenderKitUtils.getCommandLinkOnClickScript(RenderKitUtils.java:932)
com.sun.faces.renderkit.html_basic.CommandLinkRenderer.getOnClickScript(CommandLinkRenderer.java:295)
com.sun.faces.renderkit.html_basic.CommandLinkRenderer.renderAsActive(CommandLinkRenderer.java:357)
com.sun.faces.renderkit.html_basic.CommandLinkRenderer.encodeBegin(CommandLinkRenderer.java:165)
javax.faces.component.UIComponentBase.encodeBegin(UIComponentBase.java:788)
javax.faces.component.UIComponent.encodeAll(UIComponent.java:884)
javax.faces.render.Renderer.encodeChildren(Renderer.java:137)
javax.faces.component.UIComponentBase.encodeChildren(UIComponentBase.java:812)
com.sun.faces.renderkit.html_basic.HtmlBasicRenderer.encodeRecursive(HtmlBasicRenderer.java:271)
com.sun.faces.renderkit.html_basic.TableRenderer.encodeChildren(TableRenderer.java:307)
javax.faces.component.UIComponentBase.encodeChildren(UIComponentBase.java:812)
javax.faces.component.UIComponent.encodeAll(UIComponent.java:886)
javax.faces.component.UIComponent.encodeAll(UIComponent.java:892)
com.sun.faces.application.ViewHandlerImpl.doRenderView(ViewHandlerImpl.java:245)
com.sun.faces.application.ViewHandlerImpl.renderView(ViewHandlerImpl.java:176)
com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:106)
com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:251)
com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:144)
javax.faces.webapp.FacesServlet.service(FacesServlet.java:245)
org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
Hier bin ich wohl Opfer eines JSF-Bugs geworden:
http://forums.java.net/jive/message.jspa?messageID=233385 bzw.
https://javaserverfaces.dev.java.net/issues/show_bug.cgi?id=583.
Lösung:
Aktuelles Version der Referenzimplementierung (im Moment 1.2_07) herunterladen und in ein beliebiges Verzeichnis entpacken.
Version 1.2_07 findet man hier: https://javaserverfaces.dev.java.net/servlets/ProjectDocumentList?folderID=5220&expandFolder=5220&folderID=8467
(im Baum links unter "javaserverfaces" - > "release" -> " 1.2_07 (4)").
Mein erster Ansatz war, einfach die beiden Dateien "server\default\deploy\jboss-web.deployer\jsf-libs\jsf-api.jar"
und "server\default\deploy\jboss-web.deployer\jsf-libs\jsf-impl.jar" zu ersetzen. Danach lief mein Beispiel, dafür aber nichts mehr aus dem Standard-JBoss
(z.B. alle Seiten der jmx-console). Es kam immer zu dieser Fehlermeldung:
2007-12-27 21:34:42,578 ERROR [org.apache.catalina.core.ContainerBase.[jboss.web].[localhost].[/jmx-console]] Exception sending request destroyed lifecycle event to listener instance of class org.jboss.web.jsf.integration.config.JBossJSFConfigureListener
java.lang.NullPointerException
at com.sun.faces.application.WebappLifecycleListener.syncSessionScopedBeans(WebappLifecycleListener.java:312)
at com.sun.faces.application.WebappLifecycleListener.requestDestroyed(WebappLifecycleListener.java:87)
at com.sun.faces.config.ConfigureListener.requestDestroyed(ConfigureListener.java:240)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:188)
at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:179)
at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:84)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
at org.jboss.web.tomcat.service.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:157)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:262)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:844)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:446)
at java.lang.Thread.run(Thread.java:595)
Scheinbar wurden Interna der JSF-Implementation geändert, die für die JBoss-Integration wichtig sind.
Aus diesem Grund wurde ein anderer Ansatz gewählt und die JSF-Libraries lokal ins Projekt geklebt.
Zuerst muss die Library konfiguriert werden. Unter "Window" -> "Preferences" -> "Web and XML" -> "JavaServer Faces Tools" ->
"Libraries" wird eine neue Library zugefügt (hier ist schon das Ergebnis zu sehen):
Ein Name wird eingegeben, die Version ist "1.2", die beiden Dateien "jsf-impl.jar" und "jsf-api.jar" werden gewählt.
Außerdem wird die Checkbox "Is JSF implementation" gesetzt (was auch immer der für eine Bedeutung hat).
Beim Projekt-Erstellen wird im Schritt "JSF capabilities" die so erstellte JSF-Library gewählt, außerdem MUSS der Haken "Deploy" gesetzt
werden, damit sie auf den Server kopiert wird.
Schlussendlich muss in "web.xml" ein JBoss-spezifischer Konfigurationsparameter eingefügt werden, mit dem man dem Server sagt,
dass er nicht seine eingebaute JSF-Implementation verwenden soll, sondern unsere eigene:
<context-param>
<param-name>org.jboss.jbossfaces.WAR_BUNDLES_JSF_IMPL</param-name>
<param-value>true</param-value>
</context-param>
Quelle: http://wiki.jboss.org/wiki/Wiki.jsp?page=JBoss5AndMyFaces (eigentlich dazu gedacht,
alternative Implementationen wie MyFaces zu integrieren).
Überblick
Die Anwendung enthält vier Seiten:
- kuchenliste.jsp: Stellt eine Liste aller Kuchen als
dataTable
dar, mit Bearbeiten-Link in jeder Zeile und
einem Neu-Button. Beim OK-Klick springt man zur Kuchenliste zurück.
- kuchendetail.jsp: Erlaubt das Bearbeiten eines Kuchens und stellt alle Zutaten als
dataTable
dar, mit Bearbeiten-Link in jeder Zeile und
einem Neu-Button.
- kuchenzutat.jsp: Bearbeiten einer einzelnen Zutat. Beim OK-Klick springt man zum Kuchen zurück.
- index.html ist vorgeschaltet, dient dem Einstieg in die Faces-Seiten und verlinkt auf "kuchenliste.faces".
Es gibt drei Managed Beans:
KuchenListeHandler
(Request Scope): Wird in "kuchenliste.jsp" verwendet und kann die Liste aller Kuchen liefern.
KuchenDetailHandler
(Request Scope): Wird in "kuchendetail.jsp" verwendet und übernimmt alle Aktionen die mit einem Kuchen zusammenhängen.
ZutatDetailHandler
(Request Scope): Wird in "zutatdetail.jsp" verwendet und übernimmt alle Aktionen die mit einer Zutat zusammenhängen.
Die Navigation sieht so aus (alles über parameterlose Action-Methoden abgebildet):
- Von "kuchenliste.jsp" aus:
- Klick auf den Button mit der ID "kuchenneu":
kuchenDetailHandler.neuKuchen
. Rückgabe dieser Methode (Navigationsziel): kuchendetail
.
- Klick auf einen einzelnen Kuchen:
kuchenDetailHandler.editKuchen
. Rückgabe dieser Methode (Navigationsziel): kuchendetail
.
Benötigte Parameter: ID des gewählten Kuchens.
- Von "kuchendetail.jsp" aus:
- Klick auf den Button mit der ID "kuchensave" (Kuchen speichern):
kuchenDetailHandler.saveKuchen
. Rückgabe dieser Methode (Navigationsziel): kuchenliste
.
Benötigte Parameter: ID des aktuellen Kuchens.
- Klick auf eine einzeln Zutat:
zutatDetailHandler.editZutat
. Rückgabe dieser Methode (Navigationsziel): zutatdetail
.
Benötigte Parameter: ID des aktuellen Kuchens und ID der gewählten Zutat.
- Klick auf den Button mit der ID "zutatneu":
zutatDetailHandler.neuZutat
. Rückgabe dieser Methode (Navigationsziel): zutatdetail
.
- Von "zutatdetail.jsp" aus:
- Klick auf den Button mit der ID "zutatsave" (Zutat speichern):
zutatDetailHandler.saveZutat
. Rückgabe dieser Methode (Navigationsziel): kuchendetail
.
Benötigte Parameter: ID des aktuellen Kuchens und ID der aktuellen Zutat.
faces-config.xml
"faces-config.xml" sieht so aus nachdem wir damit fertig sind (drei Managed Beans und drei Navigation-Rules sind eingetragen):
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"
version="1.2">
<managed-bean>
<managed-bean-name>
kuchenListeHandler</managed-bean-name>
<managed-bean-class>
de.fhw.swtvertiefung.knauf.kuchenzutatjsf.KuchenListeHandler</managed-bean-class>
<managed-bean-scope>
request</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>
kuchenDetailHandler</managed-bean-name>
<managed-bean-class>
de.fhw.swtvertiefung.knauf.kuchenzutatjsf.KuchenDetailHandler</managed-bean-class>
<managed-bean-scope>
request</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>
zutatDetailHandler</managed-bean-name>
<managed-bean-class>
de.fhw.swtvertiefung.knauf.kuchenzutatjsf.ZutatDetailHandler</managed-bean-class>
<managed-bean-scope>
request</managed-bean-scope>
</managed-bean>
<navigation-rule>
<description>"kuchenDetail" führt immer zur Detail-Seite !</description>
<display-name>KuchenDetail-Seite</display-name>
<navigation-case>
<from-outcome>kuchendetail</from-outcome>
<to-view-id>/kuchendetail.jsp</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<description>"kuchenliste" führt immer zur Liste-Seite !</description>
<display-name>KuchenListe-Seite</display-name>
<navigation-case>
<from-outcome>kuchenliste</from-outcome>
<to-view-id>/kuchenliste.jsp</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<description>"zutatDetail" führt immer zur Detail-Seite !</description>
<display-name>ZutatDetail-Seite</display-name>
<navigation-case>
<from-outcome>zutatdetail</from-outcome>
<to-view-id>/zutatdetail.jsp</to-view-id>
</navigation-case>
</navigation-rule>
</faces-config>
Zu beachten ist dass alle Beans im Request-Scope stecken. Das hat den Nachteil dass bei jedem Request die benötigten Daten neu geladen werden.
Der Vorteil ist dass nach einem Speichern eines Kuchens und anschließendem Zurückspringen zur Kuchenliste ein neuer Request erfolgt
und dadurch die Kuchenliste mitsamt der Änderung neu eingeladen wird. Wäre die Kuchenliste (KuchenListeHandler
) in der Session,
dann müsste der geänderte Kuchen mühsam in der Liste aktualisiert werden (was von der Managed Bean aus nicht direkt möglich ist).
Die Navigation Rules sind eher simpel gehalten: jede Action-Methode die als Rückgabe "kuchendetail" hat wird automatisch zur View
"kuchendetail.jsp" weitergeleitet.
WTP bietet uns hier ein grafisches Tool für das Bearbeiten der Links:
Über die Palette rechts können wir z.B. neue Links hinzufügen.
Jede "Page" ist mit einer echten JSP-Seite verbunden. Diese wird in der Property "Page Path" angegeben.
Für einen Link können wir in der Property "From Outcome" angeben, für welchen Rückgabewert einer Action-Methode diese Regel gelten soll.
Info: im obigen Beispiel gelten die Navigation Rules für die Rückgabe aller Actions. Wenn man eine Regel nur für eine einzelne Methode
angeben will, könnte das so aussehen:
<navigation-rule>
<description>"kuchenliste" führt diesmal nur als Rückgabe der Action "kuchenDetailHandler.kuchenSave" zur Liste !</description>
<display-name>KuchenListe-Seite</display-name>
<navigation-case>
<from-action>#{kuchenDetailHandler.kuchenSave}</from-action>
<from-outcome>kuchenliste</from-outcome>
<to-view-id>/kuchenliste.jsp</to-view-id>
</navigation-case>
</navigation-rule>
Hier greift die Regel nur für die Rückgabe "kuchenliste" der Methode de.fhw.swtvertiefung.knauf.kuchenzutatjsf.KuchenDetailHandler.kuchenSave
.
Man beachte den EL-Ausdruck zur Referenzierung der Methode (leider ohne WTP-Unterstützung für die Codeergänzung).
Analog sieht es aus, wenn man auf einer JSP-Seite direkt einen Link zu einer anderen View baut.
<navigation-rule>
<description>Der Link "navigationkuchenliste" führt zur Übersichtsseite !</description>
<display-name>Link "Zur Kuchenliste" auf kuchendetail.jsp</display-name>
<from-view-id>/kuchendetail.jsp</from-view-id>
<navigation-case>
<from-outcome>kuchenliste</from-outcome>
<to-view-id>/kuchenliste.jsp</to-view-id>
</navigation-case>
</navigation-rule>
Dies greift für den CommandLink "Zur Kuchenliste" auf "kuchendetail.jsp" (eigentlich gilt die Regel für alle Links, deren Property "action"
auf "kuchenliste" steht). Die Quell-View wird im Element from-view-id
angegeben, hier also "/kuchendetail.jsp".
EJB-Injection
Alle drei Managed Beans benötigen die Stateless Session Bean de.fhw.swtvertiefung.knauf.kuchenzutatjsf.KuchenZutatWorkerBean
.
Diese können wir mittels EJB-Injection einlesen.
Das sieht so aus:
@EJB(name="java:comp/env/ejb/KuchenZutatWorkerLocal")
private KuchenZutatWorkerLocal kuchenZutatWorkerBean;
Ich habe eine Injection hier NUR über den Environment Naming Context geschafft. In web.xml muss also folgendes eingetragen sein:
...
<ejb-local-ref>
<ejb-ref-name>ejb/KuchenZutatWorkerLocal</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local-home>java.lang.Object</local-home>
<local>de.fhw.swtvertiefung.knauf.kuchenzutatjsf.KuchenZutatWorkerLocal</local>
</ejb-local-ref>
...
"jboss-web.xml" muss dieses enthalten (kompletter Inhalt der Datei):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-web PUBLIC
"-//JBoss//DTD Web Application 4.2//EN"
"http://www.jboss.org/j2ee/dtd/jboss-web_4_2.dtd">
<jboss-web>
<context-root>KuchenZutatJSFWeb</context-root>
<ejb-local-ref>
<ejb-ref-name>ejb/KuchenZutatWorkerLocal</ejb-ref-name>
<local-jndi-name>KuchenZutatJSF/KuchenZutatWorkerBean/local</local-jndi-name>
</ejb-local-ref>
</jboss-web>
Leider klappt das nur, wenn die JBoss-eigene JSF-Implementation verwendet wird. Mit 1.2_07 ist keine Injection möglich.
Um das Projekt einigermaßen flexibel zu halten, habe ich mir eine Basisklasse für alle meine Managed Beans deklariert, die
einerseits versucht, die KuchenZutatWorkerBean per Injection zu holen, andererseits mittels Hilfsmethode einen manuellen JNDI-Lookup durchführen kann.
Die Fehlerbehandlung ist hier mehr als bescheiden (fehlgeschlagene JNDI-Lookups führen nicht zu einer richtigen Fehlermeldung), also kein gutes Vorbild.
public class BaseHandler
{
@EJB(name="java:comp/env/ejb/KuchenZutatWorkerLocal")
private KuchenZutatWorkerLocal kuchenZutatWorkerBean;
protected KuchenZutatWorkerLocal getKuchenZutatWorker()
{
if (this.kuchenZutatWorkerBean == null)
{
System.out.println ("BaseHandler.getKuchenZutatWorker: JNDI-Lookup !");
try
{
this.kuchenZutatWorkerBean = (KuchenZutatWorkerLocal) (new InitialContext().lookup("java:comp/env/ejb/KuchenZutatWorkerLocal"));
}
catch (NamingException namingEx)
{
//TODO: Meldung !
namingEx.printStackTrace();
return null;
}
}
else
{
System.out.println ("BaseHandler.getKuchenZutatWorker: Injection hat geklappt !");
}
return this.kuchenZutatWorkerBean;
}
}
Parameterübergabe
An einigen Stellen muss beim Sprung von einer View zur anderen ein Parameter übergeben werden.
Dies kann auf zwei Wegen geschehen (jeweils mit Vor- und Nachteilen).
Weg 1: Hidden Field:
Aus "kuchendetail.jsp" im Formular für das Bearbeiten des aktuellen Kuchens:
<h:inputHidden id="kuchenid" value="#{kuchenDetailHandler.kuchenId}"></h:inputHidden>
Vorteil: der Wert wird beim Generieren der JSP aus der aktuellen Instanz des KuchenDetail-Handlers gelesen und
beim Absenden des Requests dort auch wieder hineingeschrieben.
Nachteil: Falls die Zielseite eine andere Seite ist, dann wird der Wert aus dem Parameter trotzdem in den KuchenDetail-Handler geschrieben.
Deshalb nicht allzu geeignet für z.B. die Übergabe einer Kuchen-ID in der Zutatenliste eines Kuchens, wenn das Ziel die Zutatenseite ist.
Fazit: einfaches, aber unflexibles Verfahren, dass nur innerhalb einer Managed Bean funktioniert (zum Beispiel in "kuchendetail.jsp" beim Klick auf "OK").
Weg 2: <f:param>
-Tag:
Innerhalb eines <h:commandButton>
oder <h:commandLink>
können Werte an das Ziel übergeben werden.
Dies kann so aussehen (Link zum Bearbeiten einer Zutat auf "kuchendetail.jsp"):
<h:commandLink id="kuchenedit" action="#{zutatDetailHandler.editZutat}"
actionListener="#{zutatDetailHandler.selectKuchenZutat}" value="Bearbeiten">
<f:param id="kuchenId" value="#{kuchenDetailHandler.kuchenId}"></f:param>
<f:param id="zutatId" value="#{zutatAktuell.id}"></f:param>
</h:commandLink>
"zutatAktuell" ist hier eine Variable im PageContext, die beim Befüllen des dataTable
aus der Zutatenliste gesetzt wurde.
Vorteil: wird an die Zielseite geschickt.
Nachteil: Keinerlei Automatismus. Die Action-Methode editZutat
hat keine Chance auf die Werte zuzugreifen. Deshalb muss
der Parameter manuell ausgewertet werden. Dazu wurde das "actionListener
"-Attribut gesetzt. Hier wird eine void
-Methode angegeben
die einen Parameter vom Typ "javax.faces.event.ActionEvent
" erwartet.
Diese Methode könnte so aussehen:
public void selectKuchenZutat(ActionEvent event)
{
UIParameter component = (UIParameter) event.getComponent().findComponent("kuchenId");
this.intKuchenId = Integer.parseInt(component.getValue().toString());
UIParameter component = (UIParameter) event.getComponent().findComponent("zutatId");
this.intZutatId = Integer.parseInt(component.getValue().toString());
}
Es werden zwei Komponenten mit den Namen "kuchenId" und "zutatId" gesucht, die vom Typ javax.faces.component.UIParameter
sind.
Deren Werte werden ausgewertet und in Membervariablen der Klasse gesetzt. Diese sind zum Zeitpunkt des Aufrufs von editZutat
vorhanden.
Phase Event
Mit obigem Vorgehen landet ich in folgender Konstellation in einer Sackgasse !
- Kuchen-Detail-Seite, aktuelle Kuchen-ID als Hidden Field im Form, mehrere Zutaten.
- Im Bearbeiten-Link einer Zutat steckten Kuchen-ID und Zutat-ID als
<f:param>
-Tags (siehe "Weg 2" im vorherigen Kapitel).
- Das führte zu einem gravierenden Problem: Nach dem Absenden des Formulars wurde zuerst (noch vor dem Bearbeiten des Action-Listeners) im KuchenDetailHandler die Zutatenliste abgerufen,
vermutlich um die Zutat zum gewählten Link zu holen. Da der
KuchenDetailHandler
im Request-Scope steckte wurde er natürlich
neu erzeugt und konnte deshalb keine Zutaten zurückliefern. Deshalb konnte die JSF-Implementation die Zutat nicht ermitteln und
setzt ohne irgendwelche Fehlermeldungen die Zutat-ID auf "0", was einer neuen Zutat entsprach ! "setKuchenId
des KuchenDetailHandler
wurde nicht aufgerufen, also keine Chance einzugreifen.
Lösungsansatz 1 dieses Dilemmas: KuchenDetailHandler
kommt in den Session Scope. Nicht schön weil dadurch kein Zurückblättern
in der Historie möglich wäre: werden nacheinander zwei Kuchen geöffnet, und wird über den Zurück-Button im Browser vom zweiten zum ersten Kuchen zurückgeblättert,
dann würde der KuchenDetailHandler
in der Session immer noch den zweiten Kuchen enthalten, mit fatalen Folgen beim Öffnen der Zutat.
Außerdem wäre einiges an Verrenkungen nötig um nach dem Speichern einer Zutat den Kuchen dazu zu bewegen seine Zutatenliste zu aktualisieren ;-).
Lösungsansatz 2: Einklinken in die Verarbeitungsphasen von JSF. Hierzu wurde im <f:view>
-Tag das beforePhase
-Attribut gesetzt:
<f:view beforePhase="#{kuchenDetailHandler.beforePhase}">
Die Methode KuchenDetailHandler.beforePhase
sucht in den Request-Parametern nach allem was mit ":kuchenId" endet (JSF setzt
die komplette Komponentenhierarchie in den Namen des Request-Parameters, in der Zutatenliste sieht das so aus: tablezutaten:0:formzutatedit:kuchenId
für die Zutat in Zeile 0, tablezutaten:1:formzutatedit:kuchenId
für die Zutat in Zeile 1. Deshalb habe ich hier keine Chance auf den
exakten Namen der Komponente zu prüfen)
Der Code der Methode sieht so aus:
public void beforePhase (javax.faces.event.PhaseEvent e)
{
//Nur etwas machen in der Phase "ApplyRequestValues" !
if (e.getPhaseId().equals( PhaseId.APPLY_REQUEST_VALUES))
{
Map<String, String> mapRequest = e.getFacesContext().getExternalContext().getRequestParameterMap();
for (Entry<String, String> entryAktuell : mapRequest.entrySet() )
{
String strKey = entryAktuell.getKey();
if (strKey.endsWith(":kuchenId"))
{
this.intKuchenId = Integer.parseInt(entryAktuell.getValue());
}
}
}
}
Das bedingt leider eine Änderung an der Parameterversorgung des Zutat-Detail-Links: <f:param>
-Tags kann ich nicht als Request-Parameter
parsen ! Deshalb verwende ich ein Hidden Field:
<h:form id="formzutatedit">
<h:inputHidden id="kuchenId" value="#{kuchenDetailHandler.kuchenId}"></h:inputHidden>
<h:commandLink id="kuchenedit" action="#{zutatDetailHandler.editZutat}"
actionListener="#{zutatDetailHandler.selectKuchenZutat}" value="Bearbeiten">
<f:param id="zutatId" value="#{zutatAktuell.id}"></f:param>
</h:commandLink>
</h:form>
Dieses Hidden Field wird zwar dank Value Binding in den KuchenDetailHandler
zurückgeschrieben, das stört uns allerdings nicht weiter.
War wir damit erreicht haben ist dass bei dem Aufruf von KuchenDetailHandler.getZutaten
die Kuchen-ID bereits gesetzt ist !
Diese schicke Lösung hat einen Nachteil: im ZutatDetailHandler
kommt die KuchenId in diesem konkreten Fall nicht mehr als UIParameter
an sondern in einem HTML-Hidden Field. Deshalb muss die Logik in ZutatDetailHandler.selectKuchen
(diese Methode wird auch
aus selectKuchenZutat
heraus aufgerufen) erweitert werden:
public void selectKuchen(ActionEvent event)
{
String strValue = null;
if (event.getComponent().findComponent("kuchenId").getClass().equals(UIParameter.class) )
{
UIParameter component = (UIParameter) event.getComponent().findComponent("kuchenId");
strValue = component.getValue().toString();
}
else if (event.getComponent().findComponent("kuchenId").getClass().equals(HtmlInputHidden.class) )
{
HtmlInputHidden component = (HtmlInputHidden) event.getComponent().findComponent("kuchenId");
strValue = component.getValue().toString();
}
else
{
throw new FacesException ("Keine Komponente kuchenId gefunden !");
}
this.intKuchenId = Integer.parseInt(strValue);
}
Hier wird also die Kuchen-ID in zwei unterschiedlichen Komponenten-Typen gesucht.
Die beforePhase
ist auch in der Validierung nötig: die Kuchen-ID im KuchenDetailHandler
wird scheinbar nicht sauber in die Managed
Bean gesetzt, nur im Phase Event kann sie aus dem Hidden Field gezogen werden ! (siehe weiter unten)
Zugriff auf Request aus ManagedBean-Action-Methode
Oben gezeigte Techniken zur Parameterübergabe lassen sich eventuell vereinfachen: es ist in der Managed Bean möglich auf Request-Parameter
direkt zuzugreifen. Dadurch hebelt man natürlich die JSF-Technik ziemlich aus ;-).
Ein Beispiel ist die Methode KuchenDetailHandler.deleteKuchen
, die von der Kuchenliste aufgerufen wird und einen Kuchen löscht.
Da die Managed Bean KuchenlisteHandler
im Request steckt und schon beim Klick auf den Löschen-Link initialisiert wurde steckt
der gelöschte Kuchen noch in der Kuchenliste und würde deshalb trotz allem angezeigt.
Lösung des Problems: auf den KuchenlisteHandler
zugreifen, der als Attribut im Request steckt, und dort den Kuchen aus der Liste entfernen:
ServletRequest request = (ServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
KuchenListeHandler kuchenListeHandler = (KuchenListeHandler) request.getAttribute("kuchenListeHandler");
kuchenListeHandler.clearKuchenliste();
Mit einer ähnlichen Vorgehensweise kann man auf die Session zugreifen um z.B. Daten darin zu speichern:
HttpSession session = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false);
session.setAttribute("MyAttribute", "MyValue");
Validierung
Im Beispiel sind alle Eingabefelder als Pflichtelement deklariert (Attribut "required" setzen).
Für den Kuchen ist die minimale Länge auf 5 Zeichen gesetzt:
<h:inputText label="Name" id="name" value="#{kuchenDetailHandler.name}" required="true">
<f:validateLength minimum="5"></f:validateLength>
</h:inputText>
Entstehen Fehlermeldungen beim Validieren dieser Komponente, können sie an beliebiger Stelle der Seite ausgegeben werden:
<h:message for="name"></h:message>
Durch das Attribut for
wird der Name der Komponente angegeben deren Validierungsfehler hier ausgegeben werden sollen.
Wird ein Attribut label
des h:input
angegeben, so enthält die Fehlermeldung diesen Namen, ansonsten den internen Namen der Komponente in einer eher häßlichen Darstellung.
Weitere Design-Tips
Formular sollten möglichst klein gehalten werden, z.B. sollte ein Navigationslink nicht im gleichen form
stecken wie die eigentlichen Eingabefelder. Grund: bei einem Klick auf den Link wird das gesamte Formular validiert, und deshalb
könnte ein nicht ausgefülltes Eingabefeld einen Klick auf den Link verbieten.
IDs: Alle Formular-Elemente sollten mit IDs versehen werden (h:form
, h:inputHidden
, h:inputText
usw.).
Dadurch ist das Element in Fehlermeldungen oder beim Auswerten des Requests einfacher zuzuordnen.
ACHTUNG: wenn man auf "kuchendetail.jsp" beim Bearbeiten eines bestehenden Kuchens einen Validierungsfehler hat, dann wird die Kuchen-ID NUR
in die Managed Bean gesetzt, wenn man sie in der beforePhase
aus dem Hidden Field zieht ! Ansonsten
würde uns die Seite einen neuen Kuchen versprechen (obwohl nach dem Klick auf OK trotzdem der korrekte Kuchen gespeichert würde).
Stand 10.01.2008
Historie:
16.12.2007: Erstellt aus Vorjahresbeispiel.
19.12.2007: Workaround für Problem bei JBoss 4.2.2 (JSF 1.2_04). Mehr Details zu Navigation Rules
28.12.2007: Workaround für das JSF 1.2_04-Problem geändert.
10.01.2008: Validierung: Anzeigetext für fehlerhaftes Feld verschönert. Case-Fehler im Hidden Field "kuchenId" korrigiert. Hinweis auf beforePhase
bei Validierung.