20.01.2013

Java Server Faces - JSF mit Ajax

In diesem Artikel wird kurz beschrieben, wie man eine JSF-Seite um Ajax erweitert, so dass bei Änderungen nicht die komplette Seite neu geladen werden muss, sondern nur der Teil, der sich auch wirklich geändert hat. Dies wird an dem "bekannten" Beispiel eines Einkaufswagens (Shopping-Cart) erklärt. Dieser Artikel ist keine Einführung in die JSF-Programmierung oder in Ajax. Er soll vielmehr zeigen, wie die grundlegende Funktionsweise von JSF-Seiten und Ajax ist. In den folgenden Artikeln zum Thema Java EE6 Web-Client werden andere Technologien vorgestellt, um Web-Oberflächen und Web-Clients zu erstellen.

Die fertige Anwendung, die in diesem Artikel erstellt wird, sieht im Browser folgendermaßen aus:

Auf der linken Seite lassen sich Artikel auswählen und über die kleinen Pfeile in den Einkaufswagen auf der rechten Seiten legen oder auch wieder entfernen. Mit dem Button Warenkorb leeren werden alle Artikel aus dem Warenkorb entfernt und wieder in der Artikelliste angezeigt.

Die folgende Abbildung zeigt den Ablauf einer Anfrage an den Web-Container und die Antwort an den Client.

Man sieht, dass ein Request vom Browser, in dem der Web-Client läuft, an den Web-Container, in dem das Faces-Servlet läuft, gestellt wird. Das Faces-Servlet dient in diesem Kontext als Controller und arbeitet die Business-Logik ab. Dann wird das Erstellen der Antwort an die JSF-Seite weitergeleitet. Die View, die der Web-Container an den Browser als Response zurückgeliefert, wird also durch das Verarbeiter der JSF-Seite erstellt. Wichtig ist, dass beim Übergang vom Faces-Servlet zur JSF-Seite, der JSF-Lifecycle durchlaufen wird. Das Ergebnis ist eine fertige HTML-Seite, die an den Browser gesendet werden kann. (Anmerkung: Die Darstellung ist sehr vereinfacht. Der JSF-Lifecycle wäre einen eigenen Artikel wert.)

Der zugehörige Java-Quellcode sieht folgendermaßen aus:

package org.hameister.shoppingcarts;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.enterprise.context.SessionScoped;
import javax.faces.event.ActionEvent;
import javax.inject.Named;

/**
 *
 * @author hameister
 */
@Named
@SessionScoped
public class CartJSFAjax implements Serializable {

    // All available items
    private List<String> items;
    // List of items in the cart
    private List<String> shoppingCart;

    private String[] selectedItems;
    private String[] selectedCartItems;

    @PostConstruct
    private void initLists() {
        items = new ArrayList<String>();
        shoppingCart = new ArrayList<String>();

        items.add("iPhone 5");
        items.add("iPhone 4S");
        items.add("iPad mini");
        items.add("MacBook Pro Retina");
        items.add("MacBook Pro");
    }

    public void putInShoppingCart(ActionEvent e) {
        if(selectedItems != null) {
            for(String item : selectedItems) {
                items.remove(item);
                shoppingCart.add(item);
            }
        }
    }

    public void removeFromShoppingCart(ActionEvent e) {
        if(selectedCartItems != null) {
            for(String item : selectedCartItems) {
                shoppingCart.remove(item);
                items.add(item);
            }
        }
    }

    public List<String> getItems() {
        return items;
    }

    public List<String> getShoppingCart() {
        return shoppingCart;
    }


    public String[] getSelectedItems() {
        return selectedItems;
    }

    public void setSelectedItems(String[] selectedItems) {
        this.selectedItems = selectedItems;
    }

    public String[] getSelectedCartItems() {
        return selectedCartItems;
    }

    public void setSelectedCartItems(String[] selectedCartItems) {
        this.selectedCartItems = selectedCartItems;
    }

    public void reset() {
        initLists();
    }

}

Was bei dieser Klasse sofort auffällt ist, dass sie sehr einfach und übersichtlich ist. (im Vergleich zu der JSP- oder auch Servlet-Lösung). Über die Annotation @Named wird mittels Dependency Injection dafür gesorgt, dass die Klasse von der JSF-Seite erreichbar ist. Und die Annotation @SessionScoped ist dafür zuständig, dass für jede neue Session ein neues Objekt der Klasse erstellt wird.

Zu beachten sind außerdem die Funktionen putInShoppingCart und removeFromShoppingCart, die aufgerufen werden, wenn die Pfeil-Buttons angeklickt werden.

Diese beiden Funktionen widerum greifen auf Arrays zu, in denen sich selektierte Artikel (items) befinden. In dem Array selectedCartItems sind alle ausgewählten Artikel des Einkaufswaren enthalten. Das Array selectedItems enthält die selektierten Artikel aus der Artikelliste. Die vier Methoden, die die selektierten Artikel setzen und abfragen, werden durch die <h:selectManyListbox>-Komponenten beim Auswählen von Artikeln durch den Benutzer der JSF-Seite angesprochen.

