Showing posts with label SelectOneMenu. Show all posts
Showing posts with label SelectOneMenu. Show all posts

Thursday, August 7, 2008

Styling options in h:selectOneMenu

Introduction

Whenever you want to style a HTML <option> element using CSS, you could just use its style or, preferably, class attribute. But in the default Sun JSF Mojarra implementation there is no comparable attribute available for that. The h:selectOneMenu, h:selectManyMenu and f:selectItem tags simply doesn't support it.

When looking at comparable attributes in other elements, you'll notice that h:dataTable has an elegant approach in form of the rowClasses attribute which accepts a commaseparated string of CSS class names which are to be applied on the <tr> elements repeatedly. Now, it would be nice to let among others the h:selectOneMenu support a similar optionClasses attribute.

This can be achieved at two ways: overriding the default renderer class and using the f:attribute to add it as an external component attribute, or overriding the default renderer class, the component class and the tag class to let it support the optionClasses attribute. It might be obvious that the first way is a bit hacky, but it costs much less effort. The second way is more elegant, but it require more code and a custom tld file which should copy all existing component attributes over (tld files unfortunately doesn't know anything about inheritance). BalusC did it and the tld file was almost 500 lines long for only the selectOneMenu and selectManyMenu. Ouch.

This article will handle only the first approach in detail.

Back to top

ExtendedMenuRenderer

Here is how the extended MenuRenderer look like:

package net.balusc.jsf.renderer.html;

import java.io.IOException;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.model.SelectItem;

import com.sun.faces.renderkit.html_basic.MenuRenderer;

/**
 * Extended menu renderer which renders the 'optionClasses' attribute above the standard menu
 * renderer. To use it, define it as follows in the render-kit tag of faces-config.xml.
 * 
 * <pre>
 * &lt;renderer&gt;
 *     &lt;component-family&gt;javax.faces.SelectOne&lt;/component-family&gt;
 *     &lt;renderer-type&gt;javax.faces.Menu&lt;/renderer-type&gt;
 *     &lt;renderer-class&gt;net.balusc.jsf.renderer.html.ExtendedMenuRenderer&lt;/renderer-class&gt;
 * &lt;/renderer&gt;
 * &lt;renderer&gt;
 *     &lt;component-family&gt;javax.faces.SelectMany&lt;/component-family&gt;
 *     &lt;renderer-type&gt;javax.faces.Menu&lt;/renderer-type&gt;
 *     &lt;renderer-class&gt;net.balusc.jsf.renderer.html.ExtendedMenuRenderer&lt;/renderer-class&gt;
 * &lt;/renderer&gt;
 * </pre>
 * 
 * And define the 'optionClasses' attribute as a f:attribute of the h:selectOneMenu or 
 * h:selectManyMenu as follows:
 * 
 * <pre>
 * &lt;f:attribute name="optionClasses" value="option1,option2,option3" /&gt;
 * </pre>
 * 
 * It accepts a comma separated string of CSS class names which are to be applied on the options
 * repeatedly (the same way as you use rowClasses in h:dataTable). The optionClasses will be
 * rendered only if there is no 'disabledClass' or 'enabledClass' being set as an attribute.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/styling-options-in-hselectonemenu.html
 */
public class ExtendedMenuRenderer extends MenuRenderer {

    // Override -----------------------------------------------------------------------------------
    
    /**
     * @see com.sun.faces.renderkit.html_basic.MenuRenderer#renderOption(
     *      javax.faces.context.FacesContext, javax.faces.component.UIComponent,
     *      javax.faces.convert.Converter, javax.faces.model.SelectItem, java.lang.Object, 
     *      java.lang.Object[])
     */
    protected void renderOption(FacesContext context, UIComponent component, Converter converter,
        SelectItem currentItem, Object currentSelections, Object[] submittedValues)
            throws IOException
    {
        // Copied from MenuRenderer#renderOption() (and a bit rewritten, but that's just me) ------

        // Get writer.
        ResponseWriter writer = context.getResponseWriter();
        assert (writer != null);

        // Write 'option' tag.
        writer.writeText("\t", component, null);
        writer.startElement("option", component);

        // Write 'value' attribute.
        String valueString = getFormattedValue(context, component, currentItem.getValue(), converter);
        writer.writeAttribute("value", valueString, "value");

        // Write 'selected' attribute.
        Object valuesArray;
        Object itemValue;
        if (containsaValue(submittedValues)) {
            valuesArray = submittedValues;
            itemValue = valueString;
        } else {
            valuesArray = currentSelections;
            itemValue = currentItem.getValue();
        }
        if (isSelected(context, itemValue, valuesArray)) {
            writer.writeAttribute("selected", true, "selected");
        }

        // Write 'disabled' attribute.
        Boolean disabledAttr = (Boolean) component.getAttributes().get("disabled");
        boolean componentDisabled = disabledAttr != null && disabledAttr.booleanValue();
        if (!componentDisabled && currentItem.isDisabled()) {
            writer.writeAttribute("disabled", true, "disabled");
        }

        // Write 'class' attribute.
        String labelClass;
        if (componentDisabled || currentItem.isDisabled()) {
            labelClass = (String) component.getAttributes().get("disabledClass");
        } else {
            labelClass = (String) component.getAttributes().get("enabledClass");
        }

        // Inserted custom code which checks the optionClasses attribute --------------------------

        if (labelClass == null) {
            String optionClasses = (String) component.getAttributes().get("optionClasses");
            if (optionClasses != null) {
                String[] labelClasses = optionClasses.split("\\s*,\\s*");
                String indexKey = component.getClientId(context) + "_currentOptionIndex";
                Integer index = (Integer) component.getAttributes().get(indexKey);
                if (index == null || index == labelClasses.length) {
                    index = 0;
                }
                labelClass = labelClasses[index];
                component.getAttributes().put(indexKey, ++index);
            }
        }

        // The remaining copy of MenuRenderer#renderOption() --------------------------------------

        if (labelClass != null) {
            writer.writeAttribute("class", labelClass, "labelClass");
        }

        // Write option body (the option label).
        if (currentItem.isEscape()) {
            String label = currentItem.getLabel();
            if (label == null) {
                label = valueString;
            }
            writer.writeText(label, component, "label");
        } else {
            writer.write(currentItem.getLabel());
        }

        // Write 'option' end tag.
        writer.endElement("option");
        writer.writeText("\n", component, null);
    }

}

Configure it as follows in the faces-config.xml:

<render-kit>
    <renderer>
        <component-family>javax.faces.SelectOne</component-family>
        <renderer-type>javax.faces.Menu</renderer-type>
        <renderer-class>net.balusc.jsf.renderer.html.ExtendedMenuRenderer</renderer-class>
    </renderer>
    <renderer>
        <component-family>javax.faces.SelectMany</component-family>
        <renderer-type>javax.faces.Menu</renderer-type>
        <renderer-class>net.balusc.jsf.renderer.html.ExtendedMenuRenderer</renderer-class>
    </renderer>
</render-kit>

That's all!

Back to top

Basic demonstration example

And now a basic demonstration example how to use it.

The relevant part of the JSF file should look like:

<h:form>
    <h:selectOneMenu value="#{myBean.selectedItem}">
        <f:attribute name="optionClasses" value="option1, option2" />
        <f:selectItems value="#{myBean.selectItems}" />
    </h:selectOneMenu>
    <h:commandButton value="submit" action="#{myBean.submit}" />
</h:form>

Note the f:attribute: this sets the optionClasses attribute value which is been picked up by the ExtendedMenuRenderer. It will apply the given CSS style classes repeatedly on the rendered option elements. You can even use EL in it so that a backing bean can generate the desired String of comma separated CSS style classes based on some conditions.

The CSS styles are definied as follows:

option.option1 {
    background-color: #ccc;
}

option.option2 {
    background-color: #fcc;
}

Note that some web browsers wouldn't apply this on the selected option in the h:selectOneMenu. If desired, you need to add a style class for the <select> element then and apply it as h:selectOneMenu styleClass="className" then.

And now the demo backing bean code, just as usual. Nothing special here.

package mypackage;

import java.util.ArrayList;
import java.util.List;

import javax.faces.model.SelectItem;

public class MyBean {

    // Properties ---------------------------------------------------------------------------------
    
    private List<SelectItem> selectItems;
    private String selectedItem;

    {
        fillSelectItems();
    }

    // Actions ------------------------------------------------------------------------------------
    
    public void submit() {
        System.out.println("Selected item: " + selectedItem);
    }

    // Getters ------------------------------------------------------------------------------------
    
    public List<SelectItem> getSelectItems() {
         return selectItems;
    }

    public String getSelectedItem() {
         return selectedItem;
    }

    // Setters ------------------------------------------------------------------------------------
    
    public void setSelectedItem(String selectedItem) {
        this.selectedItem = selectedItem;
    }

    // Helpers ------------------------------------------------------------------------------------

    private void fillSelectItems() {
        selectItems = new ArrayList<SelectItem>();
        selectItems.add(new SelectItem("value1", "label1"));
        selectItems.add(new SelectItem("value2", "label2"));
        selectItems.add(new SelectItem("value3", "label3"));
        selectItems.add(new SelectItem("value4", "label4"));
        selectItems.add(new SelectItem("value5", "label5"));
    }

}

Just run it all and you'll see that the options are colored light gray and light red repeatedly!

All the stuff is build, compiled and tested successfully with Sun JSF Mojarra 1.2_09 and Apache Tomcat 6.0.14 in Eclipse Europa IDE. The output is rendered nicely in recent versions of all commonly used browsers (FF, IE, Opera and Safari).

Back to top

And the listboxes then?

Indeed, this won't work for the listboxes (among others h:selectOneListbox and h:selectManyListbox). But you can just follow the same approach and create a renderer which extends com.sun.faces.renderkit.html_basic.ListboxRenderer which is to be configured on the renderer-type of javax.faces.Listbox.

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) August 2008, BalusC

