ASP.NET AJAX(服务器回调)
如果只用纯粹的 js ,你必须弥补 ASP.NET 服务器端抽象和有限的 HTML DOM 之间的鸿沟,这不简单,没有 VS 的智能提示和调试工具,编写无错的代码和诊断错误都非常困难。由于各种突发事件及实现的差异,编写能够在所有现代浏览器上都正确运行的脚本代码更是一项很大的挑战。
ASP.NET 的客户端回调机制部分解决了这类问题,它们提供一个服务器端模型,让你能够生成某些需要的客户端代码(使用 XMLHttpRequest 对象执行异步请求的代码)。但这个模型还很不完美,接口略显笨拙,和页面模型的整合不太平滑且没有数据类型。你必须自行把传输信息序列化为字符串,还必须编写 js 代码来接收回调,并反序列化字符串更新页面。总之:客户端回调机制是构建具有 Ajax 特性控件的极佳工具,但对设计完整网页却不是那么有吸引力。
ASP.NET AJAX 简介
ASP.NET 开发人员还有另外一个选择。使用 ASP.NET AJAX 工具包,它提供了一些特性,能够帮助构建 Ajax 风格的页面。
ASP.NET AJAX 有两个关键部分:客户端部分和服务器端部分。
客户端部分是一组 JavaScript 库。这些库在任何方面都没有绑定到 ASP.NET,非 ASP.NET 人员也可以在自己的 Web 页面里使用它们。客户端库没有提供太多的特性,不包含任何可以直接拖放到 Web 页面的预先建立的功能。它们只是建立了对开发 ASP.NET AJAX 页面的基础。这个基础扩展了 js(例如,增加了对继承的支持),并提供一些基本框架(例如,管理组件生命周期的方法,对常见的数据类型进行操作,执行反射)。
服务器端部分则在一个更高的层次上工作。它包括那些使用客户端 js 库的控件和组件。例如,一个带有 DragPanel(来自 ASP.NET AJAX 控件工具包)的 Web 表单能让用户在浏览器里随意拖动面板,而在后台,是一些自定义的 js 在工作,这些 js 又使用了客户端的 ASP.NET AJAX 库。不过,DragPanel 自动呈现它所需要的一切 js 代码,这省去了你自己编写它们的麻烦。
很明显,ASP.NET AJAX 是 ASP.NET 开发新方向的开始。ASP.NET AJAX 提供的全部特性,大体如下:
- JavaScript 语言扩展:这些扩展使 js 更接近现代的面向对象语言,它们支持命名空间、继承、接口、枚举、反射。
- 远程方法调用:ASP.NET AJAX 页面可以调用 Web 服务。能够不执行完整的页面回发就能从服务器获取到信息。它和客户端回调机制解决了同一问题,但它可以在强类型的方法工作,不必把所有数据转换为单一字符串。
- ASP.NET 服务:这项特性能够调用服务器来使用两个 ASP.NET 服务之一(一个使用表单验证信息、而另一个从当前用户配置文件获取数据)
- 局部页面刷新:新的 UpdatePanel 控件允许定义页面的一部分,这部分页面更新时不需要整个页面回发,最妙的是,你不必编写任何 js 代码。
- 预先建立的控件:流行的 ASP.NET AJAX 控件工具包有 30 多个控件和控件扩展器,它们有助于充分利用 ASP.NET AJAX。你可以展开合起控件,添加动态动画,使控件支持自动完成且可拖放,这类底层的 js 细节不需要你编写任何代码。
客户端的 ASP.NET AJAX :脚本库
ASP.NET AJAX 的客户端部分依赖于一小组 JavaScript 文件。有两个办法部署 ASP.NET AJAX 脚本文件:
- 构建 ASP.NET 3.5 应用程序,它们被嵌入在 System.Web.Extensions.dll 程序集并按需服务。
- 如果是非 ASP.NET 程序或为普通的 HTML 页面添加客户端特性,那么可以从 http://www.aps.net/ajax/downloads 单独下载这些 js 文件,它们是 Microsoft AJAX Library 的一部分。
如果要仔细查看真正的 JavaScript 代码,Microsoft AJAX Library 很值得下载。这个下载除了最终的生产版本外,还包含了 3 个核心文件的调试版本。为了减小文件,生成版本去掉了所有的空格和注释。
如果下载了 Microsoft AJAX Library,你会发现其实只使用了 3 个核心的文件:MicrosoftAjax.js、MicrosoftAjaxWebForms.js、MicrosoftAjaxTimers.js。除了这些,还有 100 多个小文件,它们用于保存国际化信息。(例如,不同情况的数据格式)
在 ASP.NET 中,你找不到客户端库的单独 JavaScript 文件,因为它被嵌入了程序集 System.Web.Extensions.dll 并为脚本资源提供服务。脚本资源能够把一个 URL 映射到嵌入在程序集里的资源。类似下面这样:
<script src="/Ajax/ScriptResource.axd?d=m4ocAED2VQt91uJZddDjK8_mxBry_xp7FrQEEm8C
q4HQGsiwPw4ltli3kH0dLyw7XK9rPijWJ-Cp9yfeEzE8MBuoYhCitA3d6lxU0PL7-klAWWEqGEWaKu7Z
j3UcCFFNbwZBk3lHslabQGmQuuCJ5A2&t=2a8ce630" type="text/javascript"></script>
ASP.NET 有一个脚本资源处理程序,它响应并处理这些请求,它检查传入的查询字符串参数,返回请求的脚本文件。
服务器端的 ASP.NET AJAX :ScriptManager
很明显,你不会愿意在每个需要 ASP.NET AJAX 的页面输入指向脚本资源的长长的 URL。解决办法是使用一个叫做 ScriptManager 的 ASP.NET 控件。
ScriptManager 是服务器端 ASP.NET AJAX 模型的核心。它是一个在页面上没有任何可视界面的 Web 控件。它执行一个主要任务:呈现到 ASP.NET AJAX JavaScript 库的链接。
使用 ASP.NET AJAX 特性的每个页面都需要且只需要一个 ScriptManager 实例。
ScriptManager 还执行一些其他重要任务:
- 呈现对其他脚本文件的引用
- 创建能够从浏览器调用 Web 服务的代理
- 管理 UpdatePanel 控件刷新内容的方式
如果整个网站使用 ASP.NET AJAX 特性,就应该把 ScriptManager 放到一个母版页里。不过这偶尔会产生一些问题。不同的内容页可能要以不同的方式配置 ScriptManager(例如添加新脚本和 Web 服务的引用)。遇到这样的问题时,解决办法是母版页中使用 ScriptManager,而内容页中使用 ScriptManagerProxy。
服务器回调
在 ASP.NET AJAX 里,回调总是通过一个单独的服务器端方法实现(从技术上而言就是 Web 服务)。这个设计改良了对逻辑的分离,帮助组织代码。更重要的是,它负责序列化工作。也就是说,你不必想方设法让自己的方法发送复杂的数据(比如之前的示例使用管道符号分离值)。
在随后的内容里,我们创建需要的 Web 服务,并考虑几种使用它的方式。
ASP.NET AJAX 中的 Web 服务
ASP.NET AJAX 执行服务器回调时,客户端的 js 代码调用服务器端 Web 服务的一个方法。
Web 方法是一个或多个服务器方法的集合,可以被远程客户调用。客户通过 HTTP 发送请求调用 Web 服务,和页面回发的过程相似,不同的是请求的主体是要传送给方法的参数。然后,ASP.NET 创建 Web 服务对象,运行对应的 Web 方法里的代码,返回结果并释放 Web 服务对象。请求和响应消息的格式可以变化,传统上,它是一个叫做 SOAP 的基于 XML 的标准,但在 ASP.NET AJAX 里,它是轻量级的基于文本的 JSON(JavaScript Object Notation),这主要是出于浏览器兼容的原因。
ASMX 和 WCF Web 服务
.NET 有两种 Web 服务技术:.asmx 和 WCF。
同样可以使用 WCF(Windows Communication Foundation) 服务作为 ASP.NET AJAX 页面的后端。从概念上说,这种方法与使用普通的 .asmx Web 服务是一样的。
WCF 是 .asmx Web 服务的继任者,是一个更加广泛的平台,包括了 .asmx Web 服务不支持的一组方案。然而,这些高级的方案对于 ASP.NET AJAX 页面而言并不可用。从实用的角度而言,这两种 Web 服务技术都针对 ASP.NET AJAX 页面提供了完全一致的能力。
虽然 ASP.NET AJAX 回调机制使用 Web 服务,但这是一个特殊的实现,意识到这一点很重要。如果你熟悉 Web 服务,会发现 ASP.NET AJAX 强加了其他一些限制:
- Web 页面不能调用非 ASP.NET AJAX Web 服务(例如,在其他平台创建的第三方 Web 服务),这是因为它们不支持 ASP.NET AJAX 所使用的简单 JSON 模型。
- Web 页面不能调用其他域(在其他 Web 服务器上)里的 Web 服务。这是因为大多数浏览器都禁止跨域使用 XMLHttpRequest 对象,以防止潜在的跨站点脚本攻击。
如果你使用 Web 服务面向富客户端、第三方开发人员或非 .NET 应用程序公开服务器端功能,那么你需要知道 ASP.NET AJAX 里对 Web 服务的应用并不能完全满足这些需求。有一些办法可解决这些限制。例如,先调用 Web 应用程序里的某个 Web 方法,然后让那个 Web 方法调用其他域里的 Web 方法。这项桥接技术之所以能够工作,是因为 Web 服务器代码并没有像浏览器那样的限制,它能够自由地跨域调用其他 Web 服务。
1. 创建 Web 服务
添加一个新项目,选择“Web 服务”模板。(如果创建的是一个无项目文件的网站,.asmx 文件将会被放到 Web 应用程序目录,而对应的 .cs 文件则放在 App_Code 文件夹中,这样它能够自动被编译。)
.asmx 文件没有什么特别的,如果你打开它,只会发现一行 WebService 指令,它标识代码的语言、代码隐藏文件的位置以及类的名字:
<%@ WebService Language="C#" CodeBehind="~/App_Code/TerritoriesService.cs" Class="TerritoriesService" %>
该代码隐藏类看起来和下面的代码类似:
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class TerritoriesService : System.Web.Services.WebService
{...}
默认情况下,ScriptService 特性被注释掉了。创建 ASP.NET AJAX 页面调用的 Web 服务时,必须使用此特性!
这个类从 System.Web.Services.WebService 派生,这是 Web 服务的传统基类。继承它是出于方便,而不是必须。通过从 WebService 继承,不必经过静态的 HttpContext.Current 属性就能获得直接访问内置对象(如 Application、Session、Server、User 等)的便利。
Web 服务类的声明由 3 个特性进行修饰:
- WebService:设置 Web 服务消息里的 XML 命名空间
- WebServiceBinding:指定 Web 服务支持的标准级别,这只在通过 SOAP 消息调用时才有用
- ScriptService:它配置 Web 服务允许 JavaScript 客户端的 JSON 调用。
2. 创建 Web 方法
每个 Web 服务都有一个或多个用 WebMethod 特性标注的方法,这个特性使方法能够被远程调用。如果添加的方法没有包括 WebMethod 特性,服务器端的代码还是可以使用它,但是客户端的 JavaScript 就不能直接调用它了。
[WebMethod]
public string DoSomething()
{...}
将方法标记为公有的(并非必须)是出于习惯和清晰性的要求,通常会这样做。
Web 方法有一定的限制。参数值和返回值的类型必须如下表:
基本类型 | 基本的 C# 数据类型、string、DateTime 等 |
枚举 | C#里枚举使用 enum 关键字,Web 服务使用枚举的字符串(非底层整型) |
自定义对象 | 任何自定义类或结构的实例,唯一限制是只有公共数据成员和属性被传递 |
数组和集合 | 必须是被支持类型的数组,ArrayList 也可以,但特殊集合如 HastTable 不可以。可以使用泛型,但集合中的元素必须能够被序列化 |
XmlNode | 这个对象是 XML 文档某个区域的表示,你可以用它发送任意的 XML |
DataSet 和 DataTable | 使用它们时被自动转换为 XML。其他数据对象不支持如 DataRows 等。 |
WebMethod 特性接收一组参数,其中大多数和 ASP.NET AJAX 没有什么关系。但 EnableSession 属性是一个例外,它的默认值是 false,因此 Web 服务里不能访问会话状态。对于非 ASP.NET AJAX 服务,这个默认值是有意义的,因为可能没有任何会话信息,而客户端也可能根本没有保留任何回话 cookie。但对于 ASP.NET AJAX Web 服务,Web 服务调用总是来自 ASP.NET Web 页面上下文,而它是在当前 Web 应用程序用户的上下文中执行的,那个用户有一个活动的会话并且会话 cookie 自动随 Web 服务调用一起传递。
这个示例让 Web 方法能够访问 Session 对象:
[WebMethod(EnableSession = true)]
public void DoSomething()
{
if (Session["myObject"]!=null)
{
// Use the object in session state.
}
else
{
// Create a new object and store in session state.
}
}
对于下拉列表示例,Web 服务必须提供一个能够取得某个区域所有地区的方法,下面的代码演示了这个 Web 服务:
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class TerritoriesService : System.Web.Services.WebService
{
[WebMethod]
public List<Territory> GetTerritoriesInRegion(int regionID)
{
SqlConnection conn = new SqlConnection(
WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString);
SqlCommand cmd = new SqlCommand(
"select * from Territories where RegionID=@RegionID", conn);
cmd.Parameters.Add(new SqlParameter("@RegionID", SqlDbType.Int, 4));
cmd.Parameters["@RegionID"].Value = regionID;
List<Territory> territories = new List<Territory>();
try
{
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
territories.Add(new Territory(reader["TerritoryID"].ToString(),
reader["TerritoryDescription"].ToString()));
}
reader.Close();
}
catch (SqlException err)
{
throw new ApplicationException("Data error.");
}
finally
{
conn.Close();
}
return territories;
}
}
有一点需要注意,和以前的客户端回调示例有所不同,这里的 Web 服务返回的是一个强类型的集合。Territory 类相对比较简单了:
public class Territory
{
public string ID;
public string Description;
public Territory() { }
public Territory(string id, string description)
{
this.ID = id;
this.Description = description;
}
}
3. 调用 Web 服务
创建了需要的 Web 服务后,下一步是配置页面使之知道 TerritoriesService 。这时,需要给页面添加 ScriptManager 控件,标签中添加 <services> 节,这个节使用 ServiceReference 元素列出了页面使用的所有服务以及它们的位置:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Services>
<asp:ServiceReference Path="~/TerritoriesService.asmx" />
</Services>
</asp:ScriptManager>
当页面在服务器端被呈现的时候,ScriptManager 会产生一个 JavaScript 代理。在客户端代码里,可使用这个代理启动你的调用。
这个 Web 表单有两个列表。第一个列表通过普通的 ASP.NET 数据绑定来填充:
<div style="font-family: Verdana; font-size: small">
Choose a Region, and then a Territory:<br />
<br />
<asp:DropDownList ID="lstRegions" runat="server" Width="210px" DataSourceID="sourceRegions"
DataTextField="RegionDescription" DataValueField="RegionID" onChange="GetTerritories(this.value);">
</asp:DropDownList>
<asp:DropDownList ID="lstTerritories" runat="server" Width="275px">
</asp:DropDownList>
<br />
<br />
<br />
<asp:Button ID="cmdOK" runat="server" Text="OK" Width="50px" OnClick="cmdOK_Click" /><br />
<br />
<asp:Label ID="lblInfo" runat="server"></asp:Label>
<asp:SqlDataSource ID="sourceRegions" runat="server" ProviderName="System.Data.SqlClient"
ConnectionString="<%$ ConnectionStrings:Northwind %>" SelectCommand="SELECT 0 As RegionID, '' AS RegionDescription UNION SELECT RegionID, RegionDescription FROM Region">
</asp:SqlDataSource>
</div>
有趣的是,它使用了 onchange 特性与一个客户端事件处理程序进行连接。当用户选择了一个新的地区时,js 函数 GetTerritories() 被触发,并且当前的列表值会被传递进去。
这里是 GetTerritories() 函数的 js 代码:
function GetTerritories(regionID) {
TerritoriesService.GetTerritoriesInRegion(regionID,
OnRequestComplete, OnError);
}
应该可以发现在调用 Web 服务时,客户端语法和 .NET 语法是不同的。客户端代码不需 new 出实例而是直接使用一个现成的代理对象,这个代理对象和 Web 服务类具有相同的名称。
客户端的 Web 服务调用是异步的,因此除了要传递原始 Web 方法的参数外,还需提供回调函数指定用于处理返回的结果,在本例中是 OnRequestComplete。此外,还可以添加另一个引用用于指向发生错误时要使用的函数,这里是 OnError():
function OnRequestComplete(result) {
var lstTerritories = document.getElementById("lstTerritories");
lstTerritories.innerHTML = "";
for (var n = 0; n < result.length; n++) {
var option = document.createElement("option");
option.value = result[n].ID;
option.innerHTML = result[n].Description;
lstTerritories.appendChild(option);
}
}
function OnError(result) {
var lbl = document.getElementById("lblInfo");
lbl.innerHTML = "<b>" + result.get_message() + "</b>";
}
这段代码唯一值得注意的部分是它能直接利用返回结果工作而不需要任何额外的反序列化工作!Web 方法返回 Territory 对象的泛型列表,ASP.NET AJAX 创建 Territory 对象的定义,然后用数组返回完整的列表,这样 js 代码就能够循环数组并检查每个项的 ID 和 Description 属性。
OnError() 方法接收一个 error 对象,包含接收错误文本的 get_message() 方法以及返回错误在哪发生的调用栈的 get_stacktrace() 方法。
这里有个小技巧,除了使用 document.getElementById() 方法外,还可以使用 ASP.NET AJAX 提供的别名 $get 执行同样的功能:
var lstTerritories = $get("lstTerritories"); // 这是 ASP.NET AJAX 页面中常见的约定
现在这个示例能够和以前曾介绍过的客户端回调的版本一样工作。区别是这个版本使用强类型的 Web 方法,没有扰人的字符串序列化代码,不需要添加和动态插入任何获取回调引用的服务器端代码。相反,可以直接使用直观的代理,以提供 Web 服务的访问。
这里演示了 ASP.NET AJAX 版本的客户端回调模型。虽然这和 ASP.NET 客户端回调机制的作用相同,但 ASP.NET AJAX 版本提供了一个基于 Web 服务构建的更健壮的基础。但无论这两种技术的哪一种,都必须编写 JavaScript 代码来更新页面!
在页面里放置 Web 方法
大多情况下,应该创建独立的 Web 服务以处理 ASP.NET AJAX 回调,页面更清晰,代码更方便调试和完善。不过,有时可能会碰到 Web 方法只为特定的页面设计且不被应用程序的其他部分重用的情况。这种情况下,你可以为每个页面创建专门的 Web 服务,或者把 Web 服务代码转移到页面里。
把 Web 方法代码放到页面中很容易,其实,所要做的只是剪切和粘帖:
- 复制 Web 方法至页面隐藏类,并将方法改为静态方法。
- 添加 WebMethod 特性和 ScriptMethod 特性;去除原 Web 服务类的方法前特性(它将不再是 Web 方法)
- 清除原先方法的实现,添加对 Web 服务的引用:
public partial class WebServiceCallback : System.Web.UI.Page
{
[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod]
public static List<Territory> GetTerritoriesInRegion(int regionID)
{
TerritoriesService service = new TerritoriesService();
return service.GetTerritoriesInRegion(regionID);
}
...
}
- 删除页面类对 ScriptManager 的 <Services> 节的引用并把 ScriptManager.EnablePageMethods 属性设置为 true:
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true">
</asp:ScriptManager>
- 最后,修改 JavaScript 代码,让它通过 PageMethods 对象调用方法:
PageMethods.GetTerritoriesInRegion(regionID, OnRequestComplete, OnError);
PageMethods 对象公开添加到当前 Web 页面的所有 Web 方法。把 Web 方法放到页面的一个好处是该方法不再通过 .asmx 文件公开添加。这样,它就不是公共 Web 服务的一部分了,别人也就不容易发现这个方法。如果你希望向那些好奇的用户隐藏某些 Web 服务,它就很有吸引力了。
另一个将 Web 方法放到页面类里的原因是为了读取页面中视图状态或控件的值。页面方法被触发时,会发生独立的页面生命周期。当然,不要修改页面的细节,因为页面还没有被呈现,所做的一切改变都会被直接忽略。
注意:
无论 Web 方法在页面里还是专门的 Web 服务里,对程序的安全没有什么区别。虽然在页面里可以向普通的用户隐藏它,但是真正的攻击者会首先检查页面的 HTML 代码,其中包含对 JavaScript 代理的引用。恶意用户可以通过 JavaScript 代理调用 Web 方法。因此 Web 方法总应该实现和 Web 页面同样的安全策略。例如,要验证收到的输入、不向未经验证的用户返回敏感信息、数据库访问应该使用参数化命令以避免 SQL 注入攻击。