Woodstock to ICEfaces Porting Guide - Part 2

Part 2 of this guide builds on the work completed in Part 1.  At the completion of Part 1, the Woodstock CreatePage was replaced with the ICEfaces IceCreatePage.  This part of the porting guide focuses on porting data tables, and is based on Page1 in the working example.    The following topics are discussed:

At the completion of this guide you will understand how to port data tables in a Woodstock application to ICEfaces.  Beyond this guide, you can examine issues related to creating Single Page Interfaces with ICEfaces in Woodstock to ICEfaces Porting Guide - Part 3.  The completed project for this step of the guide is available here.

Eliminating Component Bindings in the Working Example

As with the previous page port, we begin by eliminating the component binding in our working example.  In the case of Page1.jsp there are two component bindings that need to be addressed, one in the dropDown, and one in the tableRowGroup.

To begin with there are a bunch of com.sun.webui.jsf import statements that can be eliminated.  All of the following can be removed.

import com.sun.webui.jsf.component.Body;
import com.sun.webui.jsf.component.Button;

import com.sun.webui.jsf.component.DropDown;
import com.sun.webui.jsf.component.Form;
import com.sun.webui.jsf.component.Head;
import com.sun.webui.jsf.component.Html;
import com.sun.webui.jsf.component.Label;
import com.sun.webui.jsf.component.Link;
import com.sun.webui.jsf.component.MessageGroup;
import com.sun.webui.jsf.component.Page;
import com.sun.webui.jsf.component.StaticText;
import com.sun.webui.jsf.component.Table;
import com.sun.webui.jsf.component.TableColumn;
import com.sun.webui.jsf.component.TableRowGroup;
import com.sun.webui.jsf.model.DefaultTableDataProvider;
 

Most of the remaining code changes occur inside the Managed Component Definition fold.  For the dropDown we eliminate the component bindings in Page1.java as follows.

  1. Replace DropDown bean property code
        private DropDown personDropDown = new DropDown();
        public DropDown getPersonDropDown() {
            return personDropDown;
        }
        public void setPersonDropDown(DropDown dd) {
            this.personDropDown = dd;
        }
    with String bean property code
       private Integer currentPersonId;
        public Integer getCurrentPersonId() {
            return currentPersonId;
        }
        public void setCurrentPersonId(Integer currentPersonId) {
            this.currentPersonId = currentPersonId;
        }
  2. Remove the unneeded personDropDownConverter
        private IntegerConverter personDropDownConverter = new IntegerConverter();
        public IntegerConverter getPersonDropDownConverter() {
            return personDropDownConverter;
        }
        public void setPersonDropDownConverter(IntegerConverter ic) {
            this.personDropDownConverter = ic;
        }

  3. In the init() function modify the logic for restoring the current person and trip  by replacing
        personDropDown.setSelected(pid);
    with
        setCurrentPersonId(pid);

  4. In personDropDown_processValueChange() replace
       Integer newPersonId = (Integer) personDropDown.getSelected();
     
    with
       Integer newPersonId = (Integer)(event.getNewValue());

And finally, back in the JSP code we modify the dropDown declarations
             <webuijsf:dropDown binding="#{Page1.personDropDown}" converter="#{Page1.personDropDownConverter}" id=.../>
with
             <webuijsf:dropDown selected="#{Page1.currentPersonId}" id=.../>

The  tableRowGroup component binding is used to get the current RowKey so it gets replaced as follows..

  1. Replace
        private TableRowGroup tableRowGroup1 = new TableRowGroup();
        public TableRowGroup getTableRowGroup1() {
            return tableRowGroup1;
        }
        public void setTableRowGroup1(TableRowGroup trg) {
            this.tableRowGroup1 = trg;
        }
    with
        private RowKey currentRowKey;
        public RowKey getCurrentRowKey() {
            return currentRowKey;
        }
        public void setCuurentRowKey(RowKey currentRowKey) {
            this.currentRowKey = currentRowKey;
        }

  2. In the updateButton_action(), and deleteButton_action() replace
        RowKey rowKey = tableRowGroup1.getRowKey();
    with
        RowKey rowKey = getCurrentRowKey();

  3. In the createButton_action() replace
        getSessionBean1().setCurrentPersonId((Integer) personDropDown.getValue());
    with
        getSessionBean1().setCurrentPersonId(currentPersonId);

