中国IT动力,最新最全的IT技术教程
最新100篇 | 推荐100篇 | 专题100篇 | 排行榜 | 搜索 | 在线API文档
首 页 | 程序开发 | 操作系统 | 软件应用 | 图形图象 | 网络应用 | 精文荟萃 | 教育认证 | 硬件维护 | 未整理篇 | 站长教程
ASP JS PHP工程 ASP.NET 网站建设 UML J2EESUN .NET VC VB VFP 网络维护 数据库 DB2 SQL2000 Oracle Mysql
服务器 Win2000 Office C DreamWeaver FireWorks Flash PhotoShop 上网宝典 CorelDraw 协议大全 网络安全 微软认证
硬件维护  CPU  主板  硬盘  内存  显卡  显示器  键盘鼠标  声卡音箱  打印机  机箱电源  BIOS  网卡  C#  Java  Delphi  vs.net2005
  当前位置:> 程序开发 > 编程语言 > Java > Java与XML
Web Wizard Component, Part 2: The View
作者:未知 时间:2005-08-10 18:20 出处:Java频道 责编:chinaitpower
              摘要:Web Wizard Component, Part 2: The View

In the previous article in this series, we started creating a wizard component for a web application using a model-driven approach. We designed a set of rules represented by a linked list of wizard steps. Now it is the time to integrate these rules with a corresponding user interface (UI).

Building the Wizard User Interface

I will show how to create the wizard user interface using the Struts framework. I chose it primarily because I've used it for quite a while, and I know it well. Struts is widely adopted by the Java community, so the source code should be easy to understand. I also believe that front controller paradigm, used in Struts and the like, allows for better exploitation of the underlying HTTP protocol. On the other hand, most of the code is Struts-agnostic, and can be ported to other frameworks with a similar programming model.

The wizard UI adheres to following design principles:

  • The MVC architecture emphasizes the isolation of the presentation from underlying data.
  • Input/output separation ensures that a view is independent from any previous action.
  • Synchronized views guarantee that presentation always matches the model.
  • Stateful conversation retains the values of transient properties between requests.

Before delving into implementation details, we need to decide how many URLs (or in Struts terms, how many actions) the wizard should have. The Signup Wizard uses just one action class and only one form bean that has session scope. This compact design allows us to store all wizard data on the server and to easily share data between wizard pages. Using a single resource location also provides better control over browser page history.

Next, we need to decide what will be presented to a user when he navigates to the wizard location. If the Rule Container has been initialized, a page corresponding to the wizard's state will be shown. If the Rule Container has not been initialized, several choices are possible:

  • Initialize the Rule Container and show the first page.
  • Display an error.
  • Silently redirect to another location.
  • Display a stub page.

The Signup Wizard uses the last option, a stub page. This is a page that is displayed when the wizard is not active. We do not want to instantiate the wizard each time when a user navigates to its URL by mistake or uses a browser's Back button. The wizard is instantiated only with a specific initializing command.

What should be displayed on the stub page? It would be logical to show something relevant to the signup process. Thus, the Signup Wizard defines not one, but two stub pages. The appropriate page is chosen depending on whether a user is logged in or not.

As a result, the Signup Wizard has the following functionality:

  • A single location is used for login, logout, and signup procedures. A single Struts action class handles all requests to this location.
  • If a user is not logged in, then the "Not logged in" stub page is shown. This page contains user name and password fields and allows the user to log in. This page also contains a "New User Signup" button.
  • If a user is logged in, then the "Logged in" stub page is shown. This page displays the user name and contains a "Log Out" button.
  • When a user clicks the "New User Signup" button on the "Not Logged In" stub page, the Rule Container is instantiated and the user is presented with a series of pages, corresponding to the signup steps.
  • If the signup process finishes successfully, the user logs in automatically.
  • To log out, the user must use the Log Out button from the "Logged in" stub page.
  • The signup process may be canceled at any step. If signup is canceled, the "Not logged in" stub page is displayed.

The complete wizard will contain the following components, as seen in Figure 1:

  • The Rule Container, which consists of the wizard controller, nodes, and edges. We designed this component in the previous article.
  • Three wizard pages, corresponding to the three nodes of the Rule Container.
  • Two stub pages, which are used when the Rule Container is not initialized.
  • The UI controller, defined by Struts' action class/form bean classes.

