Search code examples
datatableprimefacesdynamic-columns

Primefaces 8.0 Datatable with Dynamic Model and Columns


I have a primefaces dataTable that displays data from several database tables. A selection list allows the user to select the specific database table to display. It works as expected except when the dataTable filtering capability is used. For example, when a user selects 'DEPT' from the selection list, the dataTable is rendered with data from the DEPT table. The user can select other tables normally. However, if the user selects another table called 'EMP' after filtering, the dataTable fails to render with the following exception:

javax.el.PropertyNotFoundException: The class 'example.dto.Dept' does not have the property 'firstName'.
at javax.el.BeanELResolver.getBeanProperty(BeanELResolver.java:576)
at javax.el.BeanELResolver.getValue(BeanELResolver.java:291)
at com.sun.faces.el.DemuxCompositeELResolver._getValue(DemuxCompositeELResolver.java:156)

Here's the .xhtml file:

<h:form prependId="false">

    <p:outputLabel for="selTbl" value="Select a table:" />
    <p:autoComplete id="selTbl" value="#{mainBean.selectedTable}"
        completeMethod="#{mainBean.filterAuditTables}" cache="true" dropdown="true" effect="fade"
        minQueryLength="3" forceSelection="true" size="35" style="margin-left: 10px;">
        <p:ajax event="itemSelect" listener="#{mainBean.onTableSelect}" process="@this" update="@form" />
    </p:autoComplete>

    <p:dataTable id="audTblData" value="#{mainBean.data}" var="row"
        filteredValue="#{mainBean.filteredData}" resizableColumns="true" resizeMode="expand"
        sortMode="multiple">

        <f:facet name="header">
            <h:outputText value="#{mainBean.selectedTable}" />
        </f:facet>

        <p:columns value="#{mainBean.tableColumns}" var="col" sortBy="#{row[col.property]}"
            filterBy="#{row[col.property]}" filterMatchMode="contains">
            <f:facet name="header">
                <h:outputText value="#{col.header}" />
            </f:facet>
            <h:outputText value="#{row[col.property]}" />
        </p:columns>

    </p:dataTable>

</h:form>

MainBean.java:

@Named
@ViewScoped
public class MainBean implements Serializable {

    private static final long serialVersionUID = 1L;

    // these are used for the audited table selection list
    private List<String> auditedTables;
    private String selectedTable;

    // these are used in the table that displays the audit data
    private List<ColumnModel> tableColumns;
    private List<Auditable> data;
    private List<Auditable> filteredData;

    @Inject
    private AudviewService audviewService;

    @PostConstruct
    public void init() {
        auditedTables = audviewService.getAuditedTables();
    }

    public List<String> filterAuditTables(String query) {
        return auditedTables
                .stream()
                .filter(t -> t.contains(query.toUpperCase()))
                .collect(Collectors.toList());
    }

    public void onTableSelect(SelectEvent<String> event) {
        retrieveTableData();
    }

    public void retrieveTableData() {

        List<String> columns = audviewService.listTableColumns(selectedTable);

        // initialize columns for <p:dataTable>
        tableColumns = new ArrayList<ColumnModel>();
        for (String col : columns) {
            tableColumns.add(new ColumnModel(col, AudviewUtil.columnToProperty(col)));
        }

        // retrieve data for the selected table
        data = audviewService.getTableData(selectedTable);
    }

    /* getters and setters */
}

Note that Auditable is an interface implemented by Dept.java and Emp.java.


Solution

  • As you noticed, the problem is switching from a filtered or sorted datatable to another, if the filtered or sorted column is a field that isn't in the new selected class. An easy solution would be moving all the needed fields into the superclass Auditable.

    Another approach is separating datatable resetting&updating in two steps, here a possible solution (I replace, for testing purpose, your service with static code, so there could be some errors adapting my solution to your code):

    xhtml

    <h:form id="formTbl" prependId="false">
    
        <p:outputLabel for="selTbl" value="Select a table:" />
        <p:autoComplete id="selTbl" value="#{mainBean.selectedTable}"
            completeMethod="#{mainBean.filterAuditTables}" cache="true"
            dropdown="true" effect="fade" minQueryLength="3"
            forceSelection="true" size="35" style="margin-left: 10px;">
            <p:ajax event="itemSelect" listener="#{mainBean.onTableSelect}"
                process="@this" onstart="PF('vtWidget').clearFilters()" />
        </p:autoComplete>
        <p:remoteCommand name="btn" process="@this" update="audTblData" />
    
        <p:dataTable id="audTblData" value="#{mainBean.data}" var="row"
            filteredValue="#{mainBean.filteredData}" resizableColumns="true"
            resizeMode="expand" sortMode="multiple" widgetVar="vtWidget">
    
            <f:facet name="header">
                <h:outputText value="#{mainBean.selectedTable}" />
            </f:facet>
    
            <p:columns value="#{mainBean.tableColumns}" var="col"
                sortBy="#{row[col.property]}" filterBy="#{row[col.property]}"
                filterMatchMode="contains">
                <f:facet name="header">
                    <h:outputText value="#{col.header}" />
                </f:facet>
                <h:outputText value="#{row[col.property]}" />
            </p:columns>
    
        </p:dataTable>
    
    </h:form>
    

    java

    public void retrieveTableData() {
    
        List<String> columns = listTableColumns(selectedTable);
    
        // initialize columns for <p:dataTable>
        tableColumns = new ArrayList<ColumnModel>();
        for (String col : columns) {
            tableColumns.add(new ColumnModel(col + " header", col));
        }
    
        // retrieve data for the selected table
        data = getData(selectedTable);
        DataTable dataTable = (DataTable) FacesContext.getCurrentInstance().getViewRoot().findComponent("formTbl:audTblData");
        if (dataTable != null) {
            dataTable.reset();
        }
        PrimeFaces.current().executeScript("btn()");
    }
    

    Pay attention, if you need also to manage pagination.