Hier werden die Scopes des Produkts JSF Ext beschrieben.

Inhalt#

Übersicht#

Jeder der mit Web-Seiten zu tun hat, insbesonder mit JSF, hat schon mit viel Enthusiasmus viele Features, Popups und Steuerelemente in eine Seite eingebaut. Hinterher war die Seite dann so langsam, dass sie eigentlich nicht mehr benutzbar war, hohe CPU-Last und Speicherverbrauch auf dem Server verursacht hat.

Scopes nehmen sich genau diesem Thema an, Komponenten können in einen Scope verpackt werden, und zwar komplett. Das heißt XHTML-Dateien, JSF-Komponentenbäume und Managed-Beans. Teile einer Seite können durch AJAX bei Bedarf geladen und auch wieder entladen werden. Damit kombinieren Sie komplexe Funktionalität auf einer Seite mit Leichtgewichtigkeit und Schnelligkeit. Zusätzlicher Vorteil: Komponenten die in Scopes liegen, können auf jeder Seite wieder verwendet werden - sogar mehrfach gleichzeitig.

Im Detail sind Scopes abgetrennte Bereiche auf einer JSF-Seite, mit eigenen Variablen und einer XHTML-View. Beim Laden können Parameter durch <f:param> übergeben werden, diese sind dann über EL-Expressions durch #{scope.<...>} und Bean-Injections @ScopeValue zugreifbar.

Grundsätzlich gibt es zwei verschiedene Arten von Scopes: Scopes mit dem Tag <e:scope> (gebundene Scopes) und Scopes ohne diesen Tag (freie Scopes).

Gebundener ScopeFreier Scope
View-IdDie View ist direkt über den Tag zugeordnet.Die View wird erst beim erzeugen durch den Load-Tag festgelegt.
AnzahlDer Scope ist durch den Tag festgelegt und kann nur geladen (load) und entladen (unload) werden.Der Scope kann beliebig oft durch einen Load-Tag instantiiert werden. Dadurch sind unabhängige und mehrfach verwendbare Popups realisierbar.
Scope-IdDer Load-Tag referenziert den Scope direkt über seine Scope-Id.Dem Scope kann über den Load-Tag eine Id zugewiesen werden, wiederholte Load-Actions greifen damit auf denselben Scope zu. Wird keine Scope-Id zugewiesen, erzeugt jeder Load-Tag einen neuen Scope und damit ein neues Element im Browser.
UnloadDer Scope kann sowohl von innen durch einen Unload-Tag entladen werden, als auch von außen durch Angabe der Scope-Id.Der Scope kann nur entladen werden, wenn eine Scope-Id angegeben wurde. Ohne Scope-Id kann der Scope von innen, durch einen Unload-Tag ohne Scope-Id entladen werden.
PositionDie View wird dort eingefügt, wo der Tag platziert wurde. Damit ist genau steuerbar, an welcher Stelle der HTML-Code gerendert wird.Die View wird im HTML-Body eingefügt. Es ist nicht weiter steuerbar, an welcher Stelle die View gerendert wird.
AnwendungszweckHauptsächlich Bildschirmelemente die an verschiedenen Positionen einer Applikation eingefügt werden sollen und dahinter liegende Scoped-Beans besitzen.Vor allem Popups, die Details einer Information anzeigen und/oder mehrfach abgebildet werden sollen.

Scope erzeugen#

In der Regel können freie Scopes verwendet werden, wenn mit Popups, frei beweglichen oder statisch platzierten Elementen gearbeitet wird.
<h:commandButton value="Test">
    <f:ajax/>
    <e:load viewId="/dialog/test.xhtml"/>
</h:commandButton>

Erklärung: Der Command-Button läd einen neuen Scope mit der View /dialog/test.xhtml. Die View wird angezeigt und hat Zugriff auf Custom-Scoped Variablen.

Hinweis: Freie Scopes werden unterhalb des Body-Tags in den Component-Tree eingefügt. Dies hat den Vorteil, dass kein zusätzlicher Tag <e:scope> auf der Seiten eingefügt werden braucht.

Gebundenen Scope erzeugen#

In Fällen, in denen ein Scope an eine bestimmte Stelle im Component-Tree platziert wird, kann der Scope mit dem Tag <e:scope> platziert werden. Der Scope hat damit bereits eine eindeutige Id bekommen und kann nur noch einmal auf der Seite vorhanden sein:

Ein direkter Scope beginnt mit einem Scope-Tag:

<e:scope id="scope-id">
    <h:outputText value="#{scope.id}"/>
</e:scope>

