26.02.2013
JSF - <h:selectOneListbox/>
-Beispiel
In dem folgenden Beispiel wird erklärt, wie eine selectOneListbox
mit einer Liste von Datenbank-Entities verbunden wird. Genauer gesagt, soll innerhalb einer JSF-Seite eine Auswahlbox angezeigt werden, die ihre SelectItem
-Elemente über eine Service-Schicht (Controller) direkt aus einer Datenbank bezieht. Zwischen der JSF-Oberfläche und dem Service wird eine Liste mit Pojos/Beans/Entites übergeben (z.B. List<Customer>
). Da die Customer
-Elemente nicht so einfach übertragen werden können, kommt ein javax.faces.convert.Converter
(@FacesConverter
) zum Einsatz.
Als Beispiel wird eine JSF-Seite erstellt, die eine Liste mit Customer
-Objekten anzeigt. Aus der Liste soll ein Customer
ausgewählt und mit einem commandButton
(Select
) bestätigt werden. Daraufhin wird über ein outputLabel
der ausgewählte Customer
unterhalb der selectOneListbox
angezeigt.
Prinzipiell ist das Verbinden der SelectBox mit dem Datenbank-Entity kein Problem. Eine Sache, die allerdings beachtet werden muss ist, dass die Methoden equals
und hashCode
der Entity
-Klasse überschrieben (@Overwrite
) werden müssen. Der Grund dafür ist, dass in der generierten HTML-Seite nur String
s angezeigt werden. D.h. diese String
s werden nach dem Klick auf den Select
-Button auch wieder zurück an den Server geschickt und müssen dort natürlich zugeordnet werden können. Dafür werden die Methoden equals
und hashCode
benötigt.
Die fertige Beispielanwendung ist auf folgenden Screenshot zu sehen:

Es wird also eine <h:SelectOneListbox/>
erstellt, über die ein selectItem
ausgewählt werden kann. Durch ein Klick auf den Button Select wird der Text hinter Select item: angepasst.
Bei ersten Start wird allerdings eine Seite mit einem Button Create Customers angezeigt. Durch einen Klick auf den Button werden einige Entity-Elemente vom Type Customer
in der Datenbank angelegt.

