JSFmatters: Immer dem Navi nach

Die Navigation zwischen den Seiten einer JSF-Anwendung wird durch so genannte Navigation Rules in der Datei faces-config.xml definiert. Dieser Blog-Eintrag demonstriert, wie man durch eine kleine Erweiterung solche Navigation Rules auch für echte Links auf einer JSF-Seite nutzbar machen kann.

Die zentrale Klasse für die Navigation in JSF ist der NavigationHandler. Jede JSF-Implementierung muss einen Standard-NavigationHandler bereitstellen, der die Navigation Rules in der faces-config.xml auswerten und verarbeiten kann. Insofern könnte die reichlich umständliche XML-Konfiguration ohne Weiteres durch etwas besseres ersetzt werden (wobei sich offenbar bisher noch nichts eindeutig besseres gefunden hat). Doch darum soll es hier nicht gehen.

Mit Hilfe der Navigation Rules wird eine Trennung zwischen logischem Ergebniss einer Aktion und der anzuzeigenden konkreten Seite ermöglicht. Auf welchen Wegen sich ein Benutzer durch eine JSF-Anwendung klicken kann, lässt sich zentral aus den Navigation Rules in der Datei faces-config.xml ablesen.

In der Regel wird JSF-Einsteigern dann auch im allerersten Hello-World-Beispiel beigebracht, dass ein einfacher Navigationslink mit einem h:commandLink

<h:commandLink action="contact" value="Kontakt" />

und einer entsprechenden Regel für das abstrakte Ziel "contact"

<navigation-rule> <from-view-id>/index.xhtml</from-view-id> <navigation-case> <from-outcome>contact</from-outcome> <to-view-id>/contact.xhtml</to-view-id> </navigation-case> </navigation-rule>

realisiert wird.

Der Haken an der Sache ist, dass dieses Vorgehen immer ein Formular und einen POST-Request erfordert, selbst wenn es gar keine weiteren Formulardaten gibt. Für einfache Navigationslinks wie den oben skizzierten ist das jedoch ziemlicher Overkill. Hier wird unnötigerweise ein State gespeichert und rekonstruiert sowie der gesamte JSF-Lifecycle durchlaufen. Zudem verhalten sich durch h:commandLink erzeugte Links nicht wie normale Links im Web, da sie per Javascript das Formular abschicken und sich nicht in einem neuen Browser-Fenster oder -Tab öffnen lassen. Zu allem Überfluss hinkt die im Browser angezeigte URL immer einen Request hinterher, was die Verwendung von Bookmarks unmöglich macht (es sei denn, man setzt redirects ein, was jedoch weitere Performance kostet und die Sache nicht wirklich besser macht).

Die übliche Strategie zur Vermeidung dieses Problems besteht darin, anstelle von h:commandLink einfache Links auf die Zielseite einzusetzen. JSF bietet dazu das Element h:outputLink, in vielen Fällen tut es das einfache HTML-a aber auch. Nachteilig ist hier, dass nun doch konkrete Linkziele in den Seiten der Webanwendung auftauchen. Außerdem muss man absolute Links einigermaßen kompliziert selbst zusammenbauen, damit der Servlet-Pfad im Link korrekt enthalten ist:

<a href="#{facesContext.externalContext.requestContextPath}/contact.html"> Kontakt </a>

Wäre es nicht praktisch, wenn man beides haben könnte? Wenn man normale HTML-Links zusammen mit den zentralen Navigationsregeln benutzen könnte?

Ein Blick auf die Klasse NavigationHandler zeigt, dass es nicht ganz so trivial ist. Diese enthält nämlich als einzige Methode handleNavigation, welche das Ergebnis der Navigation (den neuen View) sofort als Komponentenbaum aufbaut und im übergebenen FacesContext setzt. Wenn man so will, führt handleNavigation die Navigation zur neuen Seite sofort aus. Tatsächlich benötigen wir von unserem JSF-Navi aber nur die Auskunft über das neue Ziel, ohne dass wir sofort dorthin abbiegen.

Schauen wir uns anhand der MyFaces-Implementierung an, was in handleNavigation genau passiert. Nachdem die passende Navigations­regel bestimmt wurde, werden die folgenden Zeilen ausgeführt:

ViewHandler viewHandler = facesContext.getApplication().getViewHandler(); String newViewId = navigationCase.getToViewId(); UIViewRoot viewRoot = viewHandler.createView(facesContext, newViewId); facesContext.setViewRoot(viewRoot); facesContext.renderResponse();

Die Variable facesContext wird von außen als Parameter übergeben. Mit einem speziell präparierten eigenen FacesContext kann man die Aufrufe setViewRoot und renderResponse abfangen, sodass der richtige FacesContext davon nicht betroffen ist. Mit diesem Trick können wir nun zwar die angestoßene Navigation verhindern, jedoch noch nicht den vollständigen Aufbau des neuen Komponentenbaums (durch createView), der hier nämlich gar nicht benötigt wird.