Scope mit Facelet-View#

Die Scopes sind eines der mächtigsten Features im JSF Ext.

Ein Scope wird zunächst im XHTML definiert:

<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.

Scope laden#

Zum passenden Zeitpunkt soll der Scope natürlich geladen werden, das heißt das mit viewId verwiesene Facelet wird tatsächlich in den Component-Tree eingefügt und per AJAX an den Client übertragen. Dies geschieht mit:
<e:load viewId="/dialog/test.xhtml"/>

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 viewId="/dialog/test.xhtml"/>
</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 viewId="/dialog/test.xhtml">
		<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 viewId="/dialog/user-password.xhtml"/>
	<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:evaluate 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 viewId="/dialog/createUser.xhtml" 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.

Für seltene Fälle unterstützt der Load-Tag auch eine Unload-Action. Die Managed-Beans können über eine @PreDestroy-Annotation feststellen, wenn der Scope beendet wird und dort den entsprechenden Code ausführen. Bei kleinen Scopes mächte man in einigen Fällen keinen eigenen Controller, sodass diese Methode verwendet werden kann.

<e:load viewId="/dialog/createUser.xhtml" action="#{scope.projectScope.addCascade(scope)}"
        unloadAction="#{scope.projectScope.removeCascade(scope)}">
    <f:param name="projectScope" value="#{projectScope}"/>
</e:load>

Erklärung: Dem Scope "projectScope" wird ein Fenster "Create User" aufgeschaltet. Mit der action wird eine Cascade installiert, die beim Schließen des Project Scope den neuen Create User Scope ebenfalls schließt. Die Unload Action entfernt diese Cascade wieder.

Ein Scope kann auch aus einem JAR geladen werden, wenn er in META-INF/resources liegt:

<e:load viewId="scopes.xhtml" library="debug"/>

Scopes entladen#

Neben dem Tag <e:unload> können Scopes auch mit #{scopes.unload(id)} entladen werden und innerhalb des Scopes selbst mit #{scope.unload}. Dies ist praktisch, um zum Beispiel ohne Formular einen Close-Butten auf einem Popup zu platzieren:
    <p:dialog id="dialog" header="#{messages['rule.edit']}" visible="true">
        <f:ajax event="close" listener="#{scope.unload}"/>
        <h1>Content</h1>
    </p:dialog>

Java-Code#

Wird innerhalb des XHTML eine Java-Action aus einem Scope aufgerufen, so ist der Scope zugreifbar durch Scopes.getScope()

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.

Spring#

Der Scope kann auch zusammen mit Spring-Scopes verwendet werden, dabei ist die Konfiguration weiter unten in diesem Dokument zu beachten.
@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.

Verschachtelte Scopes#

Der Code demonstriert das Iterieren und Schachteln von Scopes:
	<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.

Component Injection#

Gelegentlich kann der Wunsch bestehen, einzelne Elemente in eine Scope-Komponente zu injecten. Dieses Design-Pattern kann durch die den Tag <e:insert> erreicht werden, auch im Zusammenhang mit <c:forEach> bzw. <e:for>.

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.

Recovering#

In JSF führen Exceptions in der Regel zu einem Fehler auf einer Seite, die Seite kann nicht dargestellt werden. Oft wird auf eine Fehlerseite umgeleitet. Bei komplexen Seiten, etwa bei vielen Popups, ist dieses Verhalten nicht befriedigend, da erhebliche Daten verloren gehen können.

Scopes unterstützen daher Recovering, Standardverhalten ist den betreffenden Scopes zu schließen, wenn darin eine Exception ausgelöst wurde. Zusätzlich enthält jeder Scope ein Attribut, an welches eine Method-Expression gebunden werden kann mit der Signatur boolean recover(java.lang.Throwable). Damit kann eine zusätzliche Methode angegeben werden, um ein Problem in diesem Scope zu lösen. Wurde das Problem gelöst, kann true zurückgegeben werden um den Standard-Recovery zu unterdrücken.

Scope Behaviors#

Die Scope-Komponente kennt folgende Behaviors:
  • onload: JavaScript Code der beim Laden des Scopes ausgeführt wird.
  • onunload: JavaScript Code der beim Schließen des Scopes ausgeführt wird.
<e:scope id="test" recover="testBean.recover">
    ...
</e:scope>

Onunload Tag#

Manchmal ist es geschickter, Code für den Unload eines Scopes in den entsprechenden Components zu platzieren. Das Cleanup von DOM-Teilen bleibt also zusammen bei der betreffenden Komponente. Dazu gibt es den Tag <e:onunload>:
<e:onunload script="cleanUpSomeElements();"/>