Als erstes wird ein Entity von Type Customer
angelegt:
package org.hameister.selectbox; import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; @NamedQueries ({ @NamedQuery(name = "findAllCustomers", query = "SELECT c FROM Cust c"), @NamedQuery(name = "findCustomerByName", query = "SELECT c FROM Cust c WHERE c.customerName = :customerName") }) @Entity(name="Cust") public class Customer implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String customerName; public Customer() { } public Customer(String customerName) { this.customerName = customerName; } public String getCustomerName() { return customerName; } public void setCustomerName(String customerName) { this.customerName = customerName; } /** * Needed for the binding with the h:selectOneListbox. */ @Override public boolean equals(Object other) { return (other instanceof Customer) && (customerName != null) ? customerName.equals(((Customer) other).customerName) : (other == this); } /** * Needed for the binding with the h:selectOneListbox. */ @Override public int hashCode() { return (customerName != null) ? (this.getClass().hashCode() + customerName.hashCode()) : super.hashCode(); } }
Der Customer
besitzt nur das Attribute customerName
. Zu beachten sind die NamedQueries
am Anfang der Klasse. Diese werden von der Service-Schicht (CustomerService
) verwendet. Wichtig sind die Methoden equals
und hashCode
. Sie müssen überschrieben werden, damit das Binding zwischen JSF-Seite, Konverter und Entity funktioniert.
Als nächstes wird ein @FacesConverter
für die Klasse Customer
angelegt. In dem Konverter muss das Interface javax.faces.convert.Converter
mit seinen beiden Methoden implementiert werden.
package org.hameister.selectbox; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.convert.Converter; import javax.faces.convert.FacesConverter; import javax.inject.Inject; import javax.inject.Named; @FacesConverter(forClass=Customer.class) @Named public class CustomerConverter implements Converter { @Inject CustomerService customerService; @Override public Object getAsObject(FacesContext context, UIComponent component, String customerName) { return customerService.findCustomerByName(customerName); } @Override public String getAsString(FacesContext context, UIComponent component, Object customer) { return ((Customer)customer).getCustomerName(); } }
Zum Registrieren des Konverters wird die Annotation @FacesConverter
verwendet. Damit der Konverter in der JSF-Seite erreichbar ist, wird die Annotation @Named
benötigt.
Die Methode getAsObject
wandelt den String
in ein Object von Type Customer
um. Dazu wird der customerName
verwendet, um den customerService
nach dem Customer
-Objekt zu fragen.
Der customerService
wird per Dependency-Injection (CDI) in den Converter
injiziert.
Die zweite Methode getAsString
konvertiert das Objekt customer
in einen String
. Dazu wird einfach die Methode getCustomerName()
aufgerufen.
Zum Schluss wird noch eine Stateless Session Bean benötigt, die als Controller zwischen der JSF-View und JPA-Model dient.
package org.hameister.selectbox; import java.io.Serializable; import java.util.List; import javax.ejb.Stateless; import javax.inject.Named; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Stateless @Named public class CustomerService implements Serializable { private Customer customer; @PersistenceContext EntityManager em; public List<Customer> getCustomers() { return em.createNamedQuery("findAllCustomers").getResultList(); } public Customer findCustomerByName(String name) { List<Customer> customers = em.createNamedQuery("findCustomerByName").setParameter("customerName", name).getResultList(); if (customers != null && customers.size() >= 1) { return customers.get(0); } return null; } public Customer getCustomer() { return customer; } public void setCustomer(Customer customer) { this.customer = customer; } public void store() { System.out.println("======> Store customer:" + customer.getCustomerName()); } public void createTestCustomers() { em.persist(new Customer("Customer 1")); em.persist(new Customer("Customer 2")); em.persist(new Customer("Customer 3")); em.persist(new Customer("Customer 4")); em.persist(new Customer("Customer 5")); } }
In der Variable customer
wird der in der SelectOneListbox
ausgewählte Customer
abgespeichert. Für diese Variable wird ein getter und ein setter benötigt, damit die JSF-Seite darauf zugreifen kann. Für den Zugriff auf die Datenbank wird mit der Annotation @PersistenceContext
der EntityManager
mittels CDI injiziert. Damit die JSF-Seite auf die Customer
-Objekte zugreifen kann, werden diese in der Methode getCustomers()
mit einer NamedQuery
ausgelesen. Die Methode findCustomerByName
wird, wie oben schon beschrieben, vom Konverter aufgerufen. Die Funktion store()
wird vom select
-Button aufgerufen, wenn er angeklickt wird. Mit der Methode createTestCustomer()
werden fünf Customer
-Objekte erstellt und in der Datenbank abgespeichert. Diese Methode wird von dem Create Customer
-Button aufgerufen, der nur angezeigt wird, wenn noch keine Daten in der Datenbank sind.
Was jetzt noch fehlt ist die JSF-Seite, d.h. die View mit der selectOneListBox
.
<?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" xmlns:c="http://java.sun.com/jsp/jstl/core"> <h:head> <title>Item Selection with selectOneListbox</title> </h:head> <h:body> <h:form> <c:choose> <c:when test="${empty customerService.customers}"> <h:commandButton action="#{customerService.createTestCustomers()}" value="Create Customers"/> </c:when> </c:choose> <br/> <c:choose> <c:when test="${not empty customerService.customers}"> <p> <strong><h:outputLabel value="Please select an item: "/></strong> <h:selectOneListbox size="1" value="#{customerService.customer}" converter="#{customerConverter}"> <f:selectItems value="#{customerService.customers}" var="c" itemLabel="#{c.customerName}" itemValue="#{c}"/> </h:selectOneListbox> <h:commandButton action="#{customerService.store()}" value="Select"/> </p> <p> <strong><h:outputLabel value="Selected item: "/></strong> <h:outputLabel value="#{customerService.customer.customerName}"/> </p> </c:when> </c:choose> </h:form> </h:body> </html>
Im oberen choose
-Block wird der Button Create Customers
nur eingeblendet, wenn sich noch keine Customer
-Objekte in der Datenbank befinden.
Falls sich schon Objekte in der Datenbank befinden, wird ein outputLabel
, die selectOneListbox
und ein commandButton
in der ersten Zeile angezeigt. In der zweiten Zeile werden zwei outputLabel
s benutzt, um den selektierten Customer
anzuzeigen.
Bei der selectOneListBox
sind folgende Attribute zu beachten:
selectOneListbox
:value="#{customerService.customer}"
: Das ist das Binding an die VariableCustomer
in demCustomerServer
, welches die getter und setter aufruft.converter="#{customerConverter}"
: Damit wird der Konverter verbunden.
selectItems
:value="#{customerService.customers}"
: Aufruf vongetCustomers()
imCustomerService
, d.h. das Befüllen der Combobox.var="c"
: Das "selektierte"Customer
-Objekt, welches mit dem setter an denCustomerService
übergeben wird.itemLabel="#{c.customerName}"
: Auslesen descustomerName
zum Anzeigen in der Combobox.itemValue="#{c}"
: Der Wert, d.h. dasCustomer
-Objekt
Die Datei persistenc.xml
für Eclipselink sieht folgendermaßen aus. Allerdings funktioniert das Beispiel nach kleinen Anpassungen auch mit Hibernate.
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> <persistence-unit name="SelectBoxExamplePU" transaction-type="JTA"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <jta-data-source>jdbc/sample</jta-data-source> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/> </properties> </persistence-unit> </persistence>