Samstag, 12. Dezember 2009

Spring context configured JSF tables

Icefaces

This time I had the demand to create dynamic JSF tables with icefaces. The disadvantage of the usual static way like the following is that you have barley access on the columns in the backing bean and much rendundant xhtml code for each table.

<ice:dataTable value="#{viewUsersBean.users}"
               var="item"
               rows="10">
    <ice:column>
        <f:facet name="header">
            <ice:outputText value="#{msgs['user.firstName']}"/>
        </f:facet>
        <ice:outputText value="#{item.firstName}"/>
    </ice:column>
    <ice:column>
        <f:facet name="header">
            <ice:outputText value="#{msgs['user.lastName']}"/>
        </f:facet>
        <ice:outputText value="#{item.lastName}"/>
    </ice:column>
</ice:dataTable>

A cool approach is to make a xhtml dataTable template and configure the columns in the spring context. We also need a backing table data model to access the attributes in a generic way (see TableModel#getCellValue).

viewUsers.xhtml:
<ice:form partialSubmit="true">
    <ui:decorate template="/templates/dataTableTemplate.xhtml">
        <ui:param name="tableModel" value="#{viewUsersBean.tableModel}"/>
        <ui:param name="items" value="#{viewUsersBean.users}"/>
    </ui:decorate>
</ice:form>

dataTableTemplate.xhtml:
<ice:dataTable value="#{items}"
    var="item"
     rows="10">
    <ice:columns value="#{tableModel.columnModel}" 
        var="columnConfig">
        <f:facet name="header">
            <ice:panelGroup>
                <ice:commandSortHeader columnName="#{columnConfig.field}" >
                    <ice:outputText value="#{columnConfig.label}"/>
                </ice:commandSortHeader>
            </ice:panelGroup>
        </f:facet>
        <ice:outputText value="#{tableModel.cellValue}"/>
    </ice:columns>
</ice:dataTable>

web-context.xml:
<bean id="viewUsersBean" class="com.bamboit.webapp.controller.ViewUsersBean" scope="request">
    <constructor-arg index="0" value="com.bamboit.model.User"/><constructor-arg index="1">
        <list>
            <bean class="com.bamboit.model.ColumnConfig">
                <property name="field" value="firstName"/>
                <property name="label" value="user.firstName"/>
            </bean>
            <bean class="com.bamboit.model.ColumnConfig">
                <property name="field" value="lastName"/>
                <property name="label" value="user.lastName"/>
            </bean>
        </list>
    </constructor-arg>
</bean>

public class ViewUsersBean {
    private Class entityClass;
    private TableModel tableModel;
    private UserManager userManager;

    public ViewUsersBean(Class entityClass, List<ColumnConfig> columnConfig) {
        this.entityClass = entityClass;
        this.tableModel = new TableModel( columnConfig );
    }

    public void init() {
        tableModel.setItems( userManager.getAll() );
    }
    
    public TableModel getTableModel() {
        return tableModel;
    }
    
    public void setUserManager( UserManager userManager ) {
        this.userManager = userManager;
    }
}

public class TableModel<t>() {
    private List< T > items;
    private int rowIndex;
    private List<ColumnConfig> columnConfig;
    private DataModel columnModel;

    public TableModel(List<ColumnConfig> columnConfig) {
        this.columnConfig = columnConfig;
        this.columnModel = new ListDataModel( columnConfig );
    }

    public DataModel getColumnModel() {
        return dataModel;
    }

    public void setItems( List<t> items ) {
        this.items = items;
    }

    public Object getCellValue ()
    {
        try
        {
            if ( isRowAvailable() && columnModel.isRowAvailable() )
            {
                final int col = columnModel.getRowIndex();
                final Object rowData = getRowData();
                try
                {
                    return BeanUtils.getProperty( rowData, columnConfigs.get(
                            col ).getField() );
                }
                catch ( final NestedNullException e )
                {
                    // in this case we have a nested property and a nested reference returns null
                    // for us this mean the cellvalue is null too
                    return null;
                }
            }
        }
        catch ( final IllegalAccessException e )
        {
            log.error("Failed to lookup cell value, possibly invalid column field configured",
                    e );
        }
        catch ( final InvocationTargetException e )
        {
            log.error("Failed to lookup cell value, possibly invalid column field configured",
                    e );
        }
        catch ( final NoSuchMethodException e )
        {
            log.error("Failed to lookup cell value, possibly invalid column field configured",
                    e );
        }
        return null;
    }

    public boolean isRowAvailable ()
    {
        return items != null &&
        (!(rowIndex > items.size()) || !(rowIndex < 0));
    }

    public int getRowCount ()
    {
        return items.size();
    }

    public Object getRowData ()
    {
        return items.get( rowIndex );
    }

    public int getRowIndex ()
    {
        return rowIndex;
    }

    public void setRowIndex ( int rowIndex )
    {
        this.rowIndex = rowIndex;
    }

    public Object getWrappedData ()
    {
        return null;
    }

    public void setWrappedData ( Object o )
    {
    }

}

public class ColumnConfig
{

    private String label;

    private String field;

...
}

This is a basis to add more features like:
- attribute type specific output rendering
- row selection
- table inline data editing
- data pagination
- role based column rendering
- user customized column order/visibility

I'll describe this features in my next blogs.

Kommentare:

  1. Pretty interesting concept. I think this should be added to my ICEfusion lib (http://code.google.com/p/icefusion) ;-).

    AntwortenLöschen
  2. Yes, grats for taking the time to write this. I also implemented this pattern in a few projects, but from this to getting a really generalized solution (something that could be even packed as a component) I met this challenge:
    when using ice:column instead of ice:columns you have the chance to specify the output format for "this" column (think about currencies, dates / times, etc). When you do it inside the getCellValue ... you cannot do that anymore. So something that should be done is finding a way so specify some output converters for each column when using ice:columns.

    AntwortenLöschen
  3. Congrats for the technical blog award.

    AntwortenLöschen
  4. @edykory: Thanks for grats. I do the (type) specific output rendering via the rendered attribute.

    Something like this:

    ice:panelGroup style="text-align:center;" rendered="#{columnConfig.booleanCheckBox};
    ice:selectBooleanCheckbox value="#{tableModel.cellValue}" disabled="#{tableViewBean.writeProtected or tableViewBean.columnReadonly}" /
    /ice:panelGroup;

    ice:outputText value="#{tableModel.cellValue}" rendered="#{columnConfig.timestamp}"
    f:converter converterId="timestampRenderer" /
    /ice:outputText
    (Unfortunately I have to remove the brackets)

    And so on ...

    I would be very intrested in your ice:column solution.

    AntwortenLöschen
  5. I discribed this in this post http://blog.bambo.it/2010/01/icefaces-data-table-attribute-type.html

    getCellValue only returns the plain value. The specific rendering/formatting happens in the ice:columns. Either in a generic way or as explicite as you want.

    AntwortenLöschen