Signup Wizard Components
Figure 1. Signup Wizard components

UI Controller

Assuming that all input data is submitted via POST requests, we can solve several issues at once by separating input from output with the Redirect-After-Post pattern, and by making views non-cacheable.

The server does not return a result page in response to the POST request. Instead, it redirects the user to the result page, making a roundtrip through the browser. Because pages are marked as non-cacheable, the browser reloads them every time they are navigated to. The additional time and network load are insignificant comparing to the advantages gained:

  • A page has no knowledge about any preceding action; it always displays the current model data.
  • Non-cacheable pages ensure synchronization between model and view.
  • POST requests are not resubmitted each time a page is reloaded, the model is not affected, and a user does not see an unfriendly "Do you want to re-send POST data?" dialog.

Wizard Action Class

As part of an effort to make the UI code agnostic to the web framework, classes derived from Struts framework do not perform a lot of processing. Instead, they refer to the SignupBean backing class, which encapsulates most of the plumbing between the UI and Rule Container.

The action class calls two different methods on the SignupBean class, depending on the request type. If the request has POST type, the action class assumes that input data is submitted and calls the setData method. If request has GET type, the action class loads a page using the getView method.

On the render phase, the action class obtains error messages from the signup bean and converts them to native Struts format. This differs from a regular Struts application, which returns errors from the validate method of a form bean. Signup Wizard does not define a validate method and does not use the Action.Input property of the Struts controller.

public class WizardAction extends Action {
  public ActionForward execute(
    ActionMapping actionMapping,
    ActionForm actionForm,
    HttpServletRequest request,
    HttpServletResponse response)
          throws Exception {

    WizardForm uiForm = (WizardForm) actionForm;
    SignupBean uiBean = uiForm.getSignupBean();
    String mapping = null;

    // Input phase
    if (uiForm.isInput()) {
      mapping = uiBean.setData();

    // Render phase
    } else {
      mapping = uiBean.getView();
      
      // Get errors from backing bean
      ActionErrors errors =
        getStrutsErrors(uiBean.getErrors());
      saveErrors(request, errors);
    }

    return actionMapping.findForward(mapping);
  }
}

Wizard Form Bean

The WizardForm bean has session scope and stores a reference to the SignupBean. JSP pages access the SignupBean using Struts' support for nested properties. SignupBean defines an important property, cmd, which provides information about every user action.

public class WizardForm extends ActionForm {

  // Conversation bean
  SignupBean signupBean = new SignupBean();

  // Exposes wizard properties to JSP pages
  public SignupBean getSignupBean() {
    return signupBean;
  }

  // The command informs about the user action
  public void setCmd(String cmd) {
    signupBean.setCmd(cmd);
  }
  ...
}

Wizard Signup Bean

SignupBean encapsulates login, logout, and signup procedures. If the wizard will be used only with Struts, then SignupBean can simply extend the ActionForm class. SignupBean defines the following properties:

  • User login name
  • User password
  • The option to personalize
  • The user's favorite book
  • The user's favorite movie

Login name, password, favorite book, and favorite movie are persistent properties, which would be saved in the main application domain model after the wizard finishes. The option to personalize is a transient property and is used to select or skip the Personalization step of the wizard.

Another transient property is errors. It is defined in the SignupBean class, but Rule Container has access to this property, so it could report errors back to UI layer. When SignupBean receives input data, it clears any existing errors. Then it handles input, modifies the model, and accumulates new error messages. Because SignupBean is aggregated in the form bean, and the latter has session scope, error messages survive between requests and can be displayed each time a view is shown.

Two core methods of SignupBean are setData and getView.

setData is called when input data is submitted. This method analyzes the input command and current wizard state, performs any needed model update, and returns a mapping, which is used by Struts to redirect to an appropriate JSP page.

Good Struts practice is to front JSP pages with an action class. In our case, there is only one action class serving all requests. Therefore, setData redirects back to the wizard action, using a Wizard Loop mapping.

Note that the Done command is processed in the same manner as Next. Both of these commands instruct the wizard to proceed one step forward. The wizard can be canceled from the UI wrapper at any time, but cannot be finished. Instead, Rule Container finishes automatically when it reaches the last step of the wizard.

