决战.NET
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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所示。

true

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