ajax 之取消服务器任务[转]
使用 ASP.NET AJAX 取消服务器任务 | |
![Download Image](http://msdn.microsoft.com/msdn-online/shared/graphics/icodownl.gif)
![]() |
在上个月,我构建了一个框架,用以从客户端监视正在执行的服务器端任务。使用此框架(以后称为进度监视器框架,或 PMF),您可以为 Web 用户提供关于服务器上运行的操作的进度信息,此类信息通常需要大量的自定义代码才可获得。使用 PMF,您可以使服务器端任务注册其操作的当前状态(表示为百分比形式,或是对剩余时间的估计),还可以使客户端服务通过 Ping 服务器来读取此状态信息。将状态信息下载到客户端(带外执行)后,更新用户界面将变得非常轻松。
关于上个月的专栏的部分早期反馈中指出了两处可能的改进。第一处指是否能够将 PMF 用于停止正在执行的服务器任务,第二处指寻找一种更好的方法来生成进度条标记。
将远程任务形式化
远程任务是在服务器上执行的用于响应客户端事件的一段代码。ASP.NET AJAX 客户端页面触发远程任务的方法有以下三种:使得回发由 UpdatePanel 控件管理,在通过本地 Web 服务公开的应用程序后端直接调用一种方法,使用页面方法。很快会有第四种方法:一种 Windows® Communication Foundation (WCF) 服务。
一旦触发了服务器上的某项任务,客户端将不再控制该任务。仅当由任务生成的响应已下载到客户端并经过解析后,客户端页面才能够重新控制操作。使用 PMF,您可以动态地读取任务状态,但不存在将数据动态传送给服务器任务的机制。
取消任务的简便方法
使用 ASP.NET AJAX 取消远程服务非常简单,但是存在以下两个限制。首先,该任务必须已通过 UpdatePanel 启动。其次,服务器上不需要任何额外工作来补偿任务的突然中断。图 1 显示了基于 UpdatePanel 的页面示例的源代码,在该页面中,会弹出带有“取消”按钮的进度模板,如图 2 所示。单击该按钮可以取消操作。是否单击?
如图 1 中的 abortTask 函数所示,进度模板包含一个帮定到 JavaScript 代码的客户端按钮。此函数的首要任务是检索页面请求管理器。在 Microsoft® AJAX 客户端库中,PageRequestManager 对象是部分呈现的神经中枢。进行页面初始化时,页面请求管理器会为窗体的提交事件注册一个处理程序。这样,每次回发页面时,都会调用请求管理器。此时,请求管理器会根据浏览器中所示生成请求主体的副本,并通过当前的 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 的双向版本将为您提供帮助。
当我首次实现 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 缓存创建了一个示例类(请参见图 3)。图 4 提供了每个方法的简要说明。正如我在上个月介绍的那样,远程任务重复调用 SetStatus,以跟踪其当前执行状态并标记其进度。要支持动态中断,相同的任务将定期调用 ShouldTerminate,以便在客户端请求退出时获得通知。图 5 显示了可监视的不间断任务的典型结构。
Figure 5 中显示的方法用于协调组成远程任务的各个步骤。该任务可以是应用程序的中间层的一部分,可以作为工作流实现。它在各步骤间必须是相互关联的,以便客户端插入到其中读取状态和请求终止。
客户端代码
用于触发远程任务的客户端 JavaScript 代码原样保留了上个月中的大部分内容。您可以使用页面或 Web 服务方法(如图 5 中的 ExecuteTask 方法)启动任务,或在 UpdatePanel 区域中运行服务器代码:
<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 作为应用程序后端的一部分发布的:
// Cancel the operation function Samples$PMF2$Progress$abortTask() { PageMethods.TerminateTask(_taskID, null, null, null); }
我选择使用页面方法发布此客户端可调用函数。图 6 提供了整个解决方案的架构视图。
用户单击“取消”按钮时,会触发一个带外调用以执行 TerminateTask 方法,此方法是作为页面的后续代码类上的页面方法定义的。TerminateTask 方法在内部数据存储(ASP.NET 缓存)中创建一个与任务相关的入口。此入口是按带有“Quit”后缀的任务 ID 命名的。设计为不间断的任务在执行过程中的各个阶段检查此入口。如果找到了该入口,则服务器任务中止(请参见图 7)。
通过此方式实现的任务取消将更有效。如果在 UpdatePanel 刷新过程中仅中止客户端回发,所导致的全部结果将是关闭用于接收响应的客户端套接字。对服务器上运行的代码不会产生任何影响。也不存在以编程方式停止对 Web 服务或页面方法的远程调用的内置方法。在这种情形下,JavaScript 代理类完全隐藏了正被用于推送调用的请求对象。虽然请求对象及其执行器具有中止方法,但在服务方法调用的上下文中找不到对它的引用。
最后,如果您需要允许远程任务控制,进度指示器模式是唯一可行的方法。您设置并行信道来监视状态,并向正在运行的任务传递更多信息(如退出命令)。这种相同的体系结构允许客户端动态更改参数或请求其他操作。双向进度监视器框架是双工信道,服务器任务及其 JavaScript 客户端可使用该信道交换消息形式的数据。
事务
至此,我已创建了一个框架用以监视和停止 ASP.NET AJAX 任务。关键需要注意的是,该框架只是通知任务用户请求其终止。如果设计正确,任务会立刻停止并返回。但对于已完成的工作会如何处理呢?
一般情况下,当任务突然中断时,应撤消它所做的所有更改并返回。但进度监视器框架无法实现此功能。不过,如果您将远程任务封装在事务中,即可在该任务中断后立即回滚它(假设已完成的任务仅涉及事务性资源,如 SQL Server™ 数据库)。另一种选择是使用工作流。在该情形下,您将任务封装在 TransactionScope 活动中,使用 Code 活动设置当前状态并检查是否有终止请求。如果任务必须终止,会引发异常并自动导致事务回滚(请参见 2007 年 6 月的“领先技术”专栏,获取有关 Windows Workflow Foundation 中事务性任务的更多信息)。
遗憾的是,并非所有操作都可轻松地自动回滚。一般情况下,您可以实现 TransactionScope 块内部的任务,并安全有效地使用用于实现 ITransaction 界面的所有对象。如果您这样做,则所有对象都将相应地回滚或提交。其中每个对象都了解如何撤消其更改。
底线是从客户端监视远程任务的进度,此操作相对简单,不会产生严重的负面影响。PMF 在其上增加了一些好的抽象,并提供了一些现成的编程工具。使任务不间断会引发一些其他问题,当任务具有固有的事务语义时尤其如此。编写代码来只通知任务用户请求其终止是游戏中最简单的一部分。真正复杂的部分在任务实现及其补偿策略中。
生成进度条
在本文即将结束时,让我们来了解一下如何使用 JavaScript 轻松生成进度条标记,并使其更易于维护。图 7 中显示的进度条是通过构建 HTML 表生成的,如下所示:
<table width=”100%”> <tr> <td>69% done</td> </tr> <tr> <td bgcolor=”blue” width=”69%”> </td> <td width=”31%”></td> </tr> </table>
此表包含两行:附带文本和仪表。仪表使用两单元格的行来呈现,其中的单元格已给定背景色和成比例的宽度。
仔细查看上述标记,您至少能够识别三个参数:面向用户的消息,要显示的值,以及要对“已完成”和“未完成”区域使用的颜色。这样就不再生成字符串形式的标记,创建 JavaScript 类岂不更简洁?图 8 中的代码显示了 Samples.GaugeBar 类的主要方法。
该方法使用文本和百分比,返回包含两行的 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 编程的可管理性。如果您在几年前曾经不得不处理动态 HTML 行为,就会理解我的意思。Microsoft 客户端 AJAX 库是一个很大的进步,因为使用此库编写复杂的 JavaScript 代码会轻松得多。大多数 AJAX 专业人员可能都同意这一点:要实现强大的 AJAX 编程,必须具备更丰富的 JavaScript 功能。
将您向 Dino 提出的问题和意见发送至: cutting@microsoft.com.
![]() |