一、触发远程任务
远程任务是在服务器上执行的用于响应客户端事件的一段代码。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功能。
|