Previous Section  < Day Day Up >  Next Section

10.3 Dealing with Large Tables

When the tabular data potentially spans many rows, a nice application lets the user work with just a few rows at a time. Letting the user sort the data is another nice touch. With both these features added, the reports list area looks like Figure 10-2.

Figure 10-2. The reports list area with sorting and scrolling added
figs/Jsf_1002.gif

Each column header is now a link that the user clicks to sort the table on the values in that column. Clicking the same column header link twice reverses the order.

Below the table, I've added buttons and a field for scrolling through the data. The field holds the number of rows to show per page, and the scrolling buttons scroll to the first page, the previous page, the next page, and the last page, respectively. The user can click the Refresh button to refresh the screen after changing the rows per page value.

Another difference compared to the previous version of this page is the use of background colors for the table and its rows, the font styles, and the alignment of the column values. I'll tell you how to spice up the user interface like this at the end of this section.

10.3.1 Sorting the Data

To sort the data, we first need to add a few more things to the ReportHandler class:

package com.mycompany.expense;



import java.util.Collections;

import java.util.Comparator;

...

public class ReportHandler {

    ...

    private static final Comparator ASC_TITLE_COMPARATOR = new Comparator( ) {

            public int compare(Object o1, Object o2) {

                String s1 = ((Report) o1).getTitle( );

                String s2 = ((Report) o2).getTitle( );

                return s1.compareTo(s2);

            }

        };



    private static final Comparator DESC_TITLE_COMPARATOR = new Comparator( ) {

            public int compare(Object o1, Object o2) {

                String s1 = ((Report) o1).getTitle( );

                String s2 = ((Report) o2).getTitle( );

                return s2.compareTo(s1);

            }

        };

    ...

A java.util.Comparator instance compares values, for instance, when sorting a collection. Its compare() method is called with the two objects to compare and returns a negative value if the first is less than the second, zero if they are equal, or a positive value if the first is greater than the second.

I create two static Comparator instances for each column: one for ascending order and one for descending order. I show you only the ones for the Title column here, but the others are identical (with the exception of which Report property they compare).

To sort the reports list, I add a sortReports() method:

    private void sortReports(List reports) {

        switch (sortBy) {

            case SORT_BY_TITLE:

                Collections.sort(reports, 

                    ascending ? ASC_TITLE_COMPARATOR : DESC_TITLE_COMPARATOR);

                break;

            case SORT_BY_OWNER:

                Collections.sort(reports, 

                    ascending ? ASC_OWNER_COMPARATOR : DESC_OWNER_COMPARATOR);

                break;

            case SORT_BY_DATE:

                Collections.sort(reports, 

                    ascending ? ASC_DATE_COMPARATOR : DESC_DATE_COMPARATOR);

                break;

            case SORT_BY_TOTAL:

                Collections.sort(reports, 

                    ascending ? ASC_TOTAL_COMPARATOR : DESC_TOTAL_COMPARATOR);

                break;

            case SORT_BY_STATUS:

                Collections.sort(reports, 

                    ascending ? ASC_STATUS_COMPARATOR : DESC_STATUS_COMPARATOR);

                break;

        }

    }

The method uses the java.util.Collections sort() method to sort a List of Report instances, picking one of the static Comparator instances depending on the values of a sortBy and an ascending variable:

    private static final int SORT_BY_TITLE = 0;

    private static final int SORT_BY_OWNER = 1;

    private static final int SORT_BY_DATE = 2;

    private static final int SORT_BY_TOTAL = 3;

    private static final int SORT_BY_STATUS = 4;



    private boolean ascending = false;

    private int sortBy = SORT_BY_DATE;

    ...

    public String sortByTitle( ) {

        if (sortBy == SORT_BY_TITLE) {

            ascending = !ascending;

        }

        else {

            sortBy = SORT_BY_TITLE;

            ascending = true;

        }

        return "success";

    }

    ...

One action method per column sets the values of these variables. If the same request action method is called twice, it flips the value of the ascending variable; otherwise, it sets the sortBy variable to the selected column and the ascending variable to its initial value.

The final new method is called getSortedReportsModel( ):

    public DataModel getSortedReportsModel( ) {

        if (reportsModel == null) {

            reportsModel = new ListDataModel( );

        }

        List reports = getReports( );

        sortReports(reports);

        reportsModel.setWrappedData(reports);

        return reportsModel;

    }

The only difference—compared to the getReportsModel( ) method we used previously—is that this method sorts the list by calling the sortReports() method before it populates the DataModel wrapper.

Now we have all the code we need for sorting. Example 10-2 shows how the new methods are bound to components in the JSP page.

Example 10-2. Reports list with sortable columns (expense/stage2/reportListArea.jsp)
<%@ page contentType="text/html" %>

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>



<f:view>

  <h:form>

    <h:dataTable value="#{reportHandler.sortedReportsModel}" var="report">

      <h:column>

        <f:facet name="header">

          <h:commandLink action="#{reportHandler.sortByTitle}" 

            immediate="true">

            <h:outputText value="Title" />

          </h:commandLink>

        </f:facet>