Wednesday, October 17, 2007

Populate child menu's

WARNING - OUTDATED CONTENT!

This article is targeted on JSF 1.2. For JSF 2.0, populating a child menu has become so much easier using <f:ajax>. See also this post for a code snippet.

Introduction

Having multiple h:selectOneMenu instances in one form which depends on each other and of which its values have to be obtained from the backing bean can drive JSF developers nuts. Especially if those are to be implemented in a form with at least one required field or if they are even required themselves. Validation errors ('Value not valid'), IllegalArgumentExceptions (at SelectItemsIterator#next()), unexpected submits (missing or wrong values), etcetera are flying around. You would almost become suicidal.

Back to top

Onchange, valueChangeListener, immediate, renderResponse and binding

To populate a child menu of which its contents is to be determined based on the value of the parent menu, you have to submit the form to the server on change of the parent menu. This can easily be achieved using the onchange attribute where you can specify some Javascript which have to be invoked when the menu is changed. Submitting the current form to the server using Javascript is simple, specifying "this.form.submit()" ought to be enough, or just submit() if you're lazy in typing.

You can use a valueChangeListener to retrieve the new value of the menu. But you of course want to prevent validation on the other required fields. This can technically be achieved by adding immediate="true" to all menu's so that all converters, validators and valuechange events of the menu's gets fired in the APPLY_REQUEST_VALUES phase instead of the PROCESS_VALIDATIONS phase and by adding the following line to the end of the valueChangeListener method:


FacesContext.getCurrentInstance().renderResponse();

This line will force a phase shift of the current phase to the RENDER_RESPONSE phase. When a valueChangeListener is invoked with immediate="true", then this line will cause the PROCESS_VALIDATIONS, UPDATE_MODEL_VALUES and INVOKE_APPLICATION being skipped, so that the other components which doesn't have immediate="true" set won't be converted, validated, applied nor invoked. Also see the former article Debug JSF lifecycle for more insights in the JSF lifecycle.

One concern is that the skipping of the UPDATE_MODEL_VALUES will also cause that the new values of the menu's which have immediate="true" set won't be set in the backing bean. This can partly be fixed by getting the new value from the ValueChangeEvent inside the valueChangeListener method and assign it to the appropriate property. But this won't work for other menu's of which the valueChangeListener isn't been invoked. This would cause problems if you select a child menu value and then select the parent menu back to null and then reselect it to same value again, the child menu which will show up again would remain the same selection instead of null while its child will not be rendered! To solve this we need to bind the menu's to the backing bean so that we can use UIInput#setValue() and UIInput#getValue() to set and get the actual values. The JSF lifecycle will set and get them in the RESTORE_VIEW and RENDER_RESPONSE phases respectively.

Back to top

Basic JSF code example

Here is a basic JSF code example which demonstrates three h:selectOneMenu components of which the listing of the next menu depends on the selection of the current menu. The next menu will be hidden until the selection of the current menu has a valid value. In this example we'll use a basic tree structure of area's (countries, cities and streets), which would make the most sense. There is also another required input field added to demonstrate the menu's working flawlessly in conjunction with other components.

One important detail to be mentioned is that the requireness of this menu group is set in a valueless h:inputHidden component instead of in the last menu. This is done so because of the fact that the last menu would not be rendered when its parent menu doesn't have a valid selection, so it would be pointless to set a required attribute on the last menu.

This example is developed and tested using JSF 1.2_05 in a Java EE 5.0 environment with a GlassFish V2 application server.

<h:form>
    <h:panelGrid columns="2">
        <h:outputText value="Choose area" />
        <h:panelGroup>
            <h:selectOneMenu
                binding="#{myBean.countryMenu}" converter="areaMenuConverter"
                onchange="this.form.submit();" valueChangeListener="#{myBean.changeCountryMenu}"
                immediate="true">
                <f:selectItem itemLabel="Please select country" />
                <f:selectItems value="#{myBean.countryItems}" />
            </h:selectOneMenu>
            <h:selectOneMenu
                binding="#{myBean.cityMenu}" converter="areaMenuConverter"
                onchange="this.form.submit();" valueChangeListener="#{myBean.changeCityMenu}"
                immediate="true" rendered="#{myBean.countryMenu.value != null}">
                <f:selectItem itemLabel="Please select city" />
                <f:selectItems value="#{myBean.cityItems}" />
            </h:selectOneMenu>
            <h:selectOneMenu
                binding="#{myBean.streetMenu}" converter="areaMenuConverter" 
                rendered="#{myBean.cityMenu.value != null}">
                <f:selectItem itemLabel="Please select street" />
                <f:selectItems value="#{myBean.streetItems}" />
            </h:selectOneMenu>
            <h:inputHidden
                required="#{myBean.streetMenu.value == null}" requiredMessage="Area is required." />
        </h:panelGroup>

        <h:outputText value="Enter input" />
        <h:inputText
            value="#{myBean.input}" 
            required="true" requiredMessage="Input is required." />

        <h:panelGroup />
        <h:commandButton value="Submit" action="#{myBean.submit}" />
    </h:panelGrid>

    <h:messages infoStyle="color: green;" errorStyle="color: red;" />
</h:form>

And here is the appropriate backing bean:

package mypackage;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.faces.application.FacesMessage;
import javax.faces.component.html.HtmlSelectOneMenu;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.SelectItem;

public class MyBean {

    // Init ---------------------------------------------------------------------------------------

    private static World world = new World();
    private List<SelectItem> countryItems = new ArrayList<SelectItem>();
    private List<SelectItem> cityItems = new ArrayList<SelectItem>();
    private List<SelectItem> streetItems = new ArrayList<SelectItem>();
    private HtmlSelectOneMenu countryMenu;
    private HtmlSelectOneMenu cityMenu;
    private HtmlSelectOneMenu streetMenu;
    private String input;

    {
        // Prefill country menu.
        fillAreaItems(countryItems, world.getAreas());
    }

    // Actions ------------------------------------------------------------------------------------

    public void submit() {
        // Show selection and input results as informal message.
        FacesContext.getCurrentInstance().addMessage(null,
            new FacesMessage("You have chosen: " + countryMenu.getValue() + ", "
                + cityMenu.getValue() + ", " + streetMenu.getValue()
                + " and you have entered: " + input));
    }

    // Changers -----------------------------------------------------------------------------------

    public void changeCountryMenu(ValueChangeEvent event) {
        // Get selected country.
        Country country = (Country) event.getNewValue();

        if (country != null) {
            // Fill city menu.
            fillAreaItems(cityItems, country.getAreas());
        }

        // Reset child menu's. This is only possible when using component binding.
        cityMenu.setValue(null);
        streetMenu.setValue(null);

        // Skip validation of non-immediate components and invocation of the submit() method.
        FacesContext.getCurrentInstance().renderResponse();
    }

    public void changeCityMenu(ValueChangeEvent event) {
        // Get selected city.
        City city = (City) event.getNewValue();

        if (city != null) {
            // Fill street menu.
            fillAreaItems(streetItems, city.getAreas());
        }

        // Reset child menu. This is only possible when using component binding.
        streetMenu.setValue(null);

        // Skip validation of non-immediate components and invocation of the submit() method.
        FacesContext.getCurrentInstance().renderResponse();
    }

    // Fillers ------------------------------------------------------------------------------------

    private static <A extends Area<?>> void fillAreaItems(List<SelectItem> areaItems, Set<A> areas) {
        areaItems.clear();
        for (A area : areas) {
            areaItems.add(new SelectItem(area, area.getName()));
        }
    }

    // Getters ------------------------------------------------------------------------------------

    public List<SelectItem> getCountryItems() {
        return countryItems;
    }

    public List<SelectItem> getCityItems() {
        return cityItems;
    }

    public List<SelectItem> getStreetItems() {
        return streetItems;
    }

    public HtmlSelectOneMenu getCountryMenu() {
        return countryMenu;
    }

    public HtmlSelectOneMenu getCityMenu() {
        return cityMenu;
    }

    public HtmlSelectOneMenu getStreetMenu() {
        return streetMenu;
    }

    public String getInput() {
        return input;
    }

    // Setters ------------------------------------------------------------------------------------

    public void setCountryMenu(HtmlSelectOneMenu countryMenu) {
        this.countryMenu = countryMenu;
    }

    public void setCityMenu(HtmlSelectOneMenu cityMenu) {
        this.cityMenu = cityMenu;
    }

    public void setStreetMenu(HtmlSelectOneMenu streetMenu) {
        this.streetMenu = streetMenu;
    }

    public void setInput(String input) {
        this.input = input;
    }

}

The relevant part of the faces-config.xml file look like:

<converter>
    <converter-id>areaMenuConverter</converter-id>
    <converter-class>mypackage.AreaMenuConverter</converter-class>
</converter>
<managed-bean>
    <managed-bean-name>myBean</managed-bean-name>
    <managed-bean-class>mypackage.MyBean</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

If you want to keep the bean in request scope (which is a very reasonable requirement), then you need to install Tomahawk and 'cache' the bean for the next request only using <t:saveState />.

Back to top

Menu structure

As said earlier, we're demonstrating the working of the menu's using a tree structure of area's: countries, cities and streets, because that would make the most sense. It is not necessary to take over exactly such a structure for your menu's. Just do whatever you find the best and easiest way to use, access and maintain menu items. At least I like the following relatively simple parent-child structure and the converter.

Here is the abstract class Area (please note the implementation of equals() and hashCode(), this is very important for JSF, also see Objects in h:selectOneMenu):

package mypackage;

import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

public abstract class Area<A extends Area<?>> {

    // Init ---------------------------------------------------------------------------------------

    private String name;
    private Set<A> areas;
    private Area<?> parent;

    // Constructors -------------------------------------------------------------------------------

    protected Area(String name, A[] areas) {
        this.name = name;
        this.areas = new TreeSet<A>(new AreaComparator<A>());
        for (A area : areas) {
            area.parent = this;
            this.areas.add(area);
        }
    }

    // Getters ------------------------------------------------------------------------------------

    public String getName() {
        return name;
    }

    public Set<A> getAreas() {
        return areas;
    }

    public Area<?> getParent() {
        return parent;
    }

    // Helpers ------------------------------------------------------------------------------------

    public boolean equals(Object other) {
        return other instanceof Area
            && this.getClass().equals(other.getClass())
            && name.equals(((Area<?>) other).name);
    }

    public int hashCode() {
        return this.getClass().hashCode() + name.hashCode();
    }
    
    public String toString() {
        return name;
    }

}

class AreaComparator<A extends Area<?>> implements Comparator<A> {

    // Invokes natural sorting on area name.
    public int compare(A a1, A a2) {
        return a1.getName().compareTo(a2.getName());
    }

}

And here is the class representing the World which is nothing more or less than a placeholder of the tree structure of area's. Note the stub constructor, it prefills the tree structure.

package mypackage;

public class World extends Area<Country> {

    // Constructors -------------------------------------------------------------------------------

    public World(String name, Country[] countries) {
        super(name, countries);
    }

    public World() {
        // Stub constructor.
        // May be replaced by DAO, configuration files, or wherever you want to store this stuff.
        this("Earth", new Country[] {
            new Country("The Netherlands", new City[] {
                new City("Haarlem", new Street[] {
                    new Street("Palamedesstraat"),
                    new Street("Vergierdeweg"),
                    new Street("Marsstraat")
                }),
                new City("Amsterdam", new Street[] {
                    new Street("Gyroscoopstraat"),
                    new Street("Albert Cuypstraat"),
                    new Street("De Boelelaan")
                }),
                new City("Almere", new Street[] {
                    new Street("Tarantellastraat"),
                    new Street("Salsastraat"),
                    new Street("Hollywoodlaan")
                })
            }),
            new Country("United States", new City[] {
                new City("New York", new Street[] {
                    new Street("Central Park West"),
                    new Street("Park Avenue"),
                    new Street("Amsterdam Avenue")
                }),
                new City("Los Angeles", new Street[] {
                    new Street("Main Street"),
                    new Street("Broadway"),
                    new Street("Olympic Boulevard")
                }),
                new City("Miami", new Street[] {
                    new Street("Miami Avenue"),
                    new Street("Biscayne Boulevard"),
                    new Street("Venetian Way")
                })
            }),
            new Country("France", new City[] {
                new City("Paris", new Street[] {
                    new Street("Avenue des Champs Elysees"),
                    new Street("Quai d'Orsay"),
                    new Street("Rue La Fayette")
                }),
                new City("Lyon", new Street[] {
                    new Street("Cours Lafayette"),
                    new Street("Quai Victor Augagneur"),
                    new Street("Rue Garibaldi")
                }),
                new City("Marseille", new Street[] {
                    new Street("Boulevard Longchamp"),
                    new Street("Rue de Rome"),
                    new Street("Cours Lieutaud")
                })
            })
        });
    }

}

The Country class:

package mypackage;

public class Country extends Area<City> {

    // Constructors -------------------------------------------------------------------------------

    public Country(String name, City[] cities) {
        super(name, cities);
    }

}

The City class:

package mypackage;

public class City extends Area<Street> {

    // Constructors -------------------------------------------------------------------------------

    public City(String name, Street[] streets) {
        super(name, streets);
    }

}

The Street class:

package mypackage;

public class Street extends Area<Area<?>> { 

    // Constructors -------------------------------------------------------------------------------

    public Street(String name) {
        super(name, new Area[0]);
    }

}

And finally the Converter to be used in menu's to convert between the Area type and String type and vice versa:

package mypackage;

import java.util.Set;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

public class AreaMenuConverter implements Converter {

    // Init ---------------------------------------------------------------------------------------

    private static final String AREAS = "AreaMenuConverter.areas";

    // Actions ------------------------------------------------------------------------------------

    public String getAsString(FacesContext context, UIComponent component, Object value) {
        if (value != null) {
            // Cast back to Area.
            Area<?> area = (Area<?>) value;

            // Store the areas as component attribute so that they are available in getAsObject().
            // Those represents the same values as those in f:selectItems.
            component.getAttributes().put(AREAS, area.getParent().getAreas());

            // Return String representation of area.
            return area.getName();
        }

        return null; // Value is null.
    }

    @SuppressWarnings("unchecked")
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        if (value != null) {
            // Get the areas back which were stored as component attribute in getAsString().
            Set<Area<?>> areas = (Set<Area<?>>) component.getAttributes().get(AREAS);

            // Compare name of each area with selected value.
            for (Area<?> area : areas) {
                if (area.getName().equals(value)) {
                    // Return matched area object.
                    return area;
                }
            }
        }

        return null; // Value is null or doesn't have any match.
    }

}

