概述
假設某一個系統在用戶注冊模塊中需要區別一般用戶和高級用戶,一般用戶只要提供最簡單的信息,通過一個小表單就可以搞掂了。但對於需要注冊為高級用戶的客戶來說,論壇希望他們提供詳細的注冊信息,除了用戶名、密碼、Email這些最簡單的信息外,還需要提供住址、電話以及興趣愛好之類的調查信息。通過一張大表單讓注冊者一次性填寫所有這些信息並不是一個好主意,大部分潛在的用戶當看到這樣面目猙獰的“超級表單”後都會毫不猶豫的放棄注冊。這時通過一個向導式的表單讓用戶分步填寫注冊信息將是明智的方案,雖然需要填寫的信息量不變,但心理學的經驗告訴我們,用戶會在第一感覺簡單的心理暗示下慢慢進入我們設下的“麥田圈套”中。
高級用戶注冊所需填寫的信息分解到3個表單中,並以向導方式分步完成:
1) 填寫用戶名、密碼、Email等一般的信息;
2) 填寫地址、電話等聯系的信息;
3) 填寫用戶興趣愛好的調查信息。
在其它MVC框架中開發向導式的表單並非易事,因為你需要考慮表單前進、後退、中途退出,表單分步驟校驗,數據維護等諸多的問題。幸運的是,在Spring MVC中,你不必躬身考慮這種底層工作流程的細節,AbstractWizardFormController已經編制好了向導表單的工作流程並將那些需要你確定的步驟開放出來,你只需要通過擴展現成的AbstractWizardFormController通過很少的工作,一個功能強大的向導表單就大功告成了。
我們打算通過以下頁面流程完成高級用戶注冊的操作:
圖 1 注冊高級用戶向導頁面流程
創建注冊高級用戶的向導控制器
我們構建一個向導控制器,它必須繼承AbstractWizardFormController類,FullUserRegisterController負責為注冊高級用戶提供基本的向導控制器,其代碼如下所示:
代碼清單 1 FullUserRegisterController:向導控制器
package com.baobaotao.web.user;
…
import org.springframework.web.servlet.mvc.AbstractWizardFormController;
public class FullUserRegisterController extends AbstractWizardFormController {
private String cancelView; ① 點擊取消後轉向的視圖(邏輯視圖名)
private String successView; ② 向導最終處理成功後轉向的成功頁面
private BbtForum bbtForum;
public void setBbtForum(BbtForum bbtForum) {
this.bbtForum = bbtForum;
}
③ 負責處理最後表單提交的動作
protected ModelAndView processFinish(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
FullUser fullUser = (FullUser) command;
bbtForum.registerFullUser(fullUser);
③-1轉向welcome.jsp頁面
return new ModelAndView(getSuccessView(), "fullUser", fullUser);
}
④負責處理取消的動作
protected ModelAndView processCancel(HttpServletRequest request, HttpServletResponse
response, Object command, BindException errors) throws Exception {
return new ModelAndView(getCancelView());④-1轉向main.jsp頁面
}
//省略get/setter
}
AbstractWizardFormController唯一必須實現的方法是processFinish()方法,在用戶完成整個向導的提交後,這個方法被調用執行。在FullUserRegisterController中,processFinish()方法將FullUser對象傳遞給BbtForum業務對象,保存高級用戶的注冊信息,然後轉向到注冊成功後的歡迎頁面。你可以有選擇地實現processCancel()方法,該訪問負責處理取消的動作。向導鏈中的頁面都提供一個可以讓用戶中途退出的“取消”功能,它可以增強了向導的靈活性。在④處,我們指定當用戶點擊“取消”從向導中途退出時,轉向到main.jsp頁面中。
AbstractWizardFormController並沒有提供“取消視圖”和“成功視圖”的配置屬性,這不能不說是一個遺憾。所以我們在FullUserRegisterController中①和②處分別定義了用於取消頁面和成功頁面的邏輯視圖名。
讀者可能會發出這樣的疑問:表單對象和向導鏈上的頁面視圖究竟在哪裡定義呢?你也許已經猜測到,它們應該出現在配置文件中。我們馬上看一下FullUserRegisterController的具體配置:
<bean name="/fullRegisterUser.html" class="com.baobaotao.web.user.FullUserRegisterController">
<property name="bbtForum" ref="bbtForum" /> ① 業務對象
<property name="commandClass" value="com.baobaotao.domain.FullUser"/> ②表單對象類
<property name="pages" value="fullRegister,relation,favorite"/> ③向導鏈頁面邏輯名,用逗號隔開
<property name="cancelView" value="main"/> ④取消後轉向的視圖
<property name="successView" value="welcome"/> ⑤向導處理成功後轉向的視圖
</bean>
在前面的實例中,我們都是通過在控制器構造函數中通過調用setCommandClass()方法指定表單對象(命令對象),這裡我們通過配置進行指定,如②所示。
③處通過pages屬性定義了構成向導鏈的頁面視圖,pages屬性是一個String[],你不但可以通過<list><value></value></list>的方式進行配置,也可以通過逗號分隔的字符串的方式進行配置,後者顯然要更簡潔一些。“fullRegister,relation,favorite”傳達了兩個信息:
1)向導鏈由三個視圖組成,分別是fullRegister、relation和favorite;
2)向導鏈的視圖順序是fullRegister->relation->favorite。
在④和⑤處分別定義了取消和提交表單轉向的視圖,這樣向導控制器就知道哪些頁面構成了表單,當進行取消和提交表單操作時需要轉向到哪些頁面。至此,形如圖 1所描述的整體向導流程就搭建完成了。
雖然我們已經知道了整個向導的頁面組成,但是組成向導鏈的頁面需要做哪些配合工作,以便讓向導正確串接起來呢?
將向導頁面串接起來
任何向導控制器的第一個頁面都是pages屬性指定的第一個視圖(可以通過覆蓋方法改變)。在高級用戶注冊向導中,第一個顯示的頁面是即fullRegister視圖(通過視圖解析器解析,你可以簡單地將其看為fullRegister.jsp)。
當AbstractWizardFormController接收到請求時,它根據咨詢getTargetPage()方法判斷要轉向到哪個目標視圖。getTargetPage()方法返回一個整數值,它代表向導頁面的索引值,以0為基數,也就是說0代表fullRegister,1代表relation,以此類推。
getTargetPage()方法的缺省實現是根據請求中的一個特定參數來確定的,這個參數以“_target”開頭,以數字結尾。getTargetPage()方法去掉“_target”前綴得到剩下的數字,以它作為目標頁面的索引值。例如,如果請求中的一個參數名為“_target1”,那麼用戶將被帶到索引為1的relation頁面。
了解getTargetPage()的工作原理有助於我們在HTML頁面中構造下一步和上一步的按鈕。例如對於relation視圖頁面(對應的頁面索引為1),要在這個頁面創建下一步和上一步按鈕,你要做的就是創建提交按鈕,並以_targetX進行命名,如下所示:
代碼清單 2 relation.jsp:填寫用戶聯系信息頁面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>寶寶淘論壇用戶注冊</title>
</head>
<body>
<form:form>
地 址:<form:input path="address" /><br>
電 話:<form:password path="telephone" /><br>
<input type="submit" name="_target0" value="上一步" />①導向fullRegister視圖頁面
<input type="submit" name="_target2" value="下一步" />①導向favorite視圖頁面
</form:form>
</body>
</html>
當上一步按鈕被按下時,名為_target0的參數被放到請求中,連同其它表單數據發送到FullUserRegisterController控制器中,控制器的getTargetPage()方法處理這個_target0參數,得到目標頁面的索引是0,這樣就可以將用戶帶到fullRegister的頁面中。如果用戶點擊下一步按鈕提交表單,_target2的參數被放到請求中,這時用戶將被帶來favorite頁面。
完成和取消向導
我們現在知道了如何讓向導前進和後退,如果希望中途從向導中退出,或在最後一步提交整個向導,應該如何去做呢?FullUserRegisterController提供了processCancel()和processFinish()方法,它們分別處理取消向導和提交整個向導的工作,現在最重要的問題是如何觸發這兩個操作?和前進後退的觸發機制類似,向導控制器通過兩個特殊的提交參數進行控制:當請求中包含“_cancel”參數時,調用processCancel()方法取消向導,當請求中包含“_finish”參數時,調用processFinish()完成向導。
按照上一步和下一步相似的方式,我們只需要將提交按鈕命名為“_cancel”和“_finish”就可以完成取消和完成向導的操作了。來看一下favorite頁面是如何做到這點的:
<form:form>
…
<input type="submit" name="_target1" value="上一步" />
<input type="submit" name="_finish" value="確 定" /> ①點擊該提交按鈕,完成向導
<input type="submit" name="_cancel" value="取 消" />②點擊該提交按鈕,取消向導
</form:form>
當點擊“確定”按鈕時,“_finish”被添加到請求參數列表中,FullUserRegisterController執行processFinish()方法,調用業務對象對FullUser進行業務操作,並導向到welcome頁面中。當點擊“取消”按鈕時,“_cancel”被加入到請求參數列表中,processCancel()被調用,取消向導並導向到main頁面中。
分步驟校驗表單數據
和表單控制器一樣,你可以為向導控制器配置一個校驗器,讓其負責對表單對象進行數據校驗,不過兩者的校驗流程存在區別。這是因為,如果對表單對象校驗太早,由於有些表單對象屬性值要在後續步驟中才能收集到,這將導致過早校驗的問題。反之,如果校驗時間點太晚,提供表單對象屬性的頁面距離當前已經跨越了多個頁面,當發生錯誤時將無法正確導向到調整的頁面,這無疑會影響交互性造成不好的用戶體驗。所以必須分步驟進行有針對性的校驗,而非一次性校驗,即控制器每次僅對當步提交的數據進行校驗。
在我們高級用戶注冊的向導中,第一步驟要求用戶填寫用戶名、密碼、Email的信息,當用戶點擊下一步到relation頁面時,就必須對這三個屬性值進行校驗,如果發生錯誤就應該馬上返回到提供這三個屬性值的fullRegister頁面中,以便用戶及時糾正。
在每次頁面跳轉時,validatePage()方法會被調用,用以校驗表單對象的數據合法性。該方法缺省實現是空的,我們可以通過覆蓋該方法做出自己的判斷。在校驗時需要結合page頁面索引號進行分步驟校驗。
首先我們編寫一個校驗器,它為每一步驟提供了相應的校驗方法:
代碼清單 3FullUserValidator
package com.baobaotao.domain;
…
public class FullUserValidator implements Validator {
…
public boolean supports(Class clazz) {
return clazz.equals(FullUser.class);
}
public void validate(Object command, Errors errors) {①完全校驗
…
}
public void validateStep1(Object command,Errors errors){ ②第一步的校驗
…
}
public void validateStep2(Object command,Errors errors){ ③第二步的校驗
…
}
}
FullUserValidator為第一,二步向導分別提供了校驗,而validate()用於向導最後提交時的校驗。在FullUserRegisterController在validatePage()方法中根據page屬性所標識的步驟執行相關的校驗:
代碼清單 4FullUserRegisterController# validatePage()
protected void validatePage(Object command, Errors errors, int page, boolean finish) {
FullUserValidator validator = (FullUserValidator)getValidator();
if (page == 0) { ①第一步提交的校驗
validator.validateStep1(command, errors);
}else if (page == 1) {②第二步提交的校驗
validator.validateStep2(command, errors);
}else if (finish) { ③向導最後提交的校驗
validator.validate(command, errors);
}
}
當向導頁面提交時,page表示提交的頁面索引,我們可以據此實施特定步驟的校驗。當向導最後提交時finish屬性為true,這時我們就進行整體表單對象校驗。
最後我們要做的是在配置文件中將FullUserValidator裝配到FullUserRegisterController中,如下所示:
<bean name="/fullRegisterUser.html" class="com.baobaotao.web.user.FullUserRegisterController">
<property name="bbtForum" ref="bbtForum" />
<property name="commandClass" value="com.baobaotao.domain.FullUser"/>
<property name="pages" value="fullRegister,relation,favorite"/>
<property name="cancelView" value="main"/>
<property name="successView" value="welcome"/>
<property name="validator"> ①注入校驗器
<bean class="com.baobaotao.domain.FullUserValidator" />
</property>
</bean>
在①處注入校驗器,這樣validatePage()方法就可以通過getValidator()獲取校驗器並實施分步校驗了。
小結
在Struts、WebWork等MVC框架中開發具有向導功能的模塊往往是比較復雜的,因為向導的各項處理動作和前後的銜接都必須由開發者負責,從框架中得不到任何的幫助。而在Spring MVC中開發具有向導功能的模塊是非常容易的,因為Spring MVC已經通過AbstractWizardFormController“預編制”好了向導的流程,開發者僅需要按照特定的規則實現特定的方法,使用特定的URL請求參數就可以開發出一個功能強大的向導模塊了。