Changing the order of columns in a JSF Table Component -in the client, at run-time, by the end user

4

The Rich UI components (see ADF Faces Rich Client Components – Marrying JSF and AJAX together) that the ADF Faces library will contain with the JDeveloper 11g release – and hopefully before that moment – will allow end-users to do all sorts of manipulations with Tables. They will be able to resize columns an drag & drop columns to change the order – much like you can do in Spreadsheet applications like Excel. However, those components are not yet available to us. Yesterday my customer asked me if he could have offer the option to re-order columns in a table to his end users. That means that in any table component, the user can decide which column should be displayed first, which one second and which one last. It happened to be the same customer who was after the feature to show/hide columns at run-time, a challenge easily resolved in a previous article: Having the end-user hide and display columns in a JSF Table Component.

In this article, we will see how we can implement this feature: have the end-user specify the order of the columns in the table. I have used ADF Faces for this demonstration, but any JSF implementation will do.

....
 

The starting point for the implementation of the reorder feature is a simple Table Component, created by hand, using ADF drag & drop or JHeadstart generation. It shows all columns from the EMP table (SCOTT sample schema), with Deptno and Mgr represented by Dropdown Lists based on the underlying Foreign Keys to table DEPT and EMP respectively.

 

The steps to implement the reorder columns feature are these:

  1. Add a panelBox with Shuttle-Order-Only element
  2. Add a command button and bind its action to a method on a backing bean
  3. Implement the action method on the backing bean – updating the table’s children based on the settings in the Shuttle
  4. Configure the tableBean as managed bean in the faces-config.xml 

And putting it altogether turns out to be surprisingly simple, thanks to the component nature of JSF and the ease of using Expression Language.

 

Step 1 – Add a panelBox with the Shuttle-Order-Only element

The panelBox component is positioned next to the table, separated from it using an objectSpacer. All three elements are wrapped in a panelHorizontal (table, spacer and panelBox). Inside the panelBox, there is a selectOrderShuttle element, whose selectItems represent the columns in the table. To make a match between the selectItems in the shuttle and the columns in the table, the value of the selectItems needs to correspond with a property on the columns; I have picked the sortProperty property on the columns.

Note that the Shuttle’s value is bound to a managed bean – tableBean – more specifically to the columns property in that bean – a String[]. When the form is submitted, the bean’s columns property is updated with the current value. 

The panelBox now becomes:

  </af:table>
  <af:objectSpacer width="20"/>
  <af:panelGroup layout="vertical">
    <af:panelBox>
      <af:outputLabel for="colsMonitor"
                      value="Specify the Display Order of the Columns "/>
      <af:selectOrderShuttle value="#{tableBean.columns}"
                             reorderOnly="true" id="colsMonitor">
        <af:selectItem label="Empno" value="Empno"/>
        <af:selectItem label="Name" value="Ename"/>
        <af:selectItem label="Job" value="Job"/>
        <af:selectItem label="Department" value="Deptno"/>
        <af:selectItem label="Hiredate" value="Hiredate"/>
        <af:selectItem label="Salary" value="Sal"/>
        <af:selectItem label="Commission" value="Comm"/>
        <af:selectItem label="Manager" value="Mgr"/>
      </af:selectOrderShuttle>
    </af:panelBox>
  </af:panelGroup>
</af:panelHorizontal>

Step 2 – Add a command button to the panelBox and bind its action to a backing bean

Adding the commandButton is  almost trivial. I include an objectSpacer to have some space between the Shuttle and the CommandButton. Both are inside a PanelGroup with vertical layout.

<af:objectSpacer height="15"/>
<af:commandButton text="Apply Column-settings"
                  action="#{tableBean.commandButton_action}"/>

The action property on the commandButton is bound to the ill-named method commandButton_action on bean tableBean. We have to implement that method next.

 

Step 3 – Implement the action method on the backing bean – updating the table’s children based on the settings in the Shuttle

The action method commandButton_action in the tableBean should do several things: it has to read the value of the shuttle (the ordered list of column references), get hold of the list of table children (the CoreColumn objects) and rearrange the latter based on the sequence of the former. It can use the columns property in the bean that is linked to the value property of the Shuttle Component.

The bean implementation now becomes:

package nl.amis.view;

import java.util.Arrays;
import java.util.List;

import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;