        <h:commandLink action="#{reportHandler.select}" immediate="true">

          <h:outputText value="#{report.title}" />

        </h:commandLink>

      </h:column>

      ...

    </h:dataTable>

  </h:form>

</f:view>

The first change is the value binding expression for the <h:dataTable> action. It's now bound to the method that sorts the entries before it returns the DataModel.

Next, all column header facets now use an <h:commandLink> action bound to the sort request action methods, such as sortByTitle(), with the immediate attribute set to true so sorting doesn't trigger validation and model updates. Example 10-2 shows only the Title column, because the others all follow the same pattern.

That's all there is to it, and I think this is a great example of the advantage JSF offers compared to plain JSP or similar template models—all the complex code is implemented by pure Java methods and only minimal changes are needed in the template to bind to them.

10.3.2 Scrolling Through the Data

Adding the ability to scroll through the data requires similar changes. The main pieces of information we need to control are the index of the first row to display and how many rows to display. The <h:dataTable> action element provides attributes for these values:

<h:dataTable value="#{reportHandler.sortedReportsModel}" var="report"

  first="#{reportHandler.firstRowIndex}"

  rows="#{reportHandler.noOfRows}">

  ...

</h:dataTable>

Binding these attributes to properties of the ReportHandler makes it easy to adjust their values programmatically when the scrolling buttons are clicked. Here's how the properties are implemented:

package com.mycompany.expense;

...

public class ReportHandler {

    private int noOfRows = 5;

    private int firstRowIndex = 0;

    ...

    public int getNoOfRows( ) {

        return noOfRows;

    }



    public void setNoOfRows(int noOfRows) {

        this.noOfRows = noOfRows;

    }



    public int getFirstRowIndex( ) {

        return firstRowIndex;

    }

The noOfRows property is implemented as a read/write property, i.e., with both a getter and a setter method, while the firstRowIndex property is implemented as a read-only property (its value can only be changed indirectly by clicking the scrolling buttons, not directly by the user).

Four action methods support scrolling through the rows:

    public String scrollFirst( ) {

        firstRowIndex = 0;

        return "success";

    }



    public String scrollPrevious( ) {

        firstRowIndex -= noOfRows;

        if (firstRowIndex < 0) {

            firstRowIndex = 0;

        }

        return "success";

    }



    public String scrollNext( ) {

        firstRowIndex += noOfRows;

        if (firstRowIndex >= reportsModel.getRowCount( )) {

            firstRowIndex = reportsModel.getRowCount( ) - noOfRows;

            if (firstRowIndex < 0) {

                firstRowIndex = 0;

            }

        }

        return "success";

    }



    public String scrollLast( ) {

        firstRowIndex = reportsModel.getRowCount( ) - noOfRows;

        if (firstRowIndex < 0) {

            firstRowIndex = 0;

        }

        return "success";

    }

The scrollFirst() method is simple; it just sets the row index to zero and returns "success". The scrollPrevious() method is almost as simple. It first subtracts the number of rows per page from the current index to get the next index. If this happens to result in a value less than zero, it adjusts it to zero.

The methods for scrolling forward are a little bit more complicated, because they need to ensure that the index stays within the bounds of the table. They use the DataModel getRowCount() method to get the total number of rows represented by the model. For the ListDataModel subclass I use here, this method always returns a valid value, but for the ResultSetDataModel it returns -1, signaling that the number of rows is unknown. The reason is that the only way to know how many rows a java.sql.ResultSet contains is to get them all, and that would be wasteful in many cases. If you use the ResultSetDataModel and want to implement forward scrolling, you must either find out how many rows it holds through other means (e.g., by running a SELECT COUNT(*) query before you run the real query) or use the DataModel isRowAvailable() method to decide when to stop scrolling forward.

The scrollNext() method first adds the number of rows per page to the current index, and then adjusts it if it ends up pointing beyond the table bounds. The scrollLast() method sets the first-row index for the last page by removing the number of rows per page from the total number of rows, adjusting it if the result is less that zero.

All scrolling methods—as well as the sorting methods described earlier—return "success" as the outcome, even though it's very unlikely that the outcome values ever will be used for navigation. The specification recommends returning null as the outcome from action methods that never drive navigation, but to me, that means putting logic in the action method that doesn't belong there. Whether to stay in the same view or display a new view is a decision that may change depending on the application's screen layout and is therefore better expressed as a navigation rule (or the lack of a rule) in the faces-config.xml file. An advantage of returning null, though, is that the rules aren't scanned at all, saving some processing time. Other than that, returning null or an outcome value that doesn't match a navigation rule has the same effect.


Another set of methods is needed to enable and disable the scrolling buttons appropriately:

    public boolean isScrollFirstDisabled( ) {

        return firstRowIndex == 0;

    }



    public boolean isScrollLastDisabled( ) {

        return firstRowIndex >= reportsModel.getRowCount( ) - noOfRows;

    }



    public boolean isScrollNextDisabled( ) {

        return firstRowIndex >= reportsModel.getRowCount( ) - noOfRows;

    }



