| 
				 一、触发远程任务 
远程任务是在服务器上执行的用于响应客户端事件的一段代码。ASP.NET AJAX 客户端页面触发远程任务的方法有以下三种:使得回发由 UpdatePanel 控件管理,在通过本地 Web 服务公开的应用程序后端直接调用一种方法,使用页面方法。很快会有第四种方法:一种 Windows  Communication Foundation (WCF) 服务。 
一旦触发了服务器上的某项任务,客户端将不再控制该任务。仅当由任务生成的响应已下载到客户端并经过解析后,客户端页面才能够重新控制操作。使用 PMF,可以动态地读取任务状态,但不存在将数据动态传送给服务器任务的机制。 
二、取消任务的简便方法 
使用 ASP.NET AJAX 取消远程服务非常简单,但是存在以下两个限制。首先,该任务必须已通过 UpdatePanel 启动。其次,服务器上不需要任何额外工作来补偿任务的突然中断。下面是基于 UpdatePanel 的页面源代码: 
<html xmlns=”http://www.w3.org/1999/xhtml” > 
<head runat=”server”> 
    <title>Canceling tasks</title> 
    <style type=”text/css”> 
        #UpdateProgress1  { 
            width: 270px; background-color: #ffff99; height:120px; 
            top: 40%; left: 35%; position: absolute; 
            border: solid 1px black; 
        } 
        #ProgressTemplate1  { 
            font-size: 9pt; color: navy; font-family: verdana;  
        } 
    </style> 
</head> 
<script language=”javascript” type=”text/javascript”> 
function abortTask()  { 
    var obj = Sys.WebForms.PageRequestManager.getInstance(); 
    if (obj.get_isInAsyncPostBack())  
        obj.abortPostBack(); 
} 
</script> 
<body> 
<form id=”form1” runat=”server”> 
    <asp:ScriptManager ID=”ScriptManager1” runat=”server” /> 
    <asp:UpdatePanel ID=”UpdatePanel1” runat=”server”  
            UpdateMode=”Conditional”> 
        <ContentTemplate> 
            <asp:Button runat=”server” ID=”Button1” Text=”Start Task ...”  
                onclick=”Button1_Click” /> 
            <hr /> 
            <asp:Label runat=”server” ID=”Label1” /><br /> 
        </ContentTemplate> 
    </asp:UpdatePanel> 
    <hr /> 
    <asp:UpdateProgress runat=”server” ID=”UpdateProgress1”> 
        <ProgressTemplate> 
            <div ID=”ProgressTemplate1”><p style=”margin:5px;”> 
                <img alt=”” src=”Images/indicator.gif”  
                    align=”left” />   
                <span id=”Msg”>Your request has been submitted and  
                    it may take a while to complete. 
                <br /><br />Please, wait ... </span>  
                <p align=”center”> 
                <input type=”button” value=”Cancel”  
                    onclick=”abortTask()” /></p> 
            </div></p> 
        </ProgressTemplate> 
    </asp:UpdateProgress> 
</form></body></html> 
在该页面中,会弹出带有“取消”按钮的进度模板,如图1所示。单击该按钮可以取消操作。 
 
 
  
  
 图1   带有取消按钮的进度模板 