import oracle.adf.view.faces.component.core.data.CoreColumn;
import oracle.adf.view.faces.component.core.data.CoreTable;

public class TableBean {
    public TableBean() {
    }

    String[] columns;


    public void setColumns(String[] columns) {
        this.columns = columns;
    }

    public String[] getColumns() {
        return columns;
    }


    private int findCoreColumn(List<CoreColumn> coreColumns, String sortProperty) {
      for (CoreColumn c:coreColumns) {
          if (c.getSortProperty().equalsIgnoreCase(sortProperty))
            return coreColumns.indexOf(c);
      }
      // we should never get here!!
      return -1;
    }


    public String commandButton_action() {
       // first get a handle to the table we are dealing with
       String EL = "#{EmpTableCollectionModel.table}"; // note: this EL expression corresponds with the value of the binding property on the Table Component
       FacesContext fc = FacesContext.getCurrentInstance();
       ValueBinding vb = fc.getApplication().createValueBinding(EL);
       CoreTable   tbl = (CoreTable)vb.getValue(fc);

       // we need to reorder the CoreColumns in the sequence indicated by columns
        List tblColumns = tbl.getChildren();
        // for each column in columns find the corresponding CoreColumn in tblColumns
        // and put it in the proper spot in tblColumns.
        for ( int i = 0; i < columns.length; i++) {
           int pos = findCoreColumn(tblColumns, columns[i]);
           // switch tblColumns pos and i
           Object temp = tblColumns.get(i);
           tblColumns.set(i, tblColumns.get(pos));
           tblColumns.set(pos, temp);
        }
        return null;
    }
}

Step 4 –  Configure the tableBean as managed bean in faces-config.xml

 

Finally the backing bean needs to be configured as managed bean in the faces-config.xml. This is done like this:

  <managed-bean>
    <managed-bean-name>tableBean</managed-bean-name>
    <managed-bean-class>nl.amis.view.TableBean</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
  </managed-bean>
 

If we set the Shuttle as shown above and press the commandButton, the column headers will be redisplayed in this order:

 

And beyond – combining showing/hiding and re-ordering columns

It is now a very simple step to combine two features discussed in recent articles: the re-ordering of columns as demonstrated in this article and the showing/hiding of column as demonstrated in a prior piece. If we change the shuttle from orderOnly to select and order, we can both select the column to display and order them in the same component:

 

The change in the code for this shuttle component is subtle, real subtle. See if you can spot it:

<af:selectOrderShuttle value="#{tableBean.columns}"
                       reorderOnly="false" id="colsMonitor">
  <af:selectItem label="Empno" value="Empno"/>
  <af:selectItem label="Name" value="Ename"/>
  <af:selectItem label="Job" value="Job"/>
  <af:selectItem label="Department" value="Deptno"/>
  <af:selectItem label="Hiredate" value="Hiredate"/>
  <af:selectItem label="Salary" value="Sal"/>
  <af:selectItem label="Commission" value="Comm"/>
  <af:selectItem label="Manager" value="Mgr"/>
</af:selectOrderShuttle>

Well, I will tell you: the property reorderOnly is changed from true to false. That is it!

The implementation olf the tableBean will be slightly extended, to provide for a Map based on the columns property, that can be used by the af:column elements to determined their visibility. First the bean:

package nl.amis.view;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;

import oracle.adf.view.faces.component.core.data.CoreColumn;
import oracle.adf.view.faces.component.core.data.CoreTable;

public class TableBean {
    public TableBean() {
    }

    String[] columns;
    Map colMap = new HashMap();


    public void setColumns(String[] columns) {
        this.columns = columns;
        colMap.clear();
        for (int i = 0; i < columns.length; i++) {
            colMap.put(columns[i], Boolean.TRUE);
        }
    }

    public String[] getColumns() {
        return columns;
    }

    public Map getColumnsMap() {
        return colMap;
    }

    private int findCoreColumn(List<CoreColumn> coreColumns, String sortProperty) {
      for (CoreColumn c:coreColumns) {
          if (c.getSortProperty().equalsIgnoreCase(sortProperty))
            return coreColumns.indexOf(c);
      }
      // we should never get here!!
      return -1;
    }


