1

We are running on Java EE 6 + PrimeFaces 3.5.28 + JSF 2.1.6 (currently) on GlassFish 3.1.2.2.

Our requirement for the datatables is to have dynamic columns, which can basically be of any content, that is simple output text, complex output texts, formatted dates, formatted numbers, booleans displaying Yes/No (language-dependent), enums displaying their value labels (language-dependent), buttons, links (see my first comment) - you get the point.

We have solved our requirements with the construct of a <p:dataTable> making use of a <c:forEach> + <p:column binding="">:

<p:dataTable id="data"
             widgetVar="resultDataTable" 
             value="#{depotManager.dataModel}"
             var="dep"
             rowKey="#{dep.id}"
             selection="#{depotManager.selectedEntity}"
             selectionMode="single"
             paginator="true"
             paginatorPosition="bottom"
             paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"
             rows="#{depotManager.selectedRowsPerPage}"
             rowsPerPageTemplate="25,50,100,200"
             resizableColumns="true"
             scrollable="true"
             scrollHeight="90%"
             emptyMessage="#{msg['entity.depot.list.emptyMessage']}">

    <p:ajax event="rowSelect"
            listener="#{depotManager.edit}"
            update=":content-form"
            onstart="mainStatusDialog.show();"
            oncomplete="mainStatusDialog.hide();"
            disabled="#{empty depotManager.entities}" />

    <p:ajax event="sort" listener="#{depotManagerColumnHandler.onSort}" global="false" />
    <p:ajax event="colReorder" listener="#{depotManagerColumnHandler.onReorder}" global="false" />
    <p:ajax event="colResize" listener="#{depotManagerColumnHandler.onResize}" global="false" />

    <c:forEach items="#{depotManagerColumnHandler.boundColumns}"
               var="col">                    
        <p:column id="#{col.id}" binding="#{col}" />
    </c:forEach>

    ...

This has been the only approach that we have ever gotten to work. Call it luck, but it did what our customers wanted (using Mojarra 2.1.6).