从上面代码中的abortTask函数可以看出,进度模板包含一个绑定到JavaScript代码的客户端按钮。此函数的首要任务是检索页面请求管理器。进行页面初始化时,页面请求管理器会为窗体的提交事件注册一个处理程序。这样,每次响应页面时,都会调用请求管理器。此时,请求管理器会生成请求主体的副本,并通过当前的HTTP执行器(默认指的是常见的 XMLHttpRequest 对象)运行该副本。 
页面请求管理器设置部分呈现的事件模型,并跟踪正在执行的操作。如果存在任何挂起的操作,则Boolean属性isInAsyncPostBack将返回true。 
当用户单击图1中所示的“取消”按钮时,页面请求管理器将通过其abortPostBack方法中止当前请求。页面请求管理器是一个独立对象,即所有调用都只能传递给一个实例。此情形的原因与部分呈现机制紧密相关。部分呈现由发送页面请求组成,包括在服务器上的整个常规处理过程(呈现阶段除外)。此外,这意味着视图状态将被发送,并用于重新创建服务器控件的上次已知正常状态。返回信息和状态更改事件是定期触发的,视图状态即根据这些操作进行更新。然后,更新的视图状态会与进行了部分修改的标记一起发送回来。 
由于视图状态的关系,需要对来自同一页面的两个异步回发调用进行序列化,并且每次只允许运行一个调用。由于这一原因,页面请求管理器上的abortPostBack方法不必指出要停止哪一请求,因为至多有一个挂起的请求。 
三、深入了解abortPostBack方法 
让我们进一步了解PageRequestManager 类中的 abortPostBack 方法,示例代码如下。 
function Sys$WebForms$PageRequestManager$abortPostBack()  
{ 
    if (!this._processingRequest && this._request)  
    { 
        this._request.get_executor().abort(); 
        this._request = null; 
    } 
} 
如果存在挂起的请求,则管理器将指示中止请求的执行器。执行器是从Sys.Net.WebRequestExecutor 继承的一个JavaScript类,负责发送请求和接收响应。在 Microsoft AJAX 客户端库中,只有一个执行器类(Sys.Net.XMLHttpExecutor 类),它使用XMLHttpRequest对象执行请求。当上述代码调用中止方法时,主要是告知 XMLHttpRequest 对象。从另一个角度来讲,它仅指示执行器用来接收响应数据的套接字必须关闭。 
现在,假设远程任务在服务器上执行破坏性操作。例如,假设为用户提供了一次机会,使其能够通过单击一个按钮来删除数据库表中的少量记录。通过上述过程尝试取消操作实际上不会停止服务器操作。它所能实现的所有功能就是关闭用来接收确认消息的套接字。PageRequestManager对象上的abortPostBack方法仅仅是一个客户端方法,对服务器中运行的操作不会起到任何作用。 
四、设计不间断任务 
要使中止请求对服务器操作有效,任务必须是不间断的。换句话说,任务必须定期检查是否存在来自客户端的指示任务退出的说明。 
当首次实现 PMF时,框架的客户端和服务器元素共享一个通用数据容器,服务器使用该容器写入关于其进度的数据,客户端使用该容器读取此数据,以更新用户界面。要使得服务器代码接收并处理动态客户端反馈(如单击“取消”按钮),需要用到一些增强功能。目前,进程服务器 API 基于以下约定: 
public interface IProgressMonitor 
{ 
    void SetStatus(int taskID, object message); 
    string GetStatus(int taskID); 
    bool ShouldTerminate(int taskID); 
    void RequestTermination(int taskID); 
} 
这里添加了两个新方法:ShouldTerminate 和 RequestTermination。前者返回一个 Boolean 值,表明是否应终止正在执行的任务。RequestTermination 方法为希望结束任务的客户端指示 API 中的入口点。调用此方法时,它会在数据容器(ASP.NET 缓存)中创建一个与任务相关的入口,ShouldTerminate 会检查此入口以确定是否请求了中断。 
上文中定义的 IProgressMonitor 接口指示服务器上某个应用程序的预期行为。可以在可能使用不同数据容器的各种类中实现该接口。笔者使用名为 InMemoryProgressMonitor 的 ASP.NET 缓存创建了一个示例类,核心代码如下: 
public class InMemoryProgressMonitor : IProgressMonitor  
{ 
    public const int MAX_TIME_MINUTES = 5; 
    //从任务调用此方法,它将任务的当前状态写入内部数据存储。该状态以对象的形式表示。 
    public void SetStatus(int taskID, object message) 
    { 
        HttpContext.Current.Cache.Insert( 
            taskID.ToString(), message, null, 
            DateTime.Now.AddMinutes(MAX_TIME_MINUTES),  
            Cache.NoSlidingExpiration); 
    } 
    //从内部数据存储读取指定任务的当前状态,并将其以字符串的形式返回到客户端。 
    public string GetStatus(int taskID) 
    { 
        object o = HttpContext.Current.Cache[taskID.ToString()]; 
        return o == null ? string.Empty : (string)o; 
    } 
    //如果客户端发出了终止指定任务的请求,则返回 true。 
    public bool ShouldTerminate(int taskID) 
            { 
        string taskResponseID = GetSlotForResponse(taskID); 
        return HttpContext.Current.Cache[taskResponseID] != null; 
    } 
    //在内部数据存储中创建与任务相关的入口,以指示客户端发出了终止请求。 
    public void RequestTermination(int taskID) 
    { 
        string taskResponseID = GetSlotForResponse(taskID); 
        HttpContext.Current.Cache.Insert( 
            taskResponseID, (object) false, null, 
            DateTime.Now.AddMinutes(MAX_TIME_MINUTES), 
            Cache.NoSlidingExpiration); 
    } 
    private string GetSlotForResponse(int taskID) 
    { 
        return String.Format(“{0}-Response”, taskID); 
    } 
} 
要支持动态中断,相同的任务将定期调用 ShouldTerminate,以便在客户端请求退出时获得通知。下面的代码显示了可监视的不间断任务的典型结构: 
public static string ExecuteTask(int taskID) 
{ 
    InMemoryProgressMonitor progMonitor = new InMemoryProgressMonitor(); 
   if (progMonitor.ShouldTerminate(taskID)) 
        return “Task aborted--0% done”; 
  
    // 第一步 
    progMonitor.SetStatus(taskID, “0”); 
    DoStep(1); 
    if (progMonitor.ShouldTerminate(taskID)) 
        return “Task aborted--5% done”; 
    // 第二步 
    progMonitor.SetStatus(taskID, “5”); 
    DoStep(2); 
    if (progMonitor.ShouldTerminate(taskID)) 
        return “Task aborted--45% done”; 
    // 第三步 
    progMonitor.SetStatus(taskID, “45”); 
    DoStep(3); 
    if (progMonitor.ShouldTerminate(taskID)) 
        return “Task aborted--69% done”; 
    // 最后一步 
    progMonitor.SetStatus(taskID, “69”); 
    DoStep(4); 
    if (progMonitor.ShouldTerminate(taskID)) 
        return “Task aborted--100% done”; 
    return “Task completed at: “ + DateTime.Now.ToString(); 
} 
上面代码中显示的方法用于协调组成远程任务的各个步骤。该任务可以是应用程序的中间层的一部分,也可以作为工作流实现。它在各步骤间必须是相互关联的,以便客户端插入到其中读取状态和请求终止。 
五、客户端代码 
可以使用页面或Web服务方法(如ExecuteTask方法)启动任务,或在 UpdatePanel区域中运行用于触发远程任务的JavaScript服务器代码。 
<asp:UpdatePanel runat=”server” ID=”UpdatePanel1”> 
    <ContentTemplate> 
        <asp:Button runat=”server” ID=”Button1” Text=”Start Task ...”  
             OnClick=”Button1_Click” /> 
        <hr /> 
        <asp:Label runat=”server” ID=”Label1” /><br /> 
    </ContentTemplate> 
