Revisiting the Spinner
Next, we revisit the spinner listed in
the previous section. That spinner has two serious drawbacks. First, the spinner
component renders itself, so you could not, for example, attach a separate
renderer to the spinner when you migrate your application to cell phones.
Second, the spinner requires a
roundtrip to the server every time a user clicks the increment or decrement
button. Nobody would implement an industrial-strength spinner with those
deficiencies. Now we see how to address them.
While we are at it, we will also add
another feature to the spinner—the ability to attach value change
listeners.
Using an External Renderer
In the preceding example, the
UISpinner class was in charge of its own
rendering. However, most UI classes delegate rendering to a separate class.
Using separate renderers is a good idea: It becomes easy to replace renderers,
to adapt to a different UI toolkit, or simply to achieve different HTML
effects.
In "Encoding
JavaScript to Avoid Server Roundtrips" on page 404 we see how to use an alternative renderer that uses
JavaScript to keep track of the spinner's value on the client.
Using an external renderer requires these steps:
1. |
Define an ID string for your renderer.
|
2. |
Declare the renderer in a JSF configuration file.
|
3. |
Modify your tag class to return the renderer's ID from the getRendererType method.
|
4. |
Implement the renderer class.
|
The identifier—in our case, com.corejsf.Spinner—must
be defined in a JSF configuration file, like this:
<faces-config>
...
<component>
<component-type>com.corejsf.Spinner</component-type>
<component-class>com.corejsf.UISpinner</component-class>
</component>
<render-kit>
<renderer>
<component-family>javax.faces.Input</component-family>
<renderer-type>com.corejsf.Spinner</renderer-type>
<renderer-class>com.corejsf.SpinnerRenderer</renderer-class>
</renderer>
</render-kit>
</faces-config>
The component-family element
serves to overcome a historical problem. The names of the standard HTML tags are
meant to indicate the component type and the renderer type. For example, an
h:selectOneMenu is a UISelectOne component
whose renderer has type javax.faces.Menu. That same renderer can also
be used for the h:selectManyMenu tag. But the scheme did not work so
well. The renderer for h:inputText writes an HTML input text
field. That renderer will not work for h:outputText—you do not want to
use a text field for output.
So, instead of identifying renderers
by individual components, renderers are determined by the renderer type and the
component family.
Table 9-2 shows the component families of all standard component
classes. In our case, we use the component family javax.faces.Input
because UISpinner is a subclass of
UIInput.
Table 9-2. Component Families
of Standard Component Classes
Component Class |
Component Family |
---|
UICommand |
javax.faces.Command |
UIData |
javax.faces.Data |
UIForm |
javax.faces.Form |
UIGraphic |
javax.faces.Graphic |
UIInput |
javax.faces.Input |
UIMessage |
javax.faces.Message |
UIMessages |
javax.faces.Messages |
UIOutput |
javax.faces.Output |
UIPanel |
javax.faces.Panel |
UISelectBoolean |
javax.faces.SelectBoolean |
UISelectMany |
javax.faces.SelectMany |
UISelectOne |
javax.faces.SelectOne |
The getRendererType of your tag class needs to return the renderer ID.
public class SpinnerTag extends UIComponentTag {
...
public String getComponentType() { return "com.corejsf.Spinner"; }
public String getRendererType() { return "com.corejsf.Spinner"; }
...
}
Note
|
Component IDs and renderer IDs have separate name spaces. It is okay to use the same string as a component ID and a renderer ID. |
It is also a good idea to set the
renderer type in the component constructor:
public class UISpinner extends UIInput {
public UISpinner() {
setConverter(new IntegerConverter()); // to convert the submitted value
setRendererType("com.corejsf.Spinner"); // this component has a renderer
}
}
Then the
renderer type is properly set if a component is used programmatically, without
the use of tags.
The final step is implementing the renderer itself. Renderers
extend the javax.faces.render.Renderer class. That class has seven
methods, four of which are familiar:
void encodeBegin(FacesContext context, UIComponent component)
void encodeChildren(FacesContext context, UIComponent component)
void encodeEnd(FacesContext context, UIComponent component)
void decode(FacesContext context, UIComponent component)
The renderer methods
listed above are almost identical to their component counterparts except that
the renderer methods take an additional argument: a reference to the component
being rendered. To implement those methods for the spinner renderer, we move the
component methods to the renderer and apply code changes to compensate for the
fact that the renderer is passed a reference to the component. That is easy to
do.
Here are the remaining renderer
methods:
Object getConvertedValue(FacesContext context, UIComponent component,
Object submittedValue)
boolean getRendersChildren()
String convertClientId(FacesContext context, String clientId)
The getConvertedValue
method converts a component's submitted value from a string to an object. The
default implementation in the Renderer class returns the value.
The getRendersChildren method
specifies whether a renderer is responsible for rendering its component's
children. If that method returns true, JSF will
call the renderer's encodeChildren method; if it returns
false (the default behavior), the JSF
implementation will not call that method and the children will be encoded
separately.
The convertClientId method converts an ID string (such
as _id1:monthSpinner) so that it can
be used on the client—some clients may place restrictions on IDs, such as
disallowing special characters. However, the default implementation returns the
ID string, unchanged.
If you have a component that renders
itself, it is usually a simple task to move code from the component to the
renderer. Listing
9-10 and Listing
9-11 show the code for the spinner component and
the renderer, respectively.
Listing 9-10.
spinner2/src/java/com/corejsf/UISpinner.java
1. package com.corejsf; 2. 3. import javax.faces.component.UIInput; 4. import javax.faces.convert.IntegerConverter; 5. 6. public class UISpinner extends UIInput { 7. public UISpinner() { 8. setConverter(new IntegerConverter()); // to convert the submitted value 9. } 10. }
|
Listing 9-11.
spinner2/src/java/com/corejsf/SpinnerRenderer.java
1. package com.corejsf; 2. 3. import java.io.IOException; 4. import java.util.Map; 5. import javax.faces.component.UIComponent; 6. import javax.faces.component.EditableValueHolder; 7. import javax.faces.component.UIInput; 8. import javax.faces.context.FacesContext; 9. import javax.faces.context.ResponseWriter; 10. import javax.faces.convert.ConverterException; 11. import javax.faces.render.Renderer; 12. 13. public class SpinnerRenderer extends Renderer { 14. private static final String MORE = ".more"; 15. private static final String LESS = ".less"; 16. 17. public Object getConvertedValue(FacesContext context, UIComponent component, 18. Object submittedValue) throws ConverterException { 19. return com.corejsf.util.Renderers.getConvertedValue(context, component, 20. submittedValue); 21. } 22. 23. public void encodeBegin(FacesContext context, UIComponent spinner) 24. throws IOException { 25. ResponseWriter writer = context.getResponseWriter(); 26. String clientId = spinner.getClientId(context); 27. 28. encodeInputField(spinner, writer, clientId); 29. encodeDecrementButton(spinner, writer, clientId); 30. encodeIncrementButton(spinner, writer, clientId); 31. } 32. 33. public void decode(FacesContext context, UIComponent component) { 34. EditableValueHolder spinner = (EditableValueHolder) component; 35. Map<String, String> requestMap 36. = context.getExternalContext().getRequestParameterMap(); 37. String clientId = component.getClientId(context); 38. 39. int increment; 40. if (requestMap.containsKey(clientId + MORE)) increment = 1; 41. else if (requestMap.containsKey(clientId + LESS)) increment = -1; 42. else increment = 0; 43. 44. try { 45. int submittedValue 46. = Integer.parseInt((String) requestMap.get(clientId)); 47. 48. int newValue = getIncrementedValue(component, submittedValue, 49. increment); 50. spinner.setSubmittedValue("" + newValue); 51. spinner.setValid(true); 52. } 53. catch(NumberFormatException ex) { 54. // let the converter take care of bad input, but we still have 55. // to set the submitted value, or the converter won't have 56. // any input to deal with 57. spinner.setSubmittedValue((String) requestMap.get(clientId)); 58. } 59. } 60. 61. private void encodeInputField(UIComponent spinner, ResponseWriter writer, 62. String clientId) throws IOException { 63. writer.startElement("input", spinner); 64. writer.writeAttribute("name", clientId, "clientId"); 65. 66. Object v = ((UIInput) spinner).getValue(); 67. if(v != null) 68. writer.writeAttribute("value", v.toString(), "value"); 69. 70. Integer size = (Integer) spinner.getAttributes().get("size"); 71. if(size != null) 72. writer.writeAttribute("size", size, "size"); 73. 74. writer.endElement("input"); 75. } 76. 77. private void encodeDecrementButton(UIComponent spinner, 78. ResponseWriter writer, String clientId) throws IOException { 79. writer.startElement("input", spinner); 80. writer.writeAttribute("type", "submit", null); 81. writer.writeAttribute("name", clientId + LESS, null); 82. writer.writeAttribute("value", "<", "value"); 83. writer.endElement("input"); 84. } 85. 86. private void encodeIncrementButton(UIComponent spinner, 87. ResponseWriter writer, String clientId) throws IOException { 88. writer.startElement("input", spinner); 89. writer.writeAttribute("type", "submit", null); 90. writer.writeAttribute("name", clientId + MORE, null); 91. writer.writeAttribute("value", ">", "value"); 92. writer.endElement("input"); 93. } 94. 95. private int getIncrementedValue(UIComponent spinner, int submittedValue, 96. int increment) { 97. Integer minimum = (Integer) spinner.getAttributes().get("minimum"); 98. Integer maximum = (Integer) spinner.getAttributes().get("maximum"); 99. int newValue = submittedValue + increment; 100. 101. if ((minimum == null || newValue >= minimum.intValue()) && 102. (maximum == null || newValue <= maximum.intValue())) 103. return newValue; 104. else 105. return submittedValue; 106. } 107. }
|
Calling Converters from External
Renderers
If you compare Listing 9-10 and Listing 9-11 with Listing
9-1, you will see that we moved most of the code from
the original component class to a new renderer class.
However, there is a hitch. As you can
see from Listing
9-10, the spinner handles conversions
simply by invoking setConverter() in its
constructor. Because the spinner is an input component, its
superclass—UIInput—uses the specified
converter during the Process Validations phase of the life cycle.
But when the spinner delegates to a renderer, it is
the renderer's responsibility to convert the spinner's value by overriding
Renderer.getConvertedValue(). So we must replicate the conversion code
from UIInput in a custom renderer. We
placed that code—which is required in all renderers that use a converter—in the
static getConvertedValue method of the class
com.corejsf.util.Renderers (see Listing 9-12 on page 398).
Note
|
The Renderers.getConvertedValue method shown in Listing 9-12 is a necessary evil because UIInput does not make its conversion code publicly available. That code resides in the protected UIInput.getConvertedValue method, which looks like this in the JSF 1.2 Reference Implementation:
// This code is from the javax.faces.component.UIInput class: public void getConvertedValue(FacesContext context, Object newSubmittedValue) throws ConverterException { Object newValue = newSubmittedValue; if (renderer != null) { newValue = renderer.getConvertedValue(context, this, newSubmittedValue); } else if (newSubmittedValue instanceof String) { Converter converter = getConverterWithType(context); // a private method if (converter != null) newValue = converter.getAsObject( context, this, (String) newSubmittedValue); } return newValue; }
The private getConverterWithType method looks up the appropriate converter for the component value.
Because UIInput's conversion code is buried in protected and private methods, it is not available for a renderer to reuse. Custom components that use converters must duplicate the code—see, for example, the implementation of com.sun.faces.renderkit.html_basic.HtmlBasicInputRenderer in the reference implementation. Our com.corejsf.util.Renderers class provides the code for use in your own classes. |
Supporting Value Change
Listeners
If your custom component is an input
component, you can fire value change events to interested listeners. For
example, in a calendar application, you may want to update another component
whenever a month spinner value changes.
Fortunately, it is easy to support value
change listeners. The UIInput class
automatically generates value change events whenever the input value has
changed. Recall that there are two ways of attaching a value change listener.
You can add one or more listeners with f:valueChangeListener, like
this:
<corejsf:spinner ...>
<f:valueChangeListener type="com.corejsf.SpinnerListener"/>
...
</corejsf:spinner>
Or you can use a valueChangeListener attribute:
<corejsf:spinner value="#{cardExpirationDate.month}"
id="monthSpinner" minimum="1" maximum="12" size="3"
valueChangeListener="#{cardExpirationDate.changeListener}"/>
The first way doesn't require any
effort on the part of the component implementor. The second way merely requires
that your tag handler supports the valueChangeListener attribute. The attribute value is a method expression
that requires special handling—the topic of the next section, "Supporting Method
Expressions."
In the sample program, we
demonstrate the value change listener by keeping a count of all value changes
that we display on the form (see Figure 9-7).
public class CreditCardExpiration {
private int changes = 0;
// to demonstrate the value change listener
public void changeListener(ValueChangeEvent e) {
changes++;
}
}
Supporting Method Expressions
Four commonly used attributes require
method expressions (see Table 9-3). You declare them in the
TLD file with deferred-method elements, such as the following:
<attribute>
<name>valueChangeListener</name>
<deferred-method>
<method-signature>
void valueChange(javax.faces.event.ValueChangeEvent)
</method-signature>
</deferred-method>
</attribute>
In the tag handler class, you provide setters for
MethodExpression objects.
public class SpinnerTag extends UIComponentELTag {
...
private MethodExpression valueChangeListener = null;
public void setValueChangeListener(MethodExpression newValue) {
valueChangeListener = newValue;
}
...
}
Table 9-3. Processing Method Expressions
Attribute Name |
method-signature Element in TLD |
Code in setProperties Method |
---|
|
void valueChange(javax.faces. event.ValueChangeEvent)
|
((EditableValueHolder) component) .addValueChangeListener(new MethodExpressionValueChangeListener(expr));
|
|
void validate(javax.faces. context.FacesContext, javax.faces.component. UIComponent, java.lang.Object)
|
((EditableValueHolder) component) .addValidator(new MethodExpressionValidator(expr));
|
|
void actionListener(javax. faces.event.ActionEvent)
|
((ActionSource) component) .addActionListener(new MethodExpressionActionListener(expr));
|
|
java.lang.Object action()
|
((ActionSource2) component). addAction(expr);
|
In the setProperties method of
the tag handler, you convert the MethodExpression object to an appropriate listener object and add it to
the component:
public void setProperties(UIComponent component) {
super.setProperties(component);
...
if (valueChangeListener != null)
((EditableValueHolder) component).addValueChangeListener(
new MethodExpressionValueChangeListener(valueChangeListener));
}
Table
9-3 shows how to handle the other method
attributes.
Note
|
The action attribute value can be either a method expression or a constant. In the latter case, a method is created that always returns the constant value. |
The Sample Application
Figure
9-8 shows the directory structure of the sample
application. As in the first example, we rely on the core JSF
Renderers convenience class that contains
the code for invoking the converter.
(The Renderers class also
contains a getSelectedItems method that we need later in this
chapter—ignore it for now.) Listing 9-13 contains the revised
SpinnerTag class, and Listing 9-14 shows the
faces-config.xml file.
Listing 9-12.
spinner2/src/java/com/corejsf/util/Renderers.java
1. package com.corejsf.util; 2. 3. import java.util.ArrayList; 4. import java.util.Arrays; 5. import java.util.Collection; 6. import java.util.List; 7. import java.util.Map; 8. 9. import javax.el.ValueExpression; 10. import javax.faces.application.Application; 11. import javax.faces.component.UIComponent; 12. import javax.faces.component.UIForm; 13. import javax.faces.component.UISelectItem; 14. import javax.faces.component.UISelectItems; 15. import javax.faces.component.ValueHolder; 16. import javax.faces.context.FacesContext; 17. import javax.faces.convert.Converter; 18. import javax.faces.convert.ConverterException; 19. import javax.faces.model.SelectItem; 20. 21. public class Renderers { 22. public static Object getConvertedValue(FacesContext context, 23. UIComponent component, Object submittedValue) 24. throws ConverterException { 25. if (submittedValue instanceof String) { 26. Converter converter = getConverter(context, component); 27. if (converter != null) { 28. return converter.getAsObject(context, component, 29. (String) submittedValue); 30. } 31. } 32. return submittedValue; 33. } 34. 35. public static Converter getConverter(FacesContext context, 36. UIComponent component) { 37. if (!(component instanceof ValueHolder)) return null; 38. ValueHolder holder = (ValueHolder) component; 39. 40. Converter converter = holder.getConverter(); 41. if (converter != null) 42. return converter; 43. 44. ValueExpression expr = component.getValueExpression("value"); 45. if (expr == null) return null; 46. 47. Class targetType = expr.getType(context.getELContext()); 48. if (targetType == null) return null; 49. // Version 1.0 of the reference implementation will not apply a converter 50. // if the target type is String or Object, but that is a bug. 51. 52. Application app = context.getApplication(); 53. return app.createConverter(targetType); 54. } 55. 56. public static String getFormId(FacesContext context, UIComponent component) { 57. UIComponent parent = component; 58. while (!(parent instanceof UIForm)) 59. parent = parent.getParent(); 60. return parent.getClientId(context); 61. } 62. 63. @SuppressWarnings("unchecked") 64. public static List<SelectItem> getSelectItems(UIComponent component) { 65. ArrayList<SelectItem> list = new ArrayList<SelectItem>(); 66. for (UIComponent child : component.getChildren()) { 67. if (child instanceof UISelectItem) { 68. Object value = ((UISelectItem) child).getValue(); 69. if (value == null) { 70. UISelectItem item = (UISelectItem) child; 71. list.add(new SelectItem(item.getItemValue(), 72. item.getItemLabel(), 73. item.getItemDescription(), 74. item.isItemDisabled())); 75. } else if (value instanceof SelectItem) { 76. list.add((SelectItem) value); 77. } 78. } else if (child instanceof UISelectItems) { 79. Object value = ((UISelectItems) child).getValue(); 80. if (value instanceof SelectItem) 81. list.add((SelectItem) value); 82. else if (value instanceof SelectItem[]) 83. list.addAll(Arrays.asList((SelectItem[]) value)); 84. else if (value instanceof Collection) 85. list.addAll((Collection<SelectItem>) value); // unavoidable 86. // warning 87. else if (value instanceof Map) { 88. for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) 89. list.add(new SelectItem(entry.getKey(), 90. "" + entry.getValue())); 91. } 92. } 93. } 94. return list; 95. } 96. }
|
Listing 9-13.
spinner2/src/java/com/corejsf/SpinnerTag.java
1. package com.corejsf; 2. 3. import javax.el.MethodExpression; 4. import javax.el.ValueExpression; 5. import javax.faces.component.EditableValueHolder; 6. import javax.faces.component.UIComponent; 7. import javax.faces.event.MethodExpressionValueChangeListener; 8. import javax.faces.webapp.UIComponentELTag; 9. 10. public class SpinnerTag extends UIComponentELTag { 11. private ValueExpression minimum = null; 12. private ValueExpression maximum = null; 13. private ValueExpression size = null; 14. private ValueExpression value = null; 15. private MethodExpression valueChangeListener = null; 16. 17. public String getRendererType() { return "com.corejsf.Spinner"; } 18. public String getComponentType() { return "com.corejsf.Spinner"; } 19. 20. public void setMinimum(ValueExpression newValue) { minimum = newValue; } 21. public void setMaximum(ValueExpression newValue) { maximum = newValue; } 22. public void setSize(ValueExpression newValue) { size = newValue; } 23. public void setValue(ValueExpression newValue) { value = newValue; } 24. public void setValueChangeListener(MethodExpression newValue) { 25. valueChangeListener = newValue; 26. } 27. 28. public void setProperties(UIComponent component) { 29. // always call the superclass method 30. super.setProperties(component); 31. 32. component.setValueExpression("size", size); 33. component.setValueExpression("minimum", minimum); 34. component.setValueExpression("maximum", maximum); 35. component.setValueExpression("value", value); 36. if (valueChangeListener != null) 37. ((EditableValueHolder) component).addValueChangeListener( 38. new MethodExpressionValueChangeListener(valueChangeListener)); 39. } 40. 41. public void release() { 42. // always call the superclass method 43. super.release(); 44. 45. minimum = null; 46. maximum = null; 47. size = null; 48. value = null; 49. valueChangeListener = null; 50. } 51. }
|
Listing 9-14.
spinner2/web/WEB-INF/faces-config.xml
1. <?xml version="1.0"?> 2. 3. <faces-config xmlns="http://java.sun.com/xml/ns/javaee" 4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 5. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 6. http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd" 7. version="1.2"> 8. 9. <navigation-rule> 10. <from-view-id>/index.jsp</from-view-id> 11. <navigation-case> 12. <from-outcome>next</from-outcome> 13. <to-view-id>/next.jsp</to-view-id> 14. </navigation-case> 15. </navigation-rule> 16. 17. <navigation-rule> 18. <from-view-id>/next.jsp</from-view-id> 19. <navigation-case> 20. <from-outcome>again</from-outcome> 21. <to-view-id>/index.jsp</to-view-id> 22. </navigation-case> 23. </navigation-rule> 24. 25. <managed-bean> 26. <managed-bean-name>cardExpirationDate</managed-bean-name> 27. <managed-bean-class>com.corejsf.CreditCardExpiration</managed-bean-class> 28. <managed-bean-scope>session</managed-bean-scope> 29. </managed-bean> 30. 31. <component> 32. <component-type>com.corejsf.Spinner</component-type> 33. <component-class>com.corejsf.UISpinner</component-class> 34. </component> 35. 36. <render-kit> 37. <renderer> 38. <component-family>javax.faces.Input</component-family> 39. <renderer-type>com.corejsf.Spinner</renderer-type> 40. <renderer-class>com.corejsf.SpinnerRenderer</renderer-class> 41. </renderer> 42. </render-kit> 43. 44. <application> 45. <resource-bundle> 46. <base-name>com.corejsf.messages</base-name> 47. <var>msgs</var> 48. </resource-bundle> 49. </application> 50. </faces-config>
|
javax.faces.component.EditableValueHolder
|
void addValueChangeListener(ValueChangeListener listener) JSF 1.2
Adds a value change listener to this component.
void addValidator(Validator val) JSF 1.2
Adds a validator to this component.
|
javax.faces.component.ActionSource
javax.faces.component.ActionSource2 JSF 1.2
javax.faces.event.MethodExpressionValueChangeListener
JSF 1.2
javax.faces.validator.MethodExpressionValidator
JSF 1.2
|
MethodExpressionValidator(MethodExpression m)
Constructs a validator from a method expression. The method must return void and is passed a FacesContext, a UIComponent, and an Object.
|
javax.faces.event.MethodExpressionActionListener
JSF 1.2
javax.faces.event.ValueChangeEvent
javax.faces.component.ValueHolder
javax.faces.component.UIComponent
javax.faces.context.FacesContext
javax.el.ValueExpression JSF 1.2
javax.faces.application.Application
|
Converter createConverter(Class targetClass)
Creates a converter, given its target class. JSF implementations maintain a map of valid converter types, which are typically specified in a faces configuration file. If targetClass is a key in that map, this method creates an instance of the associated converter (specified as the value for the target-Class key) and returns it.
If targetClass is not in the map, this method searches the map for a key that corresponds to targetClass's interfaces and superclasses, in that order, until it finds a matching class. Once a matching class is found, this method creates an associated converter and returns it. If no converter is found for the targetClass, its interfaces, or its superclasses, this method returns null.
|
No comments:
Post a Comment