JSF Ext oder JSF Extensions ist nun die dritte Auflage unserer beliebten Erweiterungen für JSF. Nach den Versionen Intersult Taglib für JSF 1.2, JSF Desktop für JSF 2.0 ist nun JSF Ext für JSF 2.0 und 2.1 verfügbar. JSF Ext ist eine Erweiterung von JSF 2 (2.0, 2.1) um einige grundlegende Features, die man für saubere, funktionale AJAX-Applikationen braucht.
Einige der Features:
In der pom.xml wird angegeben:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> ... <dependencies> <dependency> <groupId>com.intersult</groupId> <artifactId>jsf-ext</artifactId> <version>2.1.11-SNAPSHOT</version> </dependency> ... </dependencies> <repositories> <repository> <id>intersult-repo</id> <name>Intersult Repository</name> <url>http://repository.intersult.com/repository</url> </repository> ... </repositories> </project>
JSF Ext includes namespaces e and ext. There are two namespaces, because JSF Ext contains both classic tags and on-the-fly facelet tags. In your XHTML-Pages, include the namespaces:
xmlns:e="http://java.sun.com/jsf/ext" xmlns:ext="http://java.sun.com/jsf/composite/ext"
Damit setzen Sie komplexe, interaktive Applications im Handumdrehen auf.
Bei einigen Features und Konstellationen ist es von Vorteil, bestimmte Konfigurationen vorzunehmen. Wie bereits in JSF2 erwähnt, werden in einigen Servlet-Containern die Annotations nicht verarbeitet. JSF Ext registriert einen sogenannten InjectionProvider, was im Normalfall transparent funktioniert.
Falls man feststellt, dass die Events im JSF Ext nicht arbeiten, Oberflächenelemente nicht aktualisiert werden und Messages nicht angezeigt werden, dann arbeiten vermutlich auch übliche JSF-Annotations @ManagedBean oder @SessionScoped nicht. Abhilfe schafft dann:
<context-param> <param-name>com.sun.faces.injectionProvider</param-name> <param-value>com.intersult.jsf.event.EventInjectionProvider</param-value> </context-param>
Die Lösung besten in einem Redirect nach dem Submit, der häufig Page für Page, Action für Action in das Projekt reingezogen wird. Die Nachteile seitens Wartbarkeit, Konsistenz und Veränderbarkeit liegen auf der Hand. Eine saubere Lösung besteht darin, einen entsprechenden Navigation-Handler zu benutzen:
<navigation-handler>com.intersult.jsf.util.RedirectNavigationHandler</navigation-handler>
In der Composite Component gibt es nur den Tag <cc:insertChildren>, damit hat man keine detailierte Kontrolle, die Children werden alle an derselben Stelle eingefügt. Möchte man diese zum Beispiel durch ein SPAN-Tag wrappen, braucht man den Insert Tag.
Lösung: Man iteriert durch die Children mittels <c:forEach> und fügt diese an einer beliebigen Stelle mittel <e:insert> ein.
<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:cc="http://java.sun.com/jsf/composite" xmlns:e="http://java.sun.com/jsf/ext" xmlns:ext="http://java.sun.com/jsf/composite/ext" > <cc:interface> <cc:attribute name="align" default="center"/> </cc:interface> <cc:implementation> <div style="margin-top: 5px; text-align: #{cc.attrs.align};"> <c:forEach items="#{cc.children}" var="child"> <h:panelGroup style="padding: 5px 5px 0 0;" rendered="#{child.rendered}"> <e:insert component="#{child}"/> </h:panelGroup> </c:forEach> </div> </cc:implementation> </html>
In vielen Fällen hab man bereits Behavior-Components zu einem Tag hinzugefügt. Würde man dennoch das entsprechende Attribut verwenden (z.B. onclick bei commandButton), kann es zu Problemen kommen, wie die Reihenfolge der Snippets oder das "return false;"-Anhängsel.
Lösung bringt der Behavior-Tag von JSF Ext. Typisch ist zum Beispiel die Verwendung zusammen mit dem AJAX-Tag:
<h:commandButton value="X"> <f:ajax/> <e:unload/> <e:behavior script="#{rich:component('popup-panel')}.hide(event);"/> </h:commandButton>
Die Behavior-Snippets werden in der entsprechenden Reihenfolge produziert:
jsf.util.chain(this,event,'mojarra.ab(this,event,\'action\',0,0)','RichFaces.$(\'popup:form:popup-panel\').hide(event);');return false
<h:form id="test-form"> <h:panelGrid columns="3"> <h:outputText value="Name"/> <h:inputText id="name" value="#{bean.name}" required="true"/> <h:message for="name"/> </h:panelGrid> </h:form> <h:form id="other-form"> <h:commandButton id="tag-save" value="Tag Button"> <e:ajax source=":test-form" render=":test-form"/> </h:commandButton> </h:form>
Der Tag <e:ajax> ersetzt in diesem Fall den Tag <f:ajax>, er braucht nicht zusätzlich angegeben zu werden.
<h:panelGrid> <f:ajax event="dblclick" action="#{bean.action}"/> </h:panelGrid>
<h:graphicImage id="edit" value="edit.png"> <e:ajax event="click"> <f:setPropertyActionListener value="#{element}" target="#{editController.element}"/> <rich:componentControl target=":some-popup" operation="show"/> </e:ajax> </h:commandButton>
Der Tag <e:div> ist ein vollwertiger ClientBehaviorHolder:
<e:div id="mouse-active" tabindex="0" onkeypress="alert((event || window.event).keyCode);"> <ext:mouse-focus id="mouse-focus"/> <h1>TEST</h1> </e:div>
Damit wird das Erstellen von einfachen Komponenten mit wenig HTML-Code und flachen Komponenten-Bäumen deutlich vereinfacht.
<h:inputText id="name" value="#{bean.name}"> <e:attribute name="placeholder" value="Name eingeben..."/> </h:inputText>
<h:panelGroup> <e:attribute name="oncontextmenu"> <f:ajax listener="#{bean.render}" render=":some-menu"/> </e:attribute> </h:panelGroup>
<e:div id="#{cc.id}" styleClass="button-bar"> <e:attribute name="style" renderEmpty="false"> <e:behavior script="text-align: #{cc.attrs.align};" rendered="#{!empty cc.attrs.align}"/> <e:behavior script="background-color: #{cc.attrs.color};" rendered="#{!empty cc.attrs.color}"/> </e:attribute> ... </e:div>
<e:div> <e:attribute name="style"> <e:style name="text-align" value="#{valueEdit.permission.field.type.align}"/> <e:style name="width" value="100%"/> <e:style name="margin-left" value="10px"/> </e:attribute> </e:div>
Der Reference-Tag schafft Zugriff auf den ELContext einer andere Stelle im Komponenten-Baum. Über eine festgelegte Variable kann auf Elemente aus einem beliebigen Kontext zugegriffen werden. Einschließlich iterierte Kontexte, wie sie von <ui:repeat> oder Tabellen erzeugt werden.
In JSF werden oft Components verwendet, die durch Iteration mit <ui:repeat> oder auf andere Art vervielfältigt werden, auch Tables, Trees und ähnliche Komponenten geben wiederholten HTML-Code aus.
Gerade bei vervielfältigtem Code möchte man diesen schlank bauen, da dies für die Effizienz der Web-Seite ausschlaggebend ist. Kontext-Menüs, Popups und andere Elemente, die nur einmal für ein Element geöffnet werden brauchen, sollten also aus der Iteration herausgenommen werden. Doch früher oder später muss auf das Element wieder zugegriffen werden.
In einfachen, wenig zeitkritischen Fällen behilft man sich mit einem AXAJ-Submit. Zum Beispiel:
<h:form id="form"> <ui:repeat id="repeat" value="#{fn:split('1,2,3,4,5', ',')}" var="index"> <h:commandButton id="button" value="Button #{index}"> <f:setPropertyActionListener target="#{bean.selected}" value="#{item}"> <e:ajax onsuccess="showContextMenu();"/> </h:commandButton> </c:forEach> </h:form>
Anmerkung: In JSF 2.1.11 ist für den Tag <ui:repeat> kein Attribut "id" in der Taglib eingetragen. Da es sich um eine UIComponent handelt, kann die Id trotzdem angegeben werden und wird auch korrekt verarbeitet. Damit erreicht man, dass die Child Components der Iteration definierte Namen bekommen.
Allerdings wird dazwischen ein AJAX-Aufruf abgeschickt. Wenn in einem Popup oder Kontextmenu das Element betreffende Dinge neu gerendered werden sollen, sicher der richtige Weg. Bei einfacheren Komponenten oft hinderlich und vom Benutzer als zu "hackig" empfunden. Da kann der Reference-Tag Abhilfe schaffen:
<h:form id="form"> <c:forEach begin="1" end="5" var="index"> <h:commandButton id="button-#{index}" value="Button #{index}"> <e:setReference target=":form:ref"/> <e:behavior script="showContextMenu();" disableDefault="true"/> </h:commandButton> </c:forEach> <e:reference id="ref" var="ref"> <h:commandButton value="Show" action="#{test.actionText(ref.index)}"> <f:ajax/> </h:commandButton> </e:reference> </h:form>
Erklärung: Der Tag <e:setReference> ist ein ClientBehavior, also JavaScript für den Tag <e:reference>. Beim Klicken eines der iterierten Command-Buttons wird die Referenz des Tags <e:reference> gesetzt, ohne einen Request zum Server abzuschicken. Beim Klicken des unteren Command-Buttons "Show" wird mit "ref.index" auf den ELContext des referenzierten Buttons zugegriffen, also die Variable "index" die zum Iterieren verwendet wurde.
<e:load scopeId="test-scope"> <e:new name="bean" type="com.intersult.test.Bean"/> </e:load>
Es ist auch möglich, Properties der neuen Bean Werte zuzuweisen:
<e:new name="bean" type="com.intersult.test.Bean"> <f:param name="param1" value="#{some-expression}/> </e:new>
Hinweis: Es ist zu erwähnen, dass das Instantiieren von Beans durch den New-Tag möglicher Weise nicht die beste Praxis ist. Die erste Wahl beim Erzeugen von Beans sollte das Verwenden von Scope- und Factory-Annotations sein, entweder über JSF mit @ManagedBean oder über Spring mit @Component. An zweiter Wahl steht das Erzeugen im Java-Code, zum Beispiel mit Lazy-Initialization Pattern "if (bean == null) bean = new Bean();". <e:new> macht vor allem da Sinn, wo eine neue leere Bean erzeugt werden soll (z.B. Create Popup) und wo mehrere Parameter durch eine Bean übergeben werden sollen.
<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:e="http://java.sun.com/jsf/ext" xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:cc="http://java.sun.com/jsf/composite" > <cc:interface> <cc:attribute name="label"/> <cc:attribute name="action" targets="link" method-signature="void action()"/> <cc:actionSource name="action" targets="link" default="true"/> </cc:interface> <cc:implementation> <h:commandLink id="link" <h:outputText value="#{cc.attrs.label}"/> </h:commandLink> </cc:implementation> </html>
Dieser Tag kann wie folgt verwendet werden:
<app:myLink label="Some Link"> <e:set value="Some Value" target="#{bean.value}"/> </app:myLink>
Beim Verwenden von <f:setPropertyActionListener> müsste man explizit mit einem Attribut for="action" die Action spezifizieren. Der Tag <e:set> macht dies automatisch, bei häufigem Verwenden sieht der kurze Name einfach ausdrucksvoller und aufgeräumter aus.
Die Verwendung ist identisch mit <h:outputLabel>:
<e:outputLabel for="username:username" value="#{messages['user.username']}"/> <ext:input id="username"> <h:inputText id="username" value="#{userRegister.user.username}"/> </ext:input>
Dabei kommen die beiden CSS-Style-Klassen label-required und label-not-required zum Einsatz, die mit "font-weight: bold;" und "color: #a0a0a0;" vorbelegt sind. Diese können durch ein Benutzer-Stylesheet überschrieben werden.
In den Anwendungen werden daher unterschiedlichste Elemente in die Seite eingebaut, von rotierenden, blinkenden durch durchlaufenden Bildern bis zu Seiten abdunkelnden und sperrenden Elementen. Das übliche Verfahren von Anwendungen die Verarbeitung einer Operation zu zeigen, ist den Mauszeiger als Busy Pointer darzustellen.
Also wieso nicht auch im Browser bei JSF-Anwendungen den Busy Pointer aktivieren. Der Benutzer wird nicht abgelenkt durch zappelnde Bilder, es wird nichts gesperrt, da die Seite ja weiterhin funktional ist:
<ext:busyPointer/>
Optional kann das Attribut cursor angeben werde, das per default auf "wait" gesetzt ist.
Eine Lösung ist der Tag <ext:mouse-visibility>:
<e:div> <h:outputText id="project" value="#{processDetails.process.project.name}"/> <ext:mouse-visibility for=":process-form:image"/> <h:graphicImage id="image" name="edit.gif" library="images/bitcons"> <e:ajax event="click"> <e:load scopeId=":projectEdit"> <f:param name="project" value="#{project}"/> </e:load> </e:ajax> </h:graphicImage> </e:div>
Darüber hinaus gibt es noch die Tags <ext:mouse-display> mit welcher der Display-Style eines Elements gesteuert werden kann. Besonders schick ist auch der <ext:mouse-fade>, mit der das Element ein- und ausgefadet wird.
Composite Tags sind ein mächtiges und zugleich einfach zu handhabendes Instrument zum Erstellen eigener Taglibs. Durch einige zusätzliche Instrumente werden Composite Tags noch vielseitiger. Damit kann vielfach auf die Entwicklung von nativen Tags verzichtet werden, inklusive aufwändiger Compile, Redeploy und Restart-Zyklen. Durch die erweiterten Möglichkeiten steigt gleichzeitig der Anreiz zu Composite Tags.
Hintergrund: Die EL-Expression #{component.parent.parent.parent} (dazwischen zwei UINamingContainer und UIPanel die Facelets einfügt) würde normaler Weise den Parent-Tag liefern, evaluiert jedoch zu null. Zum einen ist zu dieser Zeit die Tag-Hierarchie noch nicht aufgebaut, zum anderen wird der UINamingContainer aus der Facelet-Hierarchie herausgenommen, da der Tag durch einen speziellen Mechanismus gerendert wird.
Lösung:
Zugriff über jsf, also #{jsf.tag.composite.parent.component}. Folgendes Beispiel zeigt eine Composite-Component für ein Mouse-Behavior:
<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:cc="http://java.sun.com/jsf/composite" xmlns:e="http://java.sun.com/jsf/ext" xmlns:ext="http://java.sun.com/jsf/composite/ext" > <cc:interface> </cc:interface> <cc:implementation> <e:behavior id="mouseover" event="mouseover" for="#{jsf.tag.composite.parent.component}" script="focus();"/> <e:behavior id="mouseout" event="mouseout" for="#{jsf.tag.composite.parent.component}" script="blur();"/> </cc:implementation> </html>
Dieser Tag ist bereits Bestandteil des JSF-Ext unter <ext:mouse-focus>. Er kann zusammen mit jedem ClientBehaviorHolder, wie dem <e:div> angewendet werden.
JSF Ext löst dieses Problem durch zusätzlichen Java-Script Code. Durch das Einbinden der JAR-Bibliothek ist das Problem transparent gelöst. Der enthaltene Java-Script Code wird immer dann eingebunden, wenn auch der JSF-AJAX-Code eingebunden wird. Er fügt den ViewState in enthaltenenen FORM-Tags ein, sodass keine zusätzlichen Render-Anweisungen benötigt werden.
Der ExtResponseWriter von JSF Ext stellt eine statische Methode getEndElementWriter(ResponseWriter responseWriter) bzw. getEndElementWriter() zur Verfügung. Damit bekommt man einen ResponseWriter, mit dem geschrieben werden kann. Der HTML-Code wir dann effektiv vor dem Schließen des End-Tags ausgegeben:
ResponseWriter writer = ExtResponseWriter.getEndElementWriter(); writer.startElement("div", component); writer.writeAttribute("id", component.getClientId(behaviorContext.getFacesContext()), null); writer.writeAttribute("style", "display: none;", null); writer.endElement("div");
Name | Java | Parameter | Beschreibung |
---|---|---|---|
javax.faces.post_construct | Event.EVENT_POST_CONSTRUCT | Object managedBean | Der Event wird nach dem Erzeugen einer managed Bean durch das JSF-System erzeugt. |
javax.faces.pre_destroy | Event.EVENT_PRE_DESTROY | Der Event wird vor dem Entfernen einer managed Bean erzeugt. | |
javax.faces.event | Event.EVENT_EVENT | String event, Object... arguments | Dieser Meta-Event wird bei jedem Event erzeugt, sodass allgemeine Event-Handler geschrieben werden können. Dies ist mit Vorsicht zu verwenden. |
javax.faces.messages | MessageHandler.EVENT | - | Der Event wird erzeugt, wenn Faces Messages vorliegen. |
login.before | Login.EVENT_LOGIN_BEFORE | - | Bevor ein Benutzer eingeloggt wird. |
login.success | Login.EVENT_LOGIN_SUCCESS | - | Nachdem ein Benutzer erfolgreich eingeloggt wurde. Der Benutzer ist über Login.instance().getUser() abrufbar. |
login.fail | Login.EVENT_LOGIN_FAIL | - | Nachdem ein Login fehlgeschlagen ist. |
login.after | Login.EVENT_LOGIN_AFTER | - | Nach dem Login-Prozess, unabhängig ob er erfolgreich oder fehlgeschlagen ist. |
login.logout | Login.EVENT_LOGOUT | - | Nach dem Logout. |
Aus diesem Grund führt JSF Ext die (Render-)Events ein. Events können zwar aus der View durch den Tag <e:raise> erzeugt werden, sinnvoll ist meist jedoch die Erzeugung durch die raise-Methode auf der Java-Seite. Dadurch bleibt die lose Kopplung von View und Controller erhalten. Die können Controller können Ereignisse auslösen, auf die sich Teile der View registrieren um neu gerendert zu werden.
Das Session basierte Event-Objekt enthält die Methode raise(String event, Object... arguments), also im einfachsten Fall:
Event.instance().raise("com.intersult.some-event");
Die Events werden in der Regel durch eine Annotation konsumiert:
@Listener("com.intersult.some-event") public void someEvent() { }
Es ist zu beachten, dass Events nur empfangen werden wenn eine Bean tatsächlich instantiiert ist.
<h:messages id="messages" globalOnly="true"> <e:render event="javax.faces.messages"/> </h:message>
Ergebnis: Die Faces-Messages werden gerendered, ohne dass bei jedem AJAX-Tag ein gesondertes Rendered-Attribut angegeben werden muss.
Lösung bisher: In vielen Projekten wird für jedes Input-Element ein Composite-Tag gebaut, in dem der Code für AJAX, Validierung, Messages und so weiter wiederholt wird.
Mit JSF Ext kann ein generischer Wrapper für Input-Elements gebaut werden:
<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:cc="http://java.sun.com/jsf/composite" xmlns:e="http://java.sun.com/jsf/ext" xmlns:ext="http://java.sun.com/jsf/composite/ext" xmlns:p="http://primefaces.org/ui" > <cc:interface> </cc:interface> <cc:implementation> <e:insert component="#{cc.children[0]}"> <f:ajax event="change"/> </e:insert> <h:message id="#{cc.children[0].id}-message" for=":#{cc.children[0].clientId}"> <e:render event=":#{cc.children[0].clientId}"/> </h:message> </cc:implementation> </html>
Die Anwendung dieses Composite-Tags ist dann wie folgt:
<h:form id="form"> <app:input id="text"> <h:inputText id="text" value="#{test.text}"/> </app:input> <h:commandButton value="Submit" action="#{bean.action}"> <f:ajax/> </h:commandButton>
Erklärung: Die Composite-Component wrappt das Input-Element, welches innerhalb des Composite Tags durch <e:insert> eingefügt wird. Der Zugriff erfolgt dabei durch die EL-Expression cc.children[0].
<e:render event=":form:some-text"/>
Der Event tritt in folgenden Fällen auf:
<h:commandButton value="Submit"> <f:ajax execute="@form"/> <e:raise name="com.intersult.test"/> </h:commandButton>
Hier ist zu beachten, dass ein AJAX-Tag zusätzlich verwendet wird. Bei vollen GET- oder POST-Requests machen Events keinen Sinn, da hier sowieso die komplette Seite gerendered werden würde.
Das Konsumieren kann wieder auf ähnliche Weise erfolgen:
<h:outputText id="output" value="#{bean.text}"> <e:render event="com.intersult.test"/> </h:outputText>
Es ist zu beachten, dass bei der Update-Component (hier outputText) eine Id angegeben wird, damit der AJAX-Handler das Rerendering zuordnen kann. Dies tritt beim Verwenden des render-Attributs nicht auf, da durch die Angabe einer Id ja eine vergeben wurde.
<e:scope id="scope-id"> <h:outputText value="#{scope.id}"/> </e:scope>
<e:scope id="popup" viewId="/popup.xhtml"/>
Diese Definition fügt einen Scope in den Component-Tree ein. Der Scope ist ein UINamingContainer, die Id wird also zu jeder enthaltenen Komponente als Prefix vorangestellt. Zum Beispiel erzeugt <h:inputText id="name" value="#{bean.name}"/> die Ausgabe von <input name="popup:name">.
Dieser Scope verweist auf die viewId "/popup.xhtml", damit ist ein Facelet innerhalb der Web-Applikation gemeint, vergleichbar zu <ui:include>. Das Facelet wird allerdings (zunächst) nicht geladen, im Komponentenbaum befindet sich ausschließlich die Scope Component.
<e:load scopeId="popup"/>
Das Load-Tag ist ein ActionListener, wird also unterhalb einer ActionSource eingefügt, wie zum Beispiel das <f:actionListener> oder <f:setPropertyActionListener>. Im Zusammenspiel mit AJAX kann nun der Scope dynamisch geladen werden:
<h:commandButton id="load-popup" value="Load Popup"> <f:ajax/> <e:load scopeId="popup"/> </h:commandButton>
Beim Laden des Scopes können zusätzlich Parameter übergeben werden:
<h:commandButton id="load-popup" value="Load Popup"> <f:ajax render=":popup:form"/> <e:load scopeId="popup"> <f:param name="someParam" value="someValue"/> <f:param name="someExpression" value="#{bean.someValue}"/> </e:load> </h:commandButton>
Wird ein Scope innerhalt eines bestehenden Scopes geladen, kann es notwendig sein innerhalb des Load-Tags auf den bestehenden Scope zuzugreifen. Dies kann über die Variable scope erfolgen, also zum Beispiel:
<e:load scopeId=":userPassword"> <f:param name="user" value="#{scope.user}"/> </e:load>
Das geladene Popup-Facelet "/popup.xhtml" kann zum Beispiel so aussehen:
<?xml version="1.0" encoding="UTF-8"?> <ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:rich="http://richfaces.org/rich" xmlns:a4j="http://richfaces.org/a4j" xmlns:t="http://siemens.com/test" xmlns:app="http://java.sun.com/jsf/composite/app" xmlns:e="http://java.sun.com/jsf/ext" xmlns:ext="http://java.sun.com/jsf/composite/ext" > <rich:popupPanel id="popup" show="true" autosized="true" modal="false"> <f:facet name="header"> <h:outputText value="Loaded Popup"/> </f:facet> <f:facet name="controls"> <h:form id="controls-form"> <h:commandButton value="X" immediate="true"> <f:ajax/> <e:unload/> <e:behavior script="#{rich:component('popup')}.hide(event);"/> </h:commandButton> </h:form> </f:facet> <h:form id="form"> <h:panelGrid columns="2"> <h:outputLabel for="popupText" value="Popup Text"/> <h:outputText id="popupText" value="#{bean.popupText}"/> <h:outputLabel for="someParam" value="Some Param"/> <h:outputText id="someParam" value="#{scope.someParam}"/> <h:outputLabel for="someExpression" value="Some Expression"/> <h:inputText id="someExpression" value="#{scope.someExpression}"/> </h:panelGrid> <ext:buttons> <h:commandButton id="save" value="Save" action="#{bean.save}"> <f:ajax execute="@form"/> <e:unload/> <e:behavior script="#{rich:component('popup')}.hide(event);"/> </h:commandButton> <h:commandButton id="cancel" value="Cancel" immediate="true"> <f:ajax/> <e:unload/> <e:behavior script="#{rich:component('popup')}.hide(event);"/> </h:commandButton> </ext:buttons> </h:form> </rich:popupPanel> </ui:composition>
Hier ist auch das Unload-Tag enthalten:
<h:commandButton value="X"> <f:ajax/> <e:unload/> <e:behavior script="#{rich:component('popup-panel')}.hide(event);"/> </h:commandButton>
Es handelt sich wie beim Unload-Tag um einen commandButton, der mit AJAX-Tag erweitert wurde. Im Gegensatz zum Load-Tag braucht beim Unload-Tag nicht zwingend die Scope-Id angegeben werden. Für den Fall dass der Unload-Tag innerhalb eines Scopes auftritt, wird automatisch dieser Scope verwendet. Damit vereinfachen sich unter anderem Close- und Save-Buttons.
Der Load-Tag unterstützt eine Action, innerhalb derer bereits der neue Scope verfügbar ist.
<h:commandButton value="Create user"> <f:ajax/> <e:load action="#{userController.create}/> </h:commandButton>
Erklärung: Würde man die Action im <h:commandButton> aufrufen, wäre der Scope nicht verfügbar, damit könnten auf die Werte aus dem Scope nicht zugegriffen werden.
Hinweis: Falls die Tags innerhalb des Scopes Header-Resourcen installieren, werden diese Transparent nachgeladen. Das bedeutet zunächst eine schlanke Web-Seite, die schnell geladen werden kann. Einzelne CSS-Dateien, Scripts und Images werden erst bei Bedarf nachgeladen.
<p:dialog id="dialog" header="#{messages['rule.edit']}" visible="true"> <f:ajax event="close" listener="#{scope.unload}"/> <h1>Content</h1> </p:dialog>
Ein Klasse im Custom-Scope kann so aussehen:
@ManagedBean @CustomScoped("#{scopes.scope}") public class FieldEdit implements Serializable { private static final long serialVersionUID = 1L; @ManagedProperty("#{scopes.scope.value}") private Field field; public Field getField() { if (field == null) field = new Field(); return field; } public void setField(Field field) { this.field = field; } public void save() { ProcessService.saveField(field); Event.instance().raise("workflow." + field.getWorkflow().getId() + ".fieldList.change"); Resource.addMessage( FacesMessage.SEVERITY_INFO, "field.save.success", field.getName(), field.getWorkflow().getName()); Scopes.instance().getScope().unload(); } }
Hier spart die save-Methode eine Menge Code, bei erfolgreicher Durchführung wird durch Scopes.instance().getScope().unload() der Scope beendet, die enthaltenen Daten freigegeben zur Garbage Collection sowie ein dahinerliegendes Popup geschlossen. Es sind keine weiteren XHTML-Elemente erforderlich, da der Render-Event dadurch ebenfalls erzeugt wird.
@Component @Scope(Scopes.SCOPE_NAME) public class SomeBean { ... }
Erklärung: Die Beans werden also bei Bedarf instantiiert und im JSF-Scope abgelegt. Dadurch kann ein Dialog- oder Unterdialog abgearbeitet werden. Am Ende des Workflow wird der Scope samt Inhalt wieder abgeräumt und der Garbage-Collection zugeführt. Dies ist sehr effizient implementiert, die Anwendung profitiert durch Verwenden von <e:load>, <e:unload> und <e:scope> von einer spürbaren Vereinfachung.
<c:forEach begin="1" end="3" var="index"> <e:scope id="scope-#{index}" load="true"> <f:param name="scopeVar" value="scope-value-#{index}"/> <div style="padding: 3px; margin: 3px; background-color: #e0e0e0; width: 400px;"> <h:outputText value="Inside: #{scope.id}, #{scope.scopeVar}"/> <e:scope id="nested" load="true"> <f:param name="innerVar" value="value-#{index}-#{scope.parent.scopeVar}"/> <h:outputText value="Nested: #{scope.innerVar}"/> </e:scope> </div> </e:scope> </c:forEach>
In der Praxis würde man eher Facelets laden mit dem Load-Tag und kein festes Attribut load="true" angeben.
Im folgenden Beispiel wird ein Save-Button einem Dialog hinzugefügt:
<app:editResource value="#{processDetails.process.title}" rendered="#{facesContext.externalContext.isUserInRole('ROLE_MANAGER')}"> <h:form id="resource-form"> <ext:buttons> <h:commandButton value="#{messages['save']}" action="#{processDetails.save}"> <f:ajax/> <e:unload scopeId=":textList"/> </h:commandButton> </ext:buttons> </h:form> </app:editResource>
Im Tag <app:editResource>:
<e:load scopeId=":textList"> <f:param name="resource" value="#{cc.attrs.value}"/> <f:param name="children" value="#{cc.children}"/> </e:load>
Und schließlich im Scope:
<c:forEach items="#{scope.children}" var="child"> <h:panelGroup style="padding: 5px 5px 0 0;" rendered="#{child.rendered}"> <e:insert component="#{child}"/> </h:panelGroup> </c:forEach>
Erklärung: Die Composite-Children werden durch die EL-Expression cc.children als Scope-Variable übergeben und durch den Insert-Tag eingefügt. Natürlich kann der Zwischenschritt über die Composite-Component auch entfallen. In diesem Fall können die Children zum Beispiel auch mit der EL-Expression component.children übergeben werden.
Hinweis: Die so eingefügten Komponenten werden an ihrer Ursprungsstelle ausgeführt, was sich insbesondere bei Action-Methoden auswirken kann, wenn auf Variablen zugegriffen wird. Component-Injection sollte daher nur für einfache Elemente verwendet werden, da es schnell zu unsauberen Code führen kann.
Alternative: Als saubere Lösung für Scopes, die in unterschiedlichen Varianten auftreten, kann der Inhalt des Scopes in eine XHTML-Datei ausgelagert werden. Dann werden mehrere Scopes mit den entsprechenden Varianten aufgebaut.
<h:selectOneMenu id="country" value="#{bean.country}"> <f:selectItems value="#{locales.countrySelectItems}"/> </h:selectOneMenu> <h:selectOneMenu id="language" value="#{bean.language}"> <f:selectItems value="#{locales.languageSelectItems}"/> </h:selectOneMenu> <h:selectOneMenu id="currency" value="#{bean.currency}"> <f:selectItems value="#{locales.currencySelectItems}"/> </h:selectOneMenu>
Zusätzlich befindet sich noch ein Getter für localeSelectItems. Dies liefert Liste vollwertiger Locale-Elemente aus Locale.getAvailableLocales() in einer auf der Oberfläche darstellbaren Form.
Des Weiteren befindet sich ein Feld für eine User-Locale in der Session-Scoped Bean. Dadurch kann ein einfaches Auswählen der Locale erreicht werden:
<h:selectOneMenu id="language" value="#{locales.locale}" converter="intersult.Language"> <f:selectItems value="#{locales.localeSelectItems}"/> </h:selectOneMenu>
Der Converter "intersult.Language" konvertiert ein Locale-Element in einen Sprach-String und zurück, damit das Locale-Objekt mit dem selectOneMenu verwendet werden kann.
<faces-config> <application> <el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver> </application> </faces-config>
<import resource="classpath:/META-INF/extContext.xml"/>
Hinweis: JSF Ext registriert zusätzlich den ViewScope von JSF.
Beim Laden des Scopes:
<e:load scopeId="rule-remove"> <f:param name="rule" value="#{rule}"/> </e:load>
Innerhalb des Scopes:
<h:commandButton value="#{messages['remove']}" action="#{ruleEditController.remove}"> <f:ajax/> <e:unload/> </h:commandButton>
Der Controller dazu:
@Component @Scope(WebApplicationContext.SCOPE_REQUEST) public class RuleEditController { @Value("#{scope.rule}") private Rule rule; @Transactional public void remove() { ... } }
Erklärung: Damit ist es möglich, eine Entity- oder andere Modell-Bean innerhalb eines Scopes zu bearbeiten, dessen Lebenszyklus an ein AJAX-Popup gekoppelt ist. Es ist keine weitere Java-Bean erforderlich, die in einem Scope abgelegt werden muss. Alle parameterisierten Beans werden beim Beenden (Unload) des Scopes freigegeben.
Hinweis: Der Controller ist mit der Annotation @Scope(WebApplicationContext.SCOPE_REQUEST), damit die Injection im richtigen Scope erfolgt. Der Scope kann sich mit jedem Request ändern.
@Component @Scope(Scopes.CUSTOM_SCOPE) public class Popup { ... }
Oder im View-Scope:
@Component @Scope(Scopes.VIEW_SCOPE) public class Popup { ... }
<bean class="com.intersult.jsf.spring.EventBeanProcessor"/>
@Component @Scope(WebApplicationContext.SCOPE_REQUEST) public class QueryController { @Transactional @Listener(Rule.EVENT_CHANGED) public void execute() { ... }
Hinweis: Es ist darauf zu achten, dass betreffende Spring-Beans mit einem entsprechenden Scope versehen werden, wie zum Beispiel @Scope(WebApplicationContext.SCOPE_REQUEST). Die Events können nicht während der Startphase der Applikation gebunden werden, da hier JSF noch nicht initialisiert ist.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" ... <h:head id="head"> ... </h:head> <h:body id="body"> ... <h:outputStylesheet name="primefaces.css" library="css"/> <h:outputScript name="primefaces.js" library="ext-js"/> </h:body> </html>
Danach erscheinen alle <h:commandButton> und andere Buttons im Primefaces Style.
Mit JSF Ext kann das Ganze so aussehen:
Die JSF-Validators werden direkt im Frontend angebracht, also in den XHTML-Dateien. Damit können Felder unterschiedlich validiert werden und Situations abhängige Logik implementiert werden. Zum Beispiel kann gesprüft werden, ob zwei Felder gleich sein müssen. Eine Sonderrolle spielt das Attribut required bei vielen UIInput-Elementen, mit dem eine Eingabe erzwungen werden kann.
Bean-Validators werden in Application Support behandelt.
<h:outputLabel for="name" value="Name"/> <h:inputText id="name" value="#{userEdit.user.name}" required="true" validator="intersult.Unique"/>
Erklärung: Der Unique-Validator holt sich die Entity-Klasse und Property über die EL-Expression für das Attribut "value" und prüft ob dieser bereits in der Datenbank vorhanden ist. Falls die Bean bereits persistent ist, wird der betreffende Wert außgenommen, sodass die Bean auch mit demselben Wert für das Property geupdatet werden kann.
Dabei kann man wählen, welche Teile man benutzen möchte. Ganz bequem geht es mit dem Tag <ext:input>. Damit gewrappte Eingabe-Steuerelemente werden automatisch AJAX-Validiert und mit Faces Messages versehen.
<h:form id="some-form"> <h:outputLabel for="name" value="Name"/> <ext:input id="name"> <h:inputText id="name" value="#{userEdit.user.name}" required="true"/> </ext:input> </h:form>
Der Input-Wrapper-Tag ist eine Composite-Component, die man so verwenden kann wie sie ist. Oder man benutzt den Code als Vorlage für ein eigenes Projekt-Layout:
<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:cc="http://java.sun.com/jsf/composite" xmlns:e="http://java.sun.com/jsf/ext" xmlns:ext="http://java.sun.com/jsf/composite/ext" <cc:interface> <cc:attribute name="inputId" default=":#{cc.children[0].clientId}"/> <cc:attribute name="styleClass"/> <cc:attribute name="style"/> </cc:interface> <cc:implementation> <h:panelGrid cellpadding="0" cellspacing="0" style="#{cc.attrs.style}" styleClass="#{cc.attrs.styleClass}"> <e:insert component="#{cc.children[0]}"> <e:ajax event="change"/> </e:insert> <ext:message id="message" for="#{cc.attrs.inputId}"/> </h:panelGrid> </cc:implementation> </html>
Der entsprechende Messages-Tag:
<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:cc="http://java.sun.com/jsf/composite" xmlns:e="http://java.sun.com/jsf/ext" xmlns:ext="http://java.sun.com/jsf/composite/ext" xmlns:p="http://primefaces.org/ui" > <cc:interface> <cc:attribute name="for"/> </cc:interface> <cc:implementation> <h:outputStylesheet name="faces-messages.css" library="css"/> <h:message id="message" for="#{cc.attrs.for}" infoClass="msg-info" warnClass="msg-warn" errorClass="msg-error"> <e:render event="#{cc.attrs.for}"/> </h:message> </cc:implementation> </html>
Die Styles befinden sich im JSF Ext und werden automatisch in den Header geladen.
<h:selectOneMenu id="start" value="#{workflowEdit.workflow.start}" converter="intersult.SelectItemConverter"> <f:selectItem itemLabel="#{messages['select']}" noSelectionOption="true"/> <f:selectItems value="#{transitionList.stateList}" var="state" itemLabel="#{state.name}" itemValue="#{state}"/> </h:selectOneMenu>
Erklärung: Der SelectItemConverter nummeriert die SelectItems durch, nutzt dann hashCode- und equals-Methoden um diese wiederzufinden. Bei den Elementen <f:selectItem> kann nun für itemValue die vollwertige Java-Bean angegeben werden, die auch beim Element <h:selectOneMenu> bei value verwendet wird.
Bemerkung: Da für den Browser jedes Element eine Nummer bekommt und nur diese an den Server zurückgeschickt wird, kann es beim zwischenzeitlichen Verändern der Liste zur Auswahl es falschen Elements kommen. Im Regelfall befindet sich die korrekte Liste noch im Component-Tree, sodass der Fehler nicht auftritt.