/*
本章讨论ASP.NET控件如何集成脚本功能与图片资源,并讨论回调(AJAX)的实现。
*/
传统的B/S开发中,开发人员必须同时通晓服务端语言和客户端的各种语言。而现在很多初学ASP.NET的开发者对HTML(XHTML)、JavaScript、CSS等客户端技术知之甚少,因为ASP.NET把大家都惯坏了。在一般的开发中,ASP.NET借助于各种简单明了的控件把开发人员和客户端代码中隔离开来,开发人员根本不用去操心客户端行为,服务端控件已经自动处理好了所有的客户端代码。
应该说ASP.NET提高了生产力,解放了生产者。但有些“科技威胁论”者却担心这样下去,程序员的作用在生产过程中越来越小,担心自己越来越没有价值了。我觉得强大的技术虽然提出了挑战,但它始终只是工具,生产者的价值始终是凌驾于生产工具之上的。
就拿古老的建筑业来说,大型挖掘机并没有让这个行业消亡,相反这个行业在不断地发展壮大,我们不仅有了专业的建筑工人,还有了专业的设计人员、勘测人员等等,而且人数在不断地增加。只是现在我们不再建造茅房泥屋,而是构建摩天大楼。
所以更强大的技术不会夺走你的饭碗,而只是让你可以做更了不起的事情。
但是我们现在关注服务端控件,关注“工具”本身,那么我们就不可避免地要了解客户端技术,并知道服务端控件是如何组织和管理它们的。
可以说任何丰富多彩的交互性Web UI,都离不开JavaScript的支持。所以在本章学完后,也许你就会跃跃欲试,希望马上写出几个令别人啧啧称赞的控件来。
5.1 ClientScriptManager与HttpBrowserCapabilities
在ASP.NET中管理脚本,正是System.Web.UI.ClientScriptManager类的特长。ClientScriptManager类通过Page.ClientScript暴露。在控件内部,可以通过this.Page.ClientScript得到ClientScriptManager实例。
5.1.1 ClientScriptManager概述
ClientScriptManager类提供了大量生成客户端代码的方法:
RegisterArrayDeclaration——添加一个JavaScript数组到页面中。
RegisterClientScriptBlock——在页面的起始<form>标签后添加JavaScript脚本。
RegisterStartScript——在页面的结束<form>标签前添加JavaScript脚本。
RegisterClientScriptInclude——在页面的起始服务端<form>标签后添加外部JavaScript文件引用。
RegisterClientScriptResource——添加已编译到程序集中的JavaScript资源文件的链接到页面中。
RegisterExpandoAttribute——生成为页面中的元素附加扩展属性的脚本(由于Javascript的动态特性,所以可以为对象随意附加属性)。
RegisterHiddenField——在页面的起始服务端<form>标签后添加一个隐藏表单域。
RegisterOnSubmitStatement——用于添加在页面回传服务端前执行的JavaScript脚本。
GetPostBackClientHyperLink——生成类似于javascript:__doPostBack(‘element’,’args’)的脚本,可用于完成回传服务端的链接。
GetPostBackEventReference——以GetPostBackClientHyperLink功能类似,只是没有javascript:前缀。
GetWebResourceUrl——生成编译到程序集中的资源文件的链接。
RegisterForEventValidation——生成事件验证代码,事件验证指事件回传后验证参数是否来源于最初呈现这些事件的服务端控件。需要在Config文件或页面指定中指定启用EnableEventValidation。
此外,ClientScriptManager类还有一大堆IsXXXResistered()方法,用来判断某个客户端代码是不是已经注册到了页面中。
ClientScriptManager的编程接口相当丰富,我们将在后面使用它们。
注:Page类也提供了有一些注册脚本的方法,比如:Page.RegisterClientScriptBlock(),这些方法的功能与ClientScriptManager中的同名方法类似,并且在ASP.NET 2.0中已不推荐使用。
5.1.2 用HttpBrowserCapabilities类检验浏览器
对于很多人来说,客户端开发最大的障碍在于浏览器的兼容性。长期以来,不少人一直在为解决浏览器间的兼容问题进行着不懈努力,包括各种制定标准的组织,比如W3C,也包括各大浏览器生产厂商。但我认为兼容性问题将在很长一段时间内困扰着我们,因为虽然各种传统浏览器对标准的支持会越来越完善,但与此同时,我们要面对的浏览器种类也越来越多,千差万别的各种手机不说,还有很多你连名字都说不上的设备都需要访问因特网。
面对这样的困境,遵循Web标准进行Web设计是基本选择,如果需要控制更多细节,ASP.NET框架也提供了支持。ASP.NET2.0新增了控件适配器(Control Adapter)对象,适配器特定于某种或某类浏览器,ASP.NET框架允许适配器截获和替换控件的关键生命周期阶段,以改变控件的呈现行为。
适配器一般在网站级应用,如果我们需要在控件级控制呈现逻辑,可以使用HttpBrowserCapacities类检测访问浏览器,HttpBrowserCapacities类的实例通过Request.Browser属性暴露出来,在控件内部,使用this.Page.Reqeust.Browser属性可以得到包装了当前浏览器信息的HttpBrowserCapacities实例,如图5-1所示。

