Creating Custom Component Classes
As explained in When to Use a Custom Component, a component class defines the state and behavior of
a UI component. The state information includes the component’s type, identifier, and local
value. The behavior defined by the component class includes the following:
Decoding (converting the request parameter to the component’s local value)
Encoding (converting the local value into the corresponding markup)
Saving the state of the component
Updating the bean value with the local value
Processing validation on the local value
Queueing events
The javax.faces.component.UIComponentBase class defines the default behavior of a component class. All the
classes representing the standard components extend from UIComponentBase. These classes add their
own behavior definitions, as your custom component class will do.
Your custom component class must either extend UIComponentBase directly or extend a class representing
one of the standard components. These classes are located in the javax.faces.component package
and their names begin with UI.
If your custom component serves the same purpose as a standard component, you
should extend that standard component rather than directly extend UIComponentBase. For example,
suppose you want to create an editable menu component. It makes sense to
have this component extend UISelectOne rather than UIComponentBase because you can reuse the
behavior already defined in UISelectOne. The only new functionality you need to define
is to make the menu editable.
Whether you decide to have your component extend UIComponentBase or a standard
component, you might also want your component to implement one or more of
these behavioral interfaces defined in the javax.faces.component package:
ActionSource: Indicates that the component can fire a javax.faces.event.ActionEvent.
ActionSource2: Extends ActionSource and allows component properties referencing methods that handle action events to use method expressions as defined by the unified EL.
EditableValueHolder: Extends ValueHolder and specifies additional features for editable components, such as validation and emitting value-change events.
NamingContainer: Mandates that each component rooted at this component have a unique ID.
StateHolder: Denotes that a component has state that must be saved between requests.
ValueHolder: Indicates that the component maintains a local value as well as the option of accessing data in the model tier.
If your component extends UIComponentBase, it automatically implements only StateHolder. Because all components
directly or indirectly extend UIComponentBase, they all implement StateHolder. Any component that implements StateHolder
also implements the StateHelper interface, which extends StateHolder and defines a Map-like contract that
makes it easy for components to save and restore a partial view state.
If your component extends one of the other standard components, it might also
implement other behavioral interfaces in addition to StateHolder. If your component extends
UICommand, it automatically implements ActionSource2. If your component extends UIOutput or one of the
component classes that extend UIOutput, it automatically implements ValueHolder. If your component extends
UIInput, it automatically implements EditableValueHolder and ValueHolder. See the JavaServer Faces API documentation
to find out what the other component classes implement.
You can also make your component explicitly implement a behavioral interface that it
doesn’t already by virtue of extending a particular standard component. For example, if
you have a component that extends UIInput and you want it to fire
action events, you must make it explicitly implement ActionSource2 because a UIInput
component doesn’t automatically implement this interface.
The Duke's Bookstore image map example has two component classes: AreaComponent and
MapComponent. The MapComponent class extends UICommand and therefore implements ActionSource2, which means it can
fire action events when a user clicks on the map. The AreaComponent
class extends the standard component UIOutput. The @FacesComponent annotation registers the components with the
JavaServer Faces implementation:
@FacesComponent("DemoMap")
public class MapComponent extends UICommand {...}
@FacesComponent("DemoArea")
public class AreaComponent extends UIOutput {...}The MapComponent class represents the component corresponding to the bookstore:map tag:
<bookstore:map id="bookMap"
current="map1"
immediate="true"
action="bookstore">
...
</bookstore:map>The AreaComponent class represents the component corresponding to the bookstore:area tag:
<bookstore:area id="map1" value="#{Book201}"
onmouseover="resources/images/book_201.jpg"
onmouseout="resources/images/book_all.jpg"
targetImage="mapImage"/>MapComponent has one or more AreaComponent instances as children. Its behavior consists
of the following actions:
Retrieving the value of the currently selected area
Defining the properties corresponding to the component's values
Generating an event when the user clicks on the image map
Queuing the event
Saving its state
Rendering the HTML map tag and the HTML input tag
MapComponent delegates the rendering of the HTML map and input tags to
the MapRenderer class.
AreaComponent is bound to a bean that stores the shape and coordinates of
the region of the image map. You will see how all this data
is accessed through the value expression in Creating the Renderer Class. The behavior of AreaComponent
consists of the following:
Retrieving the shape and coordinate data from the bean
Setting the value of the hidden tag to the id of this component
Rendering the area tag, including the JavaScript for the onmouseover, onmouseout, and onclick functions
Although these tasks are actually performed by AreaRenderer, AreaComponent must delegate the
tasks to AreaRenderer. See Delegating Rendering to a Renderer for more information.
The rest of this section describes the tasks that MapComponent performs as well
as the encoding and decoding that it delegates to MapRenderer. Handling Events for Custom Components details
how MapComponent handles events.
Specifying the Component Family
If your custom component class delegates rendering, it needs to override the getFamily
method of UIComponent to return the identifier of a component family, which is used to
refer to a component or set of components that can be rendered by
a renderer or set of renderers.
The component family is used along with the renderer type to look
up renderers that can render the component:
public String getFamily() {
return ("Map");
}The component family identifier, Map, must match that defined by the component-family elements
included in the component and renderer configurations in the application configuration resource file. Registering a Custom Renderer with a Render Kit
explains how to define the component family in the renderer configuration. Registering a Custom Component explains how
to define the component family in the component configuration.
Performing Encoding
During the Render Response phase, the JavaServer Faces implementation processes the encoding methods of
all components and their associated renderers in the view. The encoding methods convert
the current local value of the component into the corresponding markup that represents
it in the response.
The UIComponentBase class defines a set of methods for rendering markup: encodeBegin, encodeChildren,
and encodeEnd. If the component has child components, you might need to use
more than one of these methods to render the component; otherwise, all rendering
should be done in encodeEnd. Alternatively, you can use the encodeALL method, which
encompasses all the methods.
Because MapComponent is a parent component of AreaComponent, the area tags must
be rendered after the beginning map tag and before the ending map tag.
To accomplish this, the MapRenderer class renders the beginning map tag in encodeBegin
and the rest of the map tag in encodeEnd.
The JavaServer Faces implementation automatically invokes the encodeEnd method of AreaComponent's renderer
after it invokes MapRenderer's encodeBegin method and before it invokes MapRenderer's encodeEnd method. If a
component needs to perform the rendering for its children, it does this in
the encodeChildren method.
Here are the encodeBegin and encodeEnd methods of MapRenderer:
@Override
public void encodeBegin(FacesContext context, UIComponent component)
throws IOException {
if ((context == null)|| (component == null)){
throw new NullPointerException();
}
MapComponent map = (MapComponent) component;
ResponseWriter writer = context.getResponseWriter();
writer.startElement("map", map);
writer.writeAttribute("name", map.getId(), "id");
}
@Override
public void encodeEnd(FacesContext context, UIComponent component)
throws IOException {
if ((context == null) || (component == null)){
throw new NullPointerException();
}
MapComponent map = (MapComponent) component;
ResponseWriter writer = context.getResponseWriter();
writer.startElement("input", map);
writer.writeAttribute("type", "hidden", null);
writer.writeAttribute("name", getName(context,map), "clientId");(
writer.endElement("input");
writer.endElement("map");
}Notice that encodeBegin renders only the beginning map tag. The encodeEnd method
renders the input tag and the ending map tag.
The encoding methods accept a UIComponent argument and a javax.faces.context.FacesContext argument. The FacesContext
instance contains all the information associated with the current request. The UIComponent argument
is the component that needs to be rendered.
The rest of the method renders the markup to the javax.faces.context.ResponseWriter instance,
which writes out the markup to the current response. This basically involves passing
the HTML tag names and attribute names to the ResponseWriter instance as strings,
retrieving the values of the component attributes, and passing these values to the
ResponseWriter instance.
The startElement method takes a String (the name of the tag) and the
component to which the tag corresponds (in this case, map). (Passing this
information to the ResponseWriter instance helps design-time tools know which portions of the
generated markup are related to which components.)
After calling startElement, you can call writeAttribute to render the tag's attributes.
The writeAttribute method takes the name of the attribute, its value, and
the name of a property or attribute of the containing component corresponding to
the attribute. The last parameter can be null, and it won't be rendered.
The name attribute value of the map tag is retrieved using the getId
method of UIComponent, which returns the component's unique identifier. The name attribute value
of the input tag is retrieved using the getName(FacesContext, UIComponent) method of MapRenderer.
If you want your component to perform its own rendering but delegate to
a renderer if there is one, include the following lines in the
encoding method to check whether there is a renderer associated with this component:
if (getRendererType() != null) {
super.encodeEnd(context);
return;
}If there is a renderer available, this method invokes the superclass's encodeEnd
method, which does the work of finding the renderer. The MapComponent class delegates
all rendering to MapRenderer, so it does not need to check for
available renderers.
In some custom component classes that extend standard components, you might need to
implement other methods in addition to encodeEnd. For example, if you need to
retrieve the component’s value from the request parameters, you must also implement the
decode method.
Performing Decoding
During the Apply Request Values phase, the JavaServer Faces implementation processes the decode
methods of all components in the tree. The decode method extracts a component's local
value from incoming request parameters and uses a javax.faces.convert.Converter implementation to convert the value
to a type that is acceptable to the component class.
A custom component class or its renderer must implement the decode method
only if it must retrieve the local value or if it needs to
queue events. The component queues the event by calling queueEvent.
Here is the decode method of MapRenderer:
@Override
public void decode(FacesContext context, UIComponent component) {
if ((context == null) || (component == null)) {
throw new NullPointerException();
}
MapComponent map = (MapComponent) component;
String key = getName(context, map);
String value = (String) context.getExternalContext().
getRequestParameterMap().get(key);
if (value != null)
map.setCurrent(value);
}
}The decode method first gets the name of the hidden input field by
calling getName(FacesContext, UIComponent). It then uses that name as the key to the
request parameter map to retrieve the current value of the input field. This value
represents the currently selected area. Finally, it sets the value of the MapComponent
class's current attribute to the value of the input field.
Enabling Component Properties to Accept Expressions
Nearly all the attributes of the standard JavaServer Faces tags can accept expressions,
whether they are value expressions or method expressions. It is recommended that you
also enable your component attributes to accept expressions because it gives you much
more flexibility when you write Facelets pages.
To enable the attributes to accept expressions, the component class must implement getter
and setter methods for the component properties. These methods can use the facilities
offered by the StateHelper interface to store and retrieve not only the values
for these properties, but also the state of the components across multiple requests.
Because MapComponent extends UICommand, the UICommand class already does the work of getting the
ValueExpression and MethodExpression instances associated with each of the attributes that it supports.
Similarly, the UIOutput class that AreaComponent extends already obtains the ValueExpression instances for
its supported attributes. For both components, the simple getter and setter methods store and
retrieve the key values and state for the attributes, as shown in this
code fragment from AreaComponent:
enum PropertyKeys {
alt, coords, shape, targetImage;
}
public String getAlt() {
return (String) getStateHelper().eval(PropertyKeys.alt, null);
}
public void setAlt(String alt) {
getStateHelper().put(PropertyKeys.alt, alt);
}
...However, if you have a custom component class that extends UIComponentBase, you
will need to implement the methods that get the ValueExpression and MethodExpression
instances associated with those attributes that are enabled to accept expressions. For example, you
could include a method that gets the ValueExpression instance for the immediate attribute:
public boolean isImmediate() {
if (this.immediateSet) {
return (this.immediate);
}
ValueExpression ve = getValueExpression("immediate");
if (ve != null) {
Boolean value = (Boolean) ve.getValue(
getFacesContext().getELContext());
return (value.booleanValue());
} else {
return (this.immediate);
}
}The properties corresponding to the component attributes that accept method expressions must accept
and return a MethodExpression object. For example, if MapComponent extended UIComponentBase instead of
UICommand, it would need to provide an action property that returns and accepts
a MethodExpression object:
public MethodExpression getAction() {
return (this.action);
}
public void setAction(MethodExpression action) {
this.action = action;
}
Saving and Restoring State
As described in Enabling Component Properties to Accept Expressions, use of the StateHelper interface facilities allows you to
save the component's state at the same time you set and retrieve property
values. The StateHelper implementation allows partial state saving: it saves only the changes
in the state since the initial request, not the entire state, because the
full state can be restored during the Restore View phase.
Component classes that implement StateHolder may prefer to implement the saveState(FacesContext) and restoreState(FacesContext, Object)
methods to help the JavaServer Faces implementation save and restore the full state
of components across multiple requests.
To save a set of values, you can implement the saveState(FacesContext) method.
This method is called during the Render Response phase, during which the state
of the response is saved for processing on subsequent requests. Here is a
hypothetical method from MapComponent, which has only one attribute, current:
@Override
public Object saveState(FacesContext context) {
Object values[] = new Object[2];
values[0] = super.saveState(context);
values[1] = current;
return (values);
}This method initializes an array, which will hold the saved state. It next
saves all of the state associated with the component.
A component that implements StateHolder may also provide an implementation for restoreState(FacesContext, Object),
which restores the state of the component to that saved with the saveState(FacesContext)
method. The restoreState(FacesContext, Object) method is called during the Restore View phase, during which
the JavaServer Faces implementation checks whether there is any state that was saved
during the last Render Response phase and needs to be restored in preparation
for the next postback.
Here is a hypothetical restoreState(FacesContext, Object) method from MapComponent:
public void restoreState(FacesContext context, Object state) {
Object values[] = (Object[]) state;
super.restoreState(context, values[0]);
current = (String) values[1];
}This method takes a FacesContext and an Object instance, representing the array
that is holding the state for the component. This method sets the component’s
properties to the values saved in the Object array.
Whether or not you implement these methods in a component class, you can
use the javax.faces.STATE_SAVING_METHOD context parameter to specify in the deployment descriptor where you
want the state to be saved: either client or server. If state
is saved on the client, the state of the entire view is rendered
to a hidden field on the page. By default, the state is saved
on the server.
The web applications in the Duke's Forest case study save their view state
on the client.
Saving state on the client uses more bandwidth as well as more
client resources, while saving it on the server uses more server resources. You
may also want to save state on the client if you expect your
users to disable cookies.