Previous Section  < Day Day Up >  Next Section

10.4 Editing Tabular Data

Figure 10-3 shows the version of the report entries list area that is displayed when you click a link in the reports list, where the user can edit the current entries of the selected report. Each entry is represented by editable fields and a selection list, plus buttons to delete or update the entry.

Figure 10-3. The report entries list area with editable columns
figs/Jsf_1003.gif

At the top of the screen, the report title is displayed along with a link for navigating back to the reports list page. It's always a good idea to provide explicit navigation features like this in a web application, because the standard Back button behavior is unreliable. For instance, if the user updates the report entries so the total amount changes and then clicks the browser's Back button, the browser may display a cached version of the reports list page with the old total. Different browsers also handle Back button clicks differently, especially when the previous page was generated by an HTTP POST request, as it always is in this example. Some redisplay a cached version, but others ask the user if a new POST request should be sent to the server. Various tricks can improve the situation, but none is foolproof, so I recommend designing the user interface with links and buttons for navigation and teach your users to use them instead of the Back button.

Example 10-4 shows the JSP page for the entries list.

Example 10-4. Entries list with editable fields (expense/stage1/entryListArea.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:outputLink value="reportListArea.faces">

    <h:outputText value="Show all reports" />

  </h:outputLink>

  <h:form>

    Title:

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

    <h:dataTable value="#{reportHandler.reportEntriesModel}" var="entry">

      <h:column>

        <f:facet name="header">

          <h:outputText value="Date" />

        </f:facet>

        <h:inputText size="8" required="true" value="#{entry.date}"

          disabled="#{reportHandler.editDisabled}">

          <f:convertDateTime datestyle="short" />

        </h:inputText>

        <f:facet name="footer">

          <h:outputText value="Total:" />

        </f:facet>

      </h:column>

      <h:column>

        <f:facet name="header">

          <h:outputText value="Type" />

        </f:facet>

        <h:selectOneMenu id="type" required="true" value="#{entry.type}"

          disabled="#{reportHandler.editDisabled}">

          <f:selectItems value="#{entryHandler.currentChoices}"/>

        </h:selectOneMenu>

      </h:column>

      <h:column>

        <f:facet name="header">

          <h:outputText value="Amount" />

        </f:facet>

        <h:inputText size="8" required="true" value="#{entry.amount}"

          disabled="#{reportHandler.editDisabled}">

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

          <f:validateDoubleRange minimum="1" />

        </h:inputText>

        <f:facet name="footer">

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

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

          </h:outputText>

        </f:facet>

      </h:column>

      <h:column>

        <h:commandButton action="#{reportHandler.removeEntry}" 

          value="Delete"

          disabled="#{reportHandler.editDisabled}" />

      </h:column>

      <h:column>

        <h:commandButton action="#{reportHandler.updateEntry}" 

          value="Update"

          disabled="#{reportHandler.editDisabled}" />

      </h:column>

    </h:data_table>

  </h:form>

</f:view>

The columns holding editable values are represented by either an <h:inputText> or an <h:selectOneMenu> action element, bound to the properties of the bean representing the current row and configured with converters and validators as needed, just as for the read-only components in the previous examples.

In addition, each input component has a disabled attribute bound to a ReportHandler property named editDisabled. This property has the value true if the current user isn't allowed to edit the selected report, ensuring that the input elements are enabled only when the user is in fact allowed to change their values. The property getter method is implemented like this:

package com.mycompany.expense;

...



public class ReportHandler {

    private Rules rules;

    private Report currentReport;

    private String currentUser;

    private boolean isManager;

    ...

    public boolean isEditDisabled( ) {

        return !isReportNew( ) &&

            !rules.canEdit(currentUser, isManager, currentReport);

    }

    ...

}

You probably recognize it as the same type of method used in Chapter 6 to enable or disable the buttons in the menu area depending on the current report's status and the role associated with the current user. The isEditDisabled() method returns false if the report is new (i.e., it has no entries) and if the current user is allowed to edit it. The Rules class implements the last part of the condition:

package com.mycompany.expense;



public class Rules {

    public boolean canEdit(String user, boolean isManager, Report report) {

        return report.getOwner( ).equals(user) && !isLocked(report);

    }

    ...

    public boolean isLocked(Report report) {

        return report.getStatus( ) == Report.STATUS_SUBMITTED ||

            report.getStatus( ) == Report.STATUS_ACCEPTED;

    }

}

The Delete and Update buttons in each row are represented by columns that have <h:commandButton> action elements as their values. Each button is bound to an action method that is very similar to the action method that selects a report in the reports list area:

package com.mycompany.expense;

...



public class ReportHandler {

    ...

    public String removeEntry( ) {

        ReportEntry selectedEntry = 

            (ReportEntry) entriesModel.getRowData( );

        int entryId = selectedEntry.getId( );

        return removeEntry(entryId);

    }



    public String updateEntry( ) {

        ReportEntry selectedEntry = 

            (ReportEntry) entriesModel.getRowData( );

        int entryId = selectedEntry.getId( );

        return updateEntry(selectedEntry);

    }

Both methods get the ReportEntry for the row in which the button was clicked and call methods to remove or update the entry in the current report:

    public String removeEntry(int entryId) {

        try {

            refreshCache( );

        }

        catch (RegistryException e) {

            addMessage("registry_error", e.getMessage( ));

            return "error";

        }



        if (!rules.canEdit(currentUser, isManager, currentReport)) {

            addMessage("report_no_edit_access", null);

            return "error";

        }



        String outcome = "success";

        ReportEntry currentEntry = currentReport.getEntry(entryId);

        currentReport.removeEntry(entryId);

        try {

            saveReport( );

        }

        catch (RegistryException e) {

            addMessage("registry_error", e.getMessage( ));

            currentReport.addEntry(currentEntry);

            outcome = "error";

        }

        return outcome;

    }



    public String updateEntry(ReportEntry entry) {

        try {

            refreshCache( );

        }

        catch (RegistryException e) {

            addMessage("registry_error", e.getMessage( ));

            return "error";

        }



        if (!rules.canEdit(currentUser, isManager, currentReport)) {

            addMessage("report_no_edit_access", null);

            return "error";

        }



        String outcome = "success";

        ReportEntry currentEntry = currentReport.getEntry(entry.getId( ));

        currentReport.removeEntry(entry.getId( ));

        currentReport.addEntry(entry);

        try {

            saveReport( );

        }

        catch (RegistryException e) {

            addMessage("registry_error", e.getMessage( ));

            currentReport.removeEntry(entry.getId( ));

            currentReport.addEntry(currentEntry);

            outcome = "error";

        }

        return outcome;

    }

    ...

}

These methods refresh the cached copy of the current report, check if the current user is allowed to edit the report, remove or update the entry in the cached copy, and then save the updated copy in the registry. If anything goes wrong, they add an appropriate error message, restore the cached copy to its previous state, and return an "error" outcome.

Besides the editable columns and buttons, a couple of columns in Example 10-4 also have footer facets. The Date column has a footer with an <h:outputText> element with the static value "Total:", and the Amount column has a footer with an <h:outputText> element bound to the total property of the current report. This results in the effect shown in Figure 10-3. I could have defined a footer facet for the <h:dataTable> itself instead, but I choose to do it this way to get the label aligned with the first column and the total amount aligned with the third column, as opposed to getting both the label and the total amount displayed unaligned with the columns.

The information in this chapter can be applied to many applications that deal with tabular data, whether the data is displayed on one or multiple screens, with columns containing a mixture of read-only, read/write, link, and button components. For tables that are more dynamic, you may have to combine these basics with other techniques we talked about earlier. For instance, if the component type for a column needs to be chosen based on the data of the current row, use the rendered attribute to pick the right type for each column:

<h:column>

  <h:inputText value="#{entry.amount}" rendered="#{not entry.editDisabled}" />

  <h:outputText value="#{entry.amount}" rendered="#{entry.editDisabled}" />

</h:column>

In this example, I assume that individual entries in the report may or may not be edited, and I use JSF EL expressions testing a property on the entry itself to set the rendered attribute to either true or false. The result is that the column values for different rows are represented by either an input or an output component. For some tables, you may also need to use <h:panelGrid> elements to get the layout you want, and maybe even nest one <h:dataTable> within another. For the really exceptional cases, a custom renderer or a custom component may be required, but the standard <h:dataTable> takes you a long way as you've seen in this chapter.

    Previous Section  < Day Day Up >  Next Section