Selasa, 28 Januari 2014

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 . 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.




columns="2">
value="Choose area" />


binding="#{myBean.countryMenu}" converter="areaMenuConverter"
onchange="this.form.submit();" valueChangeListener="#{myBean.changeCountryMenu}"
immediate="true">
itemLabel="Please select country" />
value="#{myBean.countryItems}" />


binding="#{myBean.cityMenu}" converter="areaMenuConverter"
onchange="this.form.submit();" valueChangeListener="#{myBean.changeCityMenu}"
immediate="true" rendered="#{myBean.countryMenu.value != null}">
itemLabel="Please select city" />
value="#{myBean.cityItems}" />


binding="#{myBean.streetMenu}" converter="areaMenuConverter"
rendered="#{myBean.cityMenu.value != null}">
itemLabel="Please select street" />
value="#{myBean.streetItems}" />


required="#{myBean.streetMenu.value == null}" requiredMessage="Area is required." />


value="Enter input" />

value="#{myBean.input}"
required="true" requiredMessage="Input is required." />


value="Submit" action="#{myBean.submit}" />


infoStyle="color: green;" errorStyle="color: red;" />


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 countryItems = new ArrayList();
private List cityItems = new ArrayList();
private List streetItems = new ArrayList();
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 extends Area> void fillAreaItems(List areaItems, Set areas) {
areaItems.clear();
for (A area : areas) {
areaItems.add(new SelectItem(area, area.getName()));
}
}

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

public List getCountryItems() {
return countryItems;
}

public List getCityItems() {
return cityItems;
}

public List 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:




areaMenuConverter
mypackage.AreaMenuConverter


myBean
mypackage.MyBean
session

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 .


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 Areaextends Area> {

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

private String name;
private Set
areas;
private Area parent;

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

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

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

public String getName() {
return name;
}

public Set
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
extends Area> implements Comparator {

// 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 {

// 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 {

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

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

}


The City class:


package mypackage;

public class City extends Area {

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

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

}


The Street class:


package mypackage;

public class Street extends 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> areas = (Set>) 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



Source:http://balusc.blogspot.com/2007/10/populate-child-menus.html

Tidak ada komentar:

Posting Komentar