Thursday, October 15, 2009

Revisiting the Spinner


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 ClassComponent Family
UICommandjavax.faces.Command
UIDatajavax.faces.Data
UIFormjavax.faces.Form
UIGraphicjavax.faces.Graphic
UIInputjavax.faces.Input
UIMessagejavax.faces.Message
UIMessagesjavax.faces.Messages
UIOutputjavax.faces.Output
UIPaneljavax.faces.Panel
UISelectBooleanjavax.faces.SelectBoolean
UISelectManyjavax.faces.SelectMany
UISelectOnejavax.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).





Figure 9-7. Counting the value changes






   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 Namemethod-signature Element in TLDCode in setProperties Method

valueChangeListener


void valueChange(javax.faces.
event.ValueChangeEvent)


((EditableValueHolder) component)
.addValueChangeListener(new
MethodExpressionValueChangeListener(expr));


validator


void validate(javax.faces.
context.FacesContext,
javax.faces.component.
UIComponent, java.lang.Object)


((EditableValueHolder) component)
.addValidator(new
MethodExpressionValidator(expr));


actionListener


void actionListener(javax.
faces.event.ActionEvent)


((ActionSource) component)
.addActionListener(new
MethodExpressionActionListener(expr));


action


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.





Figure 9-8. Directory structure of
the revisited spinner example




(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










  • void addActionListener(ActionListener listener) JSF 1.2


    Adds an action listener to this component.




javax.faces.component.ActionSource2 JSF 1.2










  • void addAction(MethodExpression m)


    Adds an action to this component. The method has return type
    String and no
    parameters.




javax.faces.event.MethodExpressionValueChangeListener
JSF 1.2










  • MethodExpressionValueChangeListener(MethodExpression
    m)


    Constructs a value change listener
    from a method expression. The method must return void and is passed a
    ValueChangeEvent.




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










  • MethodExpressionActionListener(MethodExpression m)


    Constructs an action listener from a
    method expression. The method must return void and is passed an
    ActionEvent.




javax.faces.event.ValueChangeEvent










  • Object getOldValue()


    Returns the component's old value.



  • Object getNewValue()


    Returns the component's new
    value.




javax.faces.component.ValueHolder










  • Converter getConverter()


    Returns the converter associated with a component. The
    ValueHolder inter-face is implemented by input and output
    components.




javax.faces.component.UIComponent










  • ValueExpression getValueExpression(String name) JSF 1.2


    Returns the value expression associated with the given name.




javax.faces.context.FacesContext










  • ELContext getELContext() JSF
    1.2


    Returns the expression language
    context.




javax.el.ValueExpression JSF 1.2










  • Class getType(ELContext context)


    Returns the type of this value
    expression.




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