5 Die eigene JSF-Komponente
Seinem Ruf als Komponentenframework wird JavaServer Faces mehr als gerecht: Komponenten sind ein essentieller Bestandteil von JSF und bilden einen zentralen Erweiterungspunkt. Es gibt wohl bei keinem anderen Webframework so viele Möglichkeiten zur Erweiterung und zum Erstellen von eigenen Komponenten, die harmonisch mit dem Framework interagieren. Die Standardkomponenten und diverse Erweiterungen und Komponentenbibliotheken aus dem JSF-Umfeld decken bereits ein beachtliches Funktionsspektrum ab. Die meisten Entwickler werden aber früher oder später auf Anwendungsfälle stoßen, die damit nicht realisierbar sind. Spätestens dann macht eine eigene Komponente Sinn.Vor Version 2.0 von JSF war die Komponentenentwicklung jedoch relativ aufwendig. Es galt immer abzuwägen, ob sich dieser Schritt im konkreten Fall auszahlt oder sich das Problem nicht auch anderweitig lösen lässt. JSF 2.0 macht in diesem Bereich einen großen Schritt auf die Entwickler zu. Mit den neuen Kompositkomponenten (Composite-Components) existiert eine wirklich sehr einfache Möglichkeit, Komponenten deklarativ zu erstellen - ohne eine Zeile Java-Code oder XML-Konfiguration zu schreiben. Wie das funktioniert, zeigen wir Ihnen in Abschnitt [Sektion: Kompositkomponenten] .
So wichtig die Kompositkomponenten auch sind, sie können nicht jedes Problem lösen. Speziell komplexere Aufgaben lassen sich weiterhin nur mit den höchstflexiblen klassischen Komponenten realisieren. Dass aber auch deren Erstellung kein Hexenwerk ist, zeigen wir Ihnen in Abschnitt [Sektion: Klassische Komponenten] . Mit JSF 2.0 und Facelets ist es im Vergleich sogar noch etwas einfacher geworden.
In Abschnitt [Sektion: Kompositkomponenten und klassischen Komponenten kombinieren] gehen wir noch einen Schritt weiter und kombinieren Kompositkomponenten mit klassischen Komponenten. Mit diesem Ansatz lassen sich Kompositkomponenten bei Bedarf sehr einfach mit Java-Code erweitern. Sie werden sehen, dass die beiden Konzepte perfekt miteinander harmonieren und die Entwicklung von eigenen Komponenten damit noch flexibler wird.
In Abschnitt [Sektion: Alternativen zur eigenen Komponente] zeigen wir Ihnen, wie Sie einzelne Teile einer existierenden Komponente ersetzen: Es muss nicht immer eine komplette Komponente sein. Anschließend dreht sich in Abschnitt [Sektion: Überschreiben von JSF-Kernklassen] alles um das Ersetzen von JSF-Kernklassen. Das Beispiel MyGourmet 13 fasst alle im Laufe des Kapitels gemachten Änderungen zusammen und wird in Abschnitt [Sektion: MyGourmet 13: Komponenten und Services] behandelt.
5.1 Kompositkomponenten
Kompositkomponenten sind aus unserer Sicht eines der wichtigsten Features von JSF 2.0. Entwickler erhalten durch die Verbindung von Facelets und Ressourcen die Möglichkeit, Komponenten aus beinahe beliebigen Seitenfragmenten aufzubauen - daher auch der Name. Eine Kompositkomponente ist im Grunde nichts anderes als ein XHTML-Dokument, das in einer Ressourcenbibliothek abgelegt ist und die Komponente deklariert.Die Definition des Namensraums und des Tags der Komponente ergibt sich per Konvention aus der Ressourcenbibliothek und dem Namen des XHTML-Dokuments. Als erstes Beispiel wandeln wir die Box in der Seitenleiste in eine Kompositkomponente mit dem Namen panelBox um. Da alle im Laufe dieses Abschnitts erstellten Komponenten in der Bibliothek mygourmet landen, legen wir zuerst dieses Verzeichnis unter /resources an. Darin erstellen wir dann die Deklaration der Komponente mit dem Namen panelBox.xhtml . Abbildung Ressource der Kompositkomponente panelBox zeigt den Verzeichnisbaum. Ausführlichere Informationen zum Thema Ressourcen finden Sie in Abschnitt Sektion: Verwaltung von Ressourcen .
Sobald panelBox.xhtml in der Bibliothek mygourmet existiert, ist die Komponente einsatzfähig. Das Einbinden erfolgt wie bei allen Komponenten über den Namensraum und das Tag. Per Konvention leitet sich der Namensraum von Kompositkomponenten aus dem Präfix http://java.sun.com/jsf/composite/ gefolgt vom Bibliotheksnamen - in unserem Fall mygourmet - ab. Das Tag erhält seinen Namen von der Deklaration und lautet panelBox . Listing Einbinden einer Kompositkomponente zeigt, wie die Komponente in einer Seitendeklaration zum Einsatz kommt. Das gewählte Präfix mc ist wie immer beliebig, bietet sich aber als Abkürzung von MyGourmet-Components an.
<html xmlns:mc="http://java.sun.com/jsf/composite/mygourmet">
...
<mc:panelBox title="Panel-Header">
<h:outputText value="Ein Text"/>
</mc:panelBox>
...
</html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite">
<head><title>A panel box component</title></head>
<body>
<cc:interface>
<cc:attribute name="title"/>
</cc:interface>
<cc:implementation>
<div class="side_box">
<p class="header">#{cc.attrs.title}</p>
<cc:insertChildren/>
</div>
</cc:implementation>
</body>
</html>
Der Bereich innerhalb von cc:interface definiert die Schnittstelle der Komponente nach außen. Bei unserer Box fällt dieser Bereich relativ klein aus und umfasst nur ein Attribut mit dem Namen title . Der Wert dieses Attributs wird über das Tag der Komponente gesetzt, wie bereits Listing Einbinden einer Kompositkomponente gezeigt hat. Wie dieses Attribut innerhalb der Komponente verwendet wird, zeigen wir gleich.
Im Bereich cc:implementation folgt die Implementierung der Komponente, die aus einem beliebigen Mix aus JSF-Tags, HTML-Tags und Tags der Composite-Tag-Bibliothek zusammengesetzt sein kann. Unsere Box besteht aus einem div -Element, in dem als erster Absatz der Titel ausgegeben wird. Da der Titel ein Attribut der Komponente ist, müssen wir irgendwie auf dessen Wert zugreifen. Dazu führt JSF 2.0 das implizite Objekt cc als Referenz auf die aktuelle Kompositkomponente ein. Der Zugriff auf das Attribut erfolgt mit der Value-Expression#{cc.attrs.title} , wobei die Eigenschaft attrs eine Map aller Attribute zurückliefert. Das Tag cc:insertChildren veranlasst JSF dazu, sämtliche Inhalte einzufügen, die beim Einsatz der Komponente innerhalb des Tags angegeben werden. Im Beispiel aus Listing Einbinden einer Kompositkomponente ist das die h:outputText -Komponente mit dem Wert Ein Text .
Bleibt noch zu klären, wie JSF mit Kompositkomponenten in einer Seitendeklaration umgeht. Listing Seitenleiste in MyGourmet mit panelBox zeigt, wie der Inhalt der Seitenleiste aus MyGourmet mit der gerade vorgestellten Komponente panelBox aussieht.
<mc:panelBox id="menu" title="#{msgs.menu_title}">
<h:panelGrid columns="1">
<h:link outcome="providerList"
value="#{msgs.menu_provider_list}"/>
<h:link outcome="showCustomer"
value="#{msgs.menu_show_customer}"/>
</h:panelGrid>
</mc:panelBox>
<mc:panelBox id="news" title="#{msgs.news_title}">
<p>MyGourmet - jetzt mit Facelets und Templating</p>
</mc:panelBox>
Was heißt das konkret für unser Beispiel? Nach dem Erstellen des Wurzelknotens setzt Facelets die Verarbeitung mit panelBox.xhtml fort. Nachdem das div -Element und der Absatz mit dem Titel verarbeitet wurden, kommt das Tag cc:insertChildren an die Reihe. Wie Sie in Listing Seitenleiste in MyGourmet mit panelBox sehen, haben beide panelBox -Tags Inhalte, die an dieser Stelle in den Komponentenbaum aufgenommen werden.
Nach dieser kurzen Einführung folgen in Abschnitt [Sektion: Aufbau einer Kompositkomponente] einige Details zum Aufbau von Kompositkomponenten. In Abschnitt [Sektion: Die Komponente mc:panelBox] finden Sie dann die vollständige Version der Komponente panelBox mit einigen Erweiterungen bezüglich Flexibilität und Styling.
5.1.1 Aufbau einer Kompositkomponente
In diesem Abschnitt werden wir etwas genauer auf die Deklaration von Kompositkomponenten und die einzelnen Tags der Composite-Tag-Bibliothek eingehen.5.1.1.1 Der Bereich cc:interface
Der Bereich cc:interface definiert die Schnittstelle der Komponente nach außen und umfasst alle Merkmale, die für Benutzer der fertigen Komponente relevant sind. Neben der Definition von Attributen und Facets ist es auch möglich, das Verhalten einzelner interner Komponenten nach außen bekanntzugeben.Hier eine Liste aller Tags, die in der Schnittstelle einer Kompositkomponente verwendet werden können:
- cc:attribute
Dieses Tag definiert ein Attribut der Kompositkomponente. - cc:facet
Dieses Tag definiert ein Facet der Kompositkomponente. - cc:valueHolder
Dieses Tag definiert einen Namen, unter dem eine interne Komponente verfügbar ist, die ValueHolder implementiert. - cc:editableValueHolder
Dieses Tag definiert einen Namen, unter dem eine interne Komponente verfügbar ist, die EditableValueHolder implementiert. - cc:actionSource
Dieses Tag definiert einen Namen, unter dem eine interne Komponente verfügbar ist, die ActionSource2 implementiert.
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:cc="http://java.sun.com/jsf/composite">
<head><title>formButtons</title></head><body>
<cc:interface>
<cc:attribute name="saveLabel"
required="false" default="Save"/>
<cc:attribute name="cancelLabel" default="Cancel"/>
<cc:attribute name="action" required="true"
targets="save" method-signature="java.lang.String f()"/>
<cc:attribute name="cancelAction"
method-signature="java.lang.String f()"/>
<cc:actionSource name="save"/>
<cc:actionSource name="cancel"/>
<cc:actionSource name="all" targets="save cancel"/>
</cc:interface>
<cc:implementation>
<h:commandButton id="save" value="#{cc.attrs.saveLabel}"/>
<h:commandButton id="cancel" immediate="true"
value="#{cc.attrs.cancelLabel}"
action="#{cc.attrs.cancelAction}"/>
</cc:implementation>
</body></html>
Attribute erlauben auch das Setzen von Method-Expressions für Action-Methoden oder Event-Listener. Dazu muss die Signatur der Methode im Attribut method-signature angegeben werden, wobei es wichtig ist, immer voll qualifizierte Klassennamen für den Rückgabewert und die Parameter zu verwenden. Die Attribute action und cancelAction des Beispiels definieren jeweils die Signatur einer Action-Methode und sehen auf den ersten Blick sehr ähnlich aus. Sie unterscheiden sich aber in der Art der Verbindung zur internen Komponente im Implementierungsteil. cancelAction wird direkt über den Ausdruck #{cc.attrs.cancelAction} referenziert, wohingegen action über das Attribut targets an die Komponente mit dem Bezeichner save gebunden wird.
Dabei nutzen wir aus, dass JSF spezielle Attribute mit Method-Expressions wie action , actionListener , valueChangeListener und validator automatisch mit internen Komponenten verlinkt, deren IDs im Attribut targets angegeben sind. Der Wert von targets muss nicht auf eine ID beschränkt sein, sondern kann auch eine Liste von IDs enthalten, die durch Leerzeichen separiert sind.
An dieser Stelle möchten wir Sie noch darauf hinweisen, dass Facelets alle Attribute in der Kompositkomponente ablegt, die der Benutzer des Tags angibt - auch wenn sie nicht mit cc:attribute definiert sind. Wir raten Ihnen aber dazu, alle Attribute zu definieren. Damit geben Sie Benutzern der Kompositkomponente einen klaren Kontrakt über die Schnittstelle und das Verhalten in die Hand.
Mit den Tags cc:actionSource , cc:valueHolder und cc:editableValueHolder können interne Komponenten, die ein spezielles Verhaltens-Interface implementieren (siehe Abschnitt Sektion: Verhaltens-Interfaces für Details) nach außen bekannt gegeben werden. Damit bekommen Benutzer der Kompositkomponente die Möglichkeit, Objekte wie Event-Listener, Konverter oder Validatoren an diese Komponente zu binden. Unser Beispiel definiert drei verschiedene Action-Sourcen mit den Namen save , cancel und all . Die ersten beiden sind mit jeweils einer Steuerkomponente verbunden, deren ID dem Namen entspricht. Die dritte ist über das Attribut targets mit beiden Komponenten verbunden.
Bleibt noch zu klären, wie Benutzer von Kompositkomponenten Listener für Action-Sourcen definieren können. In JSF 2.0 hat das Tag f:actionListener zu diesem Zweck das Attribut for bekommen, in dem einfach der Name einer Action-Source angegeben wird. Listing Kompositkomponente formButtonsim Einsatz zeigt ein Beispiel für den Einsatz der Komponente formButtons mit drei Listenern.
<mc:formButtons action="#{customerBean.save}"
cancelAction="#{customerBean.cancel}">
<f:actionListener for="save"
binding="#{customerBean.saveListener}"/>
<f:actionListener for="cancel"
binding="#{customerBean.cancelListener}"/>
<f:actionListener for="all"
binding="#{customerBean.allListener}"/>
</mc:formButtons>
Genau wie das Tag f:actionListener verfügen auch das Tag f:valueChangeListener sowie alle Konverter- und Validator-Tags in der Core-Tag-Library ab JSF 2.0 über das Attribut for .
5.1.1.2 Der Bereich cc:implementation
Der Bereich cc:implementation enthält alle JSF-Tags, HTML-Elemente und anderweitigen Inhalte, aus denen die Kompositkomponente aufgebaut ist.Im Implementierungsteil gibt es mehrere Möglichkeiten, Inhalte einzufügen, die ein Benutzer der Kompositkomponente innerhalb der Komponenten-Tags angegeben hat. Folgende Tags der Composite-Tag-Bibliothek sind dafür relevant:
- cc:insertChildren
Dieses Tag veranlasst JSF dazu, beim Aufbau des Komponentenbaums den Inhalt des Tags aus der Seitendeklaration des Benutzers zu übernehmen. - cc:renderFacet
Dieses Tag veranlasst JSF dazu, den Inhalt des Facets mit dem Namen name in den Komponentenbaum einzufügen. Das Facet muss ein Kind der Kompositkomponente sein. - cc:insertFacet
Dieses Tag veranlasst JSF dazu, den Inhalt des Facets mit dem Namen name als Facet zu einer anderen Komponente hinzuzufügen. Das Facet muss ein Kind der Kompositkomponente sein.
Auf den praktischen Aspekt des Implementierungsteils werden wir an dieser Stelle nicht weiter eingehen. Was aber nicht heißen soll, dass wir ihn vernachlässigen. Wir möchten Sie für weiterführende Beispiele nur auf die nächsten Abschnitte verweisen, in denen wir einige Kompositkomponenten präsentieren.
5.1.2 Die Komponente mc:panelBox
Zu Beginn dieses Abschnitts haben wir bereits eine einfache Version der Kompositkomponente panelBox gezeigt, die wir noch um einige Aspekte erweitern werden. Listing Finale Version der Kompositkomponente panelBox zeigt den kompletten Sourcecode der Komponente.<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:cc="http://java.sun.com/jsf/composite">
<head><title>panelBox</title></head><body>
<cc:interface>
<cc:attribute name="styleClass"
required="false" default="box"/>
<cc:attribute name="style" required="false"/>
<cc:attribute name="headerClass"
required="false" default="box-header"/>
<cc:facet name="header" required="false"/>
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="mygourmet"
name="components.css"/>
<div class="#{cc.attrs.styleClass}"
style="#{cc.attrs.style}">
<c:if test="#{!empty cc.facets.header}">
<p class="#{cc.attrs.headerClass}">
<cc:renderFacet name="header"/>
</p>
</c:if>
<cc:insertChildren/>
</div>
</cc:implementation>
</body></html>
Der zweite interessante Aspekt dieser Kompositkomponente ist das Styling mit CSS. Alle Komponenten in der Bibliothek mygourmet sollen ein einheitliches Styling erhalten, das allerdings von außen geändert werden kann. Dazu nutzen wir einmal mehr die neuen Ressourcen von JSF 2.0. Nachdem wir uns mit der Komponente bereits in einer Bibliothek befinden, ist es ein Leichtes, dort zusätzlich das Stylesheet components.css unterzubringen. In der Komponente definieren wir mit dem Tag h:outputStylesheet eine Abhängigkeit darauf. Der Vorteil dieser Lösung ist, dass sich Benutzer der Komponente keine Gedanken darüber machen müssen. Wenn sie h:head und h:body verwenden, wird das Stylesheet automatisch in die Ansicht aufgenommen. Fürs Styling selbst kommen die Attribute styleClass und headerClass mit Defaultwerten zum Einsatz. So ist gewährleistet, dass die internen CSS-Klassen bei Bedarf mit benutzerdefinierten überschrieben werden können.
Die Deklaration der beiden panelBox -Komponenten in der Seitenleiste von MyGourmet 13 ist in Listing Kompositkomponente panelBox im Einsatz zu sehen. Abbildung Gerenderte Ausgabe von panelBox in MyGourmet 13 zeigt die gerenderte Ausgabe mit Default-Styling.
<mc:panelBox id="menu">
<f:facet name="header">
<h:outputText value="#{msgs.menu_title}"/>
</f:facet>
<h:panelGrid columns="1">
<h:link outcome="providerList"
value="#{msgs.menu_provider_list}"/>
<h:link outcome="showCustomer"
value="#{msgs.menu_show_customer}"/>
</h:panelGrid>
</mc:panelBox>
<mc:panelBox id="news">
<f:facet name="header">
<h:outputText value="#{msgs.news_title}"/>
</f:facet>
<p>MyGourmet - jetzt mit Facelets und Templating</p>
</mc:panelBox>
5.1.3 Die Komponente mc:dataTable
Die Komponente mc:dataTable zeigt, wie Kompositkomponenten die tägliche Arbeit mit JSF erleichtern können. mc:dataTable bietet keine neue Funktionalität im klassischen Sinn, sondern stellt eine Art Wrapper für h:dataTable dar. Damit ist es möglich, die Standardkomponente mit einem zentral definierten Default-Style zu erweitern. Die Deklaration der Komponente ist in Listing Kompositkomponente dataTable zu finden.<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:cc="http://java.sun.com/jsf/composite">
<head><title>dataTable</title></head><body>
<cc:interface>
<cc:attribute name="var"/>
<cc:attribute name="value"/>
<cc:facet name="header"/>
<cc:facet name="footer"/>
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="mygourmet"
name="components.css"/>
<h:dataTable id="table" value="#{cc.attrs.value}"
styleClass="mygourmet-table" headerClass=
"mygourmet-table-header" rowClasses=
"mygourmet-table-rownobg, mygourmet-table-rowbg"
columnClasses="mygourmet-table-cell">
<c:set target="#{component}" property="var"
value="#{cc.attrs.var}"/>
<cc:insertFacet name="header"/>
<cc:insertChildren/>
<cc:insertFacet name="footer"/>
</h:dataTable>
</cc:implementation>
</body></html>
Die CSS-Klassen werden bei mc:dataTable direkt gesetzt, könnten aber wie bei der Komponente panelBox auch mit Attributen, die über Defaultwerte verfügen, realisiert werden. Das Stylesheet components.css wird genau wie zuvor als Ressource eingebunden.
Der Implementierungsteil zeigt auch schön den Unterschied zwischen cc:renderFacet und cc:insertFacet . Im aktuellen Beispiel kommt cc:insertFacet zum Einsatz, um die Facets header und footer an h:dataTable weiterzureichen. Sie sollen ja nicht von der Kompositkomponente, sondern von h:dataTable gerendert werden. Zu guter Letzt bleibt noch das Tag compo-site:insertChildren übrig, mit dem die Kindelemente des Tags in der aufrufenden Deklaration an h:dataTable weitergegeben werden. mc:dataTable wird dadurch genau wie das h:dataTable -Tag verwendet, in dem der Inhalt mit h:column -Tags deklariert wird.
Abbildung Gerenderte Ausgabe von dataTable zeigt die gerenderte Ausgabe von mc:dataTable aus der Ansicht providerList.xhtml .
5.1.4 Die Komponente mc:collapsiblePanel
Die Kompositkomponente mc:collapsiblePanel rendert einen Bereich der Seite, der über eine Schaltfläche ein- und ausgeblendet werden kann. Diese Schaltfläche wird als Icon gerendert, das sich je nach Einklappzustand des Panels ändert. Direkt neben dem Icon wird der Inhalt des optionalen Facets header dargestellt. Listing Kompositkomponente collapsiblePanel zeigt die Deklaration der Komponente.<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:cc="http://java.sun.com/jsf/composite">
<head><title>collapsiblePanel</title></head><body>
<cc:interface>
<cc:attribute name="model" required="true">
<cc:attribute name="collapsed" required="true"/>
<cc:attribute name="toggle" required="true"
method-signature="java.lang.String f()"/>
</cc:attribute>
<cc:actionSource name="toggle"/>
<cc:facet name="header"/>
</cc:interface>
<cc:implementation>
<h:panelGroup layout="block"
styleClass="collapsiblePanel-header">
<h:commandButton id="toggle"
action="#{cc.attrs.model.toggle}"
styleClass="collapsiblePanel-img"
image="#{resource[cc.attrs.model.collapsed
? 'mygourmet:toggle-plus.png'
: 'mygourmet:toggle-minus.png']}"/>
<cc:renderFacet name="header"/>
</h:panelGroup>
<h:panelGroup layout="block"
rendered="#{!cc.attrs.model.collapsed}">
<cc:insertChildren/>
</h:panelGroup>
</cc:implementation>
</body></html>
Der Inhalt des Panels ist in eine h:panelGroup -Komponente eingebettet, mit deren rendered -Attribut das Ein- und Ausblenden realisiert wird. Der Wert dieses Attributs wird dazu mit dem Ausdruck #{!cc.attrs.model.collapsed} vom übergebenen Zustand abhängig gemacht. Beachten Sie hier bitte auch den Zugriff auf das geschachtelte Attribut der Kompositkomponente.
Die Schaltfläche zum Umschalten des Einklappzustands wird in Form von Bildern gerendert. Wie schon das Stylesheet aus den letzten Beispielen liegen die beiden Bilder auch direkt in der Ressourcenbibliothek. Sie werden im Attribut image der h:commandButton über das implizite Objekt resource eingebunden. Die Schaltfläche selbst ist unter dem Namen toggle als Action-Source nach außen verfügbar.
Listing Kompositkomponente collapsiblePanelim Einsatz zeigt ein einfaches Einsatzszenario der Komponente und in Abbildung Gerenderte Ausgabe von collapsiblePanel finden Sie die gerenderte Ausgabe beider Einklappzustände.
<mc:collapsiblePanel model="#{customerBean}">
<f:facet name="header"><h3>Information</h3></f:facet>
Diese Information ist klappbar.<br/>
Diese Information ist klappbar.
</mc:collapsiblePanel>
5.1.5 Die Komponente mc:inputSpinner
Die Kompositkomponente mc:inputSpinner rendert eine Eingabekomponente mit zwei Buttons, die über JavaScript den Zahlenwert des Eingabefelds erhöhen oder reduzieren. Die Schnittstelle der Komponente nach außen ist sehr übersichtlich. Sie umfasst die Attribute value für die Eingabekomponente und inc , das den Betrag definiert, der addiert beziehungsweise subtrahiert wird. Zusätzlich ist die Eingabekomponente unter dem Namen input verfügbar. Listing Kompositkomponente inputSpinner zeigt die Deklaration.<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:cc="http://java.sun.com/jsf/composite">
<head><title>inputSpinner</title></head><body>
<cc:interface>
<cc:attribute name="value" required="true"/>
<cc:attribute name="inc" default="1"/>
<cc:editableValueHolder name="input"/>
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="mygourmet"
name="components.css"/>
<h:outputScript library="mygourmet"
name="inputSpinner.js" target="head"/>
<h:panelGroup>
<h:inputText id="input" value="#{cc.attrs.value}"
styleClass="inputSpinner-input"/>
<h:panelGroup id="buttons"
styleClass="inputSpinner-buttons">
<h:graphicImage styleClass="inputSpinner-button"
name="spin-up.png" library="mygourmet"
onclick="return changeNumber(
'#{cc.clientId}:input', #{cc.attrs.inc});"/>
<h:graphicImage styleClass="inputSpinner-button"
name="spin-down.png" library="mygourmet"
onclick="return changeNumber(
'#{cc.clientId}:input', #{-cc.attrs.inc});"/>
</h:panelGroup>
</h:panelGroup>
</cc:implementation>
</body></html>
Das Erhöhen und Reduzieren des Werts der Eingabekomponente erfolgt in der Funktion changeNumber , die als Parameter die Client-ID der Eingabekomponente und den Wert zum Addieren bekommt. Durch das Übergeben der Client-ID ist es ohne Probleme möglich, mehrere input-Spinner -Komponenten in einer Ansicht zu verwenden. Listing JavaScript für inputSpinner zeigt die JavaScript-Funktion aus der Ressource inputSpinner.js .
function changeNumber(clientId, increment) {
var inc = Number(increment);
if (isNaN(inc) || inc == 0 ) inc = 1;
var input = document.getElementById(clientId);
var val = Number(input.value);
if (isNaN(val)) val = 0;
input.value = val + inc;
return false;
}
Die Komponente wird wie jede andere Eingabekomponente eingesetzt. In Abbildung Gerenderte Ausgabe von inputSpinner sehen Sie die gerenderte Ausgabe.
5.2 Klassische Komponenten
Nach der Einführung in das Thema Kompositkomponenten zeigen wir Ihnen in diesem Abschnitt, wie Sie mit JSF 2.0 eine klassische Komponente für den Einsatz mit Facelets erstellen. Auf den ersten Blick kann dieser Vorgang etwas kompliziert wirken, da er eine ganze Reihe von Schritten umfasst - vor allem auch wegen der vielen Webtechnologien, die als Grundlage für die JSF-Spezifikation selbst dienen. Aber keine Angst, wenn Sie Ihre erste eigene Komponente im Einsatz haben, werden Sie sehen, dass es im Grunde genommen ganz einfach ist.Eine Kurzanleitung für die Erstellung einer Komponente sieht folgendermaßen aus:
- Komponentenfamilie, Komponententyp und Renderertyp definieren
- Komponentenklasse schreiben
- Rendererklasse schreiben
- Komponentenklasse und Rendererklasse registrieren
- Tag-Definition schreiben und in Bibliothek aufnehmen
- Tag-Handler-Klasse schreiben
- Bibliothek in die Seite einbinden
5.2.1 Vorarbeiten: Komponentenfamilie, Komponententyp und Renderertyp definieren
Bevor wir mit dem Schreiben der Komponente beginnen, sollten wir uns darüber klar werden, ob und wie die Komponente von einer anderen Komponente ableitbar ist oder ob wir vollständig "von der grünen Wiese" weg beginnen müssen. Üblicherweise macht es Sinn, von einer bestehenden Basiskomponente abzuleiten.5.2.1.1 Die Wahl der Basisklasse
Kandidaten für die Basisklasse sind von UIComponent abgeleitete Klassen, im Standard also die folgenden Komponentenklassen:- UIOutput:
Komponente, die einen Wert darstellt (und keine Eingabe erlaubt). - UIInput:
Komponente, die die Eingabe eines Werts ermöglicht. - UISelectOne:
Komponente, die die Auswahl genau eines Werts aus einer Reihe von Werten ermöglicht. - UISelectMany:
Komponente, die die Auswahl mehrerer Werte aus einer Reihe von Werten ermöglicht. - UICommand:
Komponente, die eine Aktion ausführt. - UIPanel:
Komponente, die als Behälter für eine oder mehrere andere Komponenten dient. - UIMessage:
Komponente, die zur Anzeige einer Nachricht dient.
Wann ist das Ableiten von UIInput sinnvoll? Das ist dann angebracht, wenn die Komponente genau einen Wert aus den Geschäftsdaten nehmen und am Frontend präsentieren soll. Dinge wie Konvertierung, Validierung, Schreiben und Lesen des Werts aus und in die Geschäftsdaten werden mit der Standardimplementierung dieser Klasse bereits erledigt. Ist das Verhalten der Komponente noch spezieller, kann man eventuell von der Klasse HtmlInputText ableiten - diese Klasse und der zugehörige Renderer erledigen auch das Decodieren des Werts aus der HTTP-Anfrage, das Setzen des Werts als Submitted-Value und das Schreiben der Komponentenansicht in die HTTP-Antwort.
Wir sollten auch dann von der UIInput -Klasse ableiten, wenn wir die vom Benutzer ausgelöste Veränderung eines Komponentenattributs speichern müssen. Ein Beispiel für diese Anwendung ist die Komponente CollapsiblePanel von MyFaces, die ihren Einklappzustand im Attribut value führt und damit den Zustand über Aufrufe hinweg und vor allem bei der Verwendung in dataTable -Komponenten sichert.
Die UICommand -Klasse wird man als Basis für die eigene Komponente verwenden, wenn diese Komponente "Aktionen" auslöst. Die Behandlung von "Aktionsmethoden", Action-Listenern und das Einstellen von Ereignissen in die Event-Queue werden durch diese Basisklasse bereits erledigt.
Die Wahl der Basisklasse für unseren Input-Spinner fällt eindeutig aus. Nachdem es sich um ein "getuntes" Eingabefeld handelt, drängt sich UIInput auf.
5.2.1.2 Komponententyp, Komponentenfamilie und Renderertyp
Jede Komponente ist im System eindeutig über ihren Komponententyp identifiziert. Es darf keine andere Komponente mit demselben Komponententyp existieren. Üblicherweise wird der Komponententyp als statische Konstante definiert. Unsere Input-Spinner-Komponente, deren Komponentenklasse InputSpinner wir im nächsten Abschnitt erstellen, hat folgenden Komponententyp:public static final String COMPONENT_TYPE =
"at.irian.InputSpinner";
Beim Registrieren der Komponente im System wird ihr Komponententyp als Bezeichner
verwendet. Dieser kommt dann zum Beispiel bei der Definition des Tags der Komponente
in der Tag-Bibliothek zum Einsatz.Darüber hinaus können wir den Komponententyp übergeben, wenn wir eine Komponente erzeugen wollen und dazu nicht direkt den Konstruktor, sondern die Factory-Methode createComponent() der Ap-plication -Klasse aufrufen. Tatsächlich sollte man immer über diesen Weg gehen, damit zentral die Implementierung einer Komponente ausgetauscht werden kann. Hier das Beispiel dafür:
FacesContext fc = FacesContext.getCurrentInstance();
fc.getApplication().createComponent(
InputSpinner.COMPONENT_TYPE);
Der nächste Schritt ist die Definiton der
Komponentenfamilie - hier kann entweder eine bestehende Familie verwendet oder eine
neue Familie definiert werden. Die Familie einer Komponente wird mit einem Aufruf
von UIComponent.getFamily() bestimmt. Wenn Sie eine eigene Familie für Ihre
Komponente erstellen wollen, müssen Sie diese Methode überschreiben. In einer
Komponentenfamilie können mehrere Komponenten enthalten sein. Nachdem es sich bei
unserer Beispielkomponente um ein Eingabefeld handelt, lassen wir es bei der von
UIInput geerbten Familie javax.faces.Input .Die Komponentenfamilie wird gemeinsam mit dem Renderertyp verwendet, um einen Renderer auszuwählen. Würde hier der Komponententyp benutzt werden, könnte ein Renderer immer nur eine Art von Komponente rendern - durch die Definition der Komponentenfamilie kann für eine ganze Gruppe von Komponenten derselbe Renderer zum Tragen kommen. Der Renderertyp wird bei der Definition des Tags gemeinsam mit dem Komponententyp angegeben und beim Erzeugen der Komponente auf die Komponente gesetzt - damit wird der normalerweise im Komponentenkonstruktor gesetzte "Default"-Renderertyp überschrieben.
Im Folgenden sehen Sie einige Beispiele für die drei Einstellungen Komponententyp, Komponentenfamilie und Renderertyp für ausgewählte Standardkomponenten:
- javax.faces.component.html.HtmlCommandLink:
Die Komponente hinter dem Tag h:commandLink .
Komponententyp: javax.faces.HtmlCommandLink
Komponentenfamilie: javax.faces.Command
Renderertyp: javax.faces.Link - javax.faces.component.html.HtmlCommandButton:
Die Komponente hinter dem Tag h:commandButton .
Komponententyp: javax.faces.HtmlCommandButton
Komponentenfamilie: javax.faces.Command
Renderertyp: javax.faces.Button - javax.faces.component.html.HtmlInputText:
Die Komponente hinter dem Tag h:inputText .
Komponententyp: javax.faces.HtmlInputText
Komponentenfamilie: javax.faces.Input
Renderertyp: javax.faces.Text - javax.faces.component.UISelectItem:
Diese Komponente ist ein Spezialfall - sie hat keinen Renderer.
Komponententyp: javax.faces.SelectItem
Komponentenfamilie: javax.faces.SelectItem
Renderertyp: null
5.2.2 Komponentenklasse schreiben
Die Komponentenklasse ist das "Herzstück" der Komponentenarchitektur von JSF - die Instanz der Komponentenklasse wird im JSF-Komponentenbaum gespeichert und beinhaltet alle Daten einer Komponente. Nachdem die Komponentenklasse die eigentlich "treibende Kraft" im Ablauf einer JSF-Anfrage ist Der Ablauf jeder Phase einer JSF-Anfrage ist in Wirklichkeit der Durchlauf durch den Komponentenbaum und bedeutet, dass vom Wurzelknoten aus der Komponentenbaum durch rekursiven Aufruf der für die jeweilige Phase zuständigen Behandlungsmethoden durchwandert wird.: , kann man alle Ereignisse in JSF von hier aus steuern.Zwei der wichtigsten Aufgaben sind das Auslesen der für die Komponente relevanten Request-Parameter (Decoding) und das Rendern der Komponente (Encoding). Die Komponente kann - wenn das vom Entwickler so vorgesehen ist - diese Aufgaben selbst übernehmen (das nennt man auch Direct Implementation Model ) oder an einen Renderer delegieren ( Delegated Implementation Model ). Der größte Vorteil eines eigenen Renderers ist die klare Trennung zwischen den in der Komponente enthaltenen Daten und der daraus gerenderten Ausgabe. Die beiden Ansätze schließen sich nicht gegenseitig aus. So wäre es zum Beispiel vorstellbar, in einer ersten Version das Rendering von der Komponente durchführen zu lassen und erst später einen eigenen Renderer zu erstellen.
Wir werden uns in diesem Abschnitt um die Komponentenklasse kümmern und erst in Abschnitt [Sektion: Schreiben der Rendererklasse] einen genaueren Blick auf die Rendererklasse werfen.
5.2.2.1 Komponentenattribute
Eine Komponentenklasse enthält üblicherweise eine Eigenschaft für jedes Attribut des zugehörigen Tags. Es kann aber auch Attribute des Tags geben, die nicht explizit in der Komponente existieren - diese werden dann in einer speziellen Map in der Komponente abgelegt. Umgekehrt ist es aber selten sinnvoll, für ein Attribut in der Komponente kein Attribut im Tag vorzusehen, es sei denn, das Komponentenattribut wäre ein vererbtes Attribut, das in der Kindklasse nicht mehr benötigt wird.Die Zugriffsmethoden auf die Eigenschaften der Komponente müssen gewährleisten, dass die Eigenschaften sowohl direkt als auch über Value-Expressions angegeben werden können. Dabei gilt, dass das direkte Setzen von Attributen stärker "zieht" als das Setzen einer Value-Expression . Beim Lesen des Komponentenattributs muss dann natürlich berücksichtigt werden, ob es direkt gesetzt wurde. Ist das der Fall, wird der Komponentenwert direkt zurückgegeben, ansonsten wird die Value-Expression evaluiert. Klingt kompliziert - ist aber mit JSF 2.0 zum Kinderspiel geworden.
In JSF 2.0 werden als Grundlage für das neue Partial-State-Saving Eigenschaften von Komponenten nicht mehr in privaten Feldern, sondern in einer Map verwaltet. Zu diesem Zweck hat jede Komponente eine Instanz der Klasse StateHelper , die den Zustand intern verwaltet. Was es mit dem Zustand auf sich hat, klären wir etwas weiter unten, jetzt interessiert uns erst einmal das Lesen und Schreiben der Eigenschaften.
Listing Die Klasse InputSpinner zeigt die Klasse InputSpinner unserer Beispielkomponente. Wie Sie sehen, ist der Code sehr überschaubar und umfasst im Grunde genommen nur die Definition der Eigenschaft inc . Den Rest, wie etwa das State-Saving oder das Verwalten der anderen Attribute, erledigen die Basisklassen von JSF. Nachdem wir von UIInput abgeleitet haben, können wir auch dessen bereits vorhandenen Funktionsumfang benutzen.
public class InputSpinner extends UIInput {
public static final String COMPONENT_TYPE =
"at.irian.InputSpinner";
enum PropertyKeys {inc}
public InputSpinner() {
setRendererType("at.irian.InputSpinner");
}
public int getInc() {
return (Integer)getStateHelper().eval(
PropertyKeys.inc, 1);
}
public void setInc(int inc) {
getStateHelper().put(PropertyKeys.inc, inc);
}
}
5.2.2.2 State-Saving
Über die Zugriffsmethoden auf die Eigenschaften der Komponente hinaus hat die Komponentenklasse noch eine äußerst wichtige Aufgabe: Sie muss ihren Zustand bis zum nächsten Request sichern können. Das State-Saving ist in früheren JSF-Versionen relativ einfach gestrickt und verfolgt den Ansatz, immer den kompletten Zustand des gesamten Komponentenbaums zu speichern und wiederherzustellen. Da diese Vorgehensweise in Bezug auf Performance und Speicherverbrauch nicht optimal ist, wurde in JSF 2.0 mit dem Partial-State-Saving ein neuer Ansatz realisiert.Wie der Name bereits erahnen lässt, werden dabei nur mehr wirklich relevante Teile des Zustands gespeichert. JSF markiert dazu nach dem Aufbau des Komponentenbaums einen initialen Zustand, der ohnehin durch die Seitendeklaration definiert ist. Der Partial-State setzt sich dann aus allen Änderungen am Komponentenbaum nach Erreichen dieses Zustands zusammen. Voraussetzung für die korrekte Initialisierung ist der Aufbau der Ansicht aus der Seitendeklaration vor jedem Wiederherstellen des Zustands.
Für das State-Saving sind die Methoden saveState() und restoreState() aus dem Interface StateHolder zuständig. Vor JSF 2.0 mussten diese beiden Methoden in mühsamer Kleinarbeit für jede Komponentenklasse erstellt werden - ein fehleranfälliger Prozess dem wir keine Träne nachweinen. In JSF 2.0 sind diese Methoden bereits in UIComponentBase implementiert und müssen nur mehr bei speziellen Anforderungen überschrieben werden.
Wie funktioniert das? Die Grundlage für die Aufzeichnung des Zustands bildet der im letzten Abschnitt vorgestellte StateHelper , der den Zustand der Komponente intern verwaltet. Damit ist auch bereits der Grundstein für das Partial-State-Saving gelegt. Die "zentralisierte" Zustandsverwaltung ermöglicht das Festhalten von Änderungen nach Erreichen des initialen Zustands.
Jede Komponente, die Partial-State-Saving einsetzen will, muss das von StateHolder abgeleitete und um Methoden zur Markierung des initalen Zustands erweiterte Interface PartialStateHolder implementieren. Nachdem auch diese Methoden bereits in der Klasse UIComponentBase implementiert und in einigen anderen Basisklassen erweitert werden, müssen Sie sich auch darum keine Gedanken machen.
5.2.2.3 Komposition klassischer Komponenten
Ein interessanter Aspekt der Komponentenentwicklung ist die Komposition von Komponenten zu einem größeren Ganzen: Dazu ist die geeignete Stelle zu finden, in der Kindkomponenten der Elternkomponente hinzugefügt werden können. Im Wesentlichen gibt es hier zwei Möglichkeiten: Die einfachere Variante ist das Hinzufügen von transienten Kindkomponenten beim Rendern der Seite. Transient bedeutet, dass das Attribut transient der Komponente auf true gesetzt wurde, und die Komponente daher im State-Saving -Prozess verschwindet. Auch wenn diese Art der Komposition funktioniert, ist sie doch nicht in allen Fällen optimal, weil eine Art Komponentenfunktionalität in den Renderer ausgelagert wird.Mit JSF 2.0 ist es möglich, Kindkomponenten mit einem System-Event zu einer zusammengesetzten Komponente hinzuzufügen. JSF 2.0 definiert eine ganze Reihe von System-Events, die zu bestimmten Zeitpunkten im Lebenszyklus ausgelöst werden. Uns interessiert hier das PostAddToViewEvent . Dieses Event wird aufgerufen, nachdem eine Komponente in den Komponentenbaum eingefügt wurde. Wir werden einen Listener für dieses Ereignis erstellen, in dem wir die zusätzlichen Komponenten hinzufügen.
Listing Komposition klassischer Komponenten mit System-Events zeigt einen Ausschnitt einer Komponentenklasse, die über die Annotation @ListenerFor als Listener für das System-Event PostAddToViewEvent registriert ist. Tritt das Ereignis beim Einfügen der Komponente in die Ansicht auf, wird die im Interface ComponentSystemEventListener definierte Methode processEvent aufgerufen. Da dieses Interface bereits von der Klasse UIComponent implementiert wird, müssen wir nur die entsprechende Methode überschreiben und erweitern.
@FacesComponent("at.irian.MyPanel")
@ListenerFor(systemEventClass = PostAddToViewEvent.class)
public class MyPanel extends HtmlPanelGroup {
public void processEvent(ComponentSystemEvent ev)
throws AbortProcessingException {
if (ev instanceof PostAddToViewEvent) {
addComponents();
}
super.processEvent(ev);
}
...
}
5.2.3 Schreiben der Rendererklasse
Der Begriff Renderer steht in JSF für eine Klasse, die einer Komponente (oder einer Komponentenfamilie) zugeordnet ist und die Ansicht für diese Komponente erstellt, aber auch den Wert einer Komponente wieder aus der HTTP-Anfrage ausliest und in die Komponenteninstanz überträgt. Jede Rendererklasse muss von der abstrakten Klasse javax.faces.render.Renderer abgeleitet werden und die Methoden überschreiben, die nicht die Standardfunktionalität besitzen sollen.Der Renderer ist letztlich die Klasse, deren Schreiben am meisten Aufwand bedeutet, weil in ihr die ganze Ansichtslogik programmmiert werden muss - durch die Vielfalt der in der Webanwendungsentwicklung verwendeten Technologien wie HTML, JavaScript, CSS und XML ist diese Ansichtslogik bei größeren Komponenten sehr komplex und unübersichtlich. Das ist auch der Grund für die Spezifizierung des JSF-Standards - der "normale" Webentwickler muss sich um die Entwicklung dieser Ansichtslogik nicht mehr kümmern.
Folgende Aufzählung zeigt die Methoden der Klasse Renderer :
- void decode(FacesContext ctx, UIComponent comp)
Liest den Wert der Komponente aus den Request-Parametern. - void encodeBegin(FacesContext ctx, UIComponent comp)
Wird beim Rendern der Komponente zuerst aufgerufen. - void encodeChildren(FacesContext ctx, UIComponent comp)
Wird beim Rendern der Komponente nach encodeBegin() aufgerufen, wenn getRendersChildren() den Wert true zurückliefert. - void encodeEnd(FacesContext ctx, UIComponent comp)
Wird beim Rendern der Komponente zuletzt aufgerufen. - boolean getRendersChildren()
Liefert den Wert true zurück, wenn der Renderer alle Kindkomponenten selbst rendert. Der Defaultwert ist false . - Object getConvertedValue(FacesContext ctx, UIComponent comp,
Object submittedValue)
Konvertiert den Submitted-Value von einem String in den für die Komponente benötigten Wert. - String convertClientId(FacesContext ctx, String clientId)
Konvertiert die Client-ID in eine für den Client gültige Form.
Der Renderer für unsere Input-Spinner-Komponente wird in der Klasse InputSpinnerRenderer implementiert.
5.2.3.1 Rendern (Encoding)
Listing Die Methode encodeBegin() des Beispielrenderers zeigt die Methode encodeBegin() der Rendererklasse InputSpinnerRenderer . Als Parameter werden der Methode der Faces-Context und die Komponente, für die das Rendern erfolgen soll, übergeben. Das Schreiben der Ansicht erfolgt in den beiden privaten Methoden encodeInput() zum Rendern des Eingabefelds und encodeButtons() zum Rendern der beiden Spin-Buttons. Momentan interessiert uns nur encodeInput() , da dort das input -Element über Aufrufe der Klasse ResponseWriter geschrieben wird - die wiederum erhält man vom momentanen Faces-Context über einen Aufruf der Methode getResponseWriter() .public void encodeBegin(FacesContext ctx,
UIComponent component) throws IOException {
InputSpinner spinner = (InputSpinner)component;
String clientId = spinner.getClientId();
encodeInput(ctx, spinner, clientId);
encodeButtons(ctx, spinner, clientId);
}
private void encodeInput(FacesContext ctx,
InputSpinner spinner, String clientId)
throws IOException {
ResponseWriter writer = ctx.getResponseWriter();
writer.startElement("input", spinner);
writer.writeAttribute("id", clientId, null);
writer.writeAttribute("name", clientId, null);
Object value = getValue(ctx, spinner);
if (value != null) {
writer.writeAttribute("value", value.toString(), null);
}
writer.writeAttribute("class", "inputSpinner-input", null);
writer.endElement("input");
}
Standardmäßig wird der Komponentenbaum in JSF rekursiv durchlaufen und jede Komponente wird durch einen einmaligen Aufruf der Methode encodeAll() gerendert. Die Methode encodeAll() ruft zuerst die Methode encodeBegin() der aktuellen Komponente auf und überprüft dann, ob die Methode getRendersChildren() den Wert true zurückliefert. Wenn dem so ist, werden nicht die einzelnen Kindkomponenten durchgegangen, sondern die Methode encodeChildren() aufgerufen - damit kann also die Komponente selbst ihre Kinder in die Ansicht schreiben! Ansonsten wird die Methode encodeAll() auf den einzelnen Kindern aufgerufen und damit rekursiv der Baum einen Schritt tiefer in die Ansicht geschrieben. Für Komponenten, die ihre Kindelemente selbst verwalten und in die HTML-Ansicht schreiben wollen, ist es also wichtig, dass ihr Renderer die Methode getRendersChildren() überschreibt und true zurückliefert.
ResponseWriter: Zurück zum Schreiben der Ausgabe unserer Komponente. Dieser Vorgang funktioniert für HTML (und alle von SGML abgeleiteten Dialekte ähnlich) über das Öffnen, Schließen und Schreiben von Attributen von den zur Komponente gehörigen Tags. Das Öffnen eines Tags ist ein einfacher Aufruf des ResponseWriter :
writer.startElement("input", spinner);
startElement():
Zuerst wird an die Methode startElement() die Zeichenkette übergeben, die als Name des Tags verwendet werden soll - in unserem
Fall input . Als zweites Attribut wird die Komponente selbst übergeben. Sehr
wichtig - hier soll auf keinen Fall null übergeben werden, sondern immer die
zugehörige Komponente. Ist das HTML-Tag einer Komponente nicht eins zu eins zuzuordnen
- wenn beispielsweise ein Renderer für eine Komponente mehrere HTML-Tags erzeugt
-, sollte die Komponente jedem dieser Tags übergeben werden, das Gleiche gilt auch für
eventuelle Kind-Tags. Die Information über die zugehörige Komponente wird von grafischen
Entwicklungsumgebungen ausgewertet, um beim Rendering zur Designzeit beispielsweise alle
Tags einer Komponente mit einer speziellen Klasse auszuzeichnen.writeAttribute(): Im nächsten Schritt werden die Attribute des Tags in die HTML-Ansicht geschrieben, dazu dient die Methode writeAttribute() . Auch hier wird wieder der Name des Attributs zuerst übergeben:
writer.writeAttribute("id", clientId, "id");
Auf die Angabe des Attributsnamens folgt der zu schreibende Wert und schließlich
wieder das zugehörige Attribut der Komponente. Auch diese Verbindung wird von
grafischen Entwicklungsumgebungen genutzt und sollte nach Möglichkeit gesetzt werden,
wenn ein entsprechendes Komponentenattribut vorhanden ist. Ein Beispiel für ein
Attribut, wo das nicht möglich ist, ist das onclick -Attribut. Dieses Attribut
hat keine Entsprechung in der Komponente, es wird einfach nur in die Map der
Attribute eingetragen.writer.writeAttribute(HTML.ONCLICK_ATTR,
onClick.toString(), null);
value-Attribut:
Wichtig (und etwas anders als die Behandlung der anderen
Attribute) ist die Behandlung des value -Attributs. Wenn eine JSF-Anfrage am
Server ankommt, wird der Wert einer Komponente decodiert und dieser Wert vorerst in
das Feld submittedValue geschrieben. Tritt beim Konvertieren oder Validieren
einer Komponente des Komponentenbaums ein Fehler auf, wird die weitere Behandlung
der Anfrage abgebrochen und direkt in die Render-Response-Phase gesprungen. Statt
jetzt den Wert der Komponente auszugeben, den man über den Aufruf von
getValue() erhält, muss man also beim Rendern zuerst prüfen, ob nur der
submittedValue gesetzt worden ist. Wenn diese Bedingung zutrifft, darf man
nur diesen submittedValue schreiben.Konvertierung: Ist nicht der Submitted-Value ausschlaggebend, sondern tatsächlich ein Wert für die Komponente gesetzt, muss vor dem Rendern des Werts noch ein eventuell angegebener Konverter aufgerufen werden, dieser Konverter wird den Wert von einem beliebigen Typ in eine Zeichenkette wandeln.
Listing Auslesen des Werts einer Komponente beim Rendern zeigt die Methode getValue() der Klasse InputSpin-nerRenderer , die nach dem soeben beschriebenen Algorithmus den Wert der Komponente für die Anzeige zurückliefert.
private Object getValue(FacesContext ctx,
InputSpinner spinner) {
Object submittedValue = spinner.getSubmittedValue();
if (submittedValue != null) {
return submittedValue;
}
Object value = spinner.getValue();
Converter converter = getConverter(ctx, spinner);
if (converter != null) {
return converter.getAsString(ctx, spinner, value);
} else if (value != null) {
return value.toString();
} else {
return "";
}
}
private Converter getConverter(FacesContext ctx,
UIComponent comp) {
Converter conv = ((UIInput)comp).getConverter();
if (conv != null) return conv;
ValueExpression exp = comp.getValueExpression("value");
if (exp == null) return null;
Class valueType = exp.getType(ctx.getELContext());
if (valueType == null) return null;
return ctx.getApplication().createConverter(valueType);
}
5.2.3.2 Decodierung (Decoding)
Weiter im Programm: Genauso, wie die Komponente in die HTML-Seite geschrieben wird, muss der Wert der Komponente bei einem Postback wieder aus der HTTP-Anfrage ausgelesen werden können. Auch diese Aufgabe erledigt der Renderer, und zwar mit der Methode de-code() . Listing Decodieren eines Werts zeigt die Methode unseres Beispielrenderers.public void decode(FacesContext ctx, UIComponent component) {
Map<String, String> params = ctx
.getExternalContext().getRequestParameterMap();
String clientId = component.getClientId();
String value = params.get(clientId);
((UIInput)component).setSubmittedValue(value);
}
Konvertierung und Validierung: Jetzt geht's weiter im Lebenszyklus der HTTP-Anfrage: Die Komponente muss den Submitted-Value jetzt konvertieren und anschließend validieren. Zum Konvertieren des Werts wird die Methode getConvertedValue() des Renderers aufgerufen (siehe Listing Konvertieren des Werts im Beispielrenderer ).
public Object getConvertedValue(FacesContext ctx,
UIComponent component, Object submittedValue)
throws ConverterException {
Converter converter = getConverter(ctx, component);
if (converter != null ) {
return converter.getAsObject(
ctx, component, (String) submittedValue);
} else {
return submittedValue;
}
}
5.2.3.3 Rendern von Ressourcen
Das Rendern von Ressourcen wie Bildern, Stylesheets oder Skripten ist ein wichtiger Aspekt bei vielen Komponenten. In JSF 2.0 gibt es dafür jetzt endlich auch eine standardisierte Lösung, was das Erstellen von Komponenten erheblich vereinfacht. Das Thema Ressourcen haben wir ja schon in Abschnitt Sektion: Verwaltung von Ressourcen ausführlich behandelt. Dort wurde bereits kurz erwähnt, dass Abhängigkeiten zwischen Ressourcen und Komponenten in Form von Annotationen auf der Komponenten- oder Rendererklasse abgebildet werden können.Sehen wir uns das für unsere Input-Spinner-Komponente etwas genauer an. Aus Gründen der Einfachheit werden wir die Ressourcen der Kompositkomponente inputSpinner aus der Bibliothek mygourmet mitverwenden. Damit die Komponente richtig dargestellt wird und ordnungsgemäß funktioniert, benötigen wir das Stylesheet components.css und das Skript inputSpinner.js . Um die beiden Bilder zum Erhöhen und Reduzieren des Werts kümmern wir uns später. Für jede der beiden Ressourcen annotieren wir die Rendererklasse mit @ResourceDependency unter Angabe der Bibliothek und des Namens. Das Skript wird zusätzlich mit target="{}head"{} in den Header der gerenderten Ausgabe verfrachtet. Listing Ressourcenannotationen auf der Rendererklasse zeigt die Rendererklasse mit den Annotationen. Mehr ist nicht notwendig, um JSF die Ressourcen automatisch verwalten zu lassen - vorausgesetzt natürlich, Sie verwenden h:head und h:body . Sie können die Komponente auch mehrfach auf einer Seite einsetzen, JSF wird sie immer nur einmal rendern.
@ResourceDependencies({
@ResourceDependency(library = "mygourmet",
name = "inputSpinner.js", target = "head"),
@ResourceDependency(library = "mygourmet",
name = "components.css")}
)
public class InputSpinnerRenderer extends Renderer {
...
}
Die beiden Bilder spin-up.png und spin-down.png , die wir ebenfalls aus der Bibliothek mygourmet der Kompositkomponente übernehmen, können natürlich nicht einfach über eine Annotation mit der Komponente verbunden werden. Nachdem sie einen Teil der gerenderten Ausgabe der Komponente bilden, müssen sie manuell eingefügt werden. Dazu kommt die Klasse ResourceHandler zum Einsatz, mit der JSF intern Ressourcen verwaltet. Der Zugriff auf den für die Anwendung zuständigen Resource-Handler erfolgt über das Applikationsobjekt.
Das Rendern der Ressource selbst ist dann relativ einfach. Der wichtigste Schritt ist das Erzeugen der Ressource über die Methode createResource() des Resource-Handlers. Diese Methode nimmt als Parameter entweder nur den Namen oder den Namen und die Bibliothek der Ressource und gibt eine Instanz der Klasse Resource zurück, über die wir vollen Zugriff erhalten. Das Bild wird über den Response-Writer als img -Element gerendert, dessen src -Attribut auf eine spezielle Ressource-URL gesetzt ist. Diese URL wird von der Methode Resource.getRequestPath() berechnet. Wenn der Browser beim Darstellen der Seite das Bild mit dieser URL nachlädt, liefert JSF den Inhalt der Ressource an den Client aus. Listing Direktes Rendern von Ressourcen zeigt, wie das Rendern eines der beiden Bilder mit dem zugehörigen JavaScript-Code aussieht.
Application app = ctx.getApplication();
ResourceHandler handler = app.getResourceHandler();
Resource spinUpRes = handler.createResource(
"spin-up.png", "mygourmet");
String onclickUp = MessageFormat.format(
"return changeNumber(''{0}'', {1});",
clientId, spinner.getInc());
writer.startElement("img", spinner);
writer.writeAttribute("class", "inputSpinner-button", null);
writer.writeAttribute("src", spinUpRes.getRequestPath(), null);
writer.writeAttribute("onclick", onclickUp, null);
writer.endElement("img");
5.2.4 Registrieren der Komponenten- und der Rendererklasse
Die soeben verfassten Komponenten- und Rendererklassen müssen jetzt noch mit einem Eintrag in der faces-config.xml in der Faces-Umgebung registriert werden.Listing Registrierung einer Komponentenklasse zeigt die Registrierung der Komponentenklasse des Beispiels unter dem Komponententyp at.irian.InputSpinner .
<component>
<component-type>
at.irian.InputSpinner
</component-type>
<component-class>
at.irian.jsfatwork.gui.jsf.component.InputSpinner
</component-class>
</component>
Bei der Registrierung des Renderers muss zuerst ein Renderkit ausgewählt werden, für das die Eintragung der zusätzlichen Rendererklasse erfolgt. Die Auswahl ist meistens einfach, und fast immer bleibt es bei der Verwendung des Standard-Renderkits mit der Renderkit-ID HTML_BASIC .
Listing Registrierung einer Rendererklasse zeigt die Registrierung des Beispielrenderers unter der Komponentenfamilie javax.faces.Input und dem Renderertyp at.irian.InputSpinner .
<render-kit>
<render-kit-id>HTML_BASIC</render-kit-id>
<renderer>
<component-family>javax.faces.Input</component-family>
<renderer-type>at.irian.InputSpinner</renderer-type>
<renderer-class>
at.irian.jsfatwork.gui.jsf.component.InputSpinnerRenderer
</renderer-class>
</renderer>
</render-kit>
@FacesRenderer(componentFamily = "javax.faces.Input",
rendererType = "at.irian.InputSpinner")
public class InputSpinnerRenderer extends Renderer {
5.2.5 Tag-Definition schreiben
Alle bisherigen Schritte, das Erstellen der Komponenten- und Rendererklasse und das Registrieren der beiden Klassen im System, gestalten sich unabhängig von der eingesetzten Seitendeklarationssprache immer gleich. Bei der Definition des Tags der Komponente ist das leider nicht mehr möglich.Mit JSP ist die Definition des Tags und das damit verbundene Erstellen der Tag-Klasse ein recht mühsames Unterfangen. Da mit Version 2.0 der Spezifikation der Fokus eindeutig auf Facelets gelegt wurde und JSP nur mehr eine Nebenrolle spielt, werden wir darauf an dieser Stelle nicht mehr näher eingehen.
Freuen Sie sich, mit Facelets reduziert sich der Aufwand für das Erstellen einer Tag-Definition auf ein Minimum. Existiert bereits eine passende Tag-Bibliothek reicht die Angabe des Tag-Namens, des Komponententyps und des Renderertyps, um das Tag fertig zu spezifizieren. Ist das nicht der Fall, muss zuerst eine neue Tag-Bibliothek angelegt und dem System bekannt gemacht werden. Wie das funktioniert, zeigt Abschnitt Sektion: Tag-Bibliotheken mit Facelets erstellen . Die komplette Definition des Tags für unsere Beispielkomponente ist in Listing Definition des Tags der Beispielkomponente zu sehen.
<tag>
<tag-name>inputSpinner</tag-name>
<component>
<component-type>at.irian.InputSpinner</component-type>
<renderer-type>at.irian.InputSpinner</renderer-type>
</component>
</tag>
5.2.6 Tag-Behandlungsklasse schreiben
In seltenen Fällen wird auch für Facelet-Tags eine Behandlungsklasse benötigt. Das ist beispielsweise dann der Fall, wenn hinter dem Tag gar keine Komponente existiert, wie das für c:if aus der JSTL-Bibliothek von Facelets der Fall ist. Listing Tag-Handler für c:if zeigt den Code des entsprechenden Tag-Handlers.public final class IfHandler extends TagHandler {
private final TagAttribute test;
private final TagAttribute var;
public IfHandler(TagConfig config) {
super(config);
this.test = this.getRequiredAttribute("test");
this.var = this.getAttribute("var");
}
public void apply(FaceletContext ctx, UIComponent parent)
throws IOException, FacesException, ELException {
boolean b = this.test.getBoolean(ctx);
if (this.var != null) {
ctx.setAttribute(var.getValue(ctx), new Boolean(b));
}
if (b) this.nextHandler.apply(ctx, parent);
}
}
Tag-Handler wie der soeben beschriebene werden in der Tag-Bibliothek direkt in einer Tag-Definition verwendet und können so in der Seitendeklaration wie eine Komponente eingesetzt werden. Hier gilt es allerdings, zu beachten, dass ein Tag-Handler nur beim Aufbau des Komponentenbaums aufgerufen wird. Die Definition des Tags sieht in diesem Fall wie folgt aus:
<tag>
<tag-name>if</tag-name>
<handler-class>IfHandler</handler-class>
</tag>
Tag-Handler können aber auch für Komponenten verwendet werden, die ein spezielles
Verhalten erfordern. Zur Demonstration erstellen wir einen Tag-Handler für unsere
Input-Spinner-Komponente, der das Attribut inc verpflichtend macht. Den
Sourcecode des Tag-Handlers finden Sie in Listing Tag-Handler für die Beispielkomponente .public class InputSpinnerTagHandler extends ComponentHandler {
private TagAttribute inc;
public InputSpinnerTagHandler(ComponentConfig conf) {
super(conf);
this.inc = getRequiredAttribute("inc");
}
}
<tag>
<tag-name>inputSpinner</tag-name>
<component>
<component-type>at.irian.InputSpinner</component-type>
<renderer-type>at.irian.InputSpinner</renderer-type>
<handler-class>
at.irian.jsfatwork.gui.jsf.component.InputSpinnerTagHandler
</handler-class>
</component>
</tag>
5.2.7 Tag-Bibliothek einbinden
Zu guter Letzt bleibt nur mehr ein Schritt übrig: Bevor die Komponente in einer Deklaration zum Einsatz kommen kann, muss die Tag-Bibliothek eingebunden werden. Wie das funktioniert, haben wir ja bereits bei den Standardkomponenten und diversen anderen Gelegenheiten gezeigt und muss hier nicht mehr wiederholt werden.An dieser Stelle möchten wir Ihnen noch einmal veranschaulichen, wie JSF beim Aufbau des Komponentenbaums die Komponenten- und Rendererklasse für ein Tag auflöst. Trifft JSF beispielsweise auf das Tag mg:inputSpinner , ist aus der Definition in der Tag-Bibliothek bereits der Komponententyp at.irian.InputSpinner und der Renderertyp at.irian.InputSpinner bekannt. Über den Komponententyp kann jetzt bereits die Komponenteninstanz erstellt und in den Baum eingefügt werden. Mit dieser Information lässt sich auch die Rendererklasse auflösen. Wenn wir davon ausgehen, dass das Standard-Renderkit mit dem Bezeichner HTML_BASIC zum Einsatz kommt, kann mit dem Renderertyp und der Komponentenfamilie die Klasse des Renderers aus der Konfiguration bestimmt werden. Die Komponentenfamilie ist ja jederzeit über einen Aufruf der Methode getFamily() der Komponente abrufbar.
Das ist alles, was man über das Schreiben von eigenen Komponenten wissen muss - viel Erfolg beim Erstellen der dynamischsten, interaktivsten und schönsten JSF-Komponenten!
5.3 Kompositkomponenten und klassischen Komponenten kombinieren
Wir haben Ihnen in den letzten beiden Abschnitten die Entwicklung von Kompositkomponenten und klassischen Komponenten näher gebracht. In diesem Abschnitt werden wir Ihnen zeigen, dass sich diese Konzepte nicht gegenseitig ausschließen sondern ganz im Gegenteil sogar sehr gut harmonieren.Bei der Entwicklung von Kompositkomponenten tritt immer wieder der Fall ein, dass ein gewünschtes Verhalten nur mit Java-Code realisierbar ist. Wir benötigen also einen Erweiterungspunkt zur Integration dieses Codes. Wie wir Ihnen bereits gezeigt haben, sind Kompositkomponenten intern aus klassischen Komponenten aufgebaut. Der naheliegendste Gedanke ist daher, diesen Java-Code in Form einer klassischen Komponente zu realisieren.
JSF verfolgt exakt diesen Gedanken und erlaubt bei Kompositkomponenten die freie Wahl des Typs der Wurzelkomponente. Über das Attribut componentType im Element cc:interface kann der Komponententyp dieser Komponente explizit angegeben werden. Die eingesetzte Komponentenklasse muss als einzige Voraussetzung das Interface NamingContainer implementieren und in getFamily() die Komponentenfamilie javax.faces.NamingContainer zurückliefern. Wird componentType nicht gesetzt, erzeugt JSF automatisch eine Komponente vom Typ javax.faces.NamingContainer .
Genau das werden wir jetzt für die in Abschnitt [Sektion: Die Komponente mc:collapsiblePanel] vorgestellte Kompositkomponente collapsiblePanel machen. Dort haben wir ja bereits kritisch angemerkt, dass Benutzer der Komponente die Logik zum Ein- und Ausblenden selber bereitstellen müssen. Wir werden diese Funktionalität in der Komponente CollapsiblePanel umsetzen, die wir dann mit der Kompositkomponente verknüpfen. Die Komponente selbst kann dabei sehr einfach gehalten werden. Sie muss lediglich über die Eigenschaft collapsed und eine Ereignisbehandlungsmethode zum Ein- und Ausblenden verfügen. Listing Komponente CollapsiblePanel zeigt die Komponentenklasse, die mit @FacesComponent unter dem Komponententyp at.irian.CollapsiblePanel registriert wird.
@FacesComponent("at.irian.CollapsiblePanel")
public class CollapsiblePanel extends UINamingContainer {
enum PropertyKeys {collapsed}
public boolean isCollapsed() {
return (Boolean)getStateHelper().eval(
PropertyKeys.collapsed, Boolean.FALSE);
}
public void setCollapsed(boolean collapsed) {
getStateHelper().put(PropertyKeys.collapsed, collapsed);
}
public void toggle(ActionEvent e) {
setCollapsed(!isCollapsed());
setCollapsedValueExpression();
}
private void setCollapsedValueExpression() {
ELContext ctx = FacesContext.getCurrentInstance()
.getELContext();
ValueExpression ve = getValueExpression(
PropertyKeys.collapsed.name());
if (ve != null) ve.setValue(ctx, isCollapsed());
}
}
Wenden wir uns nun der Kompositkomponente selbst zu. Die Interna ändern sich im Vergleich zu Abschnitt [Sektion: Die Komponente mc:collapsiblePanel] nur minimal und werden etwas anders verdrahtet. Listing Kompositkomponente collapsiblePanelmit benutzerdefinierter Wurzelkomponente zeigt die aktualisierte Komponente collapsiblePanel mit gesetzem componentType -Attribut.
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:cc="http://java.sun.com/jsf/composite">
<head><title>collapsiblePanel</title></head><body>
<cc:interface componentType="at.irian.CollapsiblePanel">
<cc:attribute name="collapsed"/>
<cc:actionSource name="toggle"/>
<cc:facet name="header"/>
</cc:interface>
<cc:implementation>
<h:panelGroup layout="block"
styleClass="collapsiblePanel-header">
<h:commandButton actionListener="#{cc.toggle}"
id="toggle" styleClass="collapsiblePanel-img"
image="#{resource[cc.collapsed
? 'mygourmet:toggle-plus.png'
: 'mygourmet:toggle-minus.png']}"/>
<cc:renderFacet name="header"/>
</h:panelGroup>
<h:panelGroup layout="block" rendered="#{!cc.collapsed}">
<cc:insertChildren/>
</h:panelGroup>
</cc:implementation>
</body></html>
Die zweite Änderung betrifft die Ereignisbehandlungsmethode toggle und die Eigenschaft collapsed . Da beide jetzt direkt von der Wurzelkomponente zur Verfügung gestellt werden, ändern sich die EL-Ausdrücke für den Zugriff auf cc.toggle und cc.collapsed . Das ist möglich, da die mit cc referenzierte Komponente jetzt eine Instanz der zuvor erstellten Klasse CollapsiblePanel ist.
Damit ist die verbesserte Version der Kompositkomponente collapsiblePanel auch schon einsatzbereit. Jetzt können wir tatsächlich von einem eigenständigen und wiederverwendbaren Baustein sprechen. Der nächste logische Schritt wäre jetzt, die Komponente inklusive aller Bestandteile als Jar-Datei zur Verfügung zu stellen. Abschnitt [Sektion: Die eigene Komponentenbibliothek] zeigt wie das funktioniert.
5.4 Alternativen zur eigenen Komponente
Eine Komponente besteht aus den Teilen Komponentenklasse, Rendererklasse und einem optionalen Tag-Handler. Alle diese Teile sind miteinander verbunden, können aber auch getrennt voneinander ausgetauscht werden. Die einfachste Möglichkeit, eine Komponente zu verändern, ohne eine neue Komponente schreiben zu müssen, ist der Austausch der Rendererklasse.5.4.1 Austausch der Rendererklasse
Um die Rendererklasse auszutauschen, muss zuerst die Konfiguration in der faces-config.xml verändert werden und je nach Renderer, der überschrieben werden soll, etwa folgender Eintrag hinzugefügt werden:<render-kit>
<render-kit-id>HTML_BASIC</render-kit-id>
<renderer>
<component-family>
javax.faces.Output
</component-family>
<renderer-type>javax.faces.Label</renderer-type>
<renderer-class>
mypackage.RequiredLabelRenderer
</renderer-class>
</renderer>
</render-kit>
Beispiel: RequiredLabel:
Hier wird der Renderer für die
Label -Komponente durch die Klasse mypackage.RequiredLabelRenderer überschrieben. Jetzt bleibt nur noch, diese Klasse zu implementieren. Listing
Rendern eines Labels mit Stern
für Pflichtfelder zeigt eine Implementierung, in der
das required -Attribut der zum Label gehörenden Komponente ausgewertet wird.
Je nach dem Wert dieses Attributs wird ein Stern an die Label-Beschreibung angefügt.
Zu diesem Zweck wird der Pflichtfeldrenderer von HtmlLabelRenderer abgeleitet und die Methode encodeBeforeEnd() der Basisklasse überschrieben.
In dieser Methode wird zuerst die zum Label gehörige Komponente gesucht; anschließend
wird das required -Attribut dieser Komponente abgefragt. Gehört die Komponente
zu einem Pflichtfeld, wird ein span -Tag mit einer CSS-Klasse und einem *
als Inhalt ausgegeben. Sehr einfach und sehr effektiv! Beachten Sie aber bitte, dass
die Klasse HtmlLabelRenderer aus Apache MyFaces stammt und nicht im
Standard enthalten ist. Nichtsdestotrotz ändert sich, auch wenn Sie Mojarra einsetzen, an der grundlegenden Funktionalität nichts.public class RequiredLabelRenderer
extends HtmlLabelRenderer {
protected void encodeBeforeEnd(
FacesContext facesContext,
ResponseWriter writer,
UIComponent uiComponent)
throws IOException {
String forAttr = getFor(uiComponent);
if (forAttr != null) {
UIComponent forComponent =
uiComponent.findComponent(forAttr);
if (forComponent instanceof UIInput &&
((UIInput) forComponent).isRequired()) {
writer.startElement(HTML.SPAN_ELEM, null);
writer.writeAttribute(HTML.ID_ATTR,
uiComponent.getClientId(facesContext)+
"RequiredLabel", null);
writer.writeAttribute(HTML.CLASS_ATTR,
"requiredLabel", null);
writer.writeText("*", null);
writer.endElement(HTML.SPAN_ELEM);
}
}
}
}
5.4.2 Austausch der Komponentenklasse
Genauso wie der Austausch der Rendererklasse ist auch der Austausch der Komponentenklasse möglich - in der faces-config.xml -Datei ist ein zusätzlicher Eintrag wie folgt vorzunehmen:<component>
<component-type>
javax.faces.HtmlInputText
</component-type>
<component-class>
mypackage.SpecialHtmlInputText
</component-class>
</component>
Auto-Converter:
Mit dieser Vorgehensweise kann beispielsweise automatisch
ein Konverter mit der Komponente verbunden werden, ohne dass dazu ein eigenes
Konverter-Tag verwendet wird. Ein Beispiel für eine solche Klasse:public class SpecialHtmlInputText
extends HtmlInputText {
public SpecialHtmlInputText() {
super();
setConverter(
ConverterFactory.getSpecialConverter());
}
}
Damit kommt dieser Spezialkonverter für alle Elemente dieser Komponenten zum Einsatz!5.4.3 Benutzerdefinierte Komponente aus den Backing-Beans-- Component-Binding
Die beiden bisher verwendeten Tricks gelten für alle Elemente eines Komponententyps - was ist zu tun, wenn man nur einzelne Komponenten mit diesem Spezialverhalten auszeichnen möchte und andere nicht?Beispiel: Zeilenumbruch: Ein Beispiel aus der Praxis: Für eine Applikation wurde eine Verbindung zu einer auf einem AS400-Server laufenden Legacy-Datenbank entwickelt. Die vom Server zurückgegebenen Daten waren mit einem r zur Markierung des Zeilenumbruchs ausgezeichnet - auf dem Frontend sollte diese Markierung ebenfalls zu einem Zeilenumbruch im HTML-Markup führen, musste also als <br/> ausgegeben werden. Da nicht alle Textausgaben geparst werden sollten, wurde das Rendering-Verhalten nur für einen Teil der Komponenten ersetzt.
Lösung: Component-Binding: Um diese Ersetzung vorzunehmen, kann man entweder ein eigenes Tag erstellen und über dieses Tag einen neuen Renderer für die Komponente festlegen, man kann aber auch Component-Binding einsetzen, wobei dieser zweite Weg wesentlich einfacher ist. Dazu sind zuerst alle Komponenten, die ein spezielles Rendering-Verhalten aufweisen sollen, mit einem binding -Attribut zu versehen. Im nächsten Schritt wird dieses Attribut mit der dahinterliegenden Geschäftslogik verbunden. Das folgende Beispiel zeigt einen Ausschnitt aus einer solcherart veränderten Seitendeklaration:
<h:outputText value="#{limitDetail.limitView.comment}"
binding="#{componentBean.outputWithBreaks}"/>
Die referenzierte Methode sieht dabei so aus:UIComponent getOutputWithBreaks() {
return new OutputTextWithBreaks();
}
Jetzt benötigen wir nur noch eine Implementierung dieser Komponente - in Listing
Verändern des
Rendering-Verhaltens einer Komponente durch Component-Binding wird diese gezeigt. Es wird die
encodeEnd() -Methode überschrieben - wo ja üblicherweise ein Renderer für die
Komponente gesucht und dessen encodeEnd() -Methode aufgerufen wird. In diesem
Fall erledigen wir das Rendering gleich selbst in der Komponente. Das eigentliche
Rendering ist in der Abbildung ausgeblendet, da es exakt der Funktionalität in der
Rendererklasse entsprechen soll.public static final class OutputText extends HtmlOutputText {
public OutputText() {
super();
}
public void encodeEnd(FacesContext context)
throws IOException {
String text = RendererUtils.getStringValue(context, this);
text = HTMLEncoder.encode(text, true, true);
text = text.replaceAll("\r","<br/>");
renderOutputText(context, this, text, false);
}
public static void renderOutputText(
FacesContext ctx, UIComponent component,
String text, boolean escape)
throws IOException {
...
}
}
5.5 Überschreiben von JSF-Kernklassen
Es gibt noch eine zusätzliche Möglichkeit, JSF zu erweitern: Man kann einige der Kernklassen von JSF mit neuer Funktionalität versehen. Dazu wird ein Muster verwendet, das als Delegate-Pattern bekannt ist. Folgende Klassen sind Hotspots im JSF-Framework und können mit neuer Funktionalität überschrieben werden:- ActionListener :
Die Klasse, die für die Ausführung von Action -Methoden zuständig ist. Sie führt die Methode aus und leitet den Ausgabewert der Methode an den Navigation-Handler weiter. - NavigationHandler :
Der Navigation-Handler bekommt eine Navigationszeichenkette übergeben und leitet sie an einen bestimmten View weiter. - ElResolver :
Die Klasse, die Value-Expressions evaluiert und einen Wert zurückliefert. - StateManager :
Der State-Manager ist für das Sichern und Rücklesen der Statusinformationen zuständig. Das Überschreiben dieser Klasse ist selten notwendig und recht invasiv. - ViewHandler :
Der View-Handler wird vom Navigation-Handler aufgerufen, erzeugt eine neue Ansicht und gibt diese für das Rendering frei.
Beispiel: Back-Navigation: Das wollen wir auch im nächsten Beispiel machen, zuerst ist die Einbindung in die faces-config.xml erforderlich:
<application>
<navigation-handler>
at.jsfatwork.BackNavigationHandler
</navigation-handler>
</application>
Danach erfolgt die Implementierung dieses Navigation-Handlers wie in Listing
Benutzerdefinierter
Navigation-Handler für ein Zurückgehen in der Navigation dargestellt. Das Delegate-Pattern sieht einen Konstruktor vor, dem die übergeordnete Instanz der Klasse
NavigationHandler übergeben wird.public class BackNavigationHandler extends NavigationHandler {
private NavigationHandler navigationHandler;
public BackNavigationHandler(
NavigationHandler navigationHandler) {
this.navigationHandler = navHandler;
}
public void handleNavigation(FacesContext context,
String fromAction, String outcome) {
if (outcome != null) {
if (outcome.equals("back")) {
String lastViewId = getLastViewId();
if (lastViewId != null) {
ViewHandler viewHandler =
context.getApplication().getViewHandler();
UIViewRoot viewRoot =
viewHandler.createView(context, lastViewId);
context.setViewRoot(viewRoot);
context.renderResponse();
return;
}
} else {
setLastViewId(context.getViewRoot().getViewId());
}
}
navigationHandler.handleNavigation(
context, fromAction, outcome);
}
protected String getLastViewId(FacesContext context);
protected void setLastViewId(FacesContext context,
String lastViewId);
}
Die Methoden getLastViewId() und setLastViewId() sind hier nicht mehr fertig ausimplementiert. Sie müssen allerdings nur dafür sorgen, dass die View-IDs gespeichert werden, wenn die Methode setLastViewId() aufgerufen wird. In diesem Fall würde sich zum Beispiel eine Bean in der Session anbieten.
5.6 MyGourmet 13: Komponenten und Services
Das Beispiel MyGourmet 13 integriert alle in diesem Kapitel entwickelten Komponenten - sowohl die Kompositkomponenten aus Abschnitt [Sektion: Kompositkomponenten] als auch die klassische Komponente aus Abschnitt [Sektion: Klassische Komponenten] und deren Kombination aus Abschnitt [Sektion: Kompositkomponenten und klassischen Komponenten kombinieren] . Neben den vielen neuen Komponenten gibt es noch die neue Ansicht editProvider.xhtml zum Bearbeiten eines Anbieters. Im Zuge dieser Änderung haben wir die Architektur der Anwendung ein klein wenig optimiert und eine Serviceklasse für Objekte vom Typ Provider eingeführt. Listing MyGourmet 13: ProviderService zeigt das Interface ProviderService der Serviceklasse.public interface ProviderService {
Provider createNew();
boolean save(Provider entity);
void delete(Provider entity);
List<Provider> findAll();
Provider findById(long id);
}
Die Klasse ProviderServiceImpl implementiert das Interface ProviderService und stellt den eigentlichen Service dar. Sie steht als Managed-Bean unter dem Namen providerService im Application-Scope zur Verfügung. Listing MyGourmet 13: Implementierung des Service zeigt den Rumpf der Klasse mit den Annotationen. Die Implementierung ist sehr einfach gehalten und basiert intern auf einer Liste, die beim Erzeugen der Bean mit drei Objekten vom Typ Provider initialisiert wird.
@ManagedBean(name = "providerService")
@ApplicationScoped
public class ProviderServiceImpl implements ProviderService {
...
}
@ManagedProperty(value = "#{providerService}")
private ProviderService providerService;
public void setProviderService(
ProviderService providerService) {
this.providerService = providerService;
}
Die Komponente mg:inputSpinner , also die klassische Variante unserer Input-Spinner-Komponente, kann in gleicher Weise wie die Kompositkomponente eingesetzt werden.
5.7 Die eigene Komponentenbibliothek
In diesem Abschnitt zeigen wir, wie einfach das Erstellen einer eigenen Komponentenbibliothek mit JSF 2.0 geworden ist. Dazu packen wir exemplarisch die Kompositkomponente collapsiblePanel aus Abschnitt [Sektion: Kompositkomponenten und klassischen Komponenten kombinieren] inklusive aller benötigten Artefakte in eine Jar-Datei. Nachdem wir die Komponente selbst bereits eigenständig und wiederverwendbar gemacht haben, kann diese Jar-Datei in jeder JSF 2.0-Anwendung eingesetzt werden.Eine erste Version unserer Komponentenbibliothek ist schnell erstellt. Wir müssen lediglich das Verzeichnis der Ressourcen-Bibliothek mygourmet und die Klasse CollapsiblePanel der benutzerdefinierten Wurzelkomponente in die Jar-Datei aufnehmen.
Da die Komponente CollapsiblePanel mit der Annotation @FacesComponent im System registriert wird, brauchen wir eigentlich keine XML-Konfiguration. Im Endeffekt müssen wir aber doch eine leere faces-config.xml anlegen. JSF berücksichtigt Annotationen nur in jenen Jar-Dateien, die eine Datei mit dem Namen faces-config.xml oder mit der Endung .faces-config.xml im Verzeichnis META-INF beinhalten. Listing faces-config.xml für die Komponentenbibliothek zeigt die leere Konfigurationsdatei faces-config.xml .
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
version="2.0">
</faces-config>
Diese zugegeben sehr einfache Komponentenbibliothek bietet sich als Basis für Erweiterungen förmlich an. Sie kann neben weiteren Kompositkomponenten auch mit zusätzlichen klassischen Komponenten, Konvertern oder Validatoren ergänzt werden. Die Tags für diese Artefakte müssen allerdings im Gegensatz zu Kompositkomponenten in einer Tag-Bibliothek konfiguriert werden (siehe Abschnitt Sektion: Tag-Bibliotheken mit Facelets erstellen ). Sie stehen dann unter dem in der Tag-Bibliothek spezifizierten Namensraum für Applikationen zur Verfügung. JSF erlaubt ab Version 2.0 das Importieren von Bibliotheken mit Kompositkomponenten in eine Tag-Bibliothek. Das bietet den Vorteil, dass die Tags aller Artefakte in der Jar-Datei unter einem Namensraum zusammengefasst sind.
Für unsere Beispiel erstellen wir dazu im Verzeichnis META-INF die Tag-Bibliothek mygourmet.taglib.xml . In der Tag-Bibliothek definieren wir den Namensraum http://at.irian/mygourmet und importieren unsere Ressourcen-Bibliothek mygourmet im Element composite-library-name . Listing Konfiguration der Tag-Bibliothek für die Komponenten-bibliothek zeigt die Konfiguration der Tag-Bibliothek für unsere Komponentenbibliothek.
<facelet-taglib version="2.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/
web-facelettaglibrary_2_0.xsd">
<namespace>http://at.irian/mygourmet</namespace>
<composite-library-name>mygourmet</composite-library-name>
</facelet-taglib>
Abbildung Struktur der eigenen Komponenten-bibliothek zeigt abschließend noch die Struktur und den Inhalt der Jar-Datei für unser Beispiel.
In Abschnitt [Sektion: MyGourmet 13 mit Komponentenbibliothek] finden Sie nochmals Beispiel MyGourmet 13 - diesmal allerdings mit einer Tag-Bibliothek die alle Kompositkomponenten, Komponenten, Validatoren und Konverter in einer eigenen Jar-Datei zusammenfasst.
5.8 MyGourmet 13 mit Komponentenbibliothek
In diesem Abschnitt finden Sie eine kurze Beschreibung zu einer alternativen Version von MyGourmet 13 . In dieser Version sind die Kompositkomponenten und alle Bestandteile der eingesetzten Tag-Bibliothek in einer Komponentenbibliothek zusammengefasst.Diese Komponentenbibliothek beinhaltet zum einen im Verzeichnis META-INF/resources die Ressourcen-Bibliothek mygourmet und zum anderen die Tag-Bibliothek mygourmet.taglib.xml im Verzeichnis META-INF . Dazu kommt noch eine Reihe von Java-Klassen wie Komponenten- und Renderer-Klassen, Konverter und Validatoren. Die Kompositkomponenten sind, wie im letzten Abschnitt beschrieben, in die Tag-Bibliothek importiert. Damit stehen alle Artefakte unter dem Namensraum http://at.irian/mygourmet zur Verfügung.
Auf der Ebene des Quellcodes ist die Komponentenbibliothek als eigenes Maven-Modul realisiert. Die Projektbeschreibung pom.xml des Beispiels MyGourmet 13 mit Komponentenbibliothek umfasst zwei Module: mygourmet13-taglib enthält alle Bestandteile der Komponentenbibliothek und mygourmet13-webapp beinhaltet den Rest der Applikation. Die Verbindung zwischen den beiden Modulen ist in mygourmet13-webapp über eine Abhängigkeit auf mygourmet13-taglib definiert.
Um die Applikation zu starten, müssen Sie mvn jetty:run im Verzeichnis des Moduls mygourmet13-webapp aufrufen - zuvor muss allerdings das Modul mygourmet13-taglib oder alternativ das ganze Projekt mygourmet13 gebaut werden. Das Bauen von mygourmet13-taglib liefert als Ergebnis eine Jar-Datei mit allen Bestandteilen unserer Komponentenbibliothek.