Next, we make the necessary changes in Page1.jsp as follows.

  1. Remove component binding in tableRowGroup            
        <webuijsf:tableRowGroup binding="#{Page1.tableRowGroup1}" id="tableRowGroup1" .../>
    becomes
        <webuijsf:tableRowGroup id="tableRowGroup1" .../>

  2. Set the current row associated with the Update and Delete buttons using a <f:setPropertyActionListener/>.
       <webuijsf:button actionExpression="#{Page1.updateButton_action}" id="updateButton" text="Update">
               <f:setPropertyActionListener target="#{Page1.currentRowKey}" value="#{currentRow.tableRow}" />
       </webuijsf:button>
       <webuijsf:button actionExpression="#{Page1.deleteButton_action}" id="deleteButton" text="Delete">
               <f:setPropertyActionListener target="#{Page1.currentRowKey}" value="#{currentRow.tableRow}" />
       </webuijsf:button>

That completes the refactoring from component to value bindings.  You can run the project to prove that everything still works as expected.  We can now move on to porting the page to ICEfaces.

Building An ICEfaces Data Table

Because the JSP markup and Java data model for an ICEfaces dataTable is significantly different than a Woodstock dataTable, doing a manual port in the JSP and Java code is not the most effective way to proceed.  Instead, we will use the NetBeans Visual Design capabilities to build a replacement ICEfaces dataTable, and port the rest of the page around that. The following steps are taken to create the new ICEfaces dataTable.

  1. Start with the existing IcePage1.jsp that was created when the ICEfaces framework was added to the project.  Open it in Visual Design mode, and delete the inputTextArea that was created as part of the default page template.
  2. From the ICEfaces palette drag a dataTable onto the page.
  3. Right click the dataTable and from the context menu select Bind to Database....  This will bring up a dialog showing the existing RowSets that can be bound to.  Select tripRowSet as illustrated below.

    Bind To Database Dialog


    This will auto-generate all of the columns available in the RowSet, as illustrated below.

    Trip RowSet Data Table


  4. Right click the dataTable and from the context menu select Add Sort Header..., which will add sortable header capabilities.  You won't see any visual change in the designer, but the data model for the table will be adjusted.  More on the sortable header later.  Note: It is important to do this prior to any other modification of the data table, because doing it after will undo those modifications.
  5. Right click the dataTable and from the context menu select Table Layout..., which will bring up a dialog for manipulating the columns to be displayed in the table.  From here you can remove columns 1,2,6 and 7.  Next, modify the Header text for the remaining columns to "Date", "From", "To", and "Type".  Finally, add a new column for the buttons, and clear it's header text.  The completed dialog is illustrated below.

    Table Layout


    Once the dialog is completed, the design view looks like.

    Table Deisgh - Part 1


  6. We now have superfluous outputText components in the column facet headers that can be removed to clean up the header titles.
  7. Drag 2 ICEfaces commandButtons into the last column of the dataTable, and modify the button names to Update and Delete.  The resulting design view is illustrated below.

    Table Design - Part 2


  8. From the dataTable context menu select Add Header..., which will create a panelGrid in a facet to the dataTable called "header".  Into this panel drag an ICEfaces outputText component and change its value to "Trips".  The completed table design is illustrated below.

    Table Design - Part 3

We will now examine the JSP code that has been created for the dataTable. The table declaration is:

    <ice:dataTable id="dataTable1" sortAscending="#{IcePage1.dataTable1SortableDataModel.ascending}"
                        sortColumn="#{IcePage1.dataTable1SortableDataModel.sort}" style="left: 14px; top: 14px; position: absolute"
                        value="#{IcePage1.dataTable1SortableDataModel}" var="currentRow">

The sortAscending and sortColumn attributes are related to the sortable header which is discussed later.  The style attribute defines absolute positioning, and can be removed once the table is place in a panelGrid in the complete page.  The value is bound to a sortable data model of type com.icesoft.faces.component.jsfcl.data.CachedRowSetSortableDataModel, and the iteration variable is defined as "currentRow".

