Kuchen-Zutat-Beispiel mit Java Server Faces (JavaEE8)
Inhalt:
Überblick
Use Cases
faces-config.xml
"beans.xml"
EJB-Injection
Parameterübergabe
ViewState/LifeCycle
Phase Event
Zugriff auf andere Manged Bean
Validierung
Weitere Design-Tips
Dieses Beispiel stellt das KuchenZutat-Beispiel komplett auf JSF um und kämpft dabei mit einer Reihe von
Problemen.
Das Beispiel baut auf JavaEE8 und WildFly 26.0 auf.
Hier gibt es das Projekt als EAR-Export-Datei: KuchenZutatJSF.ear.
Nach dem Import muss man die JSF-Facet dem Projekt zufügen, siehe JSF-Facet zu bestehendem Projekt zufügen
Überblick
Die Anwendung enthält vier Seiten:
- kuchenliste.xhtml: 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.xhtml: Erlaubt das Bearbeiten eines Kuchens und stellt alle Zutaten als
dataTable
dar, mit Bearbeiten-Link in jeder Zeile und
einem Neu-Button.
- kuchenzutat.xhtml: Bearbeiten einer einzelnen Zutat. Beim OK-Klick springt man zum Kuchen zurück.
- index.xhtml ist vorgeschaltet, dient dem Einstieg in die Faces-Seiten und verlinkt auf "kuchenliste.xhtml".
Es gibt drei Managed Beans:
KuchenListeHandler
(Request Scope): Wird in "kuchenliste.xhtml" verwendet und kann die Liste aller Kuchen liefern.
KuchenDetailHandler
(Request Scope): Wird in "kuchendetail.xhtml" verwendet und übernimmt alle Aktionen die mit einem Kuchen zusammenhängen.
ZutatDetailHandler
(Request Scope): Wird in "zutatdetail.xhtml" verwendet und übernimmt alle Aktionen die mit einer Zutat zusammenhängen.
Die Navigation sieht so aus (alles über parameterlose Action-Methoden abgebildet):
- Von "kuchenliste.xhtml" 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.xhtml" 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.xhtml" 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.
Use Cases
Da die Umsetzung der Anwendungslogik sehr komplex ist, versuche ich hier, die Abläufe der wichtigsten Use Cases zu zitieren. Leider habe ich kein brauchbares
freies Sequenz-Diagramm-Tool gefunden, deshalb muss ich hier bei der textuellen Beschreibung bleiben.
Use Case "Kuchen anlegen"
- 1 - Auf
kuchenliste.xhtml
wird auf den Submit-Button "kuchenneu" des Formulars "formkuchenneu" geklickt.
- 1.1 - Das
JSF-Framework
ruft die zugehörige Action KuchenDetailHandler.neuKuchen()
auf.
- 1.1.1 -
KuchenDetailHandler.neuKuchen()
setzt die interne Kuchen-ID über KuchenDetailHandler.setKuchenId()
auf "0".
- 1.1.1.1 -
KuchenDetailHandler.setKuchenId()
weist die Kuchen-ID "0" der Variablen "intKuchenId" zu und ruft KuchenDetailHandler.initKuchen()
auf.
- 1.1.1.1.1 -
KuchenDetailHandler.initKuchen()
erkennt anhand der ID "0", dass ein neuer Kuchen angelegt werden soll, und initialisiert
die Membervariable kuchenAktuell
mit einer neuen Instanz der KuchenBean
- 1.1.2 -
KuchenDetailHandler.neuKuchen()
gibt das Navigation Result "kuchendetail" zurück.
- 1.2 - Das
JSF-Framework
rendert die Seite "kuchendetail.xhtml"
- 1.2.1 -
kuchendetail.xhtml
baut das Bearbeiten-Formular auf und ruft dabei KuchenDetailHandler.getKuchenId()
auf.
Es kommt "0" zurück.
- 1.2.2 -
kuchendetail.xhtml
baut das Bearbeiten-Formular auf und ruft dabei KuchenDetailHandler.getName()
auf.
Es kommt ein Leerstring zurück.
- 2 - Der User klickt auf
kuchendetail.xhtml
den Submit-Button "kuchensave" des Formulars "formkuchen".
- 2.1 - In der BeforePhase-Methode
KuchenDetailHandler.beforePhase()
wird die Kuchen-ID "0" aus dem Hidden Field "kuchenId" gezogen.
- 2.1.1 - Die BeforePhase-Methode
KuchenDetailHandler.beforePhase()
setzt die interne Kuchen-ID über KuchenDetailHandler.setKuchenId()
auf "0".
- 2.1.1.1 -
KuchenDetailHandler.setKuchenId()
weist die Kuchen-ID "0" der Variablen "intKuchenId" zu und ruft KuchenDetailHandler.initKuchen()
auf.
- 2.1.1.1.1 -
KuchenDetailHandler.initKuchen()
erkennt anhand der ID "0", dass ein neuer Kuchen angelegt werden soll, und initialisiert
die Membervariable kuchenAktuell
mit einer neuen Instanz der KuchenBean
- 2.2 - Das
JSF-Framework
führt die Validierung für das Feld "name" durch (Eingabe soll mindestens 5 Zeichen lang sein). Im Fehlerfall würde hier erneut das Eingabeformular gerendert.
- 2.3 - Das
JSF-Framework
wertet den Request aus und führt das Value-Binding des Formulars durch: über KuchenDetailHandler.setKuchenId()
wird die
Kuchen-ID "0" gesetzt.
- 2.3.1 -
KuchenDetailHandler.setKuchenId()
weist die Kuchen-ID "0" der Variablen "intKuchenId" zu und ruft KuchenDetailHandler.initKuchen()
auf.
- 2.3.1.1 -
KuchenDetailHandler.initKuchen()
erkennt anhand der ID "0", dass ein neuer Kuchen angelegt werden soll, und initialisiert
- 2.4 - Das
JSF-Framework
wertet den Request aus und führt das Value-Binding des Formulars durch: über KuchenDetailHandler.setName()
wird der
eingegebene Kuchen-Name in die Membervariable kuchenAktuell
gesetzt.
- 2.5 - Das
JSF-Framework
ruft die Action KuchenDetailHandler.kuchenSave
auf.
- 2.5.3 -
KuchenDetailHandler.kuchenSave
ruft KuchenZutatWorkerBean.saveKuchen
auf und speichert den neuen Kuchen. Die ID wird in der Membervariablen
intKuchenId
gespeichert.
- 2.5.4 -
KuchenDetailHandler.kuchenSave
gibt "kuchenliste" zurück.
- 2.6 - Das
JSF-Framework
rendert gemäß dieser Rückgabe die Seite "kuchenliste.xhtml".
Use Case "Kuchen bearbeiten"
Dieser Use-Case ist dem letzten sehr ähnlich, nur der Start der Aktion sowie das Initialisieren der kuchenAktuell
-Membervariablen ist unterschiedlich.
- 1 - Auf
kuchenliste.xhtml
wird auf den Submit-Button "kuchenedit" des Formulars "formkuchenedit" geklickt.
- 1.1 - Das
JSF-Framework
ruft den ActionListener KuchenDetailHandler.selectKuchen(ActionEvent)
auf. Dort wird eine Komponente UIParameter
gesucht, und aus ihrem Value
wird die ID des zu bearbeitenden Kuchens geholt und in die Membervariable kuchenId
geschrieben.
- 1.1.1 -
KuchenDetailHandler.selectKuchen(ActionEvent)
ruft die interne Methode setKuchenId(...)
auf.
- 1.1.1.1 -
KuchenDetailHandler.setKuchenId(...)
weist die übergebene ID der Membervariablen intKuchenId
zu
und ruft initKuchen
auf.
- 1.1.1.1.1 -
KuchenDetailHandler.initKuchen()
erkennt, dass ein vorhandener Kuchen (ID > 0) in dieser Klasse vorliegt, und lädt ein Objekt KuchenBean
über KuchenZutatWorkerBean.findKuchenById
.
- 1.2 - Das
JSF-Framework
ruft die zugehörige Action KuchenDetailHandler.editKuchen()
auf. Diese gibt nur das Navigation Result "kuchendetail" zurück.
- 1.3 - Das
JSF-Framework
rendert die Seite "kuchendetail.xhtml"
- 1.3.1 -
kuchendetail.xhtml
baut das Bearbeiten-Formular auf und ruft dabei KuchenDetailHandler.getKuchenId()
auf. Es kommt die Id des zu bearbeitenden Kuchens zurück, diese
wird in ein Hidden Field geschrieben.
- 1.3.2 -
kuchendetail.xhtml
baut das Bearbeiten-Formular auf und ruft dabei KuchenDetailHandler.getName()
auf. Es kommt der Name des zu bearbeitenden Kuchens zurück.
- 2 - Der User klickt auf
kuchendetail.xhtml
den Submit-Button "kuchensave" des Formulars "formkuchen".
- 2.1 - In der BeforePhase-Methode
KuchenDetailHandler.beforePhase()
wird die echte Kuchen-ID (> 0) aus dem Hidden Field "kuchenId" gezogen.
- 2.1.1 - Die BeforePhase-Methode
KuchenDetailHandler.beforePhase()
setzt die interne Kuchen-ID über KuchenDetailHandler.setKuchenId()
auf "0".
- 2.1.1.1 -
KuchenDetailHandler.setKuchenId()
weist die echte Kuchen-ID (> 0) der Variablen "intKuchenId" zu und ruft KuchenDetailHandler.initKuchen()
auf.
- 2.1.1.1.1 -
KuchenDetailHandler.initKuchen()
erkennt anhand der echten ID (> 0), dass ein vorhandener Kuchen bearbeitet wird, und
und lädt ein Objekt KuchenBean
über KuchenZutatWorkerBean.findKuchenById
- 2.2 - Das
JSF-Framework
führt die Validierung für das Feld "name" durch (es muss ein Wert vorhanden sein und die Eingabe soll mindestens 5 Zeichen lang sein). Im Fehlerfall würde hier erneut das Eingabeformular gerendert.
- 2.3 - Das
JSF-Framework
wertet den Request aus und führt das Value-Binding des Formulars durch: über KuchenDetailHandler.setKuchenId()
wird die
Kuchen-ID aus dem Hidden Field gesetzt.
- 2.3.1 -
KuchenDetailHandler.setKuchenId()
weist die echte Kuchen-ID (> 0) der Variablen "intKuchenId" zu und ruft KuchenDetailHandler.initKuchen()
auf.
Dieser und der folgende Schritt sind bereits in 2.1 geschehen, also erfolgt in diesem Use-Case ein doppeltes Laden des Kuchens.
- 2.3.1.1 -
KuchenDetailHandler.initKuchen()
erkennt anhand der echten ID (> 0), dass ein vorhandener Kuchen bearbeitet wird, und
und lädt ein Objekt KuchenBean
über KuchenZutatWorkerBean.findKuchenById
- 2.4 - Das
JSF-Framework
wertet den Request aus und führt das Value-Binding des Formulars durch: über KuchenDetailHandler.setName()
wird der
eingegebene Kuchen-Name in die Membervariable kuchenAktuell
gesetzt.
- 2.5 - Das
JSF-Framework
ruft die Action KuchenDetailHandler.kuchenSave
auf.
- 2.5.1 -
KuchenDetailHandler.kuchenSave
ruft KuchenZutatWorkerBean.saveKuchen
auf und speichert den neuen Kuchen. Die ID wird in der Membervariablen
intKuchenId
gespeichert.
- 2.5.2 -
KuchenDetailHandler.kuchenSave
gibt "kuchenliste" zurück.
- 2.6 - Das
JSF-Framework
rendert gemäß dieser Rückgabe die Seite "kuchenliste.xhtml".
Use Case "Zutat bearbeiten"
Ausgehend von der Kuchenliste wird zuerst die Kuchen-Bearbeiten-Seite aufgerufen (man könnte diesen Use-Case also als "extends" von "Kuchen bearbeiten" modellieren).
Deshalb ist der gesamte Schritt 1 und Unterschritte identisch mit "Kuchen bearbeiten". Allerdings folgen hier die Schritte für das Aufbauen der Zutatenliste.
- 1.3.3
kuchendetail.xhtml
baut die Zutatenliste auf: sie werden über KuchenDetailHandler.getZutaten
abgerufen.
- 1.3.3.1
KuchenDetailHandler.getZutaten
gibt die Zutatenliste aus kuchenAktuell.getZutaten
zurück.
- 1.3.4 Pro Zutat gibt
kuchendetail.xhtml
ID, Name und Menge der Zutat aus (ZutatBean.getId
, ZutatBean.getZutatName
, ZutatBean.getMenge
).
- 2 - Auf
kuchendetail.xhtml
wird auf den Submit-Button "zutatedit" des Formulars "formzutatedit" geklickt.
- 2.1 - Das
JSF-Framework
führt das Phase-Event KuchenDetailHandler.beforePhase(PhaseEvent)
auf. Der Grund dafür wird im Abschnitt
ViewState/LifeCycle beschrieben. Die Kuchen-ID wird aus einem Request-Parameter "kuchenId" geholt und an setKuchenId
übergeben.
- 2.1.1 -
KuchenDetailHandler.setKuchenId()
weist die echte Kuchen-ID (> 0) der Variablen "intKuchenId" zu und ruft KuchenDetailHandler.initKuchen()
auf.
- 2.1.1.1 -
KuchenDetailHandler.initKuchen()
erkennt anhand der echten ID (> 0), dass ein vorhandener Kuchen bearbeitet wird, und
und lädt ein Objekt KuchenBean
über KuchenZutatWorkerBean.findKuchenById
- 2.2 - Das
JSF-Framework
ruft den ActionListener ZutatDetailHandler.selectKuchenZutat(ActionEvent)
auf. Dort wird eine Komponente UIParameter
namens "zutatId"
gesucht, und aus ihrem Value
wird die ID der zu bearbeitenden Zutat geholt und in die Membervariable zutatId
geschrieben.
- 2.2.1 - Im ActionListener
ZutatDetailHandler.selectKuchenZutat(ActionEvent)
wird außerdem die ActionListener-Methode ZutatDetailHandler.selectKuchen(ActionEvent)
aufgerufen .
Dort wird eine Komponente HtmlInputHidden
gesucht, und aus ihrem Value
wird die ID des zu bearbeitenden Kuchens geholt und in die Membervariable kuchenId
geschrieben.
- 2.2.2 - Im ActionListener
ZutatDetailHandler.selectKuchenZutat(ActionEvent)
wird ZutatDetailHandler.initZutat()
aufgerufen.
- 2.2.2.1 - In
ZutatDetailHandler.initZutat)
wird die aktuelle ZutatBean
über KuchenZutatWorkerBean.findZutatById(this.intZutatId)
geladen.
- 2.3 - Die Action
ZutatDetailHandler.editZutat
wird aufgerufen. Es wird "zutatdetail" zurückgegeben.
- 2.4 - Das
JSF-Framework
rendert gemäß dieser Rückgabe die Seite "zutatdetail.xhtml".
- 2.4.1 -
zutatdetail.xhtml
baut das Bearbeiten-Formular auf und ruft dabei ZutatDetailHandler.getKuchenId()
und ZutatDetailHandler.getZutatId()
auf.
Die Ids werden in ein Hidden Field geschrieben.
- 2.4.2 -
kuchendetail.xhtml
baut das Bearbeiten-Formular auf und ruft dabei ZutatDetailHandler.getZutatName()
und ZutatDetailHandler.getMenge()
auf.
- 3 - Der User klickt auf
zutatdetail.xhtml
den Submit-Button "zutatsave" des Formulars "formzutatedit".
- 3.1 - In der BeforePhase-Methode
ZutatDetailHandler.beforePhase()
wird die echte Kuchen-ID (> 0) aus dem Hidden Field "kuchenId" gezogen.
- 3.1 - In der BeforePhase-Methode
ZutatDetailHandler.beforePhase()
wird die echte Zutat-ID (> 0) aus dem Hidden Field "zutatId" gezogen.
- 3.1.1 - Die BeforePhase-Methode
ZutatDetailHandler.beforePhase()
setzt die interne Zutat-ID über ZutatDetailHandler.setZutatId()
.
- 3.1.1.1 -
ZutatDetailHandler.setZutatId()
weist die echte Zutat-ID (> 0) der Variablen "intZutatId" zu und ruft ZutatDetailHandler.initZutat()
auf.
- 3.1.1.1.1 -
ZutatDetailHandler.initZutat()
erkennt anhand der echten ID (> 0), dass eine vorhandene Zutat bearbeitet wird, und
und lädt ein Objekt ZutatBean
über KuchenZutatWorkerBean.findZutatById
- 3.2 - Das
JSF-Framework
führt die Validierung für das Feld "zutatName" und "menge" durch (Werte müssen vorhanden sein). Im Fehlerfall würde hier erneut das Eingabeformular gerendert.
- 3.3 - Das
JSF-Framework
wertet den Request aus und führt das Value-Binding des Formulars durch: über ZutatDetailHandler.setKuchenId()
wird die Kuchen-ID aus dem Hidden Field gesetzt.
- 3.3 - Das
JSF-Framework
wertet den Request aus und führt das Value-Binding des Formulars durch: über
ZutatDetailHandler.setZutatId()
wird die Zutat-ID aus dem Hidden Field gesetzt.
- 3.3.1 -
ZutatDetailHandler.setZutatId()
weist die echte Zutat-ID (> 0) der Variablen "intZutatId" zu und ruft ZutatDetailHandler.initZutat()
auf.
- 3.3.1.1 -
ZutatDetailHandler.initZutat()
erkennt anhand der echten ID (> 0), dass eine vorhandene Zutat bearbeitet wird, und
und lädt ein Objekt ZutatBean
über KuchenZutatWorkerBean.findZutatById
(auch hier doppeltes Laden der Zutat).
- 3.4 - Das
JSF-Framework
wertet den Request aus und führt das Value-Binding des Formulars durch: über ZutatDetailHandler.setZutatName()
und
ZutatDetailHandler.setMenge()
werden die eingegebenen Daten in die Membervariable zutatAktuell
über ZutatBean.setZutatName()
und ZutatBean.setMenge()
gesetzt.
- 3.5 - Das
JSF-Framework
ruft den ActionListener KuchenDetailHandler.selectKuchen
auf (Grund: nach dem Speichern der Zutat soll die KuchenDetail-Seite
angezeigt werden, und diese muss mit der Kuchen-Id initialisiert sein!).
- 3.5.1 -
KuchenDetailHandler.selectKuchen(ActionEvent)
ruft die interne Methode setKuchenId()
auf.
- 3.5.1.1 -
KuchenDetailHandler.setKuchenId(...)
weist die übergebene ID der Membervariablen intKuchenId
zu
und ruft initKuchen
auf.
- 3.5.1.1.1 -
KuchenDetailHandler.initKuchen()
erkennt, dass ein vorhandener Kuchen (ID > 0) in dieser Klasse vorliegt, und lädt ein Objekt KuchenBean
über KuchenZutatWorkerBean.findKuchenById
.
- 3.6 - Das
JSF-Framework
ruft die Action ZutatDetailHandler.zutatSave
auf.
- 3.6.3 -
ZutatDetailHandler.zutatSave
ruft KuchenZutatWorkerBean.saveZutat
auf und speichert die neue Zutat. Die ID wird in der Membervariablen
intZutatId
gespeichert.
- 3.6.4 -
ZutatDetailHandler.zutatSave
setzt den in Schritt 3.5 im KuchenDetailHandler
initialisierten Kuchen übre
KuchenDetailHandler.setKuchenId
(da der Schritt 3.5 geladene Kuchen natürlich die geänderte Zutat nicht enthält)
- 3.6.4.1 -
KuchenDetailHandler.setKuchenId(...)
weist die übergebene ID der Membervariablen intKuchenId
zu
und ruft initKuchen
auf.
- 3.6.4.1.1 -
KuchenDetailHandler.initKuchen()
erkennt, dass ein vorhandener Kuchen (ID > 0) in dieser Klasse vorliegt, und lädt ein Objekt KuchenBean
über KuchenZutatWorkerBean.findKuchenById
.
- 3.6.5 -
ZutatDetailHandler.zutatSave
gibt "kuchendetail" zurück.
- 3.7 - Das
JSF-Framework
rendert gemäß dieser Rückgabe die Seite "kuchendetail.xhtml".
Der Rest des Use Cases entspricht wieder den Schritten 1.3.1 bis 1.3.3.2 dieses Use Cases.
Die anderen drei Use Cases "Kuchen löschen", "Zutat anlegen" und "Zutat löschen" haben keine schlimmen Besonderheiten und werden deshalb nicht im Detail erklärt.
faces-config.xml
"faces-config.xml" sieht so aus nachdem wir damit fertig sind (es sind nur drei Navigation-Rules eingetragen, da die ManagedBeans alle über CDI-Annotations definiert werden):
<?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://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_3.xsd"
version="2.3">
<navigation-rule>
<description>"kuchenDetail" führt immer zur Detail-Seite!</description>
<display-name>KuchenDetail-Seite</display-name>
<from-view-id>*</from-view-id>
<navigation-case>
<from-outcome>kuchendetail</from-outcome>
<to-view-id>/kuchendetail.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<description>"kuchenliste" führt immer zur Liste-Seite!</description>
<display-name>KuchenListe-Seite</display-name>
<from-view-id>*</from-view-id>
<navigation-case>
<from-outcome>kuchenliste</from-outcome>
<to-view-id>/kuchenliste.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<description>"zutatDetail" führt immer zur Detail-Seite!</description>
<display-name>ZutatDetail-Seite</display-name>
<from-view-id>*</from-view-id>
<navigation-case>
<from-outcome>zutatdetail</from-outcome>
<to-view-id>/zutatdetail.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
</faces-config>
Die Managed Beans sind alle über CDI-Annotations definiert:
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named(value="kuchenListeHandler")
@RequestScoped
public class KuchenListeHandler extends BaseHandler
{
Die Beans stecken alle im Request-Scope. 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.xhtml" weitergeleitet, und analog für die Rückgaben "zutatdetail" und "kuchenliste". Man beachte, dass im Vergleich zu meinem JSF 1.2-Beispiel
gemäß XSD in jeder "navigation-rule" eine "from-view-id" angegeben werden muss. Hier habe ich der Einfachheit halber immer "*" eingetragen,
da es keinen Grund gibt, unterschiedliche Ziele je nach Quellseite anzunavigieren.
Seit JSF 2.0 gibt es "Implicit Navigation Rules":
JSF versucht, die Rückgabe einer Action in einer JSF-Seite aufzulösen,
aus der Rückgabe "kuchendetail" von KuchenDetailHandler.editKuchen
wird ein Seitenname "kuchendetail.xhtml" erzeugt,
und da es diese Seite gibt, wird sie automatisch aufgerufen. Dadurch werden die in "faces-config.xml" definierten Navigation Rules übergangen.
Man müsste also, damit die Konfiguration in meinem Beispiel genutzt wird, die Rückgaben der Methoden anders benennen.
WTP bietet uns, sofern wir die Datei mit dem "Faces Config Editor" öffnen, ein grafisches Tool für das Bearbeiten der Links. Allerdings dürfte dies mittlerweile
relativ unnütz sein, da die Managed Beans nicht mehr in "faces-config.xml" definiert werden.
Achtung: dieser Editor läßt sich nur aufrufen, wenn wir im Projekt die Face "Java Server Faces" konfiguriert haben,
siehe auch
JSFBasics
Über die Palette können wir z.B. neue Links hinzufügen.
Jede "Page" ist mit einer echten XHTML-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.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
Hier greift die Regel nur für die Rückgabe "kuchenliste" der Methode de.fhw.komponentenarchitekturen.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 XHTML-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.xhtml</display-name>
<from-view-id>/kuchendetail.xhtml</from-view-id>
<navigation-case>
<from-outcome>kuchenliste</from-outcome>
<to-view-id>/kuchenliste.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
Dies greift für den CommandLink "Zur Kuchenliste" auf "kuchendetail.xhtml" (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.xhtml".
"beans.xml"
Neu in JavaEE6 ist ein weiterer Deployment Deskriptor namens "beans.xml" nötig. Dieser wird für CDI (Contexts and Dependency Injection) benötigt,
was eine Erweiterung der Injection-Fähigkeiten von früheren JavaEE-Versionen ist (wo man in z.B. einem Servlet nur EJBs und MDBs injizieren konnte).
Mittels dieser Datei werden keine Beans deklariert, so wie man das z.B. in "ejb-jar.xml" machen könnte. Die Datei dient dazu,
das Verhalten von vorhandenen (und vom Container gefundenen) Beans zu ändern. Beispiele sind "interceptors", "alternatives" und "decorators"
Grundlagen von CDI: http://docs.oracle.com/javaee/7/tutorial/doc/cdi-basic.htm
"beans.xml": http://docs.oracle.com/javaee/7/tutorial/doc/cdi-adv.htm
Die Spezifikation ist zu finden unter http://www.cdi-spec.org/
"alternatives": Beispiel: es soll eine Instanz von "BeanX" in eine Variable (vom Typ "BeanX") eines Servlets injiziert werden.
Von dieser "BeanX" (keine EJB, sondern eine beliebige Java-Klasse, also eine Bean) gibt es außerdem eine Subklasse "BeanY",
die die Methoden von "BeanX" erweitert um z.B. Code für einen Test. "BeanY" ist mit der Annotation @Alternative
versehen.
Per Default wird immer eine Instanz von "BeanX" erzeugt und injiziert. Deklariert man in "beans.xml" allerdings "BeanY" im "alternatives"-Element,
wird stattdessen eine Instanz dieser Bean erzeugt
"interceptor": Ein Interceptor kapselt Aufrufe von Methoden einer injizierten Bean, z.B. fürs Logging.
"decorator": ähnlich wie eine "alternative", aber hier schaltet der User nicht zwischen zwei Beans um, sondern kapselt einen "Decorator"
um den Aufruf der Bean-Methode, der zusätzliche Logik macht
Im einfachsten Fall ist diese Datei leer (Beispiel für JavaEE8):
&<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
version="2.0"
bean-discovery-mode="annotated">
</beans>
Man beachte den "bean-discovery-mode", für den man sich entscheiden muss. Ich nutze hier "annotated", aber mein Beispiel klappt auch mit "none",
da ich keine speziellen CDI-Features nutze.
Folgender Fehler trat nicht mehr auf nach Umstellung auf XHTML-Seiten und WildFly 26.1.
Fügen wir diese Datei nicht hinzu, kommt beim Aufruf der ersten JSF-Seite folgender Fehler:
21:47:20,830 ERROR [io.undertow.request] (default task-3) UT005023: Exception handling request to /KuchenZutatJSFWeb/kuchenliste.jsp: javax.servlet.ServletException: WELD-000340: A request must be associated with the context in order to load the known conversations
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:659) [jboss-jsf-api_2.2_spec-2.2.5.jar:2.2.5]
at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:85) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:61) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:25) [undertow-core-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:113) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.security.handlers.AuthenticationCallHandler.handleRequest(AuthenticationCallHandler.java:52) [undertow-core-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:45) [undertow-core-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:61) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:70) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.security.handlers.SecurityInitialHandler.handleRequest(SecurityInitialHandler.java:76) [undertow-core-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:25) [undertow-core-1.0.0.Final.jar:1.0.0.Final]
at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:25) [undertow-core-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:25) [undertow-core-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:240) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:227) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:73) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:146) [undertow-servlet-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.server.Connectors.executeRootHandler(Connectors.java:168) [undertow-core-1.0.0.Final.jar:1.0.0.Final]
at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:687) [undertow-core-1.0.0.Final.jar:1.0.0.Final]
at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [rt.jar:1.7.0_51]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [rt.jar:1.7.0_51]
at java.lang.Thread.run(Unknown Source) [rt.jar:1.7.0_51]
Caused by: java.lang.IllegalStateException: WELD-000340: A request must be associated with the context in order to load the known conversations
at org.jboss.weld.context.AbstractConversationContext.checkIsAssociated(AbstractConversationContext.java:416) [weld-core-impl-2.1.2.Final.jar:2014-01-09 09:23]
at org.jboss.weld.context.AbstractConversationContext.getCurrentConversation(AbstractConversationContext.java:431) [weld-core-impl-2.1.2.Final.jar:2014-01-09 09:23]
at org.jboss.weld.jsf.ConversationAwareViewHandler.getActionURL(ConversationAwareViewHandler.java:110) [weld-core-jsf-2.1.2.Final.jar:2014-01-09 09:23]
at javax.faces.application.ViewHandlerWrapper.getActionURL(ViewHandlerWrapper.java:189) [jboss-jsf-api_2.2_spec-2.2.5.jar:2.2.5]
at com.sun.faces.renderkit.html_basic.FormRenderer.getActionStr(FormRenderer.java:231) [jsf-impl-2.2.5-jbossorg-3.jar:]
at com.sun.faces.renderkit.html_basic.FormRenderer.encodeBegin(FormRenderer.java:134) [jsf-impl-2.2.5-jbossorg-3.jar:]
at javax.faces.component.UIComponentBase.encodeBegin(UIComponentBase.java:864) [jboss-jsf-api_2.2_spec-2.2.5.jar:2.2.5]
at javax.faces.component.UIComponent.encodeAll(UIComponent.java:1854) [jboss-jsf-api_2.2_spec-2.2.5.jar:2.2.5]
at javax.faces.component.UIComponent.encodeAll(UIComponent.java:1859) [jboss-jsf-api_2.2_spec-2.2.5.jar:2.2.5]
at com.sun.faces.application.view.JspViewHandlingStrategy.doRenderView(JspViewHandlingStrategy.java:431) [jsf-impl-2.2.5-jbossorg-3.jar:]
at com.sun.faces.application.view.JspViewHandlingStrategy.renderView(JspViewHandlingStrategy.java:232) [jsf-impl-2.2.5-jbossorg-3.jar:]
at com.sun.faces.application.view.MultiViewHandler.renderView(MultiViewHandler.java:133) [jsf-impl-2.2.5-jbossorg-3.jar:]
at javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:337) [jboss-jsf-api_2.2_spec-2.2.5.jar:2.2.5]
at javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:337) [jboss-jsf-api_2.2_spec-2.2.5.jar:2.2.5]
at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:120) [jsf-impl-2.2.5-jbossorg-3.jar:]
at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101) [jsf-impl-2.2.5-jbossorg-3.jar:]
at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:219) [jsf-impl-2.2.5-jbossorg-3.jar:]
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:647) [jboss-jsf-api_2.2_spec-2.2.5.jar:2.2.5]
... 24 more
"Weld" ist die WildFly-spezifische Implementierung des CDI-Standards.
EJB-Injection
Alle drei Managed Beans benötigen die Stateless Session Bean de.fhw.komponentenarchitekturen.knauf.kuchenzutatjsf.KuchenZutatWorkerBean
.
Diese können wir mittels EJB-Injection einlesen.
Das sieht so aus:
@EJB()
private KuchenZutatWorkerLocal kuchenZutatWorkerBean;
Da ein Zugriff über den Environment Naming Context nicht nötig ist, müssen weder in web.xml noch in jboss-web.xml EJB-Referenzen eingetragen sein
"jboss-web.xml" ist eigentlich nicht nötig, ich habe sie nur der Vollständigkeit halber angelegt. Sie sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/schema/jbossas/jboss-web_12_1.xsd"
version="12.1">
<context-root>KuchenZutatJSFWeb</context-root>
</jboss-web>
Diese Version 12.1 ist nicht die aktuellste in WildFly 24 verfügbare Version, aber es ist immerhin eine Version in der es keine Validierungsfehler in der XSD gibt.
Um die EJBs nur an einer Stelle zu deklarieren, habe ich mir eine Basisklasse für alle meine Managed Beans deklariert, die
die KuchenZutatWorkerBean per Injection holt und sie in einer privaten Variable speichert. Der Zugriff erfolgt nur durch eine get-Methode.
Es wäre durch diese Codestruktur möglich, nach Umstellung nur einer einzigen Klasse und Änderung zweier Deployment Deskriptoren
auf händischen JNDI-Lookup zu wechseln (ein Beispiel hierfür findet sich im Vorjahres-Beispiel, in dem die JSF-Library ausgetauscht werden musste
und deshalb JBoss Injection nicht unterstützte).
public class BaseHandler
{
@EJB()
private KuchenZutatWorkerLocal kuchenZutatWorkerBean;
protected KuchenZutatWorkerLocal getKuchenZutatWorker()
{
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.
Mehr Infos dazu: http://balusc.blogspot.com/2006/06/communication-in-jsf.html
(gemäß dieser Seite ist es scheinbar doch möglich, in der Action-Methode auf "f:param"-Parameter zuzugreifen, das wird bei Gelegenheit ausprobiert ;-) )
ViewState/LifeCycle
Die in diesem Abschnitt beschriebenen Informationen sind sehr wichtig, um den Ablauf in einer JSF-Anwendung zu verstehen, und um sich scheinbare
Ungereimtheiten gerade beim Bean-Scope "Request" (also: Bean bei jedem Request neu erzeugen, keinerlei Zustandsspeichern zwischen Aufrufen) zu erklären!
Es sei auf das Kapitel "2.2 Standard Request Processing Lifecycle Phases" in der JSF-1.2-Spezifikation verwiesen.
Das folgende Bild ist von http://java.sun.com/javaee/5/docs/tutorial/doc/figures/jsfIntro-lifecycle.gif geklaut
und bietet einen Überblick über den Lifecyle:
Siehe auch: http://www.fh-wedel.de/~si/seminare/ws06/Ausarbeitung/10.JavaServerFaces/jsf2.html#jsfl
Erster Seitenaufruf:
Beim ersten Aufruf einer Seite springt das JSF-Framework direkt in die "Render Response"-Phase, hier gibt es keine besonderen Fallstricke.
Postback:
Beim sogenannten "Postback" einer Seite (also beim Abschicken irgendeines Formulars) ist der Ablauf folgender:
- Als erstes wird die Phase Restore View durchlaufen, in der
anhand eines Hidden Fields "ViewState", das in jedem der JSF-Formulare steckt, ein serialisierter Komponentenbaum (enthält eine Hierarchie aller Eingabefelder
sowie deren letzte Werte) zugeordnet wird. Für die Übermittlung des ViewState gibt es zwei Möglichkeiten:
- Serverseitig (Default):
Um es explizit einzuschalten: in "web.xml" folgendes zufügen:
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value>
</context-param>
Dies führt in jedem HTML-Formular zu einem solchen Hidden Field:
<input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState" value="j_id1:j_id6" />
Der "value" dieses ViewState ist pro Seite innerhalb der aktuellen Session eindeutig (anscheinend wird pro aufgerufender Seite die letzte Zahl (im Beispiel also "6") hochgezählt).
Anhand des "value" findet das JSF-Framework wohl intern in einem Hashtable den ViewState bzw. den Komponentenbaum, der beim Rendern dieser Seite entstand.
Vorteil: Wenige Daten müssen zum Client geschickt werden.
Nachteil: Der Server muss die Resourcen bereitstellen, um die ViewStates aller Clients bis zum Session-Ende oder Session-Ablauf oder bis zu einem sonstigen Timeout
zu speichern.
- Clientseitig:
In "web.xml" folgendes eintragen:
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
Dies führt in jedem HTML-Formular zu einem solchen Hidden Field (Beispiel aus "zutatdetail.jsp" bei neuer Zutat):
<input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState" value="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" />
Vorteil: Der Server muss den ViewState nicht selbst speichern.
Nachteil 1: Sehr viele Daten müssen zum Client und zurück geschickt werden. Mir ist es in einem ähnlichen Fall vorgekommen, dass eine Anwendung über HTTPS lief und
eine Firewall nur HTTPS-Requests einer maximalen Größe zuließ und meinen Request mitsamt ViewState verweigerte.
Nachteil 2: Ein bösartiger Client könnte Unfug mit dem ViewState anstellen und ihn manipulieren.
-
Danach folgt die Phase Apply Request Values, in der die Request-Werte in den internen Komponentenbaum geschrieben werden (also noch NICHT in unsere Managed Bean).
In dieser Phase erfolgen Get-Aufrufe der im Formular enthaltenen Properties unserer Managed Bean.
Eine sehr sinnvolle Erklärung hierfür habe ich hier gefunden: http://www.jboss.com/index.html?module=bb&op=viewtopic&p=4034864#4034920
Now, why is the getter on the backing bean sometimes called in the process validations phase (or even in the Apply Request Values phase depending on the value of the immediate attribute)?
In the Process Validations phase, ValueChangeEvents are being queued. Of course, a ValueChangeEvent may only be queued when the value changed, therefore JSF evaluates the value(binding)expression, to compare the local value of the component (mind "local value", this means this normally only happens when conversion and validation were successful) with the value of the component (i.e. the value on the backing bean), so the getter on the backing bean is called.
Also wird in dieser Phase schon der originale Wert der Backing/Managed Bean abgerufen!
- Danach folgt Process Validations. In dieser Phase wird der Komponentenbaum validiert (ebenfalls ohne dass mit unserer ManagedBean etwas passiert).
Wird hier ein Validierungsfehler erkannt, wird die abgesendete Seite erneut gerendert, es geht also mit der Phase Render Response weiter.
-
Jetzt endlich geht es in Update Model Values, in der die neuen Werte aus dem Komponentenbaum in unsere Managed Bean geschrieben werden.
-
In Invoke Application werden zuerst ActionListener, danach die Action-Methode aufgerufen.
-
Ausgehend von deren Rückgabewert geht es in die Render Response-Phase der Ziel-View.
Aus diesen Phasen ergeben sich einige große Besonderheiten, die vor allem zuschlagen, wenn man Backing Beans im Request-Scope verwendet!
- Bei einem Validierungsfehler werden keine Daten in die Backing Bean geschrieben (da die Phase Update Model Values überschrieben wird).
Dies ist für Eingabefelder (einschließlich Hidden Fields) kein Problem, da ihre Werte aus Komponentenbaum gezogen werden (und dadurch die letzten, eventuell
falschen User-Eingabe enthalten). Aber
<h:outputText>
-Tags
scheinen ihren letzten Wert nicht mitzuführen. Dies führte dazu, dass meine Kuchen-Bearbeiten-Seite bei einem Eingabefelder zwar alle Felder mit den letzten Eingaben
füllte, das Textfeld "Kuchen-ID" aber den Wert aus dem uninitialisierten KuchenDetailHandler
holte und ausgab.
- Folgende Konstellation führte ebenfalls in eine 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.
Es gäbe zwei Lösungen des Dilemmas:
- Statt Request-Scope wird der Session-Scope verwendet
Dies hilft nur vordergründig weiter. Die Backing Bean behält dadurch zwar immer ihren letzten Zustand, aber dieser letzte Zustand kann falsch werden,
sobald der User z.B. zwei Browserfenster öffnet: im ersten Fenster wird Kuchen 1 geöffnet (Backing Bean enthält Daten von Kuchen 1), danach wird in einem
zweiten Fenster Kuchen 2 geöffnet (Backing Bean enthält Daten von Kuchen 2). Jetzt wird im ersten Fenster (Kuchen 1) auf Submit geklickt =>
in der Backing Bean stecken die Daten von Kuchen 2.
Ein ähnliches Problem stellt sich, wenn der User in der Browserhistorie zurückblättert: dann hat der Zustand der Backing Bean vermutlich nichts mehr mit der
gerade gewählten alten Seite zu tun.
- Manuelles Initialisieren des alten Zustands
Hier klinkt man sich in die JSF-Verarbeitungsphasen ein, indem man einen Handler für das "beforePhase"-Event der View registriert (siehe folgender Abschnitt Phase Event).
Dieses Vorgehen habe ich in meinem Beispiel gewählt, und es deckte alle Probleme ab.
Goldene Faustregel: immer, wenn ein Postback eines Formulars bzw. einer View geschieht (egal, welche Action das Ziel ist!), dann sollte der Zustand der Backing Bean in der Phase "Apply Request Values"
wiederhergestellt werden. Das heißt, dass z.B. die ID des gerade in Bearbeitung befindlichen Objekts ermittelt werden sollte und das Objekt geladen werden sollte, so
dass alle getter das gleiche Zurückliefern wie beim Aufbau des Formulars.
Phase Event
Das Einklinken in die Verarbeitungsphasen von JSF geschieht, indem im <f:view>
-Tag das beforePhase
-Attribut gesetzt wird:
<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! Man könnte zwar den gesamten Komponentenbaum der aktuellen View durchlaufen, aber damit wüßte man immer noch nicht, welches Formular gerade abgeschickt wird,
und woher deshalb die Kuchen-ID kommt. Deshalb verwende ich ein Hidden Field, und suche seinen Wert aus dem ServletRequest:
<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.
Wichtig ist, dass beim Postback eines jeden Formulars schon vor der Validierungsphase die Kuchen-ID ermittelt werden kann.
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.
Zugriff auf andere Manged Bean
An einigen Stellen des Beispiels ergeben sich auch dem Weg durch die Beans Inkonsistenzen: Beispielsweise wird das Löschen eines Kuchens in "KuchenDetailHandler.deleteKuchen" durchgeführt.
Danach soll zurück zu "kuchenliste.xhtml" gesprungen werden. Allerdings würde diese Seite in diesem Fall immer noch den eigentlich schon gelöschten Kuchen anzeigen.
Grund: Nach dem Klick auf "kuchenliste.xhtml" -> "Kuchen bearbeiten" wird der "beforePhase"-Handler des "KuchenListeHandler" aufgerufen.
Und das führt dazu, dass dort die Kuchenliste bereits initialisiert wurde. Nach dem Löschen des Kuchens enthält diese Liste ihn aber immer noch.
Deshalb muss beim Löschen der Kuchen in der Bean entfernt werden.
Die gleiche Problematik besteht im "ZutatDetailHandler": dort kommt man von "kuchendetail.xhtml", d.h. der aktuelle Kuchen wurde bereits eingeladen und enthält
dadurch noch nicht die geänderte/gelöschte Zutat.
Lösung für beides: auf den Handler zugreifen und die dort gecachten Daten zurücksetzen. Dazu wird mittels @Inject
-Annotation die Instanz des Handlers injiziert:
import javax.inject.Inject;
...
@Inject
private KuchenListeHandler kuchenListeHandler;
Anschließend kann nach dem Speichern des Kuchens die Kuchenliste im Handler zurückgesetzt werden:
public String deleteKuchen()
{
...
kuchenListeHandler.clearKuchenliste();
return "kuchenliste";
}
Mit den JSF 1-Beans, die über
@ManagedBean
oder über faces-config.xml definiert waren, konnte man die Bean-Instanz aus dem Request holen:
ServletRequest request = (ServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
KuchenListeHandler kuchenListeHandler = (KuchenListeHandler) request.getAttribute("kuchenListeHandler");
kuchenListeHandler.clearKuchenliste();
Dies funktioniert mit den CDI-Annotations nicht mehr - die Instanzen werden an anderer Stelle verwaltet.
Aber folgendes Codestück funktioniert:
KuchenListeHandler kuchenListeHandler = javax.enterprise.inject.spi.CDI.current().select(KuchenListeHandler.class).get();
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.
ACHTUNG: wenn man auf "kuchendetail.xhtml" 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! Das ist nur durch eine beforePhase
zu umgehen, siehe Abschnitt ViewState/LifeCycle.
Weitere Design-Tips
Formulare 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.
Stand 07.01.2023
Historie:
18.03.2014: Erstellt aus JBoss5-Beispiel. Angepaßt an JavaEE7 und WildFly 8 (Abschnitt "beans.xml")
10.01.2022: Aktualisiert auf JSF 2.3, Hinweis "Implicit Navigation Rules", die EAR-Datei enthielt das falsche Projekt.
27.12.2022: alle JSP-Seiten auf XHTML umgestellt
07.01.2023: Deklaration der ManagedBeans in "faces-config.xml" durch CDI-Beans ersetzt.