Tuesday, December 4, 2012

Use forEach in dataTable

Usually forEach does not work under dataTable.
Here is an example:

<h:dataTable value="#{list}" var="obj">
    <c:forEach items="#{propertylist}" var="propertyName">
        <h:column>
            <h:outputText id="text" value="#{obj[propertyName]}"></h:outputText/>
        </h:column>
    </c:forEach>
</h:dataTable>

In this example, forEach expects propertylist variable. it not depends on variable exposed by datatable at render time.  Let us assume that propertylist is available at build time (buildView()). We should have one column for each object in propertylist. We indeed has one column for each object in templist. But the  column text(id=text) is empty. So c:forEach runs correctly and the ValueExpression is evaluated correctly at build time, why there is no value for text?

When text(id=text) component is built, the "#{obj[propertyName]}" is set to Value Expression mapping.  This expression refers two variables: propertyName and obj. propertyName is only available at build time while obj is only available at render time. ValueExpression is not evaluated as you expect. What we need is to evalulate the variable 'propertyName' at build time and keep its value somehow available at render time.
To assist this, I add an extra tagHandler like f:attribute, but it evaluates ValueExpression immediately, then set the value as attribute's value instead of setting the value expression to ValueExpression Map. The value can be referred late as "component.attributes.name" at later time.


public class BuildAttributeHandler extends TagHandler implements
        AttributeHandler
{

    private final TagAttribute name;

    private final TagAttribute value;

    public BuildAttributeHandler(TagConfig config)
    {
        super(config);
        this.name = this.getRequiredAttribute("name");
        this.value = this.getRequiredAttribute("value");
    }

    @Override
    public void apply(FaceletContext ctx, UIComponent parent)
            throws IOException
    {
        if (parent == null)
        {
            throw new TagException(this.tag, "Parent UIComponent was null");
        }

        // only process if the parent is new to the tree
        if (parent.getParent() == null)
        {
            String n = getAttributeName(ctx);
            if (!parent.getAttributes().containsKey(n))
            {
               
                if (this.value.isLiteral())
                {
                    parent.getAttributes().put(n, this.value.getValue());
                } else
                {
                    parent.getAttributes().put(n, this.value.getValueExpression(ctx, Object.class).getValue(FacesContext.getCurrentInstance().getELContext()));
                }
            }
        }

    }

    @Override
    public String getAttributeName(FaceletContext ctx)
    {
        return this.name.getValue(ctx);
    }

}

This tag can be configured to taglib like this
<tag>
        <tag-name>buildAttribute</tag-name>
        <handler-class>com.flexdms.flexims.webui.BuildAttributeHandler</handler-class>
        <attribute>
            <description>name</description>
            <name>name</name>
            <required>true</required>
            <type>java.lang.String</type>
        </attribute>
        <attribute>
            <description>value</description>
            <name>value</name>
            <required>true</required>
        </attribute>
</tag>

With this tag handler, the above example can be made to work like this

<h:dataTable value="#{list}" var="obj">
    <c:forEach items="#{propertylist}" var="propertyName">
        <h:column>
            <h:outputText id="text" value="#{obj[component.attributes.propertyName]}">
                <fx:buildAttribute name="propetyName" value="#{propertyName}"></fx:buildAttribute>
            </h:outputText/>
        </h:column>
    </c:forEach>
</h:dataTable>

The tag handler is actually really useful. With it, you can export some context available at build time to render time. For example, you usually can not build a datatable with variable number of columns. Some component library provides some alternative. For example primefaces provides p:columns tag. Unfortunately, p:columns is a regular column component. The dynamics aspects is implemented by renderer. Moreover, p:columns can only be used together with p:dataTable and p:column.  This tag handler solves the issue in a simple and elegant way.

pitfall: buildView() can be called multiple times. It is called when the tree is first built. It can be called again in RestoreState stage. If you use this tag, make sure its value can be evaluated in all of these times.



No comments:

Post a Comment