注意 在客户端检测浏览器能力有两种策略:一种方案为先检测出浏览器的类型,然后根据检测出的浏览器类型,使用适应其特点的脚本。另一种方案是每次在使用不确定的浏览器功能时,检测这种功能是不是可用。一般我们会采用第二种方案,因为它更加细腻,而且你也不必记住各种浏览器不支持哪些功能。

图5-1 HttpBrowserCapabilities结合User-Agent信息和browser文件中的数据
HttpBrowserCapacities类根据访问浏览器提供的HttpHeader User-Agent信息判断出浏览器的类型,然后根据配置文件中对应的浏览器兼容性信息提供丰富的编程接口:
ActiveXControls——当浏览器支持ActiveXControl时返回true。
Browser——返回浏览器的类型(例如IE,Firefox,Opera)。
ClrVersion——返回安装在浏览器上的最新的.NET Framework版本。
Cookies——当浏览器支持Cookie时返回true。
EcmaScriptVersion——返回浏览器支持的JavaScript版本。
MajorVersion——返回浏览器主版本的整数值。
MinorVersion——返回浏览器副版本双精度数值。
MinorVersionString——返回浏览器副版本字符串值。
MSDomVersion——返回浏览器支持的Microsoft文档对象模型(DOM)版本。
Platform——返回客户端平台(例如,WinXP)。
SupportsCallback——当浏览器支持AJAX时返回true。
SupportCSS——当浏览器支持层叠样式表时返回true。
Version——返回浏览器的完整版本。
W3CDomVersion——返回浏览器支持的W3C文档对象模型(DOM)版本(例如,1.0)。
如果我们要检测浏览器是不是IE7,可以使用以下代码:
if (Page.Request.Browser.Browser=="IE"&& Page.Request.Browser.MajorVersion > 6)
{
this.IsIE7 = true;
}
注:浏览器兼容数据保存在\WINDOWS\Microsoft.NET\Framework\[version]\CONFIG\Browsers目录下,HttpBrowserCapabilities类的正确性依赖于这些数据的正确性,以及浏览器提交的User-Agent数据的正确性,所以它还是比较脆弱的。比如碰到像移动过滤User-Agent信息之类的情况,HttpBrowserCapabilities类只能无能为力了。
在了解了ClientScriptManager和HttpBrowserCapabilities这两个类之后,就可以开始步入探索客户端功能之旅了。
5.1.3 Response.Write
让我们从页面中生成一段弹出对话框的控件开始。
我见过很多ASP.NET开发者都喜欢在页面的某些方法中使用Response.Write方法直接往响应流中输出脚本或其他内容,比如:
protected void Page_Load(object sender, EventArgs e)
{
Response.Write("<script>alert('Hello world!');</script>");
}
这一段代码在像IE这样的比较宽松的浏览器中确实能“很好”地工作,如图5-2所示。