That was it! You can in fact just copypaste and run it all without any changes and then play/experiment with it further.

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) October 2007, BalusC

Friday, September 7, 2007

Objects in h:selectOneMenu

WARNING - OUTDATED CONTENT!

This article is targeted on JSF 1.2. For JSF 2.0, using objects in UISelectOne and UISelectMany components can be approached much more elegantly with help of OmniFaces SelectItemsConverter without the need to write a custom converter.

Introduction

A HTML response is in Object Oriented perspective nothing less or more than one big String value (technically: a long character array). Non-String-typed Java Objects are forced to their String representation using Object#toString() when they are about to be written to the HTML response. That's why using custom Object types in a UISelectOne or UISelectMany component, for example h:selectOneMenu, can drive unaware JSF developers nuts. If you gently have attached a <h:message /> to the component for error messages, or added the <h:messages /> tag to the page for debugging purposes, then you will likely get the following error message:

Conversion Error setting value 'mypackage.Foo@1234567' for 'null Converter'.

That roughly means that JSF cannot convert the given String value "mypackage.Foo@1234567" (which is just the String representation of the Object, obtained by Object#toString()) to the Object type which is expected by the valuebinding of the component, e.g. mypackage.Foo. The message also indicates that JSF couldn't find a suitable converter for it, pointing to the null ID in the 'null Converter' message part. Generally this will only occur if those objects are not of a String type and JSF does not have a built-in converter for it as well. That's why, next to plain String values, for example the types of the Number superclass just works in the h:selectOneMenu. At least in the newer builds of JSF, the 1.1_02 and 1.2_02 or later, due to some coerce bugs with Number types in SelectItem in the older builds.

There are two general solutions for this conversion problem: 1) implement javax.faces.convert.Converter and write logic which converts between String (or Number) and the desired Object. 2) maintain a backing map with an unique String (or Number) representation as key and the appropriate Object as value. Both approaches will be descibed here in detail.

