Previous Section  < Day Day Up >  Next Section

10.2 Processing Row-Specific Events

A very common application feature is to have one table with summary information (a master table) with each row linked to the details about the item in that row. The sample application uses this approach for selecting an individual report to work with. To implement this feature, you must figure out which master table row the user selected and make the corresponding details available for edit or display.

In the final version of the sample application, both the master table and the details are displayed on the same screen, but let's look at how to implement it using two screens first, so we can focus on one thing at the time. Figure 10-1 shows the first rough cut of the reports list screen, with the report titles as links to a report details screen.

Figure 10-1. Simple reports list screen
figs/Jsf_1001.gif

Example 10-1 shows the JSP page for this initial version.

Example 10-1. Reports list with links (expense/stage1/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.reportsModel}" var="report">

      <h:column>

        <f:facet name="header">

          <h:outputText value="Title" />

        </f:facet>

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

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

        </h:commandLink>

      </h:column>

      <h:column>

        <f:facet name="header">

          <h:outputText value="Owner" />

        </f:facet>

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

      </h:column>

      <h:column>

        <f:facet name="header">

          <h:outputText value="Dates" />

        </f:facet>

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

          <f:convertDateTime datestyle="short" />

        </h:outputText>

        <h:outputText value=" - " />

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

          <f:convertDateTime datestyle="short" />

        </h:outputText>

      </h:column>

      <h:column>

        <f:facet name="header">

          <h:outputText value="Total" />

        </f:facet>

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

          <f:convertNumber pattern="#,###.00" />

        </h:outputText>

      </h:column>

      <h:column>

        <f:facet name="header">

          <h:outputText value="Status" />

        </f:facet>

        <h:outputText value="#{statusStrings[report.status]}"/>

      </h:column>

    </h:dataTable>

  </h:form>

</f:view>

Example 10-1 looks a lot like the read-only table example we saw earlier, but there are a number of important differences. The two most important differences are that the Title column uses an <h:commandLink> component for its value and that the <h:dataTable> action element's value attribute in Example 10-1 contains an expression that evaluates to an instance of javax.faces.model.DataModel instead of a List. These two differences are related.

As you may recall from Chapter 9, the <h:commandLink> action element creates a command component rendered as an HTML link. Clicking on the link submits the form it's contained in and fires an ActionEvent for the command component, just as with the <h:commandButton>. There are two things to be aware of, though: <h:commandLink> only works if the client supports JavaScript, and because it submits a form, it must be nested within an <h:form> element. The command component's children, often represented by one or more <h:outputText> action elements as in Example 10-1, render the link text.

The tricky part is how to determine which row the link is clicked for, when handling the ActionEvent. Theoretically, you could do it with elaborate use of a query string parameter or a hidden field to hold a row identifier value, but JSF offers a much easier way to deal with it through the DataModel class.[1] This is an abstract class representing tabular data, containing the properties described in Table 10-1.

[1] As well as lots of complex behavior implemented by the javax.faces.component.UIData class.

Table 10-1. javax.faces.model.DataModel properties

Property name

Java type

Access

Description

rowAvailable
boolean

Read

true when the rowIndex property holds the index of an existing row.

rowCount
int

Read

The number of rows represented by this instance, or -1 if unknown.

rowData
Object

Read

The Object representing the row at rowIndex, or null if rowIndex is -1.

rowIndex
int

Read/write

The zero-based index for the currently selected row, or -1 if no row is selected.

wrappedData
Object

Read/write

The Object representing the table.

When the UIData component processes a request, it repeatedly increments the rowIndex property and gives the column components a chance to process each row represented by the rowData property, as long as rowAvailable is true. In other words, it doesn't create one component instance per column and row; it creates one component instance per column and lets that one instance process the column's values for all rows. It also fiddles with the component IDs so that the single component is rendered with a unique ID per row. When a link or a button in a certain row is clicked, this row-unique ID is sent to the server and UIData uses it to determine for which row it was clicked. The details are pretty hairy, but all you need to know in order to associate the event with the correct row is that when the event is processed, the DataModel rowIndex property is set to the appropriate index.

Let's use Example 10-1 to illustrate how it works, starting with the getter method for the reportHandler bean's reportModel property that the <h:dataTable> action uses as its data model:

package com.mycompany.expense;



import java.util.Date;

import java.util.List;

import javax.faces.model.DataModel;

import javax.faces.model.ListDataModel;

...



public class ReportHandler {

    ...

    private DataModel reportsModel;

    private Date from;

    private Date to;

    private int[] status;

    ...

    public DataModel getReportsModel( ) {

        if (reportsModel == null) {

            reportsModel = new ListDataModel( );

        }

        reportsModel.setWrappedData(getReports( ));

        return reportsModel;

    }



