The <h:dataTable/>
, although easy for many to understand to use is often not understood in how it actually works. Since my article on component tree construction and rendering with respect to JSTL seemed to be received pretty well, I have decided to cover how the <h:dataTable/>
works.
Table Of Contents
- Looping components
- General Architecture
- Encoding the Children
- Understanding var
- Other Phases
- Understanding Client IDs
- Event Broadcasting
- Binding
- Conclusion
Looping components
The dataTable is one of many components that loop, although it is unfortunately the only one provided with core JSF. Other looping components are from 3rd party libraries. Here are some:
Each of these components, although renders quite different HTML all work using a similar design, modeled by the core <h:dataTable/>
.
General Architecture
The API is simple, but the architecture, not so much. The API involves the construction of a set of components that produce an outer HTML element followed by inner HTML elements. For example, the <h:dataTable/>
creates an outer <TABLE>
with a <TR/>
for each iteration of the loop. Others like <t:dataList>
, <ui:repeat>
and <tr:iterator>
produce no HTML usually, so they simply allow one to loop over a set of components for many objects in a model.
Each of these components have value
and var
attributes, although their names may change slightly from component to component. The value
provides some kind of model data to the component. This may be a model class (like a tree model for a tree component), or just object arrays or collections of objects. Each component differs on what values they support, but either way all point to a model of data that contains many objects. When the component is rendered, each object in the model is iterated over. In order for page developers to access the data, an EL variable is injected into scope temporarily. This variable has a name given by the var
attribute. Take for example this code that shows all of the cookies sent to the server in a table in a servlet environment:
<h:dataTable
var="_cookie"
value="#{facesContext.externalContext.request.cookies}">
<h:column>
<f:facet name="header">
<h:outputText value="Name" />
</f:facet>
<h:outputText value="#{_cookie.name}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Value" />
</f:facet>
<h:outputText value="#{_cookie.value}" />
</h:column>
</h:dataTable>
The value
is an array of Cookie
objects. The var
is "_cookie" (I use an underscore to make sure the name does not intefere with any scoped objects). The table renders the header and footer as a normal JSF component, but then iterates over the data. In this case, it converts the array to a DataModel. It then sets the rowIndex
which caused the rowData
to return the current cookie (so a row index of 0 returns the first cookie and setting the index to 1 will cause the 2nd cookie to be returned). Basically this is a simple iterator pattern. Some components loop through all, some loop only through a subset (the table can loop through a portion using the rows
and first
attributes).
The most important thing to note is that even though there may be several cookies, there are only ever 4 outputText
components (1 in each header and one in each column).
Encoding the Children
What happens to produce multiple <TR/>
elements is that the TableRenderer
encodes its children in a loop. Here are the steps of TableRenderer.encodeChildren
:
- get the first row to be rendered from
UIData.getFirst()
- get the number of rows to be rendered from
UIData.getRows()
- write the
<TBODY/>
start tag - loop until the correct number of rows have been rendered
- if row is not available then stop
- render the row, encoding all the children recursively
Understanding "var"
The var
is not controlled by the renderer, but the component, UIData
to be precise. Non-UIData
components that loop perform similar logic if designed after the JSF table. The scope of the var
is limited to when the <t:dataTable/>
is currently iterating. To be exact, it is available when the rowIndex
is not -1. In the UIData.setRowIndex(int)
method, if the index is being set to a value not equal to -1, then the row state is updated for the appropriate index. If the value is -1, the state is cleared.
The state is comprised of all the children components that implement EditableValueHolder. So before the row index is changed, UIData
saves the value
, localValueSet
, valid
and submittedValue
properties for each child below the table and restores any state saved for the new index. This means that only EditableValueHolder
components may vary state for different rows. Non-EditableValueHolder
components may only vary their behavior (a.k.a. attributes) per row by using EL expressions that use a value that changes per row, like var
.
The code stores the row data for the current row into the request map using the value of var
as the key. When the row index is set to -1, the key and value are removed from the request map. This means that if the var
is the same as another request map variable, it will be overridden and then lost. Therefore, it is very important to use a value for var
that will not conflict with other request map variables. This is why I always prepend my var
values with and underscore.
Note:
Thevar
is only in scope when theUIData
's row index is not -1. This means that it is not available when the component tree is being built, or usually when accessed from outside the component. If you would like to get access a child component ofUIData
with the correct scope, you must set the row index to the desired value, otherwise the data will not be there.
Other Phases
The decode, validate and updating processes of the <h:dataTable/>
work the same way as the rendering. They loop through each row index and invoke the appropriate methods on the children components. Since the code to alter the var
and save and restore the states is in UIData
, the behavior is shared.
Understanding Client IDs
You may have noticed that the client IDs of looped components are altered in a table. Take this use case:
<h:dataTable
id="cookies"
var="_cookie"
value="#{facesContext.externalContext.request.cookies}">
<h:column>
<h:outputText id="cookieName" value="#{_cookie.name}" />
</h:column>
</h:dataTable>
<h:outputText id="after" value="After the table" />
The two <h:outputText/>
components will have different client IDs. The one outside the table will just be "after" (assuming that there are no parent naming containers). The one in the table however will be "cookies:0:cookieName". The number, '0' in this example, will be the row index. This happens because the UIData
component returns a different ID from getClientId when the row index is not -1. The code from the JSF RI (a.k.a. Mojarra) is:
@Override
public String getClientId(FacesContext context)
{
String clientId = super.getClientId(context);
int rowIndex = getRowIndex();
if (rowIndex == -1)
{
return clientId;
}
return clientId + NamingContainer.SEPARATOR_CHAR + rowIndex;
}
As you can see, NamingContainer.SEPARATOR_CHAR + rowIndex
is added onto the end of the table's client ID when the row index is not -1. This ID convention stops ID collision in the HTML, but it also is used for event broadcasting.
Event Broadcasting
When a child of the table is decoded, it decodes correctly due to the client ID. Take for example an <h:inputText id="myInputText"/>
component that is in a table. Using just two rows as an example here is a map of the client IDs:
Row Index | Client ID |
-1 | myTable:myInputText |
0 | myTable:0:myInputText |
1 | myTable:1:myInputText |
When the renderer for the input text is decoding, it looks for a value submitted by the client using the client ID as the key. Since the client ID changes, the input text correctly decodes the value from the client for the current row index.
Now this is great for decoding, but a different mechanism must be used for the queuing and the broadcasting of events. The UIData
component wraps all events in a wrapper class that stores the event being queued, the client ID of the UIData
component and the current row index. Here is what each property is used for:
- Client ID
- The client ID is used in case this table is nested in another table. By identifying the event to broadcast using this ID, the framework can ensure events are broadcast to the correct state of a nested looping component.
- Event
- The original event that will be unwrapped during broadcast. The wrapper delegates much of its functionality to the wrapped event (like the phase ID).
- Row Index
- During broadcasting of the event, this stored index is used to correctly re-position the model to the correct row so that the event is fired in the same row state as when it was queued.
Binding
When binding looping components or children of them to managed beans, it is important to realize what the row index is. For example, if you have an <h:commandLink action=#{bean.actionMethod}"/>
component as a child of a column in a table, the action method is run in the scope of the correct row index. This is because the event is wrapped knowing the correct row index. Therefore, your action method may access the current row data.
Also since the var
's scope is only valid during iteration, it is not accessible during the construction of the component tree. Therefore and JSP tags or Facelets TagHandler
s may not attempt to retrieve row data using the UIData
APIs.
Conclusion
The data table component and looping components like it re-use a set of components encoded several times, once for each item to be rendered. The value, submitted value, if the component is valid and if the local value is set properties of EditableValueHolder
children components are supported across iteration of the model. Other properties of components are not saved and thus must use EL to vary their behavior based on the current row.
Client IDs and event wrappers are used to ensure that the event model of JSF is not broken by the iteration of the component.
Hopefully this helps you understand looping components better and helps you design better pages as a result. Thank you for reading.
3 comments:
Very good post, Andrew!
Agree, good post.
Please keep them coming.
I would appreciate the opportunity to buy you a couple rounds. Great post, incredibly helpful. I was trying to do some binding="" magic inside a datagrid and this helped me get there.
Post a Comment