</asp:UpdatePanel> 
在 Button1_Click 事件处理程序中,定义了远程任务,并使其调用进度监视器对象以及SetStatus和 ShouldTerminate方法。要突然终止一个远程任务,需要在进度模板中添加一个“取消”按钮,它可以是 UpdateProgress 控件,也可以是用户定义的一个 <div> 块。此时,“取消”按钮的单击处理程序不指向页面请求管理器中的 abortPostBack 方法,而是指向客户端进度 API 中自己的中止方法。 
<script type=”text/javascript”> 
var progressManager = null; 
var taskID = null; 
function pageLoad() { 
   progressManager = new Samples.PMF2.Progress(); 
} 
function abortTask() { 
    progressManager.abortTask(taskID); 
} 
... 
</script> 
下面让我们来看一下经过修改的客户端进度 API。此 API 在 progress.js 文件中进行编码,因此必须链接到计划使用不间断或可监视任务的每个 ASP.NET AJAX 页面。 
<asp:ScriptManager ID=”ScriptManager1” runat=”server”  
    EnablePageMethods=”true”> 
    <Scripts> 
        <asp:ScriptReference path=”random.js” /> 
        <asp:ScriptReference path=”progress.js” /> 
    </Scripts> 
</asp:ScriptManager> 
random.js 文件与 progress.js 相关,定义了一种可生成随机数量任务的方法。要从客户端跟踪远程任务的状态,需要定期轮询服务器。要停止正在执行的任务,或者更确切地说,要发出一个请求以停止任务,需要调用一个服务器方法,该方法是由进度监视器服务器 API 作为应用程序后端的一部分发布的。 
// 取消操作 
function Samples$PMF2$Progress$abortTask() {  
    PageMethods.TerminateTask(_taskID, null, null, null); 
} 
笔者选择使用页面方法发布此客户端可调用函数。整个解决方案的架构如图2所示。 
  
 
  
  图2  双向进度监视器框架 
