JavaScript And Ajax(在客户端回调中使用 Ajax)
采用 Ajax 的方式,可以创建令人印象深刻、高度响应的网页。然而,编写客户端代码非常耗时。VS 不能为之提供丰富的设计体验,也没有调试工具追踪那些松散的 js 语言中不可避免的错误;甚至你成功完成了工作,还要在各种浏览器上进行测试,除非你非常熟悉各种浏览器对 js 支持的微小差别。
由于这些原因,许多开发人员不手工编写客户端脚本,甚至在设计 Ajax 风格的页面时也是如此。相反,他们更乐意使用能够生成他们需要的脚本的高级组件。
一个例子是免费的第三方 Ajax.NET 库(可从 http://ajax.schwarz-interactive.de/csharpsample 获得)。Ajax.NET 使用特性标记方法,然后这些方法就可以通过客户端回调和自定义 HTTP 处理程序远程调用。
另一个例子是 ASP.NET AJAX,它是更加全面的 Ajax 工具集,以后会介绍。
虽然两者都是很好的选择,你还能够执行最核心的 Ajax 任务(发送异步请求到服务器),使用 ASP.NET 中更加直观的客户端回调功能。客户端回调让你能刷新页面的部分数据而不需要触发完整的回传。最妙的是,你不需要使用 XMLHttpRequest 对象的脚本代码,不过,仍然需要编写处理服务器端响应的客户端脚本。
创建客户端回调
要在 ASP.NET 里创建客户端回调,首先要计划如何让通信正常工作。这里是最基本的模型:
- 在某一时刻,js 事件发生,触发服务器回调。
- 此时,正常的页面生命周期开始,所有正常的服务器端事件发生。
- 这个过程完成时,且页面正确初始化后,ASP.NET 执行服务器端回调方法。此方法具有固定的签名:接收并返回一个字符串。
- 页面从服务器端方法接收到响应后,使用 js 代码相应的修改网页。
ASP.NET 架构用于抽象出通信过程,这样你可以构建使用回调的页面而不用考虑底层逻辑,就像你从视图状态以及页面生命周期得到的便利一样。
这个例子,你将会看到有两个下拉列表的页面。第一个填充来自 Northwind 数据库的一系列区域,它在页面第一次加载时完成。第二个列表开始为空,直到用户在第一个列表中作出选择,此时第二个列表的内容通过回调获得并被插入到列表里。
1. 构建基本页面
填充第一个列表很容易,声明性的绑定到数据源控件:
<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">
</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" ConnectionString="<%$ ConnectionStrings:Northwind %>"
SelectCommand="SELECT 0 As RegionID, '' AS RegionDescription UNION SELECT RegionID, RegionDescription FROM Region">
</asp:SqlDataSource>
</div>
2. 实现回调
要接收一个回调,需要实现 ICallbackEventHandler 接口的类。如果你知道这个回调会用到多个页面,为它创建专门的类是有意义的。如果是只为单一页面实现的功能,可以在这个网页里直接实现 ICallbackEventHandler:
public partial class ClientCallback : System.Web.UI.Page, ICallbackEventHandler
{ … }
ICallbackEventHandler 接口定义了 2 个方法:
- RaiseCallbackEvent() :以字符串参数的形式得到浏览器的事件数据。它首先被触发。
- GetCallbackResult() :它紧接着被触发,它把结果返回给页面。
ASP.NET 客户端回调的主要限制是它强制你使用单个字符串传送数据。如果要传送较复杂的信息,必须设计一个方法把它序列化为一个字符串,然后在客户端反序列化它。
private string eventArgument;
public void RaiseCallbackEvent(string eventArgument)
{
this.eventArgument = eventArgument;
}
public string GetCallbackResult()
{
SqlConnection con = new SqlConnection(
WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString);
SqlCommand cmd = new SqlCommand(
"SELECT * FROM Territories WHERE RegionID=@RegionID", con);
cmd.Parameters.Add(new SqlParameter("@RegionID", SqlDbType.Int, 4));
cmd.Parameters["@RegionID"].Value = Int32.Parse(eventArgument);
StringBuilder results = new StringBuilder();
try
{
con.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
results.Append(reader["TerritoryDescription"]);
results.Append("|");
results.Append(reader["TerritoryID"]);
results.Append("||");
}
reader.Close();
}
catch (SqlException err)
{
// Hide errors.
}
finally
{
con.Close();
}
return results.ToString();
}
在这个例子中,不能使用声明性的数据绑定。因为回调方法不能直接访问页面上的控件!和回传不同,调用 RaiseCallbackEvent() 时,页面还没执行重建过程。相反,RaiseCallbackEvent() 方法只是被外部调用以请求一些额外的信息。这些问题由回调方法自己解决。
因为结果要以一个字符串返回(此字符串还要在 js 代码中执行逆向工程),代码有点繁琐。一个管道符号分隔2个字段,2个管道符号表示新行的开始。显然,这种方式有点脆弱,只要地区记录包含管道符号,它就会带来明显的问题!
3. 编写客户端脚本
客户端脚本涉及到服务器和客户端的数据交互。服务器端需要一个方法准备结果,客户端也需要一个方法接收并处理结果。
这个方法名称可以任意,但必须接收 2 个参数,如下:
function ClientCallback(result, context) { …}
结果参数含有序列化的字符串。对于本例,由客户端脚本解析这个字符串并填入相应的列表框:
<script type="text/javascript">
function ClientCallback(result, context) {
var lstTerritories = document.getElementById("lstTerritories");
lstTerritories.innerHTML = "";
var rows = result.split("||");
for (var i = 0; i < rows.length - 1; ++i) {
var fields = rows[i].split("|");
var territoryDesc = fields[0];
var territoryID = fields[1];
var option = document.createElement("option");
option.value = territoryID;
option.innerHTML = territoryDesc;
lstTerritories.appendChild(option);
}
}
</script>
还缺少一个细节。虽然已经定义了两端的消息交互,但还没有把它们真正的联到一起。你需要一个客户端触发器来调用回调。对于这个例子,可以响应地区列表的 onchange 事件:
protected void Page_Load(object sender, EventArgs e)
{lstRegions.Attributes["onChange"] = callbackRef;
……
}
callbackRef 是调用回调的 js 代码。不过,你究竟要怎么编写这段代码呢?ASP.NET 提供了一个方便的 GetCallbackEventReference() 方法,它能构建你需要的回调引用:
protected void Page_Load(object sender, EventArgs e)
{
string callbackRef = Page.ClientScript.GetCallbackEventReference(
this, "document.getElementById('lstRegions').value", "ClientCallback", "null", true);
lstRegions.Attributes["onChange"] = callbackRef;
}
参数1:处理回调的 ICallbackEventHandler 对象的引用。
参数2:客户端向服务器端传递的信息(一个字符串)。
参数3:从服务器回调获取结果的客户端 js 的函数名称。
参数4:要传给客户端函数的上下文信息。如果同一个 js 函数处理多个回调并且要区分它们时,这个参数很有用。
参数5:是否异步执行回调。应一直为 true;避免发生网络问题时页面被锁定。
4. 禁用事件验证
POST 注入攻击是恶意用户修改发送到服务器的 HTTP POST 请求,使之包含在相应控件中没用的值的攻击。例如,用户可能把发送的参数修改为不在列表中的列表选项值,如果不检查这样的值,代码可能会泄漏敏感数据。
ASP.NET 使用事件验证避免 POST 注入攻击。事件验证验证所有提交的数据必须在 ASP.NET 执行页面生命周期之前就已经存在。遗憾的是,事件验证常会在 Ajax 风格的页面中产生问题。对于此例,项动态的添加到地区列表。用户选定一个区域并回传到页面时,ASP.NET 将会引发“无效回发或回调参数”的错误,因为选中的区域没有定义到服务器端控件里。
事件验证 不是所有控件都支持的功能。只有那些使用了 SupportsEventValidaion 特性的控件类才实现该功能。在 ASP.NET 里,大多依赖回传数据的控件使用该特性(如 ListBox、DropDownList、CheckBox、TreeView、Calendar 等),那些不限制允许值的控件例外,。例如,TextBox 不使用事件验证,因为允许用户在其中输入任意值。
有 2 个办法可以解决事件验证的问题。
最安全的办法是显式告诉 ASP.NET 控件允许的额外值(使用叫做 _EVENTVALIDATION 的隐藏输入标签跟踪允许的值)。遗憾的是,这种方法单调乏味,甚至有时不切实际!要使用这种方法必须为每个可能的值调用 Page.ClientScript.RegisterForEventValidation() 方法,必须覆盖 Page.Render() 方法在呈现阶段完成这个任务。
下面这个例子允许用户在 lstTerritories 控件中选择 TerritoryID 为 10 的区域:
protected override void Render(HtmlTextWriter writer)
{
Page.ClientScript.RegisterForEventValidation(lstRegions.UniqueID, "10");
base.Render(writer);
}
一个明显的问题是,很多情况下你不知道所有可能的值。它们可能是动态产生的或者来自其他数据源(如 Web 服务)。对于本例,你需要从数据库获取所有 TerritoryID 值,遍历并注册每个值。它不仅带来了额外的工作,如果页面出现后加入了更多区域它还会带来其他问题!
唯一理想的解决方案是禁用事件验证。遗憾的是,不能为单一的控件禁用事件验证。必须使用 Page 指令的 EnableEventValidation 属性为整个页面把它关闭:
<%@ Page EnableEventValidation="false" ... %>
也可设置在配置文件中禁用整个网站的事件验证。然而,不推荐使用这一种方式,会为其他页面带来安全风险。
要用代码获得选中的地区,不能使用 lstTerritories 控件。因为 lstTerritories 控件是服务器端版本的列表,所以它不包括动态添加的值。相反,要直接从 Request.Forms 集合中获取选定的值:
protected void cmdOK_Click(object sender, EventArgs e)
{
// The server-side control doesn't have the territory list, so
// you need to get the selected territory from the Request object.
// Remember to check for injection attacks if the Territories
// table contains sensitive data.
lblInfo.Text = "You selected territory ID #" + Request.Form["lstTerritories"];
// Reset the region list box (because the territory list box will be empty).
lstRegions.SelectedIndex = 0;
}
现在测试整个页面的效果,如下:
客户端回调提供了强大的功能。它能让你构建平滑的动态页面,不过要记住,这依赖 XMLHttpRequest 对象,它限制用户只能使用现代浏览器,有些浏览器支持 JavaScript 但不支持客户端回调。可以使用 Request.Browser.SupportsCallback 属性检查浏览器是否支持回调。