The basic structure for the data columns is:

    <ice:column id="column3">
        <f:facet name="header">
             <ice:commandSortHeader arrow="true" columnName="TRIP.DEPDATE" id="commandSortHeader3" value="Date"/>
        </f:facet>
        <ice:outputText id="outputText6" value="#{currentRow['TRIP.DEPDATE']}"/>
    </ice:column>

Each column contains a header facet containing a commandSortHeader used for sorting the column, and a header value for column title.  Each cell in the column contains an outputText that is bound back into the trip data provider.

The column containing the buttons looks slightly different.

    <ice:column id="column1">
        <f:facet name="header">
            <ice:outputText id="outputText2" value=""/>
        </f:facet>
        <ice:commandButton id="button1" value="Update"/>
        <ice:commandButton id="button2" value="Delete"/>
    </ice:column>

In this case, the header facet contains only a title, as it is not a sortable column.  The table cells contain Update and Delete buttons that require action bindings and should be modified to look like:

        <ice:commandButton id="button1" value="Update" action="#{IcePage1.updateButtonAction}"/>
        <ice:commandButton id="button2" value="Delete" action="#{IcePage1.deleteButtonAction}"/>

Completing The Working Example

With the newly created ICEfaces dataTable in place, we can now complete the port from Page1.jsp to IcePage1.jsp.  The work required includes:

  1. Port the rest of the Page1.jsp around the ICEfaces dataTable
  2. Modify the backing bean to support the page changes
  3. Modify the page navigation
  4. Add the necessary sortable header logic

Porting the remainder of the page

The basic page structure from Page1.jsp can be cut and pasted around the new ICEfaces dataTable and ported to ICEfaces in the same manner as was done in Part 1.  After the cut and paste exercise the basic page structure looks like:

Page1 Structure


In order to maintain a consistent layout between IcePage1, and IceCreatePage, the style for the body, and panelGrids are modified to match those in IceCreatePage, so we have:
            <body id="outputBody1" style="-rave-layout: grid">
                <ice:form id="form1">
                    <div style="position: absolute; left: 0px; top: 0px">
                        <jsp:directive.include file="iceMasthead.jspf"/>
                    </div>
                    <h:panelGrid id="mainPanel" style="margin: 5px; padding: 5px; left: 0px; top: 160px; position: absolute; width: 760px">

The following steps are needed to complete the port.
  1. The panelGrid containing the dropDown is modified to look like:
     <h:panelGrid columns="4" id="personPanel" style="">
            <ice:outputLabel id="label1" value="Person"/>
            <ice:selectOneMenu value="#{IcePage1.currentPersonId}" id="personDropDown" partialSubmit="true"
                                               valueChangeListener="#{IcePage1.personDropDownProcessValueChange}">
                    <f:converter converterId="javax.faces.Integer"/>
                    <f:selectItems id="personSelect" value="#{IcePage1.personDataProvider.options['PERSON.PERSONID,PERSON.NAME']}"/>
            </ice:selectOneMenu>
            <ice:outputLabel id="label2" value="Job Title"/>
            <ice:outputText id="jobTitleText"/>
    </h:panelGrid>
    The selectOneMenu has partialSubmit="true" which will cause the form to submit as the user selects a name from the list.  It also has a valueChangeListener that will be used to update the data displayed in the table when the name changes.
  2. The panelGrid containing the dataTable is modified to look like:
     <h:panelGrid id="tablePanel" style="">
          <ice:dataTable>...</ice:dataTable>
                    <ice:commandButton action="#{IcePage1.createButton_action}" id="createButton" value="Create"/>
                    <ice:messages id="messageGroup1" layout="table" style="color:red;"/>
                </h:panelGrid>
    The commandButton is bound to an action listener in the bean that will be used to cause the navigation to IceCreatePage.jsp.  In this case we will be reusing the logic in Page1.java.
  3. In the dataTable itself, the style attribute containing the absolute positioning can be removed.

Modifying the backing bean