    public boolean isScrollPreviousDisabled( ) {

        return firstRowIndex == 0;

    }

These methods return false if there are enough rows to scroll the requested amount of rows in the requested direction represented by each method.

Example 10-3 shows the report list JSP page modified to support scrolling, with the help of these new ReportHandler methods.

Example 10-3. Reports list with scrolling support (expense/stage3/reportListArea.jsp)
<%@ page contentType="text/html" %>

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

<html>

  <head>

    <title>Expense Reports</title>

    <link rel="stylesheet" type="text/css" 

      href="${pageContext.request.contextPath}/style.css">

  </head>

  <body>

    <f:view>

      <h:form>

        <h:dataTable value="#{reportHandler.sortedReportsModel}" var="report"

          first="#{reportHandler.firstRowIndex}"

          rows="#{reportHandler.noOfRows}" 

          styleClass="tablebg" rowClasses="oddRow, evenRow" 

          columnClasses="left, left, left, right, left">

          ...

        </h:dataTable>

        <h:commandButton value="<<" 

          disabled="#{reportHandler.scrollFirstDisabled}" 

          action="#{reportHandler.scrollFirst}" />

        <h:commandButton value="<" 

          disabled="#{reportHandler.scrollPreviousDisabled}" 

          action="#{reportHandler.scrollPrevious}" />

        <h:commandButton value=">" 

          disabled="#{reportHandler.scrollNextDisabled}" 

          action="#{reportHandler.scrollNext}" />

        <h:commandButton value=">>" 

          disabled="#{reportHandler.scrollLastDisabled}" 

          action="#{reportHandler.scrollLast}" />

        Rows/page: 

        <h:inputText value="#{reportHandler.noOfRows}" size="3"/>

        <h:commandButton value="Refresh" />

      </h:form>

    </f:view>

  </body>

</html>

I've omitted the <h:column> elements in Example 10-3, because they are identical to the ones in Example 10-2.

The first and rows attributes for the <h:dataTable> action are bound to the corresponding ReportHandler properties, as we discussed earlier. After the <h:dataTable> action element comes the four scrolling buttons, each with a disabled attribute and an action attribute, bound to the corresponding properties and action methods.

The <h:inputText> element for the number of rows per page field is bound to the noOfRows property so that the user can easily change the value. The Refresh button, finally, is represented by an <h:commandButton> action. Because all it needs to do is submit the form to set the new page per rows value, it's not bound to any method.

10.3.3 Giving the Table Some Style

Let's talk about style. The preferred way to describe the look of HTML documents nowadays is with Cascading Style Sheets (CSS), so JSF supports this mechanism. All of the HTML component action elements support one or more attributes that let you specify CSS classes that you then declare in a style sheet.

The <h:dataTable> element is a good example:

<h:dataTable value="#{reportHandler.sortedReportsModel}" var="report"

  first="#{reportHandler.firstRowIndex}"

  rows="#{reportHandler.noOfRows}" 

  styleClass="tablebg" rowClasses="oddRow, evenRow" 

  columnClasses="left, left, left, right, left">

  ...

</h:dataTable>

All JSF HTML components support the styleClass attribute. Its value is used as is as the value of the class attribute of the generated HTML element. When you specify it for the <h:dataTable> element, it ends up as the class attribute value of the HTML <table> element. For a component type that isn't rendered normally as an HTML element (such as a plain output component), specifying a styleClass value results in a <span> element with the class attribute, rendered around the component's value.

You can also specify classes to use for the <tr> and <td> elements the <h:dataTable> action generates. The rowClasses attribute takes a comma-separated list of class names that are used for the <tr> elements. If two classes are specified, for instance, the first one is used for the first row and the second one for the second row, then the first class is used again for the third row, and so on. The columnClasses attribute also takes a comma-separated list of class names, used in order for the <td> elements of each row. There are two more CSS attributes that I don't use in this example, namely the headerClass and footerClass attributes for specifying classes for the header and footer elements.

Just as for regular HTML, you can include the CSS declarations directly in the JSP page or write them in a separate file referenced by a <link> element in the page header section:

    <link rel="stylesheet" type="text/css" 

      href="${pageContext.request.contextPath}/style.css">

As I described in Chapter 4, I use a JSP 2.0 EL expression as part of the href attribute value to create an absolute path for the style sheet file. Details about CSS are out of the scope of this book, but here are the declarations for the style classes used in Example 10-3:

.tablebg {

  background-color: #EEF3FB;

}

.oddRow {

  background-color: #FFFFFF;

}

.evenRow {

  background-color: #EEF3FB;

}

.left {

  text-align: left;

}

.right {

  text-align: right;

}

If you want to learn about CSS, the specifications available at http://www.w3c.org are fairly easy to read. There are also books about CSS that show you practical applications of style sheets, such as Eric Meyer's Cascading Style Sheets: The Definitive Guide (O'Reilly).

As an alternative to CSS, the JSF HTML components also support all the HTML 4.01 attributes that affect the style directly, such as bgcolor, border, cellpadding, cellspacing, frame, rules, style, and width for the <h:dataTable> component element.

    Previous Section  < Day Day Up >  Next Section