用户单击“取消”按钮时,会触发一个带外调用以执行TerminateTask方法,此方法是作为页面的后续代码类上的页面方法定义的。TerminateTask方法在内部数据存储(ASP.NET 缓存)中创建一个与任务相关的入口。此入口是按带有“Quit”后缀的任务 ID 命名的。设计为不间断的任务在执行过程中的各个阶段检查此入口。如果找到了该入口,则服务器任务中止,如图3所示。 
 
 
  
  
图3  用户单击“取消”按钮,结束服务器任务 
通过此方式实现的任务取消将更有效。如果在 UpdatePanel 刷新过程中仅中止客户端回发,所导致的全部结果将是关闭用于接收响应的客户端套接字。对服务器上运行的代码不会产生任何影响,也不存在以编程方式停止对 Web 服务或页面方法的远程调用的内置方法。在这种情形下,JavaScript 代理类完全隐藏了正被用于推送调用的请求对象。虽然请求对象及其执行器具有中止方法,但在服务方法调用的上下文中找不到对它的引用。 
最后,如果需要允许远程任务控制,进度指示器模式是唯一可行的方法。设置并行信道来监视状态,并向正在运行的任务传递更多信息(如退出命令)。这种相同的体系结构允许客户端动态更改参数或请求其他操作。双向进度监视器框架是双工信道,服务器任务及其 JavaScript 客户端可使用该信道交换消息形式的数据。 
六、事务 
至此,已创建了一个框架用以监视和停止 ASP.NET AJAX 任务。关键需要注意的是,该框架只是通知任务用户请求其终止。如果设计正确,任务会立刻停止并返回。但对于已完成的工作会如何处理呢? 
一般情况下,当任务突然中断时,应撤消它所做的所有更改并返回。但进度监视器框架无法实现此功能。不过,如果将远程任务封装在事务中,即可在该任务中断后立即回滚。另一种选择是使用工作流。在这种情形下,将任务封装在TransactionScope活动中,使用 Code 活动设置当前状态并检查是否有终止请求。如果任务必须终止,会引发异常并自动导致事务回滚。并非所有操作都可轻松地自动回滚,一般情况下,可以实现TransactionScope块内部的任务,并安全有效地使用用于实现Transaction界面的所有对象。如果这样做,则所有对象都将相应地回滚或提交。其中每个对象都了解如何撤消其更改。 
底线是从客户端监视远程任务的进度,此操作相对简单,不会产生严重的负面影响。PMF在其上增加了一些好的抽象,并提供了一些现成的编程工具。使任务不间断会引发一些其他问题,当任务具有固有的事务语义时尤其如此。编写代码来只通知任务用户请求其终止是游戏中相当简单的一部分。真正复杂的部分在任务实现及其补偿策略中。 
七、生成进度条 
在本文即将结束时,介绍一下如何使用 JavaScript 轻松生成进度条标记,并使其更易于维护。进度条可以通过构建 HTML 表生成,代码如下: 
<table width=”100%”> 
  <tr> 
    <td>69% done</td> 
  </tr> 
  <tr> 
    <td bgcolor=”blue” width=”69%”> </td> 
    <td width=”31%”></td> 
  </tr> 
