自定义服务器控件(处理不同的浏览器)
不同的浏览器所支持的功能大相径庭,要创建一个兼容所有浏览器,又提供最好的用户体验的应用程序是一项很大的挑战。ASP.NET 提供了一些新的功能来帮助你为不同的设备编写正确的标记类型。
HtmlTextWriter
ASP.NET 对客户端的标记类型作了一个大致的区分,因此有的客户端见到 HTML 3.2,有的见到 HTML 4.0,而有的见到 XHTML 1.1,你甚至不会意识到这种区分的发生。
所有这些的发生都是通过 HtmlTextWriter 类完成的。这个类有几个派生类。HtmlTextWriter 用于输出 HTML 4.0 标记,但它的派生类却大为不同:Html32TextWriter 能够输出 HTML 3.2 标记,而 XhtmlTextWriter 输出 XHTML 1.1。
因为所有这些类都从 HtmlTextWriter 派生,所以你可以在自己的呈现代码中自由使用 HtmlTextWriter 方法中相同的基本集合。然而,这些方法的实现却大都不同,因此,根据你使用的对象不同,输出可能也不同。
例如,如果使用以下呈现代码:
output.RenderBeginTag(HtmlTextWriterTag.Div);
认为结果如下:
<div>
但是,你用 Html32TextWriter 时将会看到这样的结果(假设 Html32TextWriter.ShouldPerformDivTableSubstitution 被设置为 true):
<table cellpadding=”0” cellspacing=”0” border=”0” width=”100%”><tr><td>
另一方面,如果使用下面这段代码,不管目标设备的能力如何,你的呈现输出将是完全不灵活的和永远不变的:
output.Write(“<div>”);
相似的,如果你从 WebControl 派生类来得到样式属性的自动支持,不同的呈现器会有不同的实现。
这里总的经验是:
避免编写原始的 HTML(使用 Write() 方法),而应在任何可能的地方使用高级方法(如 RenderBeginTag(),RenderEndTag() 等),这样,控件会有更强的灵活性。ASP.NET 将基于请求页面的浏览器的能力,创建和传入正确的 HtmlTextWriter ,HTML 标记能自适应。
这个问题不像过去那样重要了,因为现在常用的浏览器几乎都支持 XHTML 。不过,这仍然是一个很好的设计,因为它确保了当 ASP.NET 被更新以支持那些具有不同支持范围的浏览器后,你的代码还能够完美的工作。
浏览器检测
ASP.NET 是怎么判断哪一种文本编写器适用于某一个特定的客户端呢?
这完全取决于客户端发起请求时提供的用户代理字符串。ASP.NET 尝试将这个字符串与一个包含所有已知浏览器的大型目录做比对。
你可以在下面的路径找到这个目录:
C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\Config\Browsers
在那里,有许多 .browser 文件。每一个都是 XML 文件,每个文件将用户代理字符串映射到一组功能和一个文本编辑器。
每个 .browser 文件拥有如下基本结构:
<browsers>
<browser id="IE" parentID="Mozilla">
<identification>
<userAgent match="MSIE (?'version'(?'major'\d+)(\.(?'minor'\d+)?)(?'letters'\w*))(?'extra'[^)]*)" />
<userAgent nonMatch="IEMobile" />
</identification>
<capture>
<userAgent match="Trident/(?'layoutVersion'\d+)" />
</capture>
<capabilities>
<capability name="browser" value="IE" />
<capability name="layoutEngine" value="Trident" />
<capability name="layoutEngineVersion" value="${layoutVersion}" />
<capability name="extra" value="${extra}" />
...
</capabilities>
</browser>
...
</browsers>
对这一模型进一步分析可知,你能够创建浏览器的子类。为了进一步分类,<browser> 元素包含了 parentID 特性,该特性引用了另一个 <browser> 定义,并从其继承所有设置。
遗憾的是,这是一个在某种程度上易碎的系统。你并不能保证一个浏览器不会遇到一个与任何已知模式都不匹配的浏览器字符串,或者一个浏览器不会提交错误的字符串。然而,在松耦合的 Web 世界里,这是一种必要的折中,而且 ASP.NET 团队已经努力工作以确保与 ASP.NET 4 一同交付的浏览器信息比早期版本更可靠也更新。另外,你也完全可以随意定制预制的浏览器信息,或者为不同的用户代理字符串增加新的定义。
浏览器属性
你可以使用 HttpRequest 对象的 Browser 属性来检测当前浏览器的配置,该属性返回一个对 HttpBrowserCapabilities 对象的引用。(也可以从 UserAgent 属性获得用户代理字符串。)
当客户端发出一个 HTTP 请求的时候,HttpBrowserCapabilities 对象就被创建了,并且基于相对应的 .browser 文件的浏览器功能信息填充了数据。在 HttpBrowserCapabilities 类中提供的信息包括浏览器类别、版本、在客户端是否支持脚本等。通过检测浏览器的能力,可以定制输出来针对不同的浏览器提供不同的行为。通过这种方式,你能够完全发掘上层客户端的潜在能力,同时又不会破坏下层客户端。
HttpBrowserCapabilities 属性
Browser | 得到在用户代理首部中与请求一起发送的浏览器字符串 |
MajorVersion | 得到客户端浏览器的主版本号(例如,返回版本号 4.5 中的 4) |
MinorVersion | 得到客户端浏览器的次版本号(例如,返回版本号 4.5 中的 5) |
Type | 得到客户端浏览器的名称与主版本号 |
Version | 得到客户端浏览器的版本号 |
Beta | 如果客户端浏览器是作为一个 beta 版发布的,则返回 true |
AOL | 如果客户端浏览器是一个 AOL(美国在线)浏览器,则返回 true |
Platform | 提供客户端用户使用的操作系统平台名称 |
Win16 | 如果客户端是一个基于 Win16 的计算机,则返回 true |
Win32 | 如果客户端是一个基于 Win32 的计算机,则返回 true |
ClrVersion | 提供客户端所安装的 .NET CLR 的最高版本号。你也可以使用 GetClrVersions() 方法得到所有安装的 CLR 版本的信息。这个设置仅仅当你在网页中嵌入了 .NET Windows Form 控件的时候才有意义。客户端浏览器并不需要 CLR 来运行普通的 ASP.NET 网页。 |
ActiveXControls | 如果客户端浏览器支持 ActiveX 控件,则返回 true |
BackgroundSounds | 如果客户端浏览器支持背景声音,则返回 true |
Cookies | 如果客户端浏览器支持 Cookie,则返回 true |
Frames | 如果客户端浏览器支持框架,则返回 true |
Tables | 如果客户端浏览器支持 HTML 表格,则返回 true |
JavaScript | 此属性已经过时(推荐你测试 EcmaScriptVersion 属性) |
VBScript | 如果客户端浏览器支持 VBScript,则返回 true |
JavaApplets | 如果客户端浏览器支持嵌入的 Java 小程序,则返回 true |
EcmaScriptVersion | 得到客户端浏览器支持的 ECMA 脚本的版本号 |
MSDomVersion | 得到客户端浏览器支持的微软 HTML DOM 的版本号 |
Crawler | 如果客户端浏览器是一个 Web 爬虫(crawler)搜索引擎,则返回 true |
HttpBrowserCapabilities 类有一个显著的局限,即仅限于判断预期的浏览器内置功能。它不能判断浏览器功能的当前状态。例如,浏览器支持 JavaScript ,然而用户却可以关闭脚本功能。换言之,你只能知道浏览器应该能做什么,但却不会知道浏览器当前能够做什么。客户端不会发送任何关于浏览器是如何配置的信息。
你可以依赖于 HttpBrowserCapabilities 来判断浏览器功能,进而基于此信息构建程序逻辑。但也需要容忍偶然的错误。一个更健壮的方法是编写自己的代码来实际测试所需的功能是否被支持。例如,对于 Cookie(跨 2 个网页),你可以尝试设置一个 Cookie,然后尝试读取它,读取失败则说明 Cookie 支持被禁用了。同理,你也可以添加一段 JavaScript 代码在页面里,写入一个隐藏的表单变量,然后在服务器端检测它。这些步骤有点笨拙且零碎,但这是绝对确定浏览器功能的唯一方式。
覆盖浏览器类型侦测
ASP.NET 也让你能够完全决定页面如何呈现而不必依赖于自动的浏览器侦测。技巧在于通过编程(在 Page.PreInit 阶段)或者声明性(通过 Page 指令)的设置 Page.ClientTarget 属性。设置了 Page.ClientTarget 属性后,自动的浏览器侦测就被禁用了,并且 ASP.NET 在之后的请求中使用你指定的浏览器设置。
使用 Page.ClientTarget 属性唯一要注意的一点是只能够使用定义的假名。每个假名被映射到一个特定的用户代理字符串(这个用户代理的浏览器设置在对应的 .browser 文件中声明)。
例如,你要测试页面在旧浏览器(如 IE 5.0)上的呈现效果。首先,需要在 <clientTarget> 节创建假名,它把正确的用户代理字符串映射到我们使用的假名上。此时,它的假名是 ie5 。
<configuration>
<system.web>
<clientTarget>
<add alias="ie5" userAgent="Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0)"/>
</clientTarget>
...
</system.web>
</configuration>
现在,你可以强制页面使用这个假名呈现自身,就好像 IE 5.0 在请求一样(通过在 Page.ClientTarget 特性):
<%@ Page ClientTarget=”ie5” … />
自适应呈现
理想而言,我们能够在所有主流浏览器上呈现正确的标记。但有些情况下,你会发现自己在编写浏览器特定的逻辑。最糟的时候,看起来会像这样:
protected override void RenderContents(HtmlTextWriter writer)
{
base.RenderContents(writer);
if (Page.Request.Browser.EcmaScriptVersion.Major >= 1)
{
writer.Write("<i>You support JavaScript.</i><br />");
}
if (Page.Request.Browser.Browser == "IE")
{
writer.Write("<i>Output configured for IE.</i><br />");
}
if (Page.Request.Browser.Browser == "Netscape")
{
writer.Write("<i>Output configured for Netscape.</i><br />");
}
}
一个更好的办法是让控件输出标准呈现,同时创建一个控件适配器来为特定浏览器输出特定的呈现。这种控件适配器模型使得创建一个可以用于多种类型设备的控件成为可能。其中最可贵的是,因为控件与适配器分离,第三方的开发者可以为现存的控件编写适配器,以便使得这些控件能够用于其他平台。
你可以通过 .browser 文件把任意控件与一个适配器关联起来。例如,你可以创建一个 FirefoxSlideMenuAdapter 适配器来修改 SlideMenu 控件中呈现的代码,以便它能更好的在 Firefox 浏览器里工作,然后即可编辑 mozilla.browser 文件,以特别表明在所有的 Firefox 浏览器里,这个适配器可以用于所有的控件。
控件适配器被插入呈现过程而起作用。适配器在 Web 控件生命周期中的每一个状态都被 ASP.NET 调用,这就使得适配器能够调整呈现过程并且处理其他细节,如设备专属的试图状态逻辑。
为了创建一个适配器,需要从 System.Web.UI.Adapters.ControlAdapter 派生出一个新类(你的控件是从 Control 类派生)或者从 System.Web.UI.WebControls.Adapters.WebControlAdapter 派生出一个新类(你的控件从 WebControl 类派生)。然后,你可以通过覆盖一些方法实现想要的功能。每一个方法都对应于自定义控件类中的一个方法,当你在控件适配器里覆盖了一个方法之后,控件适配器的方法将代替控件中的方法被使用。
注意:
与服务器控件一样,将控件适配器放在单独的 DLL 程序集中总是一个比较好的选择。
你可以覆盖 ControlAdapter 中的方法,如 Onit()、Render()、RenderChildren()。对于 WebControlAdapter 类,你也可以覆盖 RenderBeginTag()、RenderEndTag()、RenderContents()。