/china/msdn/library/webservices/asp.net/us0501CuttingEdge.mspx?mfr=true
控件腳本回調基本知識
ASP.NET 腳本回調機制由兩個關鍵元素組成:響應用戶操作的服務器端代碼,以及客戶端上處理服務器端事件所生成結果的 JavaScript 回調代碼。在頁面回調自身的情況下,正如我在前面提到的文章中所述的那樣,您可以在執行對用戶不可見的回發的頁面按鈕中附加壹些 ASP.NET 生成的腳本代碼。因為該請求的目標是當前頁,所以該頁會發布到自身,這與它在壹個普通回發事件中的行為方式相似,只是頁面的生命周期縮短了。該頁必須實現 ICallbackEventHandler 接口,以便可以調用壹個具有預定義簽名的方法,來為客戶端生成結果。
那麽,當控件觸發帶外調用時,該方案又有什麽不同呢?在這種情況下,“不可見”回發的目標 URL 是承載該調用方控件的頁面的 URL。該控件必須實現 ICallbackEventHandler 才能提供為客戶端生成某些結果的方法。同樣,該控件負責在承載頁中插入處理結果和刷新該頁所需的任何 JavaScript 代碼。
具有回調功能的控件只是壹個實現 ICallbackContainer 和 ICallbackEventHandler 接口的控件,兩個接口都各有壹個方法。ICallbackContainer 接口具有的方法可以返回觸發遠程調用的腳本代碼;ICallbackEventHandler 接口則提供了在調用期間執行的服務器端代碼。ICallbackEventHandler 也是壹個具有回調功能的頁面必須實現的接口。壹個實現回調接口的自定義控件示例的聲明如下面的代碼所示:
public class CallbackValidator : WebControl,
INamingContainer, ICallbackContainer, ICallbackEventHandler
在 ICallbackContainer 接口的實現中,您可能需要放入壹個對該頁 GetCallbackEventReference 方法的調用,以獲得壹個可啟動服務器事件的正確 JavaScript 調用。稍後我再講述這些內容。
返回頁首
CallbackValidator 控件
為了解具有回調功能的服務器控件,我們來看壹個具有 ASP.NET 腳本回調功能的自定義驗證器控件示例。在 ASP.NET 中,驗證控件用於檢查並驗證網頁中定義的窗體域的輸入。驗證器是壹個服務器控件,它是從 BaseValidator 類繼承的,而該類又是從 Label 繼承的。
每個驗證控件都引用壹個位於該頁其他位置的輸入控件。當頁面要提交時,任何受監視服務器控件的內容都會傳遞到該驗證器,以進行進壹步處理。每個驗證器都執行壹種不同類型的驗證。例如,CompareValidator 控件使用比較運算符(如小於、等於或大於)將用戶的輸入與壹個固定值進行比較。RangeValidator 確保用戶輸入位於某個指定範圍內,而 RegularExpressionValidator 只在匹配某個常規表達式定義的模式時才驗證用戶輸入。
通常,驗證都在服務器上發生。然而 ASP.NET 還為大多數驗證控件提供了壹個完整的客戶端實現,並允許用戶為其余驗證控件編寫自定義客戶端腳本。這就使得具有 DHTML 功能的瀏覽器(如 Microsoft?Internet Explorer 4.0 和更高版本)在用戶點擊或單擊受監視輸入域之外的位置後,能夠立即在客戶端上執行驗證。在很多情況下,客戶端驗證足夠強大,可以檢測出許多重大錯誤並通知用戶。例如,RequiredFieldValidator 控件可驗證給定域不能保留為空。無需回發到服務器即可驗證當前值。
如果客戶端驗證打開,則在所有輸入域均包含有效數據之前,該頁不會回發。為了運行安全代碼,以及防止惡意和秘密的攻擊,您還是應該在服務器上驗證數據;服務器端驗證始終由驗證器控件執行,即使同時要執行客戶端驗證也是如此。另外,並非所有類型的驗證都能在客戶端上完成。實際上,如果您需要針對數據庫進行驗證,則沒有別的選擇,只能回發到服務器。而這也正是發生問題的地方。
常規回發涉及整個頁面。上載整個視圖狀態,處理整個頁面,生成、下載和呈現同樣的大型響應。如果您能夠向服務器發出經過優化的帶外請求,並只檢查驗證之下的控件的狀態,那豈不是很好?
在 ASP.NET 中,沒有這樣的控件。那麽我們就來編寫壹個這樣的控件吧,我將其命名為 CallbackValidator。CallbackValidator 是壹個自定義 ASP.NET 2.0 控件,我構建這個控件的目的是為了演示控件可以如何實現對承載頁的帶外調用,以及如何在服務器上自行處理事件。
在我開始著手此項目時,實際上並沒有如此雄心勃勃的目標:我原先的目標只是修改 CustomValidator 標準控件。對於該記錄,CustomValidator 控件采用了以編程方式定義的驗證邏輯來檢查用戶輸入的有效性。如果預先不知道要檢查的值,則應該使用此方法。CallbackValidator 控件的最初意圖是提供壹種方法,以便在不回發整個頁面的情況下執行服務器端驗證。我意識到無需太多的額外努力,就可以擁有壹個類似於自定義按鈕的控件,這個控件可以在不回發整個頁面的情況下在服務器上對許多輸入域進行驗證,而此時我的修改工作已經完成了壹半。這個行為就是 CallbackValidator 控件的全部。
在我深入講述該控件的精髓之前,我們先來看壹下圖 1。該頁面上的 Submit 按鈕只會按照普通的方式將所有值發布到服務器上。實際上,這些值將在客戶端上進行處理,如果所有這些值都需要傳遞,那麽該控件就會將其傳遞到服務器上,在該服務器上,所有控件輸入都將使用服務器端驗證代碼(如果有的話)進行驗證。Validate 按鈕會觸發壹個對 Web 服務器的帶外調用,並只驗證指定的輸入控件。在它返回時,您就會知道哪些值已經通過了服務器的驗證。例如,在圖 1 中,您將在嘗試提交其余數據之前了解到是否已經采用了該用戶 ID。
圖 1 帶有具有回調功能驗證的輸入窗體
圖 2 顯示了該頁面的源代碼。正如您可以看到的那樣,它包含了壹個 HTML 服務器窗體、壹些文本框(每個文本框都綁定到壹個標準的驗證控件)以及該自定義 CallbackValidator 控件的壹個實例。此控件實際上負責創建並顯示 Validate 按鈕。
返回頁首
該控件如何工作
該 CallbackValidator 控件從 WebControl 繼承,並實現了 INamingContainer 接口。另外,它還實現了 ICallbackContainer 和 ICallbackEventHandler 接口,以便獲得回調支持。
ICallbackContainer 接口需要方法 GetCallbackScript 按照下列方式聲明:
string GetCallbackScript(IButtonControl buttonControl, string argument)
GetCallbackScript 采用兩個參數。第壹個是對預期要觸發回調的頁面控件的引用。第二個參數(字符串)表示調用方希望傳遞給方法以幫助構建輸出的任何上下文。從名稱可以看出,GetCallbackScript 方法使用 JavaScript 函數調用來準備和返回字符串,以便附加到指定的按鈕控件來觸發遠程調用。
該按鈕控件參數使您能夠精確地指定要對控件 UI 中的哪個按鈕進行 JavaScript 調用。該示例 CallbackValidator 控件只有壹個可單擊按鈕;而 GridView 控件則具有很多可單擊按鈕,每個按鈕都用於頁導航或標頭中的壹個鏈接按鈕。在 ASP.NET 2.0 中,所有充當窗體中按鈕角色的控件都需要實現壹個新的接口 — IButtonControl。該接口在圖 3 中進行了詳細說明,它是由下列 Web 控件實現的:Button、LinkButton 和 ImageButton。HTML 按鈕控件不能實現該接口。請註意,在 Microsoft .NET Framework 1.x 中,IButtonControl 接口僅對於 Windows?Forms 按鈕控件存在(盡管成員集合完全不同)。
具有回調功能的控件所需的第二個接口是 ICallbackEventHandler — 在支持腳本回調的頁面上也需要這個接口。該接口由壹個方法組成:
string RaiseCallbackEvent(string eventArgument)
該方法以字符串的形式接收輸入值,執行壹些服務器端的工作,再以字符串的形式返回響應。此處非常重要的壹點是,輸入和輸出數據都可以打包為字符串進行傳遞;而該字符串真正的內容和格式則由編碼人員決定。
在我討論 CallbackValidator 控件的實現之前,讓我們先來看壹下圖 4,該圖說明了該控件如何適應頁面中處理 ASPX 資源請求的 HTTP 處理程序。CallbackValidator 控件看起來就像壹個附加了某些腳本代碼的按鈕。該腳本代碼就是它的 GetCallbackScript 方法所返回的內容。單擊該按鈕 (Validate) 時,它會激發後臺回發以發送視圖狀態、當前輸入值,以及兩個名為 CALLBACKPARAM 和 CALLBACKID 的自定義字符串。前者包含了 RaiseCallbackEvent 在 GetCallbackScript 方法的正文中創建時的輸入值,CALLBACKID 則負責識別處理服務器事件的服務器端對象。從 ASP.NET 運行庫提取請求的頁面 HTTP 處理程序壹旦位於服務器之後,它就會嘗試查找具有該 ID 並實現 ICallbackEventHandler 的控件。如果成功,則會調用該控件的 RaiseCallbackEvent 方法,並將其輸出返回到客戶端。如果 CALLBACKID 的目標為該頁,該 HTTP 處理程序就會查看該頁是否實現該接口,然後按照通常方式繼續。
返回頁首
構建控件
CallbackValidator 是壹個合成控件,其用戶界面由壹個簡單的按鈕組成。通過添加壹些屬性來設置該按鈕的樣式(鏈接、按、圖像或其他什麽內容),您可以輕松地擴展該控件的這方面內容。要在按鈕上顯示的文本由 ButtonText 屬性設置。壹個名為 ControlsToValidate 的集合屬性會收集回調期間要在服務器上測試的所有頁驗證器的 ID。該屬性被實現為 StringCollection 類型,它在開始時是空的。圖 5 中的代碼只允許您在運行時添加控件 ID,而通常情況下,這是在 Page_Load 事件中完成的:
void Page_Load(object sender, EventArgs e) {
CallbackValidator1.ControlsToValidate.Add("valUserId");
CallbackValidator1.ControlsToValidate.Add("valEmail");
}
請註意,該集合類不會保持它的視圖狀態內容。因此,在傳入請求時,您必須總是重新對其進行初始化。另外還要註意,您應該在該集合中添加驗證控件而不是輸入控件。在遠程調用過程中,CallbackValidator 控件會針對關聯的控件調用 Validate 方法,並存儲客戶端回調的響應。CallbackValidator 控件與現有的驗證器協同工作,使得壹個帶外調用就能夠對它們全部進行測試;它實際上並不是壹種新型的驗證器。
正如您在圖 5中看到的那樣,CallbackValidator 控件會創建壹個 Button 控件,並向其 OnClientClick 屬性附加壹些代碼。OnClientClick 是 ASP.NET 2.0 中引入的壹個新屬性,用於向 HTML onclick 事件添加 JavaScript 調用。在 ASP.NET 2.0 中,下面兩行代碼的作用完全相同:
button.Attributes["onclick"] = js; // ASP.NET 1.x; still works in 2.0
button.OnClientClick = js; // ASP.NET 2.0
與該驗證按鈕相關聯的代碼是通過壹個包裝在 ICallbackContainer 接口中的特定方法獲取的。請註意,截止到 Beta 1,ICallbackContainer 接口的使用並不是必須的,但是使用它確實有助於保持代碼的簡潔。在上個月的示例中,我沒有使用該接口,甚至在討論腳本回調的 MSDN Beta 1 文檔中根本沒有提到該接口。盡管如此,從腳本回調獲益的 ASP.NET 控件(大多為 GridView)都會實現該接口。使用 ICallbackContainer 接口的唯壹組件就是該控件本身,這就意味著您可以輕松編寫壹個具有回調功能但不使用該接口的控件。
GetCallbackScript 返回的 JavaScript 函數調用來自以下語句:
Page.GetCallbackEventReference(
this, args, "CallbackValidator_UpdateUI", "null"));
第壹個參數 (this) 表示當前控件,它在發出的請求中設置 CALLBACKID 域。第二個參數 (args) 為服務器端 RaiseCallbackEvent 方法的輸入字符串。第三個參數為 JavaScript 回調函數的名稱,用於處理 RaiseCallbackEvent 方法的輸出。最後,第四個參數在這裏為空,它表示壹個作為回調函數上下文傳遞的 JavaScript 對象。
CallbackValidator 控件必須確保 JavaScript 回調是在承載頁中定義的,並且必須決定該 args 參數的格式和內容。RaiseCallbackEvent 實現只需要來自其客戶端調用方的壹種類型的信息:要測試的驗證器列表。下面是將所有驗證器 ID 串聯到壹個由豎杠分隔的字符串中的代碼:
int i = 0;
StringBuilder sb = new StringBuilder("");
foreach (string s in ControlsToValidate)
{
if (i>0) sb.Append("|");
sb.Append(s);
i++;
}
string args = String.Format("'{0}'", sb.ToString());
綁定到圖 2 中的 Validate 按鈕的 JavaScript 代碼示例可能與以下代碼類似:
WebForm_DoCallback(
'ctl00$PageBody$CallbackValidator1',
'valUserId|valEmail',
CallbackValidator_UpdateUI,
null,
null);
請註意,CallbackValidator 控件的 ID 是由服務器生成的,以便可以唯壹標識頁面上的每個控件。
返回頁首
解決狀態問題
正如圖 4 中顯示的那樣,回發的請求中包含壹些輸入域。除了前面提到的 CALLBACKID 和 CALLBACKPARAM 域之外,該請求還包含了其他壹些輸入域。更準確地說,它包含了窗體中的所有輸入域,以及兩個特定於該回調操作的輸入域。換句話說,視圖狀態是與這些輸入域(文本框、下拉列表等)的當前值壹起回發的。
在檢查請求的回調狀態並找出哪個對象(Page 還是控件)要調用 RaiseCallbackEvent 之前,HTTP 頁面處理程序會還原視圖狀態和發布的值。根據這點,您可能會猜想 RaiseCallbackEvent 方法中定義的代碼將以壹種壹致和更新的狀態執行。特別是,您可能會猜想 CallbackValidator 控件的 RaiseCallbackEvent 方法所調用的所有驗證器都將測試它們所綁定的輸入控件的當前值。但是,從本部分的標題中可以看出,實際上並不是這樣。請看壹下圖 6 中的 RaiseCallbackEvent 代碼。
該方法會檢索驗證器控件,並調用它的 Validate 方法。該方法執行其任務(具體取決於驗證器的類型)並將 IsValid 屬性設置為 True(有效)或 False(無效)。接下來,RaiseCallbackEvent 方法會構建它對客戶端頁面的響應。返回字符串是壹個以豎杠分隔的字符串集合,每個字符串的格式如下:
controlID:valid (0/1):message:tooltip
第壹個標記為該控件的客戶端 ID,它是由於 Master Pages 和命名容器而考慮創建的完全限定 ID。第二個標記為 0 或 1,具體取決於 IsValid 的值。第三個標記為驗證器在壹個完整回發後要顯示的消息。這與驗證器的 Text(默認)或 ErrorMessage 屬性相對應。如果這兩個屬性都為空,我還強制了壹個 * 字符串。按照設計,Text 預期會包含壹些只將域標記為無效的文本,而 ErrorMessage 則提供更為詳細的錯誤說明。如果 CallbackValidator 的 ShowDetailedInfo 屬性為 true,我就會使用 ErrorMessage 字符串作為壹個工具提示,如圖 7 所示。
圖 7 驗證錯誤消息
那麽與狀態相關的討論在哪裏呢?這個機制紙上談兵還行,但是涉及到真正的值就不行了。進行調試時,我意識到驗證測試的結果完全不可靠。例如,User ID 文本框被設計為接受除“Dino”和空字符串之外的任何內容。但是,它只對於常規回調運行正常,如果使用回調驗證時就不行了。有些位置設置得很好的斷點則顯示所有文本框都保持了它們的原始值,而忽略了在嘗試驗證之前鍵入的內容。這個問題與視圖狀態無關,而是發布值的問題。頁機制像平時壹樣運行正常;只是不能接收來自客戶端且我認為正確的值。
如果您滾動查看使用 ASP.NET 回調的頁的 HTML 源代碼,就會發現在加載頁時會調用壹個名為 WebForm_InitCallback 的 JavaScript 函數。此函數是 ASP.NET 2.0 基礎架構的壹部分,它是通過 WebResource.axd 系統處理程序插入頁面的。看壹下此函數的源代碼是否運行良好。(有關如何獲得該代碼的詳細信息,請參閱上個月的專欄。)基本上,WebForm_InitCallback 在加載頁時構建 POST 請求的正文。該頁的正文是壹個字符串(其名稱為 __theFormPostData),其中填滿了視圖狀態的內容以及窗體中的所有輸入域。該代碼是正確的;但是執行的時間不是我所預期的!這些輸入域的內容是在加載時收集的,但是在進行回發時沒有使用用戶提供的值進行更新。這就是服務器狀態看起來不正確的原因。為了解決此問題,我只是在開始帶外調用之前重復了對 WebForm_InitCallback 的調用(請參見圖 6)。請註意,這實際上是預期的行為,而不是什麽錯誤。針對系統在帶外調用之前調用 WebForm_InitCallback 的討論是面向這樣的情況的 — 即,用戶希望在最初從服務器下發的數據上下文中執行回調。
返回頁首
JavaScript 文件作為嵌入式資源
在該專欄的最後,我來討論壹個非常好用的技術,它大大簡化了 ASP.NET 2.0 自定義控件中的 JavaScript 代碼插入。該技術在 ASP.NET 1.x 中也能使用。其理念很簡單:在常規 JS 文件中寫入您的 JavaScript 代碼,並將其作為嵌入式資源添加到項目中。(在 Visual Studio?.NET 屬性窗口中設置 Build Action 屬性)。另外,還要為資源名稱添加該組件的命名空間作為前綴。接下來,當您需要在代碼中使用該腳本時,請執行 EmbedScriptCode 方法在圖 6中所作的操作。您使用壹點技巧,就可以在代碼維護和可讀性方面收獲很多。
返回頁首
小結
ASP.NET 腳本回調是壹個功能非常強大的功能,可以節省每個頁更新的時間。盡管發出的是服務器請求,但是整個頁在瀏覽器窗口中保持不變。其優點是雙重的。首先,用戶以為無需回發,會繼續讀取或使用該頁。第二,只有頁的壹些部分會刷新(使用 HTML 對象模型)。在 2004 年 8 月和 12 月專欄中,我深入講述了該基本功能的內容。本月我講述了如何在自定義控件中實現回調,以在所構建的控件中提供更大的靈活性。