Dienstag, 22. Mai 2012


Artikel

September 2006 | Artikel

JavaServer Faces: Integrationsstrategien für AJAX Fortsetzung, Teil 2

Teil 1   Teil 2   Teil 3   

AJAX, übernehmen Sie!
Hinter dem URL der ersten Strategie verbirgt sich ein Servlet (Listing 2), das den HTTP-Parameter form:eingabe ausliest, um eine Menge möglicher Benutzernamen zu laden. Damit die JavaScript-Klasse Autocompleter diese im Webbrowser anzeigen kann, müssen sie in einer ungeordneten HTML-Liste zurückgesendet werden (Listing 2). Da die JSF-URLs im Standard auf JSP-Dateien abgebildet sind, wurde ein Servlet verwendet. Allerdings ist es möglich, die Request-Verarbeitung der JSF-Engine zu überlassen.

Probleme der ersten Strategie
Für ein AJAX-basiertes Eingabefeld wird eine nicht unerhebliche Menge an Zusatzinformationen benötigt. Die im Beispiel eingebundenen JavaScript-Dateien benötigen ihrerseits wiederum weitere JavaScript-Dateien. Dies ist bei einer Portierung des Eingabefeldes auf andere JSPs nicht auf den ersten Blick ersichtlich. Bei Änderungen der IDs muss ebenfalls die JavaScript-Anweisung für den AJAX-Request geändert werden. Das Gleiche gilt für das <div>-Element. Ein weiteres Problem liegt bei der <h:inputText/>-Komponente. Moderne Webbrowser bieten oft selbst eine automatische Vervollständigung bei Eingabefeldern an. Dazu unterstützen sie das nicht standardisierte Attribut autocomplete. Setzt der Webentwickler seinen Wert auf off, wird die automatische Vervollständigung für das korrespondierende Eingabefeld nicht mehr angeboten. Leider besitzt die <h:inputText/>-Komponente erst in der JSF-Version 1.2 ein autocomplete-Attribut, sodass es mit JSF 1.1 zu Problemen bei der Eingabe kommen kann. Dieses sehr einfach gehaltene Beispiel verdeutlicht bereits die Notwendigkeit von einfach zu bedienenden AJAX-Komponenten, die einem Page Author die Arbeit erleichtern.

  1. Listing 3
  2. ---------------------------------------------------------------
  3. <f:view>
  4. <h:form id="form">
  5. <h:panelGrid columns="2">
  6. <h:outputText value="Eingabe"/>
  7. <ajax:inputText id="eingabe" value="#{ajaxbacking.eingabe}" resourceURL="ajax/suggest"/>
  8. <h:commandButton value="Senden"/>
  9. <h:outputText escape="false" value="Wert: <b>#{ajaxbacking.eingabe}</b>" rendered="#{!empty ajaxbacking.eingabe}"/>
  10. </h:panelGrid>
  11. </h:form>
  12. </f:view>
  13. </body>
  14. </html>
  1. Listing 4
  2. -----------------------------------------------------------------
  3. package net.wessendorf.jm.faces.ajax.component.simple;
  4. import javax.faces.application.Application;
  5. import javax.faces.component.UIComponent;
  6. import javax.faces.context.FacesContext;
  7. import javax.faces.el.ValueBinding;
  8. import javax.faces.webapp.UIComponentTag;
  9. public class InputSuggestTag extends UIComponentTag {
  10. private String value;
  11. private String resourceURL;
  12. //setter
  13. public String getComponentType() {
  14. return ("javax.faces.Input");
  15. }
  16. public String getRendererType() {
  17. return ("net.wessendorf.InputSuggest");
  18. }
  19. protected void setProperties(UIComponent component) {
  20. super.setProperties(component);
  21. setStringProperty(component, "value", this.value);
  22. setStringProperty(component, "resourceURL", this.resourceURL);
  23. }
  24. protected void setStringProperty(UIComponent component, String attrName, String value) {
  25. if (value == null)
  26. return;
  27. if (isValueReference(value)){
  28. component.setValueBinding(attrName, createValueBinding(value));
  29. } else {
  30. component.getAttributes().put(attrName, value);
  31. }
  32. }
  33. protected ValueBinding createValueBinding(String value){
  34. FacesContext context = FacesContext.getCurrentInstance();
  35. Application application = context.getApplication();
  36. return application.createValueBinding(value);
  37. }
  38. public void release() {
  39. super.release();
  40. this.value = null;
  41. this.resourceURL = null;
  42. }
  43. }