Scope Hierarchie#

Ein Scope kann direkt aus der Page geöffnet werden oder aus einem anderen Scope heraus. Im letzten Fall wird im neuen Scope der Parent Scope eingetragen und dem Parent Scope eine Cascade-Anweisung.

Cascading#

Wird der Parent Scope geschlossen, werden automatisch alle daraus geöffneten Scopes mit geschlossen. Cascading kann unterbunden werden, wenn bei beim Load-Tag cascade="false" angegeben wird:
<e:load viewId="/dialog/test.xhtml" cascade="false"/>

Programmatisches Cascading#

Scopes können auch manuell verlinkt werden, indem die Methode scope.addCascade(Scope scope) aufgerufen wird. Das Cascading kann durch scope.removeCascade(Scope scope) wieder entfernt werden. Dies ist dann interessant, wenn Popup-Fenster über einen Umweg auf andere aufgeschaltet werden.

Die Scope-Liste gebraucht diesen Mechanismus:

<e:load scopeId="edit-#{element.id}" viewId="/dialog/object.xhtml"
        action="#{scope.scope.addCascade(scope)}"
        unloadAction="#{scope.scope.removeCascade(scope)}">
    <f:param name="scope" value="#{element}"/>
    <f:param name="name" value="#{element.id}"/>
    <f:param name="object" value="#{element}"/>
</e:load>

Debugging#

Wie in der Session oder anderen Scopes, können im Custom-Scope viele Werte liegen. Zu Debugging-Zwecken kann es interessant sein, in die Scopes zu sehen. Das Scope-Debugging ist mit Primefaces implementiert. Das heißt, wenn sie die Scope-Debug-Views verwenden wollen, brauchen sie Primefaces im Projekt.

Der Einstieg erfolgt durch:

<e:load viewId="scopes.xhtml" library="debug"/>

Das sieht dann so aus:

JSF Ext/scope debug.PNG

Lifecycle#

Scopes sind Lifecycles unterworfen, da enthaltene Objekte wieder freigegeben werden müssen. Je nachdem target Lifecycle (Request, View, Page, Session oder Application), wird ein Scope nach unterschiedlich langer Lebenszeit wieder entfernt. Verantwortlich dafür sind sogenannte Destruction-Callbacks. Diese werden vom Scope-Manager aufgerufen, bevor ein Scope entfernt wird. Der Scope enthält dadurch die Möglichkeit, Ressourcen frei zu geben.

Veraltet: ExtRequestContextListener#

Die Klasse wird nicht mehr benötigt. In der web.xml sollte wieder der Standard Spring-Listener org.springframework.web.context.request.RequestContextListener eingetragen werden.

Lifecycle#

Scopes können an unterschiedliche Lifecycles gebunden werden. Dies wird durch das Load-Attribut "lifecycle" festgelegt. Folgende Werte stehen zur Verfügung:
  • Request: Der Scope wird nach Beenden des Requests wieder entfernt. Wird selten benutzt, da mit Request-Annotierte Controller ohnehin nach einem Request wieder abgebaut werden. Ein Anwendungszweck ist, mehrere identische Components voneinander zu isolieren.
  • View: Der Scope ist nur innerhalb des View-Context verfügbar. Der Scope ist also nach Neuladen der Web-Page nicht mehr verfügbar. In der Regel ist erwünscht, dass Popups und andere dynamische Scope-Elemente einen Refresh überleben, dafür ist der Page-Lifecycle verwendbar. Der View-Lifecycle kann verwendet werden für parameterisierte HTTP-GET-Pages.
  • Page: Dies ist der Default-Lifecycle. Der Scope ist auf die Page mit der View-Id gebunden. Neuladen der Page hat keine Auswirkungen auf den Scope. Popups und andere Scope-Elemente bleiben bei Browser-Refresh erhalten.
  • Session: Der Lifecycle erstreckt sich auf die gesamte User-Session. Dieser Lifecycle kann verwendet werden, um Popups und andere Elemente auf jeder Seite eines eingeloggten Benutzers anzuzeigen. Dies können beispielsweise Chat- oder Informations-Elemente sein.
  • Application: Dieser Lifecycle geht über die gesamten eingeloggten bzw. anonym verbundenen Benutzersessions. Damit können Daten über die gesamte Applikation genutzt werden, jedem Nutzer wird dasselbe Element angezeigt. Es ist geplant, dass dieser Lifecycle noch weiter ausgebaut wird, insbesondere für AJAX-Push Anwendungen.