3.5 真實的Async-Postback進度顯示
雖然3.4節中已經告訴讀者如何實現工作中的進度回報,但如果要做的是Async-Postback運行時期的進度回報呢?在知道UpdateProgress控件可以在UpdatePanel控件進行刷新期間顯示信息、Timer控件可以定時顯示信息這兩點后,讀者們可能會想,將這兩者結合之后,是不是就能夠于UpdatePanel控件進行刷新期間,在UpdateProgress控件中顯示進度呢?答案是否定的,因為Timer控件有一個特性,不會在其他Async-Postback動作未完成前觸發,這也就是說,利用Timer控件來顯示進度的想法是無法實現的!這與前述之其他Async-Postback未完成前,UpdatePanel控件的再次刷新動作會遺失前次刷新結果的情況類似,這些現象都告訴了我們,當使用ASP.NET AJAX時,同時間只能有一個Async-Postback可完全正常運作。這個限制來自于ASP.NET AJAX的架構設計,稍后的章節會詳細討論為何會設計成這個樣子。那如果真有此需求,該如何做呢?既然ASP.NET AJAX不允許多個Async-Postback同時運行,那么我們就利用另一個不受限于此的機制,就是ASP.NET所提供的Callback機制,這個機制不會受限于ASP.NET AJAX,而且因為其簡單的設計,設計師會擁有較寬廣的控制空間。
1. 創建一個新網頁,命名為WorkingUpdateProgressWithReport.aspx。
2. 在頁面中加入一個ScriptManager控件。
3. 加入一個UpdatePanel控件,ID為UpdatePanel1。
4. 將UpdatePanel1控件的UpdateMode設為Conditional。
5. 加入一個UpdateProgress控件,ID為UpdateProgress1。
6. 在UpdatePanel控件中加入一個Button控件,ID為Button1。
7. 在UpdateProgress控件中加入一個Label控件,ID為Label1,Text為Updating...。
8. 在UpdateProgress控件中加入一個Label控件,ID為Label2。
9. 在Button1控件的Click事件中鍵入程序3-16中的Button1_Click內代碼。
10. 在此網頁的Page類實現ICallbackEventHandler界面,如程序3-11。
11. 在此網頁的網頁源碼中,鍵入程序3-12的JavaScript代碼。
程序3-11
Samples\3\AjaxDemo1\WorkingUpdateProgressWithReport.aspx.cs using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class WorkingUpdateProgressWithReport : System.Web.UI.Page, ICallbackEventHandler { private string currentArgs = string.Empty; protected void Page_Load(object sender, EventArgs e) { if (!IsPostback) { Guid guid = Guid.NewGuid(); string script = "function CallServer(controlID,arg)\r\n"+ "{\r\n"+ " WebForm_DoCallback(controlID,'"+ guid.ToString()+"',ReceiveData, null,null,true);\r\n "+ " if(!isEnd)\r\n"+ " window.setTimeout(\"CallServer ('__Page',null)\",1000);\r\n"+ "}"; //要求產生Callback的初始化程序代碼. Page.ClientScript.GetCallbackEventReference(this, "arg", "ReceiveServerData", "context"); Page.ClientScript.RegisterClientScriptBlock(typeof(Page), "CallBackScript", script,true); ViewState["Current_TaskID"] = guid.ToString(); } } protected void Button1_Click(object sender, EventArgs e) { Cache[((string)ViewState["Current_TaskID"]) + "$Button1_Progress"] = 0; for (int i = 0; i < 10; i++) { Cache[((string)ViewState["Current_TaskID"]) + "$Button1_Progress"] = i * 10; System.Threading.Thread.Sleep(1000); } } #region ICallbackEventHandler Members public string GetCallbackResult() { if (Cache[currentArgs + "$Button1_Progress"] != null) return Cache[currentArgs + "$Button1_Progress"].ToString(); return string.Empty; } public void RaiseCallbackEvent(string eventArgument) { currentArgs = eventArgument; } #endregion }
程序3-12
Samples\3\AjaxDemo1\WorkingUpdateProgressWithReport.aspx <%@ Page Language="C#" AutoEventWireup="true" CodeFile= "WorkingUpdateProgressWithReport.aspx.cs" Inherits="WorkingUpdateProgressWithReport" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www. w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <asp:ScriptManager ID="ScriptManager1" runat="server"> </asp:ScriptManager> <script language=javascript> var prm = Sys.WebForms.PageRequestManager.getInstance(); var isEnd = false; prm.add_initializeRequest(InitRequest); prm.add_endRequest(EndRequest); function InitRequest(sender,args) { window.setTimeout("CallServer('__Page',null)",1000); } function EndRequest(sender,args) { _isEnd = true; } function ReceiveData(rvalue,context) { var lb = $get("Label2"); lb.innerText = rvalue; } </script> </div> <asp:UpdatePanel ID="UpdatePanel1" runat="server"> <ContentTemplate> <asp:Button ID="Button1" runat="server" OnClick= "Button1_Click" Text="Button" /> </ContentTemplate> </asp:UpdatePanel> <asp:UpdateProgress ID="UpdateProgress1" runat="server"> <ProgressTemplate> <asp:Label ID="Label1" runat="server" Text= "Updating:"></asp:Label> <asp:Label ID="Label2" runat="server" Text= "Label"></asp:Label> </ProgressTemplate> </asp:UpdateProgress> </form> </body> </html>
運行此程序并點擊按鈕后,將會看到進度表由1到10逐步地顯示,如圖3-10所示。

圖3-10
那這個程序究竟是如何實現這個功能的呢?在用戶點擊Button按鈕后,此程序模擬需長時間工作的情況,每次循環均延遲1 秒,并將計數的值放到Cache中,當有Callback回來時,GetCallbackResult函數便會被調用,此時傳回Cache中的計數值在客戶端顯示,這便是Server端的運作流程。此處有個很特別的設計,放入Cache時的鍵值是利用Guid.NewGuid所產生出來的,此鍵值為產生CallBack Script時所使用的參數,因此在Callback發生時調用RaiseCallbackEvent,此參數便會被送上來,接著GetCallbackResult以此參數為鍵值由Cache中取出計數值。在Client Script部分,一如以往一樣在Async-Postback前后掛載事件,值得注意的是,在InitRequest時,我們以JavaScript的setTimeout來創建一個Timer,這是JavaScript的Timer,別跟ASP.NET AJAX的Timer搞混了,setTimeout會于指定的延遲時間后運行傳入的程序代碼,此處便是運行Page_Load時產生的CallServer函數(JavaScript的),而CallServer函數會在運行完畢后再次調用setTimeout來繼續下一次的進度更新。當Callback完成后,ReceiveData函數會被調用,此時便用$get函數來取得Label2 對象,并設定其innerText值(在FireFox中,需改為設定innerHTML值),這個$get函數是ASP.NET AJAX Client Library所提供的,可以讓我們以HTML元素的名稱來找到所需要的HTML元素對象。
我們做了什么?
這一節所使用的是違背ASP.NET AJAX的設計架構之技巧,也就是逆天而行的手法,既是如此,又為何這么做呢?我會使用此手法的原因是因為任職顧問時期,有客戶詢問UpdatePanel控件刷新時的進度回報功能,原本我建議使用第3.4節的手法,但是客戶在幾天后回報此法不可行。在仔細看過程序后,我察覺到客戶在Thread中嘗試訪問頁面上的控件,而這是不對的行為,因為在啟動Thread后,Server端并不會等待Thread運行完畢,而是繼續原本的網頁繪制動作后送出,此時原有的頁面上控件均已被釋放掉了,訪問它們只會產生異常。所以在不改動原有程序的情況下,我提供了本節的手法,在這個手法中并未使用Thread,而在事件中也可正常訪問頁面中的控件。不過使用此手法時必須特別注意ScriptManager控件的AsyncPostbackTimeout屬性值的設定,若設得太短,很快就會產生異常,此屬性將于后面章節中詳細說明。