Die folgende XML-Datei enthält die JSF-Seite.

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:head>
        <title>Ajax Shopping Cart</title>
        <h:outputStylesheet library="css" name="cart.css"/>
    </h:head>
    <h:body>
        <h:form id="shoppingCartForm">
            <h:panelGrid styleClass="cartTable" columns="3" rowClasses="cartRows">
                <!-- ZEILE 1 -->
                <h:outputLabel value="Artikel Liste" styleClass="cartHeader"/>
                <h:outputLabel value="" styleClass="cartHeader"/>
                <h:outputLabel value="Warenkorb" styleClass="cartHeader"/>

                <!-- ZEILE 2 -->
                <h:selectManyListbox id="itemList" value="#{cartJSFAjax.selectedItems}" styleClass="cartSelect" size="4">
                    <f:selectItems value="#{cartJSFAjax.items}"/>
                </h:selectManyListbox>

                <h:panelGroup id="buttons">
                    <h:commandButton id="removeButton" value="&larr;" actionListener="#{cartJSFAjax.removeFromShoppingCart(e)}">
                        <!-- execute contains informations for the server; selected items of the selectManyListbox with the id cartItemList; call actionlistener -->
                        <!-- render contains all elements which must be rendered after the change -->
                        <f:ajax execute="shoppingCartForm:cartItemList" render="shoppingCartForm:itemList shoppingCartForm:cartItemList"/>
                    </h:commandButton>
                    <h:commandButton id="addButton" value="&rarr;" actionListener="#{cartJSFAjax.putInShoppingCart(e)}">
                        <f:ajax execute="shoppingCartForm:itemList" render="shoppingCartForm:itemList shoppingCartForm:cartItemList"/>
                    </h:commandButton>
                </h:panelGroup>

                <h:selectManyListbox id="cartItemList" value="#{cartJSFAjax.selectedCartItems}" styleClass="cartSelect" size="4">
                    <f:selectItems value="#{cartJSFAjax.shoppingCart}"/>
                </h:selectManyListbox>

                <!-- ZEILE 3 -->
                <h:outputLabel value=""/>
                <h:outputLabel value=""/>
                <h:commandButton id="resetButton" value="Warenkorb leeren" actionListener="#{cartJSFAjax.reset}">
                    <f:ajax execute="shoppingCartForm:cartItemList shoppingCartForm:itemList" render="shoppingCartForm:itemList shoppingCartForm:cartItemList"/>
                </h:commandButton>
            </h:panelGrid>

        </h:form>
    </h:body>
</html>

Zum Aufbau der JSF-Seite läßt sich erstmal folgendes sagen:

  • Die JSF-Seite besteht aus einer <form> mit dem Attribut id und dem Wert shoppingCartForm. In diese ist ein <h:panelGrid> mit drei Spalten eingebettet. Die erste Zeile des <h:panelGrid> enthält drei <h:outputLabel> (ein Label für jede Spalte).
  • Die nächste Zeile besteht aus einer <h:selectManyListbox>, einer <h:panelGroup> und noch einer <h:selectManyListbox>.
  • Die letze Zeile enthält zwei <h:outputLabel>s ohne Inhalt und einen <h:commandButton>.

Die <h:selectManyListbox>en funktionieren folgendermaßen:

  • Sie benötigen eine id damit aus der <h:panelGroup> darauf zugegriffen werden kann.
  • Über value werden die selektierten Elemente bei der Bean abgefragt. (Aufruf der getter und setter).
  • Die styleClass greift auf die css-Datei (siehe unten) zu und sorgt für das Aussehen der Komponente.
  • Das Attribut size legt fest, dass nur 4 Zeilen in der Listbox zu sehen sind.
  • In der <h:selectManyListbox> ist ein Element selectItems enthalten, welches über das Attribut value die Werte vom Bean abfragt.

Die <h:panelGroup> besteht aus zwei <h:commandButton>s mit den kleinen Pfeil-Symbolen, die ein Verschieben der Artikel ermöglichen. Funktionieren tun die Buttons folgendermaßen:

  • Das Attribute id ist in dem Beispiel eher unwichtig.
  • Das Attribute value enthält den Text des Buttons. In dem Beispiel sind es die kleinen Pfeile.
  • Wichtig ist der actionListener, der beim Klick auf einen Button die Methode im Bean aufruft.
  • Innerhalb des <h:commandButton>s befindet sich das Ajax-Element, welches dafür sorgt, dass nicht die gesamte Seite neu geladen wird, wenn sich etwas ändert. D.h., wenn ein Button angeklickt wird.
  • Das Attribut execute stellt die Verbindung zu den <h:selectManyListbox>-Elementen her. Das Format ist folgendes: FORM_ID:SELECT_MANY_LISTBOX_ID.
  • Im Attribut render werden alle Elemente aufgeführt, die nach dem Klick auf den Button aktualisiert werden müssen.

Mit diesen Informationen ist klar, wie der Reset-Button in der letzten Zeile funktioniert.

Damit der Warenkorb "ansprechender" aussieht, wurde mittels eines CSS-Stylesheets die Breite der Komponenten und die Farben angepaßt. Die CSS-Datei muss sich in einem Ordner css im Verzeichnis resources befinden und den Namen cart.css haben, damit sie von der JSF-Seite gefunden wird.

TABLE.cartTable {
    width: 1000px;
}

.cartSelect {
    width: 400px;
}

.cartHeader{
    text-align:center;
    font-size: 16px;
    font-weight: bold;
}

.cartRows{
    text-align:center;
    background:none repeat scroll 0 0 #97ff83;
    border-top:1px solid #BBBBBB;
}

Weitere Informationen

Weitere Informationen zu Web-Clients findet man in den folgenden Artikeln:

  1. Einleitung - Teil 1
  2. Servlets - Teil 2
  3. JSP-Seiten - Teil 3
  4. JSP-Seiten mit JSTL - Teil 4
  5. JSF-Seiten - Teil 5
  6. JSF-Seiten mit Ajax - Teil 6
  7. JSF-Templates - Teil 7