Glücklicherweise lässt sich der dazu verwendete ViewHandler in JSF leicht umkonfigurieren, sodass wir hier ebenfalls eine eigene Implementierung verwenden können, die sich abhängig vom übergebenen FacesContext unterschiedlich verhält.

In der Datei faces-config.xml tragen wir dazu das Folgende ein:

<application> <view-handler>com.sun.facelets.FaceletViewHandler</view-handler> <view-handler>de.mindmatters.faces.NavigationViewHandler</view-handler> </application>

Hier wird zunächst Facelets als View-Technologie und als zweites dann unser eigener ViewHandler (de.mindmatters.​faces.​NavigationViewHandler) konfiguriert. Beide sind in einer Kette hintereinandergeschaltet, sodass der NavigationViewHandler immer zuerst angesprochen wird und die eigentliche Arbeit an den FaceletViewHandler delegieren kann.

Der NavigationViewHandler erbt von der in JSF dazu vorgesehenen Klasse ViewHandlerWrapper und überschreibt die Methode createView wie folgt:

public UIViewRoot createView(FacesContext context, String viewId) { UIViewRoot root; if (context instanceof FacesContextFake) { root = new UIViewRoot(); root.setViewId(viewId); } else { root = super.createView(context, viewId); } return root; }

Wie man sieht, unterscheidet die Methode createView anhand des FacesContext, ob sie tatsächlich einen vollständigen Baum aufbauen muss oder ob es für unsere Navigationslinks ausreicht, nur die ID in einer ansonsten leeren Wurzel zu erzeugen. Zu diesem Zweck müssen wir noch die Klasse FacesContextFake schreiben. Leider bietet JSF hier keine Hilfs-Klasse FacesContextWrapper, sodass wir gezwungen sind, direkt von FacesContext abzuleiten und sämtliche Methodenaufrufe selbst zu delegieren. Die Ausnahmen davon sind getViewRoot und setViewRoot, die auf eine entsprechende Member-Variable zugreifen, sowie renderResponse, welche in diesem Fall nichts tut.

Jetzt fehlt nur noch eine Stelle, an der wir mit Hilfe des NavigationHandlers unsere Navigationslinks erzeugen können. Eine einfache Möglichkeit ist eine Facelets-Funktion, die in einer statischen Methode wie folgt implementiert wird:

public static String toViewTarget(String outcome) { FacesContext facesContext = FacesContext.getCurrentInstance(); ExternalContext externalContext = facesContext.getExternalContext(); NavigationHandler handler = facesContext.getApplication() .getNavigationHandler(); FacesContextFake fakeContext = new FacesContextFake(facesContext); handler.handleNavigation(fakeContext, null, outcome); String viewId = fakeContext.getViewRoot().getViewId(); // Dateiendung anpassen String suffix = externalContext .getInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME); if (viewId.endsWith(suffix)) { String currentPath = externalContext.getRequestServletPath(); viewId = viewId.substring(0, viewId.lastIndexOf(suffix)) + currentPath.substring(currentPath.lastIndexOf('.')); } return externalContext.getRequestContextPath() + viewId; }

Wie zu sehen ist, erzeugt toViewTarget eine neue Instanz unseres FacesContextFake und übergibt diese zusammen mit dem abstrakten Navigationsziel outcome an den NavigationHandler. Anschließend können wir vom FacesContextFake die neue viewId erfragen. Für einen korrekten Link müssen wir noch die Dateiendung der viewId entsprechend anpassen (also beispielsweise .xhtml durch .html ersetzen) und schließlich den Kontextpfad voranstellen.

Auf einer JSF-Seite lässt sich die neue Funktion (nach entsprechender Konfiguration in der Datei taglib.xml) dann folgendermaßen aufrufen:

<a href="#{n:toViewTarget('contact')}">Kontakt</a>

Damit haben wir das Ziel erreicht und können einen normalen HTML-Link verwenden, dessen Linkziel sich aus den Navigation Rules ableitet.

Wird mit JSF 2.0 nun alles besser?

Zum einen stellt sich überhaupt die Frage nach der Zukunft der Navigation Rules. Gemäß des von Rails so erfolgreich demonstrierten Prinzips Convention over Configuration sollten nur noch die Ausnahmen konfiguriert werden müssen. Für die Standardfälle könnte man beispielsweise den Namen der Zielseite aus dem logischen Ergebnis­string ableiten (siehe hierzu XML-less JSF Navigations). Unabhängig von Art und Umfang der Konfiguration wird jedoch die Klasse NavigationHandler bestehen bleiben. Angedacht (siehe Issue 179 für JSF 2.0) ist beispielsweise eine neue Methode getViewId, mit der die beiden selbstgeschriebenen Hilfsklassen NavigationViewHandler und FacesContextFake überflüssig werden.

Übrigens ist in der aktuellen MyFaces-Implementierung 1.2.5 die Methode getView bereits enthalten. Um sie zu benutzen, muss man jedoch den über den FacesContext erhaltenen NavigationHandler explizit auf die MyFaces-Implementierungklasse casten und programmiert dann nicht mehr gegen die offizielle JSF-API.

Kommentare zu diesem Eintrag sind geschlossen.