AJAX – die zweite!
Eine AJAX-Komponente, die über einen Tag eingebunden wird und die sich selbst um die notwendigen Zusatzinformationen wie Ressourcen oder JavaScript-Anweisungen kümmert, nimmt dem Page Author viel Tipparbeit ab. Listing 3 zeigt eine JSP, die eine solche Komponente (<ajax:inputText/>) enthält. Es wird deutlich, dass diese JSP wesentlich kompakter ist. Neben dem Attribut value besitzt der Tag das resourceURL-Attribut, das auf das Servlet verweist. Um solch kompakte JSPs zu ermöglichen, muss der Component Writer eine Renderer-Klasse und einen speziellen Tag liefern. Eine eigene JSF-Komponente wird in diesem Beispiel nicht benötigt, da es sich bei der AJAX-Komponente um eine gewöhnliche Texteingabe handelt, sodass die Standard-JSF-Komponente wieder verwendet wird. Als Faustregel kann gesagt werden, dass neue Komponenten nur dann programmiert werden sollten, wenn es für das gewünschte Verhalten keine Komponente am Markt gibt. Der neue AJAX-Tag (Listing 4) verbindet also die Standard-Komponente mit dem neuen AJAX-Renderer. Er besitzt die Eigenschaften value und resourceURL und deren setter-Methoden, die den entsprechenden Attributwert aus der JSP entgegennehmen. Innerhalb der Methode getComponentType() wird die Verwendung der Standard-Eingabekomponente (javax.faces.Input) bestimmt. Die Methode getRendererType() gibt den Typ des eigens programmierten Renderers an. Innerhalb der Methode setProperties() werden die Werte des Tags an die JSF-Komponente übermittelt. Das Attribut resourceURL ist für unseren Fall eingeführt worden, um den URL des Servlets flexibel zu halten. Diese Eigenschaft ist allerdings nicht im Standard enthalten. Das stellt aber kein Problem dar, denn über die Anweisung component.getAttributes().put(attrName, value) können den JSF-Komponenten beliebige Attribute übergeben werden. Das Rendering der AJAX-Komponente wird durch den Renderer des Listings 5 übernommen. Die Methode decode() bewerkstelligt die Verarbeitung des HTTP-Requests, nach dem Drücken des Senden-Buttons. Die abgesendete Eingabe wird aus einer Map-Datenstruktur, die alle HTTP-Parameter enthält, gelesen und mit der Methode setSubmittedValue() an die Komponente übergeben. Diese Methode sollte nur innerhalb der decode()-Methode genutzt werden.

  1. Listing 5
  2. ----------------------------------------------------------------------
  3. package net.wessendorf.jm.faces.ajax.component.simple;
  4. import java.io.IOException;
  5. import java.util.Map;
  6. import javax.faces.component.*;
  7. import javax.faces.context.*;
  8. import javax.faces.render.Renderer;
  9. import net.wessendorf.jm.faces.ajax.common.JSHelper;
  10. public class InputSuggestRenderer extends Renderer {
  11. private final static String DIV_ID = "results";
  12. public void decode(FacesContext context, UIComponent component) {
  13. ExternalContext external = context.getExternalContext();
  14. Map params = external.getRequestParameterMap();
  15. UIInput input = (UIInput) component;
  16. String clientId = input.getClientId(context);
  17. String submittedValue = (String)params.get(clientId);
  18. input.setSubmittedValue(submittedValue);
  19. }
  20. public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
  21. this.encodeResources(context, component);
  22. super.encodeBegin(context, component);
  23. ResponseWriter out = context.getResponseWriter();
  24. String clientId = component.getClientId(context);
  25. out.startElement("input", component);
  26. out.writeAttribute("name", clientId, null);
  27. out.writeAttribute("id", clientId, null);
  28. out.writeAttribute("autocomplete", "off", null);
  29. out.endElement("input");
  30. out.startElement("div", null);
  31. out.writeAttribute("id", DIV_ID, null);
  32. out.writeAttribute("class", "ajax", null);
  33. out.endElement("div");
  34. out.startElement("script", null);
  35. out.writeAttribute("type", "text/javascript", null);
  36. out.writeText("\ntry {\n" +
  37. "new Ajax.Autocompleter('"+component.getClientId(context)+"', '"+DIV_ID+"', '"+component.getAttributes().get("resourceURL")+"');\n" +
  38. "} catch(e) {\n" +
  39. "alert(e.message);\n"+
  40. "}", null);
  41. out.endElement("script");
  42. }
  43. private void encodeResources(FacesContext context, UIComponent component) throws IOException {
  44. JSHelper.addJavaScript(InputSuggestRenderer.class, "controls.js", "resourcefilter?script=", context);
  45. ...
  46. }
  47. }
HTML-Schreiberlein
Die Methode encodeBegin() generiert das notwendige HTML. Als Erstes wird das Eingabefeld gerendert, dessen autocomplete-Attribut auf off gesetzt wird. Nach der Ausgabe des <div>-Elements folgt der interessanteste Teil, die JavaScript-Anweisung zum Absenden des AJAX-Requests. Der Autocompleter-Klasse werden innerhalb des Renderers dynamisch die drei Parameter übergeben. Die ID des Eingabefeldes wird durch die Anweisung component.getClientId(context) gewonnen. Für das <div>-Element wird dessen ID aus der Konstante DIV_ID ausgelesen und die URL des Servlets wird über die Map-Datenstruktur der Methode getAttributes() direkt aus der JSF-Komponente ausgelesen. Die Renderer-Klasse hat noch eine weitere Methode, die private Methode encodeResources(). Sie nutzt die eigens implementierte Hilfsklasse JSHelper, um die Ressourcen wie CSS- oder JavaScript-Dateien auszuliefern. Die statischen Methoden addJavaScript() und addCSS() machen nichts anderes, als URLs für JavaScript oder CSS zu rendern. Die Auslieferung erfolgt über einen Servlet-Filter. Die Idee zu einem Servlet-Filter für die Auslieferung von Ressourcen stammt aus dem Apache-MyFaces-Projekt. Allerdings sei angemerkt, dass ein Servlet-Filter innerhalb einer Portlet-Umgebung zu Problemen führen kann. Doch nun fragt sich der angehende Component Writer: "Wohin mit den JavaScript- und CSS-Dateien?". Die Antwort ist einfach: sie werden im gleichen Paket abgelegt wie die Klassen der AJAX-Komponente. Das hat den Vorteil, dass der Component Writer eine einzige JAR-Datei, statt etlicher Dateien, ausliefert. Konfiguriert wird die Komponente innerhalb der faces-config.xml-Datei.

Teil 1   Teil 2   Teil 3   

Kommentare