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:
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:
- 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.
- 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...
- 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