記得早年在鄉間,對外的通訊往來主要依靠一種特殊職業的人:信客。外出謀生的人多了,少不了要帶幾封平安家信、捎一點衣物食品的,那就用得著信客了。信客要有一點文化,知道各大碼頭的情形,還要一副強健的筋骨,背得動重重的行李。信客沉重的腳步,是鄉村和城市的紐帶。
-- 餘秋雨《文化苦旅·信客》
■ 一個饅頭引發的血案 - 回傳與事件
基於WEB的分布式系統中,使用者往往是通過提交表單,瀏覽器產生相應的HTTP POST請求來完成互動過程,這個過程稱為回傳(PostBack)。在同一個網頁中,常會有許多HTML標籤可能引起回傳,申請交於伺服器處理。
控制項對應著用戶端的HTML標籤,有著自己的狀態和行為。使用者操作引起每一次回傳,會調用頁面中一個或多個控制項行為修改其狀態,也就是說,杯中的粉圓(《隨想十》中對控制項的比喻)之間是有關聯的,使用者撥動其中一個,可能引起其它粉圓震動。拓展開來,當使用者操作或系統內部引髮狀態改變時,類需要發送一個訊息給關聯類別,讓關聯類別做相應的狀態調整。在.NET架構中,這個訊息被稱為事件(event),發接訊息的類被稱為事件來源(event source),關聯類別被稱為事件接收者(event sink)。回傳的處理過程,實質上是事件來源呼叫事件接收者的行為函數,稱為回調(callback)。
我們不希望在編譯時間就確定回調的對象,否則這種強耦合關係就意味著每次使用時需要拎一串關聯粉圓放到杯子中。相反,我們希望到運行時再來確定回調關係,在.NET架構中,這種方式被定義成委託(delegate),我們在《隨想七》和《隨想八》已經對其有了初步的認識。事件基於發布-訂閱機制,每一個產生事件的類都有一個委託成員(發布機制),在系統初始化時,接收器或其它類需要將具體的事件處理常式綁定到委託成員(訂閱機制),運行時,系統自動完成回調。
■ 口信 -使用者操作引發的伺服器端事件
"終於有婦女來給信客說悄悄話:'關照他,往後帶東西幾次並一次,不要雞零狗碎的';'你給他說說,那些貨色不能在上海存存?我一個女人家,來強盜來賊怎麽辦'……信客沉穩地點點頭。"
使用者會對用戶端瀏覽器中的頁面元素做出各種操作,瀏覽器可以通過JavaSript之類的指令碼語言來捕獲這些操作並且做出相應回應,但對伺服器而言,它卻常常視而不見。要產生伺服器端事件,就必須在設計期讓事件來源對應的表單元素引髮帶有鮮明特徵的回傳,從而讓頁面能夠正確識別,並傳遞給控制項以做相應回調,完成使用者操作到事件的映射過程。
ASP.NET用介面IPostBackEventHandler做為信客的口信,帶回遠方的訊息,它包含一個方法:RaisePostBackEvent。在回傳後,頁面會在控制項樹中尋找與引發回傳HTML元素的UniqueID相匹配的控制項,並調用該方法,下例為依賴於使用者點擊引發事件的自訂控制項範例。
// MyControls.cs 自訂控制項集 using System; using System.ComponentModel; using System.Web.UI; using System.Web.UI.WebControls;
namespace essay { public class myButton:WebControl,IPostBackEventHandler { //定義控制項屬性Text public virtual string Text { get { string s =(string)ViewState["Text"]; return (s==null)?string.Empty:s; } set {ViewState["Text"]=value;} } //產生控制項對應的HTML代碼 protected override void Render(HtmlTextWriter writer) { writer.Write("<INPUT TYPE=submit name=" + this.UniqueID + " Value='"+this.Text+"' />"); } //定義Click事件委託 public event EventHandler Click; //把用戶端提交映射到自訂的Click事件 void IPostBackEventHandler.RaisePostBackEvent(string eventArgument) { OnClick(EventArgs.Empty); } //實現回調 protected virtual void OnClick(EventArgs e) { if(Click!=null)Click(this,e); } } } |
■ 行李 - 回傳資料引發的伺服器端事件
"一次,村裡一戶人家的姑娘要出嫁,姑娘的父親在上海謀生,托老信客帶來兩匹紅綢。"
除了依賴於使用者操作引發事件外,我們時常還需要根據回傳的使用者資料,來修改相應控制項的狀態,從而引發事件。
回傳的用戶端表單資料會被集中整理到包含資料名/值集的一個System.Collections.Specialized.NameValueCollection執行個體中,頁面會利用UniqueID在控制項樹中尋找匹配控制項,如果匹配控制項實現介面IpostBackDataHandler,則調用LoadPostData方法更新狀態並返回更新標識,RaisePostDataChangedEvent方法檢查標識從而引發事件。下例為依賴於狀態變化引發事件的自訂控制項範例。拓展一下,可以更加靈活地使用這個事件機制,例如當使用者輸入特定資料時,也可以在此引發特定事件。
using System; using System.Web; using System.Web.UI; using System.Collections.Specialized;
namespace essay { public class MyTextBox: Control, IPostBackDataHandler { //定義控制項屬性Text public String Text { get {return (String) ViewState["Text"];} set {ViewState["Text"] = value;} } //產生控制項對應的HTML代碼 protected override void Render(HtmlTextWriter output) { writer.Write("<INPUT TYPE=text name=" + this.UniqueID + " Value='"+this.Text+"' />"); } //定義TextChanged事件委託 public event EventHandler TextChanged; //更新控制項的Text狀態並返回更新標識 //參數NameValueCollection為回傳資料集 public virtual bool LoadPostData(string postDataKey, NameValueCollection values) { String presentValue = Text; String postedValue = values[postDataKey]; if (!presentValue.Equals(postedValue)) { Text = postedValue; return true; } return false; } //檢查更新標識引發自訂事件TextChanged public virtual void RaisePostDataChangedEvent() {OnTextChanged(EventArgs.Empty);} //實現回調 protected virtual void OnTextChanged(EventArgs e) {if (TextChanged != null)TextChanged(this,e);} } } |
■ 眼神 - 非回傳事件與完整的控制項執行生命週期
"只要信客一回村,他家裡總是人頭濟濟。多數都不是來收發信、物的,只是來看個熱鬧。農民的眼光裡,有羨慕,有嫉妒;比較得多了,也有輕蔑,有嘲笑。這些眼神,是千年故土對城市的探詢。"
以上兩個事件皆與回傳有直接關係,利用.NET的事件架構,我們可以在控制項中的任何一個地方引發非回傳事件。例如我們可以在頁面中加入對使用者透明的使用者行為分析處理控制項,窺視其它控制項狀態從而引發其特定的事件。
至此,我們已經深入瞭解與控制項執行相關的各種要素細節,最後,通過圖11-2,我們小結一下控制項完整的執行生命週期。