
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属性值的设定,若设得太短,很快就会产生异常,此属性将于后面章节中详细说明。