We now turn our attention to the Java code in the backing bean IcePage1.java.  During the visual design process for the dataTable, the backing bean code was automatically created, but only includes the data model for the dataTable itself.  The rest of the now-portable data model for the page exists in Page1.java after the refactoring to remove the component bindings.  With the motivation to reuse this code as much as possible, we will modify IcePage1 to extend Page1, and inherit the existing data model and logic, so
    public class IcePage1 extends AbstractPageBean {
becomes
    public class IcePage1 extends Page1 {

With this modification preprocess(), prerender(), and destroy() methods in IcePage1 should be modified to call the superclass counterparts. Next, we will add the necessary action and value changed listeners with the following steps.

  1. The value change listener associated with the selectOneMenu is implemented as follows.
        public void personDropDownProcessValueChange(ValueChangeEvent event) {
            super.personDropDown_processValueChange(event);
            dataTable1SortableDataModel.setWrappedData(getSessionBean1().getTripRowSet());
        }
    Basically, this listener relies on the logic from the Page1 superclass, except that we need to refresh the data model in our table based on the name selected by calling setWrappedData() on our sortable data model.
  2. The action listener for the Update button is implemented as follows.
        public String updateButtonAction() {
            TreeMap map = (TreeMap) dataTable1SortableDataModel.getRowData();
            Integer tripID = (Integer) map.get("TRIP.TRIPID");
            setCurrentRowKey(getTripDataProvider().findFirst("TRIP.TRIPID", tripID));
            return(super.updateButton_action());
        }
    Because of the sortable nature of our data table, we cannot assume the row number in the table matches the row number in our data provider, so it is necessary to find it based on the trip id.  Once the currentRowSet is established, we can rely on the Page1 superclass logic to handle the action.  Because this action will ultimately cause a page navigation, there is no need to update the dataTable model.
  3. The action listener for the Delete button is implemented as follows.
        public String deleteButtonAction() {
            TreeMap map = (TreeMap) dataTable1SortableDataModel.getRowData();
            Integer tripID = (Integer) map.get("TRIP.TRIPID");
            setCurrentRowKey(getTripDataProvider().findFirst("TRIP.TRIPID", tripID));
            String returnVal = super.deleteButton_action();
            dataTable1SortableDataModel.setWrappedData(getSessionBean1().getTripRowSet());
            return returnVal;
        }
    The currentRowKey is established as it was for the Update button, and then the Page1 superclass handles the action.  In the case of delete, it is necessary to call setWrappedData() in order to update the contents of the dataTable to reflect the changes.
  4. The action listener for the Create button can simply rely on the logic in the Page1 superclass and is bound directly to createButton_action() in that class.
        <ice:commandButton action="#{IcePage1.createButton_action}" id="createButton" value="Create"/>
  5. While functionally unnecessary, for Design View to work properly, the following getters and setters are required in the base class IcePage1.
        public CachedRowSetDataProvider getPersonDataProvider() {
            return super.getPersonDataProvider();
        }

        public void setPersonDataProvider(CachedRowSetDataProvider crsdp) {
            super.setPersonDataProvider(crsdp);
        }

        public Integer getCurrentPersonId() {
            return super.getCurrentPersonId();
        }
        public void setCurrentPersonId(Integer currentPersonId) {
            super.setCurrentPersonId(currentPersonId);
        }
  6. Fix the imports as necessary
        import java.util.TreeMap;
        import javax.faces.FacesException;
        import javax.faces.event.ValueChangeEvent;

With all these changes in place, it is now possible to view the page in Design View again, as illustrated below.

Design View - Part 4


Now we can try running it.  From the project context menu select properties, and then modify the Run properties as illustrated.

Project Run Properties


When you run the project, the rendered page should now look like:

IcePage1


You will noticed a marked improvement in the rendering of this page compared with the original Woodstock page.  As you select different people for the selectOneMenu, the dataTable updates smoothly with only the minimum re-rendering required, unlike the Woodstock page that re-renders most everything.  We can now move on to cleaning up the navigation rules.

Modifying the Navigation

With our primary page now an ICEfaces page, we need to modify the navigation rules as follows.

  1. IcePage1 to IceCreatePage is now ICEfaces to ICEfaces, so redirect is not required.  The same applies in the reverse direction.
  2. IcePage1 to UpdatePage is now ICEfaces to Woodstock, so redirect is required.  The same applies in the reverse direction.
The modified navigation rules are:

    <navigation-rule>
        <from-view-id>/IcePage1.jsp</from-view-id>
        <navigation-case>
            <from-outcome>createCase</from-outcome>
            <to-view-id>/IceCreatePage.iface</to-view-id>
            <redirect/>
        </navigation-case>
        <navigation-case>
            <from-outcome>updateCase</from-outcome>
            <to-view-id>/faces/UpdatePage.jsp</to-view-id>
            <redirect/>
        </navigation-case>
    </navigation-rule>

    <navigation-rule>
        <from-view-id>/IceCreatePage.jsp</from-view-id>
        <navigation-case>
            <from-outcome>created</from-outcome>
            <to-view-id>/IcePage1.iface</to-view-id>
        </navigation-case>
        <navigation-case>
            <from-outcome>canceled</from-outcome>
            <to-view-id>/IcePage1.iface</to-view-id>
        </navigation-case>
    </navigation-rule>

    <navigation-rule>
        <from-view-id>/UpdatePage.jsp</from-view-id>
        <navigation-case>
            <from-outcome>updated</from-outcome>
            <to-view-id>/IcePage1.iface</to-view-id>
            <redirect/>
        </navigation-case>
        <navigation-case>
            <from-outcome>noUpdate</from-outcome>
            <to-view-id>/IcePage1.iface</to-view-id>
            <redirect/>
        </navigation-case>
    </navigation-rule>

With the navigation rules in place, we once again have a fully functional version of the application, with 2 of the 3 Woodstock pages ported to ICEfaces.  There is one task left to get the sort headers working in the dataTable.

DataTable sortable header

Re-examining the declaration of the dataTable shows 2 attributes sortAssending, and sortColumn are bound into the dataTableSortableDataModel, as shown below.

    <ice:dataTable id="dataTable1" sortAscending="#{IcePage1.dataTable1SortableDataModel.ascending}"
                                                        sortColumn="#{IcePage1.dataTable1SortableDataModel.sort}"
                                                        value="#{IcePage1.dataTable1SortableDataModel}" var="currentRow">

As you would expect, these properties identify what column the sort occurs on, and if that sort is ascending or descending.  Because these bindings are into a request scoped bean, the current sort header state will be lost between requests.  Since the working example is currently using standard request scope, it is necessary to maintain this state in session scope for it to exist across requests.  It is important to note, that when we examine single page interfaces in Part 3 of this guide, we will be moving to ICEfaces extended request scope, and will be looking to move session scoped state back into request scope, thereby allowing us to build a multi-document application, but for now session scope will have to do.  The necessary steps to implement the sortable header are:

  1. Add the needed state to SessionBean1.java
       private boolean sortAscending;
       public boolean isSortAscending() {
           return sortAscending;
       }
       public void setSortAscending(boolean sortAscending) {
           this.sortAscending = sortAscending;
       }

       private String sortColumn;
       public String getSortColumn() {
           return sortColumn;
       }
       public void setSortColumn (String sortColumn) {
           this.sortColumn = sortColumn;
       }
  2. Add the following getters and setters to IcePage1.java
        public boolean isSortAscending() {
            return getSessionBean1().isSortAscending();
        }
        public void setSortAscending(boolean ascending) {
            getSessionBean1().setSortAscending(ascending);
            if (ascending != dataTable1SortableDataModel.isAscending()) {
                dataTable1SortableDataModel.setAscending(ascending);
            }
            if (getSessionBean1().getSortColumn() != null) {
                dataTable1SortableDataModel.setSort(getSessionBean1().getSortColumn());
            }
        }
        public String getSortColumn() {
            return getSessionBean1().getSortColumn();
        }
        public void setSortColumn(String sortColumn) {
            getSessionBean1().setSortColumn(sortColumn);
        }

  3. Modify the bindings in the dataTable declaration in IcePage1.jsp.
        <ice:dataTable id="dataTable1" sortAscending="#{IcePage1.sortAscending}"
                                                            sortColumn="#{IcePage1.sortColumn}"
                                                            value="#{IcePage1.dataTable1SortableDataModel}" var="currentRow">

You can now rerun the example and test the table sorting.  You will notice that the page updates very smoothly as you apply different sorting, and that the sort state persists across page navigations.

Finished Part 2

You have now completed Part 2 of the Woodstock to ICEfaces Porting Guide.  The completed project for this step of the guide is available here.  At this point you can go back to the table of contents or Part 1 of the guide, or move on to Part 3 of the guide, which focuses on aspects of creating a Single Page Interface using ICEfaces.