Back to top

Using a Converter

Using a Converter is fairly simple. You just need to write some logic how to Convert between String and the desired Object type. Instead of String you can also use any Number type, for example Long, because JSF already has built-in converters for it.

Let's start with a relevant part of the JSF page:

<h:form>
    <h:selectOneMenu value="#{myBean.selectedItem}">
        <f:selectItems value="#{myBean.selectItems}" />
        <f:converter converterId="fooConverter" />
    </h:selectOneMenu>
    <h:commandButton value="submit" action="#{myBean.action}" />
    <h:messages />
</h:form>

Note the f:converter facet. The value of its converterId attribute must match with one of the converters as definied in the faces-config.xml. The faces-config.xml example will be shown later in this paragraph.

And here is the appropriate backing bean MyBean:

package mypackage;

import java.util.ArrayList;
import java.util.List;

import javax.faces.model.SelectItem;

public class MyBean {

    // Init ---------------------------------------------------------------------------------------

    private static FooDAO fooDAO = new FooDAO();
    private List<SelectItem> selectItems;
    private Foo selectedItem;

    {
        fillSelectItems();
    }

    // Actions ------------------------------------------------------------------------------------

    public void action() {
        System.out.println("Selected Foo item: " + selectedItem);
    }

    // Getters ------------------------------------------------------------------------------------