    public String commandButton_action() {
       // first get a handle to the table we are dealing with
       String EL = "#{EmpTableCollectionModel.table}";
       FacesContext fc = FacesContext.getCurrentInstance();
       ValueBinding vb = fc.getApplication().createValueBinding(EL);
       CoreTable   tbl = (CoreTable)vb.getValue(fc);

       // we need to reorder the CoreColumns in the sequence indicated by columns
        List tblColumns = tbl.getChildren();
        // for each column in columns find the corresponding CoreColumn in tblColumns
        // and put it in the proper spot in tblColumns.
        for ( int i = 0; i < columns.length; i++) {
           int pos = findCoreColumn(tblColumns, columns[i]);
           // switch tblColumns pos and i
           Object temp = tblColumns.get(i);
           tblColumns.set(i, tblColumns.get(pos));
           tblColumns.set(pos, temp);
        }
        // do we need to set tblColumns on tbl??


        return null;
    }
}

Note: the initialization of the columns property of the tableBean has now gained some significance: any column not included in the initial value of columns is initially invisible in the table. However, it will be available from the shuttle for subsequent selection and displayal. The following managed bean configuration can be used:

  <managed-bean>
    <managed-bean-name>tableBean</managed-bean-name>
    <managed-bean-class>nl.amis.view.TableBean</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
    <managed-property>
      <property-name>columns</property-name>
      <list-entries>
        <value>Empno</value>
        <value>Ename</value>
        <value>Job</value>
      </list-entries>
    </managed-property>
  </managed-bean>
 

Finally, to have columns hidden or displayed, we will set an EL expression on the rendered attribute of each of the columns in our table:

rendered="#{tableBean.columnsMap['Deptno']==true}" 

For each column, the value of the key of the map-entry will be different; it is always the same as the value of the sortProperty attribute. For example:

<af:column sortable="true" noWrap="true" sortProperty="Sal"
           formatType="number"
           rendered="#{tableBean.columnsMap['Sal']==true}">
 

This EL expression specifies that the column is visible in the client if the columnsMap property (a HashMap) on the tableBean contains an entry with the same String used for sortProperty of the Column used for a key and the Boolen static true as value. The columnsMap is directly derived from the columns property: any entry in the columns array will be in the columnsMap.

The result of the initialization of the managed bean tableBean and the binding of the rendered attribute is something like this for the initial screen:

It is easier than simple to go from the picture above to the one below:

Resources

Having the end-user hide and display columns in a JSF Table Component – article on AMIS Technology Blog 

 

Share.

About Author

Lucas Jellema, active in IT (and with Oracle) since 1994. Oracle ACE Director for Fusion Middleware. Consultant, trainer and instructor on diverse areas including Oracle Database (SQL & PLSQL), Service Oriented Architecture, BPM, ADF, Java in various shapes and forms and many other things. Author of the Oracle Press book: Oracle SOA Suite 11g Handbook. Frequent presenter on conferences such as JavaOne, Oracle OpenWorld, ODTUG Kaleidoscope, Devoxx and OBUG. Presenter for Oracle University Celebrity specials.

4 Comments

  1. Can you give more detail and where i find “EmpTableCollectionModel.table”

    // first get a handle to the table we are dealing with
    String EL = “#{EmpTableCollectionModel.table}”;

    when i have en Entity “Emp” and a view “EmpView”

    best regards.

  2. I have the following errors in this code Line:
    private int findCoreColumn ( List coreColumns, String sortProperty) …
    Error(16,38): identifier expected
    Error(17,39): ) expected
    how can i solve it?

  3. Hi Luc,

    Thanks for your very kind words. And you got it right: it takes a lot of thinking upfront and trying out to arrive at what turns out to be a very simple solution. I was pleasantly surprised to find out how easy it really was.

    And unfortunately no – I do not have additional insights into the release date of the ADF Faces Rich Client libraries. I will try to harras some people during OOW to find out. Once I do, it will be on the blog, so keep reading.

    best regards,

    Lucas

  4. Hi Lucas, I’m impressed….. It is an example of how extended knowledge can result in very usable solutions that are, in the end, actually very simple to implement. Don’t get me wrong here, I know how that works. It takes days to find the solution and then all at once….. "Eureka". Do you have any inside information on the release date of the new ADF faces library ? As I said, I’m impressed, and you guys deserve, as so often, my credits for another very usable solution. Thanks. Luc Bors