</table> 
此表包含两行:附带文本和仪表。仪表使用两单元格的行来呈现,其中的单元格已给定背景色和成比例的宽度。 
仔细查看上述标记,至少能够识别三个参数:面向用户的消息、要显示的值,以及要对“已完成”和“未完成”区域使用的颜色。这样就不再生成字符串形式的标记,创建 JavaScript 类会更简洁。Samples.GaugeBar 类的实现实现方法如下: 
function Samples$GaugeBar$generateMarkup(text, perc) { 
    var builder = new Sys.StringBuilder(“”); 
    builder.append(“<table width=’100%’><tr><td colspan=’2’> ”); 
    builder.append(text); 
    builder.append(“</td></tr><tr><td bgcolor=”); 
    builder.append(this._doneBackColor); 
    builder.append(“ width=’”); 
    builder.append(perc + “%’>”); 
    builder.append(“ </td><td bgcolor=”); 
    builder.append(this._todoBackColor); 
    builder.append(“ width=’”); 
    builder.append(100-perc + “%’>”); 
    builder.append(“</td></tr></table>”); 
    return builder.toString(); 
} 
该方法使用文本和百分比,返回包含两行的 HTML 表。顶行仅显示文本,底行分为两个单元格,分别带有不同的颜色。 
标记字符串是使用JavaScript版本的Microsoft .NET Framework StringBuilder对象构建的。JavaScript StringBuilder 对象是在系统命名空间中定义的,其编程接口类似于.NET Framework 接口。向StringBuilder的内部缓冲区发送文本,然后使用toString方法输出文本。 
Samples.GaugeBar类具有一个generateMarkup方法,以及“已完成”和“未完成”区域的背景色、附带文本的前景色等属性。由于性能方面的原因,此类作为单例来使用。这个类不是很大,但每次需要更新进度条时,仍然不必为其创建新实例。因此,可以为该类定义一个静态实例,并添加一些静态方法和属性: 
Samples.GaugeBar.registerClass(‘Samples.GaugeBar’); 
Samples.GaugeBar._staticInstance = new Samples.GaugeBar(); 
Samples.GaugeBar.generateMarkup = function(text, perc) {  
   return Samples.GaugeBar._staticInstance.generateMarkup(text, perc); 
} 
要更改颜色,请执行以下操作: 
Samples.GaugeBar.set_DoneBackColor(“#ff00ee”); 
Samples.GaugeBar.set_TodoBackColor(“#ffccee”); 
同样,可以通过为表的“已完成”单元格定义开始边框样式,添加美观的 3D 效果,实现代码如下: 
if (this._effect3D) 
    builder.append(“ style=’border:outset white 2px;’”); 
通过创建一个类来公开功能可大大提高 JavaScript 编程的可管理性。Microsoft 客户端AJAX库是一个很大的进步,因为使用此库编写复杂的JavaScript代码会轻松得多。大多数AJAX专业人员可能都同意这一点:要实现强大的 AJAX 编程,必须具备更丰富的 JavaScript功能。 			
				 |