    public List<SelectItem> getSelectItems() {
        return selectItems;
    }

    public Foo getSelectedItem() {
        return selectedItem;
    }

    // Setters ------------------------------------------------------------------------------------

    public void setSelectedItem(Foo selectedItem) {
        this.selectedItem = selectedItem;
    }

    // Helpers ------------------------------------------------------------------------------------

    private void fillSelectItems() {
        selectItems = new ArrayList<SelectItem>();
        for (Foo foo : fooDAO.list()) {
            selectItems.add(new SelectItem(foo, foo.getValue()));
        }
    }

}

This is how the Foo object type look like. It is actually a random Data Transfer Object (DTO). Please note the Object#equals() implementation. This is very important for JSF. After conversion, it will compare the selected item against the items in the list. As the Object#equals() also require Object#hashCode(), this is implemented as well.

package mypackage;

public class Foo {

    // Init ---------------------------------------------------------------------------------------

    private String key; // You can also use any Number type, e.g. Long.
    private String value;

    // Constructors -------------------------------------------------------------------------------

    public Foo() {
        // Default constructor, keep alive.
    }

    public Foo(String key, String value) {
        this.key = key;
        this.value = value;
    }

    // Getters ------------------------------------------------------------------------------------

    public String getKey() {
        return key;
    }