This worked for us until now. (We also realized, that while our column handler beans are tagged as @ViewScoped, they're actually behaving as @RequestScoped.)

For a moment, here's the DepotManagerColumnHandler bean for the c:forEach to iterate over the columns, which basically just contains ALL PrimeFaces Column instantiations:

@Named
@ViewScoped // behaves request-scoped under Mojarra 2.1.6
public class DepotManagerColumnHandler extends BaseColumnHandler
{
    private static final long serialVersionUID = 1L;

    @Override
    protected List<Column> newConfigurableColumns()
    {
        DefaultColumnFactory columnFactory = new DefaultColumnFactory( "dep", StringUtils.uncapitalize( this.getClass().getSimpleName() ), EModule.COMPLIANCE );

        List<Column> columns = new ArrayList<Column>();

        // content columns
        columns.add( columnFactory.newActiveOutputTextColumn() );

        columns.add( this.createColumnWithCss( "nbr", null, "entity.depot.nbr.header", false, 100 ) );
        columns.add( this.createColumnWithCss( "bank.code", null, "entity.bank.code.header", true, 70 ) );
        columns.add( this.createColumnWithCss( "bank.name", null, "entity.bank.name.header", true, 200 ) );

        columns.add( this.createColumnWithCss( "employee.nbr",
                                               "#{not empty dep.employee ? dep.employee.nbr : ''}",
                                               "entity.employee.nbr.header", true, 80 ) );
        columns.add( this.createColumnWithCss( "employee.firstName",
                                               "#{not empty dep.employee ? dep.employee.firstName : ''}",
                                               "entity.employee.firstName.header", true, 120 ) );
        columns.add( this.createColumnWithCss( "employee.lastName",
                                               "#{not empty dep.employee ? dep.employee.lastName : ''}",
                                               "entity.employee.lastName.header", true, 120 ) );
        columns.add( this.createColumnWithCss( "employee.employeeStatus.name",
                                               "#{not empty dep.employee and not empty dep.employee.employeeStatus ? dep.employee.employeeStatus.name : ''}",
                                               "entity.employeeStatus.singular.header", true, 50 ) );

        return columns;
    }

    private Column createColumnWithCss( String attr, String outputTextExpr, String headerTextMsgKey, boolean removable, int defaultWidth )
    {
        DefaultColumnFactory columnFactory = new DefaultColumnFactory( "dep", StringUtils.uncapitalize( this.getClass().getSimpleName() ),
                                                                       EModule.COMPLIANCE );
        columnFactory.setTooltipEnabled( true );
        return columnFactory.newOutputTextColumn( attr, String.class, outputTextExpr, null, headerTextMsgKey, removable, true, true,
                                                  defaultWidth, true, "#{true}", "overflow-text-hidden", null );
    }
}

Here's the super class' BaseColumnHandler relevant part:

public abstract class BaseColumnHandler implements ColumnHandler, Serializable
{
    // recent order of columns as string, e.g. "active nbr code bank-code ..."
    private String       recentOrder;

    private List<Column> boundColumns;

    /**
     * The getter for the Facelet c:forEach. (Will reload and parse the recent order on any request from the UI.)
     */
    public List<Column> getBoundColumns()
    {
        if ( this.boundColumns == null )
        {
            List<Column> shownColumns = new ArrayList<Column>();

            // parse the string of visible columns as getRecentOrder() loaded it from the DB
            String order = this.getRecentOrder().trim();
            String[] columnIds = StringUtils.split( order, ' ' );

            // sub classes must implement newConfigurableColumns()
            List<Column> shownColumns = this.newConfigurableColumns();

            for ( String columnId : columnIds )
            {
                // some filtering going on to filter hidden columns (assume this does what it should)
                ...
            }

            // assign visible columns only
            this.boundColumns = shownColumns;
        }

        return this.boundColumns;
    }

    /**
     * Creates a new list of all generally configurable columns.
     * 
     * @return
     */
    protected abstract List<Column> newConfigurableColumns();

    ...
}

Here's a picture of the column handler expressions in place:

enter image description here

We have to go towards JSF 2.2 and Java EE 7 in the future, so we're currently trying to upgrade to a much more recent Mojarra version, here 2.1.22.

What I tried so far:

  1. Upgrading to Mojarra 2.1.22 and leaving the code as-is results in duplicate column ID's, but only when removing columns:

Code:

<c:forEach items="#{depotManagerColumnHandler.boundColumns}"
           var="col">                    
    <p:column id="#{col.id}" binding="#{col}" />
</c:forEach>

Exception:

java.lang.IllegalStateException: Component ID content-form:data:employee-employee-status-name has already been found in the view.  
  1. Using <p:columns>:

If you look at the more complex JSF expressions in DepotManagerColumnHandler like "#{not empty dep.employee ? dep.employee.nbr : ''}", this results in columns displaying an empty string if there's no employee relationship present (the employee is optional). The standard PrimeFaces solution to use p:columns as shown in the showcase http://www.primefaces.org/showcase/ui/data/datatable/columns.xhtml doesn't work (only the header text displays correctly):

Code:

<p:columns value="#{depotManagerColumnHandler.boundColumns}"
           var="col">
    <f:facet name="header">
        <h:outputText value="#{col.headerText}"
                      title="#{col.headerText}" />
    </f:facet>
    <h:outputText value="#{dep[col.property]}" />
</p:columns>

We don't automatically have <h:outputText value="#{dep[col.property]}" /> for each column... we might have columns with buttons or links etc.

Further <h:outputText value="#{dep[col.property]}" /> cannot work, because there's no way what col.property should return for more complex paths "bank.code" or even custom expressions "#{not empty dep.employee ? dep.employee.nbr : ''}" or "#{dep.active ? msg['common.yes.label'] : msg['common.no.label']}". col.property seems to be made for simple paths like "nbr", "code" only...

  1. Using <p:columns ... var="col"> while trying to get a binding via <p:column id="#{col.id}" binding="#{col}">:

Code:

<p:columns value="#{depotManagerColumnHandler.boundColumns}"
           var="col">
    <p:column id="#{col.id}" binding="#{col}" />
</p:columns>

This try is quite similar to 1., however this results in an exception:

Caused by: java.lang.IllegalArgumentException: Empty id attribute is not allowed
    at javax.faces.component.UIComponentBase.validateId(UIComponentBase.java:566)
    at javax.faces.component.UIComponentBase.setId(UIComponentBase.java:370)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.assignUniqueId(ComponentTagHandlerDelegateImpl.java:369)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:172)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.apply(DelegatingMetaTagHandler.java:120)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.applyNextHandler(DelegatingMetaTagHandler.java:137)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:187)

QUESTION:

How do you solve dynamic columns in a PrimeFaces datatable in a Mojarra 2.1.22+ environment having the requirement of completely custom column content (best using view-scoped beans)?

BTW, we're also using Seam 3 to get view scope with CDI... @Named + @ViewScoped

Kawu
  • 13,647
  • 34
  • 123
  • 195
  • Well, you say you are using columns with buttons, links, and so on, but in the `createColumnWithCss` method you only create output text columns, right? What am I missing? I have never seen this kind of flexibility requirement in a component, curious. I believe there's a third option which would provide you even more flexibility, using [a custom java-based component](http://jdevelopment.nl/simple-java-based-jsf-22-custom-component/) and writing your own encoder. This way you define the component's rendering behaviour in java, not in xhtml, which is has more restrictions. – Aritz Jul 05 '16 at 21:01
  • Well, the example I posted is from one of the simpler applications, so in effect the link and button columns aren't shown. As I thought about it, we are **likely** going to get away with the button and link columns as they are usually to the very left or right of the configurable columns. So we might define static ``s for them before or after the actual content columns. I updated the question with another example/sub question that is not working (you can omit the button/link column requirement for now). – Kawu Jul 06 '16 at 05:33
  • Oh and yes, the `createColumnWithCss` is just a local helper method to make the output text content not wrap, but display a ... instead. – Kawu Jul 06 '16 at 05:55

0 Answers0