    public List getReports( ) {

        String user = null;

        if (!isManager) {

            user = getCurrentUser( );

        }

        List l = null;

        try {

            l = registry.getReports(user, from, to, status);

        }

        catch (RegistryException e) {

            // Queue an error message

            ...

        }

        return l;

    }

The getReportsModel() method creates an instance of the javax.faces.model.ListDataModel class if it doesn't exist. The ListDataModel class is a standard concrete subclass of the abstract DataModel class that adapts a List so it can be used as the model. In addition to the ListDataModel, JSF provides concrete implementations for other common tabular data types: ArrayDataModel, ResultDataModel (for a JSTL Result), ResultSetDataModel (for a JDBC ResultSet), and ScalarDataModel (for any type, representing a one-row data model).

The getReportsModel() method then populates the ListDataModel with the List returned by the getReports() method. This List contains all reports from the registry that match the current filter criteria values held by the from, to, and status instance variables—limited to reports owned by the current user, unless it's a manager.

The <h:commandLink> for the Title column is bound to a ReportHandler action method named select(). As I mentioned earlier, the UIData together with its renderer ensure that the DataModel is positioned at the row corresponding to the component that fired the ActionEvent when the action method is invoked, so we can use the DataModel to get the data for the selected row in the select() method:

    public String select( ) {

        Report selectedReport = (Report) reportsModel.getRowData( );

        if (!rules.canView(currentUser, isManager, selectedReport)) {

            // Queue error message

            ...

            return "error";

        }

        setCurrentReport(selectedReport);

        return "success";

    }

    ...

}

The select() method calls the getRowData() method to get a reference to the Report instance representing the selected row. If the current user is allowed to see the report, the currentReport variable is updated so that when the details screen asks for the current report, it gets a reference to the selected report.

The last piece of the puzzle is to bring up the details screen. That's handled by a navigation rule in the faces-config.xml file:

  <navigation-rule>

    <from-view-id>/expense/stage1/reportListArea.jsp</from-view-id>

    <navigation-case>

      <from-action>#{reportHandler.select}</from-action>

      <from-outcome>success</from-outcome>

      <to-view-id>/expense/stage1/entryListArea.jsp</to-view-id>

      <redirect/>

    </navigation-case>

  </navigation-rule>

If the select() method returns "success", the browser is redirected to the details view, as described in Chapter 9; otherwise, the report list view is re-rendered.

There are a couple of other details in Example 10-1 that are noteworthy. First, look at the <h:column> element for the Dates column:

      <h:column>

        <f:facet name="header">

          <h:outputText value="Dates" />

        </f:facet>

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

          <f:convertDateTime datestyle="short" />

        </h:outputText>

        <h:outputText value=" - " />

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

          <f:convertDateTime  datestyle="short" />

        </h:outputText>

      </h:column>

As illustrated by this example, you're not limited to using one component per column. You can have as many component action elements within an <h:column> element as you need, e.g., to combine multiple column values in the underlying data model into one value in the rendered table as I do here to display the date range for each report. What you can't use within an <h:column> element, though, is plain template text or non-JSF component action elements. If a column should contain fixed text, use an <h:outputText> element with a static value or use an <f:verbatim> action element to wrap the non-JSF elements:

      <h:column>

        <f:verbatim>

          This is some template text followed by a JSTL action.

          <x:out select="$doc/title" />

        </f:verbatim>

      </h:column>

The <f:verbatim> action creates a JSF output component and sets its value to the body evaluation result.

The other interesting detail is how the Report bean's numeric status property value is converted into the corresponding text value.

      <h:column>

        <f:facet name="header">

          <h:outputText value="Status" />

        </f:facet>

        <h:outputText value="#{statusStrings[report.status]}" />

      </h:column>

I could have added a read-only statusText property to the Report bean and used it here to get the text value, but that would have meant mixing user interface concerns with business logic, and I want to avoid that as much as possible. A simple solution is to create a List populated with String values, where the index corresponds to a numeric status code. This faces-config.xml declaration does the trick:

  <managed-bean>

    <managed-bean-name>statusStrings</managed-bean-name>

    <managed-bean-class>java.util.ArrayList</managed-bean-class>

    <managed-bean-scope>request</managed-bean-scope>

    <list-entries>

      <null-value/>

      <value>Open</value>

      <value>Submitted</value>

      <value>Accepted</value>

      <value>Rejected</value>

    </list-entries>

  </managed-bean>

The status codes used by the Report class are 1 through 4, so I populate the first element (i.e., the element at index 0) with a null value, and then the rest with the appropriate String values. With a List declared like this, I can use a simple JSF EL expression for the value attribute, using the report status code as the index for the List element I want.

    Previous Section  < Day Day Up >  Next Section