    public String getValue() {
        return value;
    }

    // Setters ------------------------------------------------------------------------------------

    public void setKey(String key) {
        this.key = key;
    }

    public void setValue(String value) {
        this.value = value;
    }

    // Helpers ------------------------------------------------------------------------------------

    // This must return true for another Foo object with same key/id.
    public boolean equals(Object other) {
        return other instanceof Foo && (key != null) ? key.equals(((Foo) other).key) : (other == this);
    }

    // This must return the same hashcode for every Foo object with the same key.
    public int hashCode() {
        return key != null ? this.getClass().hashCode() + key.hashCode() : super.hashCode();
    }

    // Override Object#toString() so that it returns a human readable String representation.
    // It is not required by the Converter or so, it just pleases the reading in the logs.
    public String toString() {
        return "Foo[" + key + "," + value + "]";
    }

}

And here is a fake DAO which maintains the Foo DTO's. Take note that the database is simulated by a static backing map, this doesn't need to occur in real life. It should actually be mapped to a database or maybe even configuration files or so. For more information about the DAO pattern, check this article: DAO tutorial - the data layer.

package mypackage;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class FooDAO {

    // Init ---------------------------------------------------------------------------------------

    private static Map<String, Foo> fooMap;

    static {
        loadFooMap(); // Preload the fake database.
    }

    // Actions ------------------------------------------------------------------------------------

    public Foo find(String key) {
        return fooMap.get(key);
    }

    public List<Foo> list() {
        return new ArrayList<Foo>(fooMap.values());
    }

    public Map<String, Foo> map() {
        return fooMap;
    }

    // Helpers ------------------------------------------------------------------------------------

    private static void loadFooMap() {
        // This is just a fake database. We're using LinkedHashMap as it maintains the ordering.
        fooMap = new LinkedHashMap<String, Foo>();
        fooMap.put("fooKey1", new Foo("fooKey1", "fooValue1"));
        fooMap.put("fooKey2", new Foo("fooKey2", "fooValue2"));
        fooMap.put("fooKey3", new Foo("fooKey3", "fooValue3"));
    }

}