图5-2 IE是一个宽松的浏览器
但是这段代码也许会让一个支持XHTML的手机浏览器死机,因这个页面最终呈现这样的客户端代码:
<script>alert('Hello world!');</script>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3. org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head><title>
Hello world
</title></head>
<body>
<form name="form1" method="post" action="Default.aspx" id="form1">
<div>
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUJNzgzNDMwNTMzZGTx6EI+PAxNT6AKrLWgBOrSl69iWw==" />
</div>
<div>
</div>
</form>
</body>
</html>
可以看到PageLoad方法中输出的脚本代码出现在页面最前端,独立于整个文档结构。由于这句代码,整个文档已经不是一个合法的xHTML文档,因为XML要求<!DOCTYPE>语句应该在文档的最前面。
回顾页面生命周期,页面的内容在执行Render()方法时呈现到客户端,它是页面生命周期中最后阶段执行的方法,远远晚于PageLoad和各个控件的回传事件,所以我们在PageLoad或ButtonClick这些事件处理程序中用Response.Write()方法输入的内容将出现在客户端代码的最前端,最终破坏整个页面结构。
所以用Response.Write()方法输出内容是一个非常差的主意。
要往页面中呈现脚本,我们可以选择ClientScriptManager.RegisterClientScriptBlock()方法或ClientScriptManager.RegisterStartupScript()方法。这两个方法的参数和功能都差不多,不同之处在于它们将代码呈现在页面的不同的地方,前一个方法将客户端代码呈现在页面表单的最前面,也就是这些代码位于所有页面元素之前,这使得脚本最先被浏览器解析;后一个方法将客户端代码呈现在页面表单的最末端,页面解析这些脚本时,页面上的各种元素已经完成解析,这就使得这些脚本可以马上操作页面上的元素。
所以我们常用前一个方法呈现一些不会马上执行的代码,比如声明某个按钮的Click事件处理程序,而用后一个方法呈现页面加载完成后马上执行的代码,比如定义某个变量的初始值,对页面元素进行某种操作。
比如前面的HelloWorld例子,可以改写成这样:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.ClientScript.IsClientScriptBlockRegistered(
"HelloWorldDeclaration"))
{
Page.ClientScript.RegisterClientScriptBlock(typeof(string),
"HelloWorldDeclaration",
"function sayHello(){alert('Hello world!');}", true);
}
if (!Page.ClientScript.IsStartupScriptRegistered(
"HelloWorldExecution"))
{
Page.ClientScript.RegisterStartupScript(typeof(string),
"HelloWorldExecution", "sayHello();", true);
}
}
在这段代码中,我们分别使用RegisterClientScriptBlock()方法声明sayHello()方法,用RegisterStartupScript()方法呈现执行sayHello()方法的代码,这两个方法的最后一个参数设为true,使得这两个方法将自动生成<script>标签对。在使用这两个方法的过程中,还使用配套的IsXXXRegistered()方法对代码是否已经注册过进行判断。在这个页面中使用这些判断并没有太大的意义,但在控件中却很有必要性,因为你不知道用户会放多少个同样的控件到页面中,如果每个控件实例都呈现一遍相同的脚本,则会在页面中产生大量冗余代码。
以上代码执行的效果和之前的代码在IE中执行的效果一样,它呈现的代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head><title>
Hello world
</title></head>
<body>
<form name="form1" method="post" action="Default.aspx" id="form1">
<div>
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUJNzgzNDMwNTMzZGTx6EI+PAxNT6AKrLWgBOrSl69iWw==" />
</div>
<script type="text/javascript">
<!--
function sayHello(){alert('Hello world!');}// -->
</script>
<div>
</div>
<script type="text/javascript">
<!--
sayHello();// -->
</script>
</form>
</body>
</html>
结果完全符合xHTML标准的要求,大大减少了在客户端出现错误的机率。







