本文讨论:
| 本文使用了以下技术: ASP.NET AJAX |
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
当今,网站的使用者可以像发布者那样,对网站的内容、发展方向及其成功承担责任。每天都会涌现出许多样式新颖的网站,如社交站点、博客、在线相册集和 Wiki,这只是其中的一部分,而这种趋势才刚刚开始。虽然您的网站可以提供最好的内容,但是,如果不能让用户参与网站的发展与成长,您的网站很快就会毫 无用武之地。
作 为一名开发人员,您的任务是使用便利的工具来满足普通使用者的需求。要使一个站点更加引人注目,从 Web 浏览器本身入手是再自然不过的了,因为它是站点中最贴近使用者的部分。遗憾的是,当前 Web 浏览器中的许多功能并不总是那么容易利用。编写与浏览器交互的代码必须付出巨大的艰辛,因为存在着不计其数的浏览器与操作系统组合,缺乏一致性的问题令人 伤透脑筋。若能采用单一平台并行地构建富客户端和 Web 应用程序,那将会十分有益,这样一来,针对某一浏览器环境而完善最终用户体验所付出的劳动就不需要针对另一浏览器环境而重复进行。
Microsoft 发布的 ASP.NET AJAX 可以满足这种 Web 应用程序开发中的需求。本文旨在扩展您对 ASP.NET AJAX 的中心组件(名为 ScriptManager 控件)的认识,并展示如何使用它实现 ASP.NET AJAX 高级编程。ScriptManager 是放置在 Web 窗体上的服务器端控件,在 ASP.NET AJAX 中发挥核心作用。其主要任务是调解 Web 窗体上的所有其他 ASP.NET AJAX 控件,并将适当的脚本库添加到 Web 浏览器中,从而使 ASP.NET AJAX 的客户端部分能够正常工作。您经常会发现自己使用 ScriptManager 注册其他控件、Web 服务和客户端脚本。
作 为服务器端的控件,ScriptManager 回应 ASP.NET 页面生命周期中的事件,利用这些事件协调 ASP.NET AJAX 使用的所有控件、选项和代码的活动。ScriptManager 将关联某一特定事件,当事件发生时获得通知,并根据环境配置多个设置;此过程将通过 ASP.NET 页面的呈现循环多次重复进行。不过,它所配置的设置往往正是您无缝使用 ASP.NET AJAX 所需的设置。
首 先,我们将了解 ScriptManager 控件可帮助您实现的 ASP.NET AJAX 的主要功能,然后开始探讨该控件在服务器上的生命周期。通过了解 ScriptManager 的内部结构,您会对该控件为 Web 应用程序开发提供的选项有更深入的认识,并了解如何从中获得最大好处。
让 我们从脚本开始,因为它是 ASP.NET AJAX 的中心元素。实际上,ASP.NET AJAX 的所有功能均依赖其脚本库。我们随后将浏览 ASP.NET AJAX 中对 AJAX 支持的某些功能,与 Web 服务的交互方式,最后谈一谈有关身份验证的问题。在对各个问题的讨论过程中,还将向您展示如何通过 ScriptManager 对选项进行调整。
使用 ScriptManager 编制脚本
图 1 中的代码块演示了在 ASP.NET AJAX 中定义类的标准方法。客户端脚本库的内部结构不在本文讨论范围内,但总体来说,创建一个基于 ASP.NET AJAX 脚本扩展的类,其常见的必要步骤如下:
- 向 ASP.NET AJAX 注册命名空间。
- 创建一个构造函数 方法。
- 通过填写成员方法 及其功能,创建类的原型。
- 向 ASP.NET AJAX 注册这个类。
- 向通过 ScriptManager 添加的客户端脚本发出通知,已经到达类型定义的结尾(调用 Sys.Application.notifyScriptLoaded)。
该 类只向客户端公开功能。不过,通过 ScriptManager 控件,您可以利用 ASP.NET AJAX 中制作脚本更为有趣的一面,其中,您的类可以同时向客户端的 JavaScript 和服务器上的 Microsoft® .NET Framework 代码公开功能。例如,如果您的控件的使用者对控件的一个实例设置了如下属性
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
this.MyCustomButton.AlertMessage = "Hey, stop clicking me!";
}
}
通 过在浏览器中使用如下脚本,还可以直观地与该控件实例进行交互:
<script type="text/javascript">
function get_text()
{
var mb = $find("MyCustomButton");
var text = mb.get_AlertMessage();
// do something with the text
}
</script>
请 注意,我使用同一个名称 MyCustomButton 来引用服务器端和客户端的控件;我还与属性 AlertMessage 进行交互,就如同该值无缝地跨越服务器/客户端的边界。这让人感到十分自然,但在 ASP.NET AJAX 之前,这种统一的服务器/客户端编程模型很难使用,而且要求使用大量的自定义代码。如今,在 ASP.NET AJAX 中,已集成并完全支持这种统一的服务器/客户端模式。
这 种服务器/客户端紧密集成的奇妙功能源于两个新接口,即 IScriptControl 和 IExtenderControl。要使用这些接口,首先通过继承的方式定义您的 ASP.NET Web 控件,实现所需的接口方法,然后通过页面的 ScriptManager 控件来注册控件。例如,下面的服务器端控件代码框架实现了 IScriptControl 所需的方法:
class CustomButton : Button, IScriptControl
{
IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
{
...
}
IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
{
...
}
}
GetScriptDescriptors 和 GetScriptReferences 方法将向 ScriptManager 控件返回所有必需的信息,以便从逻辑上将您的控件代码表示为服务器和客户端对象。最终结果就如同您之前看到的那样,对象实例将跨越服务器/客户端边界。
GetScriptReferences 将返回您的控件代码所需的脚本文件列表。在它必须返回的一个脚本文件中包含将您的控件定义为脚本类(即服务器端控件的客户端脚本版本)的内容。
另 一方面,GetScriptDescriptors 返回称为 ScriptDescriptors 的信息或描述客户端类的属性和事件的对象。可将 ScriptDescriptors 看作持有客户端类的元数据,其中包括属性及其关联值。
图 2 显示了前面概述的服务器控件的更为完整的示例。在图中,您可以看到已填写完的 GetScriptDescriptors 和 GetScriptReferences 方法的主体。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
private string _alertMessage = null;
public string AlertMessage
{
get { return _alertMessage; } set { alertMessage = value; }
}
public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
"CustomButton", this.ClientID);
descriptor.AddProperty("AlertMessage", this._alertMessage);
return new ScriptDescriptor[] { descriptor };
}
public IEnumerable<ScriptReference> GetScriptReferences()
{
ScriptReference reference = new ScriptReference(
"MyCustomContent.JScript1.js", "MyCustomContent");
return new ScriptReference[] { reference };
}
在 GetScriptDescriptors 的主体中,我实例化了一个 ScriptDescriptor 对象。ScriptDescriptor 的构造函数使用了我当前所述的 CustomButton 控件的名称。我随后向 ScriptDescriptor 中添加了 AlertMessage 属性和它的值(由私有成员 _alertMessage 持有)。现在,我希望从客户端代码中与之交互的属性 AlertMessage 已经在 ASP.NET AJAX 中得到了描述。
在 GetScriptReferences 的主体中,需要注意的是我要创建并返回一个指向 .js 文件的 ScriptReference 对象。此 .js 文件保存着该控件的客户端代码,因而保存着此控件的所有客户端功能。
现 在,我可以使用 ScriptManager 注册此控件,使之意识到此控件的存在(稍候我们再探讨这一步骤)。当呈现该页面时,ScriptManager 控件将对它得到通知的各种事件做出回应,并调用 GetScriptDescriptors 和 GetScriptReferences。随后,这两个方法会向 ScriptManager 控件返回包含所有必需信息的 ScriptDescriptors 和 ScriptReferences,以便将客户端对象与相应的服务器对象关联起来。本例中,ScriptDescriptor 描述了一个名为 CustomButton 的自定义控件,该控件具有 AlertMessage 属性。ScriptDescriptor 中还包含在 GetScriptDescriptors 中设置的属性的值。
ScriptManager 控件还将获得一个对名为 MyCustomContent.JSScript1.js 的外部 .js 文件的引用。对脚本文件的引用将采取从 GetScriptReferences 返回的 ScriptReference 对象的形式。对 .js 文件中控件的客户端类的原型和脚注的定义如
图 3 所示。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
CustomButton.prototype = {
initialize : function() {
// initialize the base
CustomButton.callBaseMethod(this,'initialize');
this._onclick =
Function.createDelegate(this, this._onclick);
Sys.UI.DomEvent.addHandler(
this.get_element(), 'click', this._onclick);
},
dispose : function() {
// release the handlers
$clearHandlers(this.get_element());
// call to the base to do its dispose
CustomButton.callBaseMethod(this,'dispose');
},
get_AlertMessage: function(){
return this._alertMessage;
},
set_AlertMessage: function(value){
this._alertMessage = value;
return;
},
_onclick: function(e){
alert(this._alertMessage);
return;
}
}
CustomButton.registerClass("CustomButton",Sys.UI.Control);
if(typeof(Sys)!=='undefined') Sys.Application.notifyScriptLoaded();
首 先应该注意的是,此客户端类对于 AlertMessage 属性具有 get_ 和 set_ 方法。ScriptManager 控件可将该属性值设置为您在服务器上通过 ScriptDescriptor 对象指定的任何值。因为我在服务器代码中公开了 AlertMessage,那么,无论该属性在服务器上设为何值,现在都会在客户端对象所公开的 AlertMessage 属性中得到反映。
AJAX 和 ScriptManager
许 多开发人员在首次使用 ASP.NET AJAX 时都会从 UpdatePanel 控件开始。如果该页面中包含一个 ScriptManager 控件,并且 UpdatePanel 包含了任何控件,那么,通过 AJAX 提供的便利功能,可对 UpdatePanel 中的控件进行异步更新。在 Visual Studio® 的设计器图面上,设置通常与
图 4 类似。
![](http://i.msdn.microsoft.com/cc163354.fig04%28zh-cn%29.gif)
图 4
一个简单的异步控件
这 确实是最基本的了。如果有人在该设置中单击“Button”,则 Button 控件将引发一个回发事件,该事件将由 UpdatePanel 控件捕获。然后,UpdatePanel 会将该回发事件作为一个部分回发而重新提交,其内容将得到异步更新(浏览器无需完全重新加载页面)。
然 而,可能会出现许多有趣的情形,您的期望将会落空,并给您留下一片混乱。例如,使用 UpdatePanel 控件和 AJAX 会造成一种分离脚本引用的离奇的新方式。在过去,由于出现完全回发,您可能向页面中添加了如下脚本:
protected void Button1_Click(object sender, EventArgs e)
{
Page.ClientScript.RegisterStartupScript(
this.GetType(),"myscript","alert('hello world!');");
}
但 通过 ASP.NET AJAX,此新方法却可以分离脚本引用。这是为什么?因为 ClientScriptManager 是不会响应部分回发和部分呈现的控件,因此,用它注册的脚本代码不会包含在部分回发的页面响应中。
相 反,如果您希望该按钮将客户端脚本引入到页面输出中,则可以运用 ScriptManager:
protected void Button1_Click(object sender, EventArgs e)
{
ScriptManager.RegisterStartupScript(
this,this.GetType(),"myscript","alert('hello world!');",true);
}
结 果是相同的,但方法略有差别。实际上,ClientScriptManager 控件的方法现在已作为静态方法(如 RegisterStartupScript)包含在 ScriptManager 控件中。现在,每当您利用 ASP.NET AJAX 并想通过部分回发使用脚本时,都必须改用 ScriptManager 控件公开的方法。
作 为另一种高级方案,请考虑
图 5 中的控件设置。在正常情况下,一旦单击 LinkButton,就会引起 Web 窗体的完全回发。但是,如何才能做到单击 LinkButton 而使它异步刷新 UpdatePanel?换句话说,您希望单击 LinkButton 而使 UpdatePanel 表现得如同 LinkButton 在其内部一样。要做到这一点,请使用 ScriptManager 控件上名为 RegisterAsyncPostBackControl 的方法:
![](http://i.msdn.microsoft.com/cc163354.fig05%28zh-cn%29.gif)
图 5
异步使用 LinkButton
protected void Page_Load(object sender, EventArgs e)
{
// register the LinkButton1 control as one that can
// cause partial postbacks.
ScriptManager1.RegisterAsyncPostBackControl(LinkButton1);
}
现 在,单击 LinkButton 将引起 UpdatePanel 异步刷新,就好像 LinkButton 控件真的在 UpdatePanel 中一样。
作 为一个相反行为的例子,您也可使一个 UpdatePanel 中的元素引起整个页面刷新,即完全回发。要做到这一点,只需使用 ScriptManager 上另一个名为 RegisterPostBackControl 的方法:
protected void Page_Load(object sender, EventArgs e)
{
// Button1 will cause a full post-back no matter
// where it is on the page.
ScriptManager1.RegisterPostBackControl(Button1);
}
此 代码将引起 Button1 控件对页面执行完全回发,即使 Button1 位于 UpdatePanel 的内部。
现 在让我们进一步深入讨论。您已经了解如何使用 ScriptManager 调整触发 UpdatePanel 控件的部分回发事件的控件,但是,当 UpdatePanel 刷新时,如何才能对页面上某一位置的控件(完全在 UpdatePanel 之外)进行更新?这同样可以通过 ScriptManager 控件实现。
通 过 ScriptManager 的 RegisterDataItem 方法,可以很轻松地刷新 UpdatePanel 之外的控件或数据。RegisterDataItem 允许在 UpdatePanel 控件回发时将您选择的额外数据发送到客户端;该数据可供您对客户端编写的脚本使用。
例 如,您遇到一种与
图 6 中的控件类似的情形。在这个例子中,我想用 Calendar 控件中被单击的任何值更新 Label。这看起来很简单,但 Calendar 控件在 UpdatePanel 内部,而 Label 不在 UpdatePanel 内部。如何才能让 UpdatePanel 刷新该 Label?答案很简单:如果我在服务器端代码中使用 RegisterDataItem,则可以将额外数据发送到我的客户端代码。客户端可以使用从 RegisterDataItem 发送的数据,并用它刷新 Label:
![](http://i.msdn.microsoft.com/cc163354.fig06%28zh-cn%29.gif)
图 6
更新 UpdatePanel 之外的控件
protected void Calendar1_SelectionChanged(object sender, EventArgs e)
{
ScriptManager1.RegisterDataItem(
Label1, Calendar1.SelectedDate.ToShortDateString());
}
RegisterDataItem 将您打算更新的控件作为第一个参数,将您希望用来更新控件的原始数据作为第二个参数。ScriptManager 控件接受您传递的数据,将其打包,然后将它作为对部分回发事件的响应的一部分而发送给客户端。在客户端代码中,您可以在事件完成后检索从 ScriptManager 控件发送的数据,如下所示:
<script type="text/javascript">
Sys.WebForms.PageRequestManager.getInstance().add_pageLoading(
PageLoadingHandler);
function PageLoadingHandler(sender,args){
var dataItems = args.get_dataItems();
if($get('Label1')!==null){
$get('Label1').innerHTML = dataItems['Label1'];
}
return;
}
</script>
请 看一下脚本代码。该脚本可完成多项工作。首先,它通过 PageRequestManager 客户端类注册了 pageLoading 事件。接着,它为 pageLoading 事件实现了事件处理程序 PageLoadingHandler。它从第二个参数 args 中获取了数据项的名称/值集合。最后,它使用您为服务器上的 RegisterDataItem 提供的用作第一个参数的控件名称检索所需的值。
Web 服务和 ScriptManager
现 在,您可以轻松地从 ASP.NET AJAX 中的脚本以异步方式与 Web 服务进行交互,处理响应(包括错误),从而获得强大的能力以使您的页面真正达到用途广泛、出类拔萃。从点到点(浏览器到服务器),通过 ASP.NET AJAX,以一种最直观的方式利用当前最新的 Web 技术。
对 于 ASP.NET AJAX 应用程序中所需的各种服务,ScriptManager 控件起到全局注册器的作用。
图 7 显示了通过 Visual Studio 所见的 ScriptManager 控件的属性菜单。
![](http://i.msdn.microsoft.com/cc163354.fig07%28zh-cn%29.gif)
图 7
ScriptManager 属性
您 可以看到,我突出显示了 Scripts 集合,在其下方还有一个 Services(Web 服务)集合。您需要从您的客户端代码中用 ScriptManager 注册任何想要与之交互的服务。要向 ScriptManager 控件中添加服务引用,只需展开 Services 集合并添加引用,如
图 8 所示。
![](http://i.msdn.microsoft.com/cc163354.fig08%28zh-cn%29.gif)
图 8
添加 Web 服务引用 (单击该图像获 得较大视图)
这 样做到底做有什么作用?稍后我会进行详细解释,不过,如果您希望通过 ScriptManager 查看引用某个服务的页面的源文件,可能会发现其内容中含有与以下类似的代码:
<script src="WebService.asmx/jsdebug" type="text/javascript"></script>
此 外部引用由 ScriptManager 添加到页面输出,因为 Web 服务已向其注册。请注意,Web 服务的名称后面附加了 /jsdebug;如果这是一个发布版,就会只在名称后面附加 /js。当 ASP.NET AJAX 管道发现对此服务的请求,且请求后面附带 /js 时,就会返回一个包装 Web 服务方法的脚本类(称为代理脚本)。该代理脚本类的每个方法的主体都将执行异步调用相应的 Web 服务方法。因此,要调用 Web 服务方法,只需调用 ASP.NET AJAX Web 服务框架为您构建的脚本类上的相应方法即可。就是如此简单。
例 如,一个名为 WebService 的服务公开了一个名为 Add 的方法,如下所示
[WebMethod]
public int Add(int a, int b) { return a + b; }
可 以从此脚本中调用该方法:
function CallAdd()
{
// method will return immediately
// processing done asynchronously
WebService.Add(0,6, OnMethodSucceeded, OnMethodFailed);
}
在 本例中,由框架送交给您的自动生成的脚本代理类包装了 Web 服务方法调用,这样您便可以从脚本中与之交互。因此,WebService.Add 会调用相应的 Web 服务方法 Add。
在 调用某个 Web 服务时,通常定义两个回调:一个用于成功的情况,另一个用于失败的情况。由于 Web 服务调用是异步执行的,所以必须通过回调来了解调用的实际完成情况。例如,下面的方法分别实现成功回调和失败回调:
function OnMethodSucceeded(result, eventArgs)
{
var label = Sys.UI.DomElement.getElementById('Label1');
var sb = new Sys.StringBuilder();
sb.append('You got back a value of ');
sb.append(result.toString());
label.innerHTML = sb.toString();
}
function OnMethodFailed(error)
{
alert(error.get_message());
}
从 Web 服务中以异步方式发送请求和接收结果,因此最终结果与使用 UpdatePanels 控件类似。使用从 Web 服务接收的数据即可对内容进行刷新,无需完全的浏览器回发。在前面所示的示例代码中,我通过 Web 服务调用 Web 方法 Add 的结果对 Label1 进行了异步更新。最终结果可能如
图 9 所示。不会发生任何完全回发,最终结果绝对是无缝的。
![](http://i.msdn.microsoft.com/cc163354.fig09%28zh-cn%29.gif)
图 9
更新后的 Label
身份验证和个性化
ASP.NET AJAX 的高级功能(比如身份验证和个性化)同样使用 Web 服务。ASP.NET AJAX 中的身份验证服务实现了两种方法:一种用于用户登录,另一种用于用户注销:
Sys.Services.AuthenticationService.login
Sys.Services.AuthenticationService.logout
ASP.NET AJAX 使用表单身份验证,这就是说,当一个有效的表单身份验证 cookie 被 Web 服务器插入到浏览器的会话中时,用户就算登录了。ASP.NET AJAX 中 cookie 数据插入到会话中的方式与通过完全回发所用的方式相同,因此,实际上它们所采用的机制没有任何区别。一旦将 cookie 插入到浏览器的会话中,客户端就通过了服务器的身份验证,从而可以查看受限的页面和内容。
能 够从客户端脚本对特定用户进行身份验证并完成登录或注销,这对基于交互性很高的用户的系统来说,产生的结果可能会非常有意义。通过 AuthenticationService 和脚本实现用户登录的例子如下所示:
<script type="text/javascript">
function MyMethod(username, password)
{
Sys.Services.AuthenticationService.login(username,
password,false,null,null,null,null,"User Context");
}
</script>
在 AJAX 应用程序中启用身份验证的步骤具有完备的文档可查(参见 ajax.asp.net),所以,此处无需赘述。不过,您可以清楚地看到,身份验证一经启用,就可通过脚本轻松地实现表单身份验证。
如 果标准的 ASP.NET 身份验证服务不符合需求,您可以自行创建服务并通过 ScriptManager 控件进行注册。Web 服务所需的实现身份验证功能的方法如下所示:
[WebMethod]
public bool Login(string userName,
string password, bool createPersistentCookie)
{
... // code to check user credentials and log user in
}
[WebMethod]
public void Logout()
{
... // code to log user out.
}
在 填写完您的实现代码之后,必须使用 ScriptManager 控件注册您的新身份验证服务。在注册了新身份验证服务之后,您以前在客户端使用的与 ASP.NET AJAX 默认身份验证服务进行交互的任何代码现在都将改用您的服务。因此,在客户端脚本这边,无需做任何更改就可使用您自行定义的身份验证 Web 服务。
以 下是通过 ScriptManager 以声明的方式注册一个身份验证服务的示例:
<asp:ScriptManager ID="ScriptManager1"
runat="server" >
<AuthenticationService
Path="WebService.asmx" />
</asp:ScriptManager>
此 时,您还可以在 ASP.NET AJAX 中利用您自己的配置文件服务,而无需具体实现该服务。配置文件服务可通过 ScriptManager 控件以相同方式注册为一个身份验证服务。配置文件服务使您能够针对特定的用户定制一个网站。由于这种定制是通过客户端脚本完成的,因此用户看到的结果将是 直观和无缝的。通过 ajax.asp.net 可获得多个示例。
工作原理
ScriptManager 控件的存在有两个基本阶段。在第一个阶段,它确认环境可以支持 ASP.NET AJAX 的所有丰富的功能,并为支持这些功能完成所有必要的工作。在第二个阶段,它通过客户端上运行的代码真正执行异步通信,从而使脚本可以完成必要的页面更新。 因为它是服务器端控件,而 ASP.NET 中的 Web 编程是事件驱动的,所以,ScriptManager 控件的核心在于它如何注册和响应事件。
图 10 显示了 ScriptManager 控件响应的事件的概述。
![](http://i.msdn.microsoft.com/cc163354.fig10%28zh-cn%29.gif)
图 10
在 ASP.NET AJAX 中定义类
使用 ScriptManager 注册对象
可 以使用 ScriptManager 以声明的方式添加脚本和服务引用。不过,您也可以通过 ScriptManager 控件上的 Services 和 Scripts 属性以编程方式添加它们。您还可以选择要发送到客户端的特定数据,而且可以设计控件,使其作为特殊的脚本控件与 ScriptManager 配合使用。所有这些功能均要求您使用 ScriptManager 控件引用您的对象。
那 么 ScriptManager 如何处理所有这些引用呢?当浏览器首次请求一个承载 ScriptManager 控件和 ASP.NET AJAX 功能的页面时,页面子控件的初始化阶段将会调用 ScriptManager 的 OnInit 方法。OnInit 完成若干重要步骤,包括检查是否只有一个 ScriptManager 控件驻留在页面上,以及页面当前是否正在进行部分回发。不过,OnInit 执行的最重要的步骤是注册一系列由宿主页面生成的事件,如 InitComplete、PreRenderComplete 和 PreRender。ScriptManager 控件需要知道这些事件在其父页面上的发生时间,因为 ASP.NET AJAX 通过利用这些页面事件而进行工作。
将 脚本和服务引用加载到浏览器中的最重要的页面事件是页面的 PreRenderComplete 事件。该事件的处理程序叫做 OnPagePreRenderComplete(参见
图 11),它是 ScriptManager 控件本身的一个方法。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
private void OnPagePreRenderComplete(object sender, EventArgs e)
{
if (!IsInAsyncPostBack)
{
if (SupportsPartialRendering)
{
IPage.ClientScript.GetPostBackEventReference(
new PostBackOptions(this, null, null, false,
false, false, false, true, null));
}
RegisterGlobalizationScriptBlock();
RegisterScripts();
RegisterServices();
}
else RegisterScripts();
}
OnPagePreRenderComplete 负责处理通过 ScriptManager 控件注册到页面的 PreRenderComplete 事件的所有脚本和服务(包括脚本控件所需的脚本、ScriptManagerProxy 控件引用的脚本等)。如果页面没有进行部分回发,则会在注册所有脚本和服务的同时注册一个全局化脚本块。另一方面,如果进行部分回发,则只注册脚本。需要 为部分回发和完全回发注册脚本,因为 ScriptManager 控件提供随时包含脚本的功能。
您 的所有脚本和服务都在这个阶段注册,但这意味着什么?脚本或引用的注册如何转化为页面上的某种输出?
请 记住,您仍在处理第一个页面请求,所以,您并未处理部分回发。正因为如此,您仍可以使用 ClientScriptManager 来注册脚本。例如,RegisterScripts 遍历每个使用 ScriptManager 控件注册的脚本,然后将其转交给页面的默认 ClientScriptManager 实例。当呈现页面时,将会调用 ClientScriptManager,脚本引用将被添加到页面输出,就像 ASP.NET AJAX 未出现之前的处理方式一样。
在 异步回发过程中还会调用 RegisterScripts(由于
图 11 中所示的 else 子句)。该方法仍然会调用 ClientScriptManager,但是,因为 ClientScriptManager 不知道如何处理异步回发,不会发生任何操作。RegisterScripts 会转而调用内部方法 RegisterScriptIncludeInternal,该方法会将脚本引用收藏在一个内部数组中,供以后使用。
如 何处理 Web 服务引用?我们知道,通过将 Web 服务引用添加到 ScriptManager 控件,页面中就会添加对脚本的引用。该脚本引用会导致浏览器发出一个对特定 URL 的请求。ASP.NET AJAX 框架将检查实现 Web 服务的类,并返回一个可在客户端代码中使用的代理脚本类。浏览器会下载这个自动生成的代理类,这将成为您的 Web 服务引用。
为 了将脚本引用自动生成到代理类,RegisterScripts 将遍历您拥有的每个 Web 服务引用,最后调用下列方法:
private string GetProxyPath(Control containingControl, bool debug)
{
if (debug)
return GetServicePath(containingControl, true) +
RestHandlerFactory.ClientDebugProxyRequestPathInfo;
else
return GetServicePath(containingControl, true) +
RestHandlerFactory.ClientProxyRequestPathInfo;
}
由 GetProxyPath 生成的脚本引用将按照与常规脚本引用相同的方式添加到页面中:通过利用 ClientScriptManager 处理第一次页面请求以及将引用“收藏”到某个内部数组中。
这 个内部数组是什么?实际上它是多个数组。回想一下,对于第一个页面请求,ScriptManager 依赖传统的方法,只是将脚本和服务引用添加到页面的 ClientScriptManager 类。但在部分回发过程中,使用 ClientScriptManager 不会起作用。ScriptManager 必须构造其他的替代方法,这就是使用内部数组的缘由。您将会看到,对部分回发的响应实际上是格式良好、易于解析的数据块,客户端框架将对其进行检查和使 用。ScriptManager 控件的 Render 事件将会被调用,接着,该事件会调用另一个类 PageRequestManager 的成员方法 ProcessScriptRegistration(参见
图 12)。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
private void ProcessScriptRegistration(HtmlTextWriter writer)
{
owner.ScriptRegistration.RenderActiveArrayDeclarations(
updatePanelsToRefresh, writer);
owner.ScriptRegistration.RenderActiveScripts(
updatePanelsToRefresh, writer);
owner.ScriptRegistration.RenderActiveSubmitStatements(
updatePanelsToRefresh, writer);
owner.ScriptRegistration.RenderActiveExpandos(
updatePanelsToRefresh, writer);
owner.ScriptRegistration.RenderActiveHiddenFields(
updatePanelsToRefresh, writer);
owner.ScriptRegistration.RenderActiveScriptDisposes(
updatePanelsToRefresh, writer);
}
在 这个位置上,脚本引用将转化为 ASP.NET AJAX 的页面输出。每个方法都将接受传递给它的 HtmlTextWriter 以供 Render 阶段使用,并写入必要的内容。每个方法(比如 RenderActiveScripts)都将分别引用以前填入的各个内部数组。
将 AJAX 置入 ASP.NET AJAX
如 果您使用 Fiddler HTTP 调试器代理程序 (fiddlertool.com/fiddler),就可以跟踪所有流经您计算机上 Internet Explorer® 的 Web 通信。浏览 ajax.asp.net 上的 AJAX 示例,通过 Fiddler 查看究竟发生了什么;您可以看到,每次调用一个部分回发事件时都会发生一次真正的 HTTP POST。HTTP POST 包含常规的 HTTP 头,而且还包含您可能以前从未见过的标头:
x-microsoftajax: Delta=true
此 标头十分关键。ScriptManager 控件一旦识别此标头,它不会被动地将引用注入到页面输出,而是检查表单 POST 中发送的数据,然后以客户端脚本能够理解的格式将响应呈送给客户端。
除 了将服务和脚本引用写入到最初的页面输出之外,ScriptManager 控件还将初始化客户端功能。在第一次呈现时,ScriptManager 控件要确保两个启动脚本通过 ClientScriptManager 完成注册。一个脚本调用一个对客户端运行时进行初始化的方法。另一个脚本对前面讨论的 PageRequestManager 类客户端版本进行初始化。出于对 AJAX 的了解,第二个初始化脚本(即初始化 PageRequestManager 的脚本)最为重要。
该 客户端脚本通过一个叫做 RenderPageRequestManagerScript 的方法写入到页面输出中。我删除了该方法中的许多非关键代码,使其更易于阅读,但一般来说它类似于以下代码:
internal void RenderPageRequestManagerScript(HtmlTextWriter writer)
{
...
writer.Write(_owner.UniqueID);
...
writer.Write(_owner.IPage.Form.ClientID);
...
RenderUpdatePanelIDsFromList(writer, _allUpdatePanels);
...
writer.Write(GetAsyncPostBackControlIDs(true));
...
writer.Write(GetPostBackControlIDs(true));
...
}
请 留意 UpdatePanel、PostBackControl ID 和 AsyncPostBackControl ID 是如何写到页面中的;这些控件都是体验 ASP.NET AJAX 所需的控件。客户端 PageRequestManager 负责跟踪所有注册到 ScriptManager 的控件生成的事件;RenderPageRequestManagerScript 方法用它应该查看的确切控件对客户端上运行的 PageRequestManager 进行初始化。
当 客户端引发一个回发事件时,PageRequestManager 将确认该事件是否由 RenderPageRequestManagerScript 中编写的脚本所指明的任何控件所引起。如果是,PageRequestManager 就会取消该回发事件,然后重新打包。随后,来自回发事件的新打包的数据将使用客户端类 Sys.Net.WebRequest(这是公用类并可从您的客户端代码中使用)传输到服务器。标头 x-microsoftajax:通过 Sys.Net.WebRequest 发送到服务器的 POST 请求中将会设置 Delta=true。
在 服务器端,ScriptManager 控件现在已被实例化并被载入页面的控件树。ScriptManager 是一个服务器端控件,所以会通过 ASP.NET 的 LoadPostData 方法知悉表单 POST 中的数据,该方法允许各控件对表单 POST 进行过滤,以获得相关的信息。(请注意,这是一个标准事件,并非专用于 ASP.NET AJAX。)若参照
图 10 中的事件图表,您会看到 LoadPostData 在页面上的 InitComplete 之后立即发生。在 LoadPostData 内部,ScriptManager 控件识别导致表单 POST 的控件(如果适用,还包括控件所在的 UpdatePanel)。引起回发事件的控件的标识将被收藏起来,供以后使用。
到 目前为止,ScriptManager 控件已认识到正在发生部分回发,并找出了引起回发的控件。现在,您可以构建对客户端的响应。ScriptManager 控件彻底覆盖了其宿主页面的默认 Render 方法,接管了对 ASP.NET 页面的呈现。要知道,到目前为止,您一直只是将 ScriptManager 控件当作调整您的 ASP.NET AJAX 应用程序中各种设置的某种手段。现在,最终可以看到,您向它提供这些各种不同的选项,因为它(应该)可以成为您的新页面类,它自身具有完备的格式,可将各 控件呈现为页面输出流。
实 际上,ScriptManager 将确保两个对象的默认呈现方法已被覆盖:一个是页面自身,另一个是页面上的 Web 窗体。因此,当 ASP.NET 页面框架请求页面呈现时,将调用 ScriptManager 自身的内部实现。通过了解哪些控件引起回发及其相互关系(是与其他控件同在一个 UpdatePanel 内?还是回发事件源链接到了某个 UpdatePanel?),ScriptManager 控件就会知道应该要求哪些子控件进行呈现,哪些子控件应予忽略。在 ASP.NET 中某个 Page 类的默认行为要求每个子控件呈现的情况下,ScriptManager 只会对它认为有必要的子控件提出请求。
同 样值得注意的是,在覆盖 Form 对象的呈现方法过程中,ScriptManager 对于所有使用 ScriptManager 注册的额外数据,都将通过 RegisterDataItem 方法进行处理并发送到客户端。因此,在 Form 对象得到将自身呈现给客户端的请求之前,您必须准备好要发送给客户端的数据,这一点十分重要。发送给客户端的数据将采用原始格式或 JavaScript Object Notation (JSON) 序列化格式编码。
最 后,客户端框架从服务器获取异步响应并解析出数据。ScriptManager 控件已将所有的控件 ID 和新标记打包到响应中,因此客户端框架只需在浏览器的文档对象模型上完成脚本处理即可更新页面内容。如果整个处理过程都以异步方式进行,浏览器将得到快速 的自动更新,Web 页的用户将获得更好的体验。
研习脚本
ASP.NET AJAX 是一项功能强大的技术。为了在您的 Web 应用程序中充分利用 ASP.NET AJAX 的许多功能,您需要按照本文所演示的方式使用 ScriptManager 控件。
ScriptManager 控件为您处理 ASP.NET AJAX 实现过程中的许多细节。现在您应该注意到了,在 UpdatePanel 等控件的默认行为并非您真正需要的情况下,就会感觉到 ScriptManager 的频繁出现。除了脚本功能和 AJAX 之外,ScriptManager 控件还支持身份验证和个性化等高端功能。
脚 本和服务必须使用 ScriptManager 控件通过页面的 PreRenderComplete 事件进行注册。在完全回发或部分回发过程中,可将脚本引用添加到页面中。当注册脚本时,要求其他对象将其所有脚本和服务引用真正递交给 ScriptManager。
最 后,通过改写宿主页面默认的呈现实现方法,ASP.NET AJAX 在客户端得以实现。从此,ScriptManager 将掌管页面的呈现循环,并向客户端发送一个易于解析的响应,客户端用它来更新浏览器中的元素。通过改写默认的页面呈现方法,可将合适的控件以特定的格式呈 现给客户端,包括使用 ScriptManager 注册的数据。
Ben Rush是 一名中西部地区的 Microsoft .NET 顾问,他专门研究 ASP.NET 技术。在
ben-rush.net/blog 上,您可以了解更多有关 Ben 的信息。
以上是mscn的原文转载,以前只是简单使用ScriptManager,后来发现他并不是我使用的那么简单,这篇文章对内幕有个大概了解,但还不太理解