Finally here is the long-awaiting FooConverter which can be used in JSF components. It converts from Foo to String and vice versa. You can attach it to almost any HTML input or output component using the converter attribute or using the f:converter facet.

package mypackage;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

public class FooConverter implements Converter {

    // Init ---------------------------------------------------------------------------------------
    
    private static FooDAO fooDAO = new FooDAO();

    // Actions ------------------------------------------------------------------------------------
    
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        // Convert the unique String representation of Foo to the actual Foo object.
        return fooDAO.find(value);
    }

    public String getAsString(FacesContext context, UIComponent component, Object value) {
        // Convert the Foo object to its unique String representation.
        return ((Foo) value).getKey();
    }

}

Take care with runtime exceptions as NullPointerException, ClassCastException and maybe some exceptions which can be thrown by your DAO. Those have to be checked and/or caught and should be thrown as a new ConverterException with the appropriate message which would be shown in the attached h:message(s) tag.

The MyBean and the FooConverter are definied in the faces-config.xml as follows:

<converter>
    <converter-id>fooConverter</converter-id>
    <converter-class>mypackage.FooConverter</converter-class>
</converter>
<managed-bean>
    <managed-bean-name>myBean</managed-bean-name>
    <managed-bean-class>mypackage.MyBean</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
</managed-bean>