synchronized public String setData() {
  // Always clear errors on input,
  // model state may have been changed
  clearErrors();

  String mapping = null;

  // Rule Container exists, navigate wizard
  if (signupWizard != null) {

    // Initialize wizard
    if ("Init".equals(cmd)) {
      initWizard();
      mapping = "Wizard Loop";

    // Move one step forward if possible
    } else if ("Next".equals(cmd) ||
               "Done".equals(cmd)) {
      signupWizard.traverseForward();
      if (finishWizard()) {
        mapping = "Done";
      } else {
        mapping = "Wizard Loop";
      }

    // Move one step back if possible
    } else if ("Back".equals(cmd)) {
      signupWizard.traverseBackward();
      mapping = "Wizard Loop";

    // Cancel wizard, show the stub page
    } else if ("Cancel".equals(cmd)) {
      cancelWizard();
      mapping = "Canceled";

  // No Rule Container, show stub page
  } else {

    // User wants to log in
    if ("Log In".equals(cmd)) {
      login();
      mapping = "User Page";

    // User wants to log out
    } else if ("Log Out".equals(cmd)) {
      logout(request);
      mapping = "Wizard Loop";
    }
  }
  return mapping;
}

The getView method is called when the action class receives a GET request. By convention, a GET request method means that client has asked for a view, usually after being redirected from a previous POST request. getView returns a mapping to a JSP page. Notice how the node names of Rule Container are used for view mapping.

public String getView() {

  String mapping = null;

  // Wizard is active, use node name for mapping
  if (signupWizard != null) {
    mapping = signupWizard.getCurrentNode().
              getNodeName();

  // Wizard is not instantiated, show stub page
  } else {

    // Check if a user is [still] logged in
    String username = 
      UserAccounts.currentUser(request);

    // User is logged in, show "Logged In" page
    if (username != null) {
      mapping = "Logged In Stub";

    // User is not logged in, 
    // show "Not Logged In" stub page
    } else {
      mapping = "Not Logged In Stub";
    }
  }
  return mapping;
}

Wizard Pages

The wizard defines five JSP pages: one page for each wizard step, and two stub pages. All pages have the following common features:

  • HTML forms are submitted with the POST method.
  • Input is always submitted to the same URL, which is served by the same Struts action.
  • Input controls have access to wizard data through nested properties.
  • Each button submits a command parameter.
  • Errors are not cleared on refresh, or when a user leaves the application and returns later.

Here is the page corresponding to the first wizard step, Identification. Other pages look similar.

<html:form action="/signupWizard.do">
  User Name: <html:text name="WizardForm"
    property="signupBean.userName"/><br>
  Password: <html:text name="WizardForm"
    property="signupBean.userPassword"/><br>
  Personalize: <html:checkbox name="WizardForm"
    property="signupBean.personalize"/><br>
  <input type="submit" name="cmd" value="Cancel">
  <input type="submit" name="cmd" value="Next">
</html:form>

Web Flow Configuration

Finally, the wizard flow, defined in the struts-config.xml file:

<struts-config>
  <form-beans>
    <form-bean name="WizardForm"
      type = "com.superinterface.loginwizard.
                   struts.WizardForm"/>
  </form-beans>
  <action-mappings>
    <action path="/signupWizard"
      type="com.superinterface.loginwizard.
               struts.WizardAction"
      name="WizardForm"
      scope="session">

      <!-- Obtain input -->
      <forward name="Wizard Loop" 
        path="/signupWizard.do" redirect="true"/>
      <forward name="Done" 
        path="/userPage.do" redirect="true"/>
      <forward name="Canceled"
        path="/signupWizard.do" redirect="true"/>

      <!-- Show view -->
      <forward name="Identification Node" 
        path="/WEB-INF/JSP/signup_start.jsp"/>
      <forward name="Personalization Node" 
        path="/WEB-INF/JSP/signup_details.jsp"/>
      <forward name="Confirmation Node" 
        path="/WEB-INF/JSP/signup_confirm.jsp"/>
      <forward name="Not Logged In Stub" 
        path="/WEB-INF/JSP/stub_loggedoff.jsp"/>
      <forward name="Logged In Stub" 
        path="/WEB-INF/JSP/stub_loggedin.jsp"/>
    </action>

    <action path="/userPage"
      type="org.apache.struts.
               actions.ForwardAction"
      scope="request"
      parameter="/WEB-INF/JSP/user_page.jsp"/>
  </action-mappings>
  <controller nocache="true"/>
</struts-config>

The most important mapping is Wizard Loop. It is used to redirect the browser back to the wizard action after input data is submitted. The action then chooses the appropriate view and forwards to the JSP page.

The Not Logged In Stub and Logged In Stub mappings are used to display stub pages, when the wizard's Rule Container is not initialized. The "Not logged in" page allows a user either to log in or to start the signup process. The "Logged in" page displays the name of the current user and allows him to log out. Figure 2 shows what the stub pages look like.

Stub pages
Figure 2. "Logged in" and "Not logged in" stub pages

If the Rule Container is active, then Identification Node, Personalization Node, and Confirmation Node mappings are used to forward to the page corresponding to the current wizard step. Figure 3 shows the wizard pages.

Wizard pages
Figure 3. Signup Wizard steps

The Canceled mapping in this configuration transfers back to the wizard action. Alternatively, this mapping may redirect to some other page, informing the user about the failure.

The Done mapping is the only mapping that goes outside of the wizard's action; it shows the user's home page after a successful login.

Controlling the Browser

Signup Wizard improves the well known Redirect-After-Post technique, using a couple of tricks.

The first is to serve different content from the same location. And by saying "same" I mean exactly the same, including the number, names, and content of request query parameters. Internet Explorer and Mozilla/Firefox build session history based on resource location, and do not include resources from the same location into the history.

Another trick is redirection: according to HTTP specs, when a browser is redirected from a POST request, the resource identified by original request must not be stored in the session history. Some browsers like Internet Explorer go further and do not cache response for original request even if the request has the GET type.

These tricks result in a neat effect: a browser thinks that it keeps reloading the same page, so it does not enable its Back and Forward buttons. Thus, the browser prevents a user from going back to see the stale data or to resubmit the stale form.

If this approach does not work with a particular browser, and the browser accumulates every page in its history, then the application falls back to the Redirect-After-Post pattern.

The Signup Wizard was tested on Windows 2000 with Internet Explorer, Netscape Navigator, Opera, and Firefox (make sure that you use the official release of Firefox, which fixes the bug with no-store cache-control header). Opera is the bad boy; it tries to cache everything. It interprets the HTTP standard differently than Internet Explorer and Firefox, and does not reload a page when a user navigates back. Thus, it is possible to resubmit a stale form. The Signup Wizard tries to check for View-Model consistency to prevent it from accepting data from the wrong page.

Conclusion

In this article, we have discussed a server-centric approach to web applications. We developed a multi-page wizard component, which provides synchronization of View and Model, separates input from output, separates one view from another, disables page caching, and tries its best to control the browser's session history. In short, we developed the component that has as few page interdependencies as possible and as much server state as possible. It is up to you to decide if the compromises justify the outcome. Software is created for consumers. So if a certain approach makes an application more robust and a user experience more trouble-free, than it definitely worth some extra computer cycles.

The trick with the same resource location, described in the article, may not be appropriate for every web application. Signup Wizard is not a regular application. A Signup Wizard instance exists only while a user performs a one-time job. It does not retain its state for long; its pages should not be bookmarked. All wizard pages share the same data. The Signup Wizard does not accept direct page location from a browser.

A regular CRUD (CReate, Update, Delete) application is different. Its data is usually persisted in the database and can be loaded at any time. It may be important to be able to bookmark a page; thus, a URL should fully describe a particular item. It is also important to provide the correct behavior for simple page refresh, for reloading a form with an error message, and for navigating back and forth. Thus, request query parameters become an important component of an application and cannot be omitted, like they are in the Signup Wizard.

Resources

Michael Jouravlev lives in California and has a degree in computer science from the Moscow Aviation Institute in Moscow

关闭本页
 
首页 | 投资与合作 | 服务条款 | 隐私政策 | 收藏本站 | 设为首页 | 新用户注册 | 免责声明 | 使用帮助
Copyright ©2005-2008 chinaitpower.com All rights reserved. www.chinaitpower.com 版权所有