You can however also use converter-for-class instead of converter-id so that JSF will always use the specified converter class for the given class. This way you don't need to specify the f:converter in your JSF code.

<converter>
    <converter-for-class>mypackage.Foo</converter-for-class>
    <converter-class>mypackage.FooConverter</converter-class>
</converter>

That's all, folks!

Back to top

Using a backing Map

You can also decide to use a so-called backing Map to maintain String-Object pairs and convert against it in the backing bean logic. The String keys can be used in the view layer (JSF pages) and the Object values can be used in the business and data layers (backing beans and DAO's). You can also use Number-Object pairs however.

The relevant part of the JSF page is almost the same as in the case of using a converter, only the f:converter tag is removed:

<h:form>
    <h:selectOneMenu value="#{myBean.selectedItem}">
        <f:selectItems value="#{myBean.selectItems}" />
    </h:selectOneMenu>
    <h:commandButton value="submit" action="#{myBean.action}" />
    <h:messages />
</h:form>

And here is the appropriate backing bean MyBean, it has a static Map of String-Foo pairs. It's your choice how you would load/maintain it, this is just a basic example. Take note that the selectedItem is now a String instead of Foo.

package mypackage;

import java.util.ArrayList;
import java.util.List;

import javax.faces.model.SelectItem;

public class MyBean {

    // Init ---------------------------------------------------------------------------------------

    private static Map<String, Foo> fooMap = new FooDAO().map();
    private List<SelectItem> selectItems;
    private String selectedItem; // You can also use any Number type, e.g. Long.

    {
        fillSelectItems();
    }

    // Actions ------------------------------------------------------------------------------------

    public void action() {
        System.out.println("Selected Foo item: " + fooMap.get(selectedItem));
    }

    // Getters ------------------------------------------------------------------------------------

    public List<SelectItem> getSelectItems() {
        return selectItems;
    }

    public String getSelectedItem() {
        return selectedItem;
    }

    // Setters ------------------------------------------------------------------------------------

    public void setSelectedItem(String selectedItem) {
        this.selectedItem = selectedItem;
    }

    // Helpers ------------------------------------------------------------------------------------

    private void fillSelectItems() {
        selectItems = new ArrayList<SelectItem>();
        for (Foo foo : fooMap.values()) {
            selectItems.add(new SelectItem(foo.getKey(), foo.getValue()));
        }
    }

}

That's it! You can just reuse the Foo DTO, the FooDAO DAO and the faces-config.xml snippet from the Using a Converter paragraph here above. No FooConverter is needed in here and you can also safely remove it from the faces-config.xml.

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) September 2007, BalusC