第3章 WEB窗体

来源:互联网 发布:企业网络构架方案 编辑:程序博客网 时间:2024/06/16 00:33

(译自 Pro ASP.NET 2.0 IN C# 2005 ,很多地方译得不好,欢迎批评指正)

3 WEB窗体

ASP.NET页面(正式名称叫Web窗体)是ASP.NET应用至关重要的一部分。它们提供了Web应用的真实输出,即客户请求网页并在浏览器里浏览。

尽管网页并不是新事物,但Web窗体却是ASP.NET独有的。本质上,Web窗体允许你Windows应用那样使用基于控件的接口来创建Web应用。为了运行ASP.NET Web窗体,ASP.NET ISAPI扩展读取整个文件,产生相应的对象,并且释放一系列事件。使用完全面向对象的代码可以对这些事件进行处理。

本章深入介绍了Web窗体。你将学习到它们的工作原理和如何使用它们来创建简单的网页。你还将深入了解页面处理的生命周期和ASP.NET服务器控件模型。

WEB窗体在.NET2.0里的变化

l         Web窗体模型在ASP.NET2.0里有了些许的调优,而不是巨大的变化。一些变化在表面是看不出来的。例如,页面在生存周期中包含了更多的事件,那样它们就可以插入其它的ASP.NET功能,如主题和新的数据绑定模型。

l         视图状态分片(View state chunking):并不将所有视图状态信息存放在单个域中,可以让ASP.NET将其存入几个大小适当的域中。这个特征主要解决代理服务器的问题,这些代理服务器不支持大块的隐含输入域。

l         XHMTL支持(XHTML support):Web窗体使用XHTML兼容的标签来进行渲染,这是从ASP.NET 1.1里移植过来的,但在.NET2.0中使用几乎不会有任何问题。

l         可编程的页头(Programmable page header):网页的<head>部分现在是HtmlHead服务器控件的一个实例。可以通过程序改变标题、添加metadata或者链接的样式表到页面。

如果你是一个经验丰富的ASP.NET 1.X开发者,在阅读本章的过程中,你会体验到这些变化。

 

页面处理(Page Processing

ASP.NET的核心思想,是创建一个模型来帮助开发者采用与Windows开发者在桌面应用中构建定做的(made-to-measure)窗口一样的方法来快速开发Web窗体。当然,Web应用与传统的胖客户端应用大大不同。有两个关键的绊脚石:

l         Web应用在服务器端执行(Web applications execute on the server):例如,假定你创建了一个窗体,允许客户选择产品记录并且更新信息。用户在浏览器中执行这些任务,但为了执行要求的操作(如更新数据库),你的程序需要在Web 服务器上执行。为了处理这个界线(divide),ASP.NET采用了回滚(postback)技术,执行一定的动作时,客户端就发送页面(和用户输入的所有信息)到服务器。一旦ASP.NET接到了页面,便触发服务器端相应的事件来通知你的程序。

l         Web应用是无状态的(Web applications are stateless):换句话说,在渲染过的HTML发送给用户之前,网页对象被销毁,所有客户端特定的信息都被丢弃。这个模型在高可扩展性和重负载的应用中表现良好,但要创建无缝的用户体验还有些困难。ASP.NET内置了几个工具来帮助克服这个障碍。最引人注目的是视图状态(view state),它是一种坚持机制(persist mechanism),能够自动在已渲染的HTML页的隐含域里嵌入页面信息。

在后面的小节中,你将会学习回滚和视图状态功能。同时,这些机制为底层HTMLHTTP细节的抽象,允许程序员根据对象和事件来进行操作提供了方便。

 

HTML窗体(HTML Forms

如果熟悉HTML,你就会知道将客户端的数据发送给服务器的最简单的方法就是使用<form>标签。在<form>标签里,你可以放置<input>标签来代表基本的用户接口(UI)要素,如按钮(button)、文本框(text box)、列表框(list box)和单选按钮(radio button))

例如,下面的窗体中有一个提交(sumbmit)按钮、两个检查框、两个文本框,一共5<input>标签:

<html>

<head>

<title>Programmer Questionnaire</title>

</head>

<body>

<form method="post" action="page.aspx">

<p>Enter your first name:&nbsp;

<input type="text" name="FirstName"/><br>

Enter your last name:&nbsp;

<input type="text" name="LastName"/><p>

<p>You program with:<br>

&nbsp;&nbsp;&nbsp;

<input type="checkbox" name="C"/>C#<br>

&nbsp;&nbsp;&nbsp;

<input type="checkbox" name="VB"/>VB .NET<br><br>

<input type="submit" value="Submit" id="OK"/>

</p>

</form>

</body>

</html>

3-1显示了这个最基本的页面的浏览结果:

一个简单的HTML窗体

3-1 一个简单的HTML窗体

当用户点击提交按钮时,浏览器收集每个控件的当前值,并且在一个长字符串里将它们粘在一起。通过HTTP POST操作,这个字符串随后被发送给<form>标签里指定的网页(在本例中是page.aspx)。

在这个例子中,Web服务器可能接收到带有该信息串的请求:

FirstName = Matthew&LastName = MacDonald&C=on&VB =on

浏览器构建这个串时遵循一些规则。信息是作为一系列名字/值对来发送的,名字与值之间有一个等号(=),多个名字/值对之间用and符号(&)来分隔。检查框在没被选定的时候会被忽略,选定后,以on作为检查框的值。关于HTML窗体标准的完整内容,请参阅http://www.w3.org/TR/REC-html40/interact/forms.html

事实上,所有服务器端的编程框架在原始窗体数据上添加了一个抽象层。它们解析这个字符串,并且以更有用的方式来使用它。例如,JSPASPASP.NET都允许你使用一个瘦对象层来获取窗体控件的值。在ASPASP.NE中,可以在Request.Form集里通过名字查寻值。在ASP.NET里的示例如下:

String firstName = Request.Form[“FirstName”];

在真实的POST信息上面构建一个瘦的装饰层(thin veneer)是非常有用的,但这还远不是面向对象框架。这也是ASP.NET进行更多改进的原因。当页面传回ASP.NET,它析取串值,组装窗体集(为了向ASP代码提供兼容),然后配置对应的控件对象,使你能够使用如下更加直观的语句来获取信息:

String firstName = txtFirstName.Text;

这段代码也增强了类型的安全。也就是说,如果你要获取检查框的状态,你将会接收到一个布尔的truefalse值,而不是单词on。这样,开发人员就可以避免编写奇怪的HTML代码。

注意,在ASP.NET中,控件放置在<form>标记中间。这个标签有runat = “server”属性,表明允许它在服务器端工作。ASP.NET不允许创建多个服务器端的窗体标签,尽管可能使用交叉传递技术来创建向另一个页面传递信息的网页,第6章对此有详细介绍。

 

动态接口(Dynamic Interfaces

显然,控件模型使获取窗体信息更为方便,但更为引人注目的是它简化了添加信息到网页的过程。几乎所有的Web控件都是可读或可写的,你可以读取一样简便地设置文本框控件的Text属性。

举个例子,考虑一下,如果想在网页上更新一段文本来反映用户先前输入的信息,会发生什么呢?在经典的ASP中,你可能需要查找合适的位置来插入脚本块来编写原始的HTML。下面的示例中,用突出的颜色显示了的欢迎信息:

string message = "<span style=/"color:Red/">Welcome " +

FirstName + " " + LastName + "</span>";

Response.Write(message);

另一方面,如果在ASP.NET中定义标签控件,问题就变得更为简单:

<asp:Label id="lblWelcome" runat="server" />

这样,就可以简单地设置它的属性:

lblWelcome.Text = "Welcome " + FirstName + " " + LastName;

lblWelcome.ForeColor = Color.Red;

这种代码主要有这几个优点:首先,编写更容易(不会出现错误编写错误)。在本例中,这点好处看起来微乎其微。但如果需要动态渲染包含了链接、图像和样式的完整的HTML块时,其优点就显出来了。

其次,基于控件的代码更容易放置到网页中。你可以在任何动作产生的地方编写相应的ASP.NET代码。相比而言,在经典的ASP中,你需要考虑内容在页面的什么位置显示,然后合理组织脚本代码块。如果页面有几个动态的区域,脚本块就会出混乱,显示不出清晰的组织和关系。

最后,控件模型精细但同样惊人的优点是它隐藏底层HTML细节的方法。不仅使你不需要学习HTML就能编写代码,而且使你的网页支持多种不同的浏览器。因为控件对自身进行渲染,它能够调整输出来支持广泛的浏览器、增强的客户端特征,甚至是其它HTML相关的标准,如XHTMLWML(用于移动浏览器)。本质上,程序与HTML的关系变得更加松散。

 

ASP.NET事件模型(the ASP.NET Event Model

经典ASP使用线性处理模型。这意味着网页上的代码从开始到结尾按顺序执行的。正因为如此,即便只是一个简单的网页,ASP程序员仍需要编写大量的事件代码。经典的例子是一个网页包含了3个提交按钮,有3个不同的操作。提交网页时,脚本代码需要小心区分点击了哪个按钮,然后正确响应动作。

ASP.NET通过提供新的事件驱动的模型,使事件处理有了全新的改变。在这个模型中,将控件添加到Web窗体,然后确定相应的响应事件。每个事件处理器都封装在一个单独的方法中,这样保持了页面的简洁和组织性。这个模型其实并不新颖,但直到ASP.NET出现之后,它才成为胖客户端应用(rich client application)中Windows UI编程的专门的域。

那么,ASP.NET是如何工作的呢?事实上,这是相当显而易见的。下面是简要的介绍:

1、    网页在第一次运行时,ASP.NET创建页面和控件对象。执行初始化代码,然后页面渲染为HTML并返回给客户端。页面对象在服务器内存中释放。

2、    在某个时刻,用户进行操作,如单击一个按钮,触发回传的操作。此时,窗体数据随着页面提交。

3、    ASP.NET捕获返回的页面,并且重新创建页面对象,并应用视图状态信息。

4、    ASP.NET随后检查触发回送的操作,并唤起对应的事件(如Button.Click),执行事件处理代码。通常情况下,会执行一些服务器端的操作(如更新数据库或从文件读取数据),并且修改控件对象来显示新的信息。

5、    修改后的页面渲染为HTML并返回给客户端。页面对象从内存中释放。如果发生另一次回传,ASP.NET重复第24步的过程。

换句话说, ASP.NET并不直接使用窗体数据来配置控件对象,一般情况下,它使用窗体来确定释放了哪个事件。例如,最后一次回传以后,如果发现文件框中的文本发生改变,ASP.NET就触发一个事件来通知页面,是否对其进行响应取决于你的代码。

请记住,由于HTML是完全无状态的,所有状态需要ASP.NET重新创建才可获得,因此事件驱动模型是竞争的。在这种背景下,ASP.NET在后台执行好几个任务以支持这个模型,这将在后面的小节中看到。这个概念的优美之处在于入门级程序员不需要熟悉系统基础就能使用服务器端的事件。

 

自动回传(Automatic Postbacks

当然,在前面描述的事件系统里有一个缺陷。Windows开发人员早已非常熟悉丰富的事件模型,允许程序响应鼠标移动、按键和精细的控件交互。但在ASP.NET中,客户端事件是发生在客户端的,而服务器进程则位于Web服务器上。这意味着在响应事件时有大量棘手的负载需要处理。正因为如此,在ASP.NET世界里快速释放事件(如鼠标移动)是完全不切实际的。

注意,如果你想完成一定的UI效果,你可能需要使用客户端JAVAScript处理快速事件,如鼠标移动。或者,你可以使用内置了这些事件的自定义的ASP.NET控件。但是,所有的业务代码都必须在安全、功能完备的服务器环境中执行。

如果你熟悉HTML窗体,你会知道这种提交页面的基本方法,即通过点击提交按钮来提交。如果你在使用标准的HTML服务器控件,这仍是你唯一的选择。但是,一旦页面回传,ASP.NET就能够同时释放其它事件(即,指明输入控件中的值已经发生改变的事件)。

显而易见,这点对于创建丰富的Web窗体是很不够的。幸运的是,ASP.NET Web控件使用自动回传(automatic postback)功能来扩展了这个模型。有了这个功能,输入控件可以释放不同的事件,而且服务器端的代码能够立即对其进行响应。例如,当用户点击一个检查框、改变列表中的选择,或者文本框中的文本并且移动到它其区域时,就会触发回传。这些事件虽然不象Windows应用中的事件那样易获得,但它们相对于提交按钮,则是很大的进步。

 

自动回传机理(Automatic Postbacks “Under the Hood”

为了使用自动回传,只需要简单地将Web控件的AutoPostBack属性设置为true(默认值是false,目的是保证无事件响应时优化性能)。这样,ASP.NET使用客户端的JavaScript来桥接客户端和服务器端的程序。

工作过程是这样的:如果你创建了包含一个或多个Web控件的网页,Web控件被设置为使用AutoPostBack(即将其值设置为true),那么,ASP.NET添加名为_doPostBack()JavaScript函数来渲染HTML页。调用时,它触发回传,将带有所有信息的页面传回Web服务器。

ASP.NET也添加了两个隐含的输入区域,用于_doPostBack()函数传递信息到服务器。这个信息包含了触发事件的控件ID和其它相关信息。区域初始为空。如下所示:

<input type="hidden" name="__EVENTTARGET" value="" />

<input type="hidden" name="__EVENTARGUMENT" value="" />

_doPostBack()函数里应当具有设置有关事件的值和提交窗体功能的功能。下面是_doPostBack()的示例:

<script language="javascript">

<!--

function __doPostBack(eventTarget, eventArgument) {

var theform = document.Form1;

theform.__EVENTTARGET.value = eventTarget;

theform.__EVENTARGUMENT.value = eventArgument;

theform.submit();

}

// -->

</script>

记住,_doPostBack()函数由ASP.NET自动产生。当添加更多的AutoPostBack控件到页面时,这段代码会变得更长,因为必须为每个控件设置事件数据。

最后,任何一个将AutoPostBack属性设置为true的控件都会使用onClickonChange属性来连接到_doPostBack()函数。这些属性表明,为了响应客户端的JavaScript事件onClickonChange,浏览器应当执行相应动作。

下面的示例显示了一个用于自动回传的列表控件的标签,标签命名为lstCountry。无论何时用户改变了列表的选择项,就会释放客户端的onChange事件。浏览器于是调用_doPostBack()函数,将页面发送回服务器。

<select id="lstCountry" onchange="__doPostBack('lstBackColor','')"

language="javascript">

换句话说,ASP.NET使用_doPostBack()函数作为中介,自动将客户端的JavaScript事件转换为服务器端的ASP.NET事件。如果你是一位有经验的ASP开发人员,你可能要为传统的ASP网页手动创建类似的解决方法。但在ASP.NET中,自动处理了这些细节,大大节约了时间。

提示,请记住,ASP.NET包含两个控件模型:纯粹的HTML服务器控件和功能更丰富的web控件。自动回传仅web控件中可用。

 

视图状态(View State

ASP.NET模型中,最为重要的要素是新的视图状态机制。视图状态解决了由于HTTP与生俱来的无状态(变化信息丢失)带来的问题。

页面每次回传,你接收所有用户在<form><input>控件中输入的信息。ASP.NET于是以它本来的状态加载网页(基于布局和定义),并且根据这些新信息对页面进行调整。但在动态的Web窗体中,程序改变的内容更多,问题就出现了。例如,你可能编程改变页头的颜色,修改一段静态文本,隐藏或显示控件面板,甚至绑定一个完整的表的数据到Grid。这些动作在它们初始状态时使页面发生变化。然而,这些信息并没有在回传的窗体数据中反映出来。这意味这些信息会在回传后丢失。传统的方法是,使用简单的cookie、基于会话的cookie和各种其它的工作区,可以克服无状态的问题。

为了解决这个限制,ASP.NET设计了综合的状态序列化机制。本质上,一旦页面代码运行完毕(渲染成HTML发送到客户端之前),ASP.NET检查页面上所有控件的所有属性,如果任何一个属性在初始状态过程中发生了改变,ASP.NET将这个信息保存在名字/值集中。最后,ASP.NET将所有收集到的信息序列化为Base64的字符串(Base64串保证了没有无效的HTML特殊字符)。然后插入<form>节中间作为新的隐含字段。

页面回传时,ASP.NET执行如下步骤:

1ASP.NET重新创建基于默认值的页面和控件对象。因此,页面与第一次被请求时具有相同的状态。

2、接下来,反序列化并应用视图状态信息,并且更新所有控件,使页面返回到最后发送给用户前的状态。

3、最后,ASP.NET基于传回的窗体数据调整页面。例如,如果用户在文本框控件中输入了新的文本,或者在列表框里有了新的选择,那些信息存于窗体集中,ASP.NET使用这些信息来修改对应的控件。完成这一步之后,页面反映当前状态,正如呈现给用户一样。

4、现在该轮到事件处理代码工作了。ASP.NET触发相应的事件,响应程序相应地改变页面,跳转到新页面,或者执行一个完全不同的操作。

使用视图状态,是一个了不起的解决方案,因为服务器资源在每次请求之后就释放了,因此保证了可扩展性,使服务器响应成百上千的请求而不会瘫痪。然后,这仍会付出代价,因为视图状态存储于网页之中,导致页面尺寸的增加。这对客户端的影响是双重的,因为客户端不仅需要接受一个更大的页面,而且客户端在下一次回传时,仍需要将隐含的视图状态数据发送回服务器,从而导致接收和发送网页所需时间更长。对于一个简单的页面,负载很小,但如果配置了复杂的、数据量大的控件,如GridView,视图状态信息增长到一定程度会耗尽资源。在这些情况下,可以通过设置EnalbeViewStatent属性为false禁用视图状态。然后,在每个回传里重新初始化控件。

注意,即便将EnableViewState设置为false控件仍能够获取少量的视图状态信息,保证其能正常工作。这种特有的视图状态信息也叫控件状态,它永远也不能被禁用。然而,在设计良好的控件中,控件状态的大小应该非常小。在ASP.NET2.0中,控件状态是一个新的功能,在第27章设计自定义控件时,你将了解到它的工作原理。

ASP.NET仅仅在页面和控件属性中使用视图状态,在成员变量和其它数据中并没有这一过程。但是,在本书的后面,你仍可以将其它的数据类型放入视图状态中,并且随后手工获取这个信息。

3-2提供了端到端的浏览,汇总了页面请求的全部概念。

ASP.NET页面请求

3-2 ASP.NET页面请求

注意,作为一个ASP.NET程序员,记住这一点是很重要的,即Web窗体在每一轮往返过程中都要重新创建。它在内存中保存的时间不会比将其渲染一个请求的时间更长。

 

视图状态机理(View State “Uder the Hood”

如果你仔细查看了ASP.NET渲染的HTML,你可以很容易发现带了视图状态信息的隐藏输入域。下面的示例显示了使用一个标签Web控件的页面,标签设置为动态信息“Hello world”:

<html>

<head runat="server">

<title>Hello World Page</title>>

</head>>

<form name="Form1" method="post" action="WebForm1.aspx" id="Form1">

<div>

<input type="hidden" name="__VIEWSTATE" value="/wEPDwUKLTE2MjY5MTY1

NQ9kFgICAw9kFgICAQ8PFgIeBFRleHQFDEhlbGxvIFdvcmxkIWRkZPsbiNOyNAufEt7OvNIbVYc

GWHqf" />

</div>

<div>

<input type="submit" name="Button1" value="Button" id="Button1" />

<span id="lbl">Hello, world</span>

</div>

</form>

</html>

视图状态串是不可阅读的,它看起来一系列随机的字符。然而,不可忽视的是,用户要描述这个数据却轻而易举。下面的小段.NET代码完成了这个工作,并且将信息解码到网而中。

// viewStateString contains the view state information.

// Convert the Base64 string to an ordinary array of bytes

// representing ASCII characters.

byte[] stringBytes = Convert.FromBase64String(viewStateString);

// Deserialize and display the string.

string decodedViewState = System.Text.Encoding.ASCII.GetString(stringBytes);

lbl.Text = decodedViewState;

在网页中,你可以看到下面这样的内容:

可以看到,控件文本就一目了然了(其它不可打印的字符是空文本框的渲染结果)。这意味着,如果不采取额外的措施,视图状态并不是存储不允许看到的敏感信息的最佳位置。这类敏感数据应当存放在服务器上。另外,基于视图状态,你不能够确定是否客户端篡改了视图状态数据,从而威胁应用的安全。

幸运的是,要提高视图状态的安全性是可能的。你可以启用自动哈希编码来阻止视图状态篡改,甚至可以对视图状态进行加密来防止被解码。这个技术使隐含域从笨拙的工作区提升为更为强健和高质量的架构。在第6章中可以了解到这些技术。

注意,如果你曾经使用ASP.NET1.1编过程,你可能会注意到,在ASP.NET2.0中的视图状态序列化模型与以前版本有所不同。不再采用分号和尖括号来分离值的方式,ASP.NET2.0使用了不可打印字符,使处理字符串更为高效(因为从标记中区分序列化数据更容易)和精简。ASP.NET2.0也为许多普通数据类型减少了序列化的大小,包括布尔值、整型、多次重复的字符串(这很常见,因为不同的控件常常有相同的属性名)。这细微的改变能够产生显著的效果。根据序列化的视图状态的分隔符的数量、使用的数据类型,数据负载重的控件,能够将其视图状态缩小一半甚至更多。

 

视图状态分片(View State Chuncking

隐含的视图状态域并没有大小限制。但是,一些代理服务器和防火墙会禁止隐含域大于某个值的网页通过。为了解决这个问题,可以使用视图状态分片(view state chunking)技术,它能自动将视图状态切分到多个域,保证隐含域的大小不会超过设置的阀值。

为了使用视图状态,你仅需要设置maxPageStateFieldLength属性,这个属性是web.config文件的<pages>的属性。它指定了视图状态的最大大小(单位为字节)。下面的示例将视图状态上限设置为1KB

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

<system.web>

<pages maxPageStateFieldLength = "1024" />

</system.web>

</configuration>

当请求的页面所产生的视图状态大于这个值时,就会创建几个隐含的输入域:

<input type="hidden" name="__VIEWSTATEFIELDCOUNT" value="3" />

<input type="hidden" name="__VIEWSTATE" value="..." />

<input type="hidden" name="__VIEWSTATE1" value="..." />

<input type="hidden" name="__VIEWSTATE2" value="..." />

记住,视图状态分片是一个简单的机制,是为了避免特定的代理产生的问题(相对来说发生较少)。视图状态分片不能改善性能(反而增加了少量额外的序列化负载)。在大量良好的设计当中,应该尽可能减少视图状态中的信息,这样可以提升性能。

 

XHTML兼容

ASP.NET1.1中移植过来的大部分ASP.NET 2.0Web控件都是与XHTML1.1标准兼容的。但是,网页其余部分的动作规则取决于你,ASP.NET并没有采取任何措施来强制XHTML与你的网页兼容。

注意,XHTML支持并不向网页中添加任何函数,网页并不一定要使用HTML4.01。但是,由于XHTML是一个严格的标准,它仍有一些好处。例如,你可以确认XHTML网页来捕获在特定浏览器中无法正常工作的细小错误。最为重要的是,XHTML也是有效的XML文档,应用程序对其进行读取和分析更为容易,并且使其具有可扩展性。现在普遍认为XHTML将会替代HTML。了解XHTML的更多内容,参阅规范 http://www.w3.org/TR/xhtml11

除了几个例外情况,所有的ASP.NET服务器控件能够使用XHTML兼容的标签来渲染自身。这意味着标签要遵循XHTML规则,这些规则包含:

l         标签和属性必须是小写。

l         所有元素必须关闭,要么是专门的关闭标签(<p></p>),要么使用空标签进行自关闭(<br/>)。

l         所有的值必须包含在引号之中(如,runat = “server”)。

l         必须使用id属性而不使用名字属性。

XHTML移除了一些在HTML中支持的功能,如框架和不使用CSS的内嵌格式。在多数情况下, XHTML能够满足需要。当然,仍可以使用target属性来创建一个链接,目标页面在新窗口打开。下面的ASP.NET控件可能使用target属性:

• AdRotator

• TreeNode

• HyperLink

• HyperLinkColumn

• BulletedList

使用target属性,在现代的浏览器中不会出现问题。但是,如果你想要创建完全兼容XHTML的站点,应该避免使用这些控件。

需要注意的是,目前使用XHTML不会获得更多的好处。但是,一些公司和组织基于对未来标准的展望,要求使用XHTML。未来,XHMTL将使设计网页变得更为容易,能够适应不同类型的平台,能够被其它应用处理,并且使用新的标签功能使其具有扩展性。例如,你可能使用XSLTXSL转换)、另一种基于XML的标准,将XHTML文档转换为其它文档。而这些功能在HTML网页中无法获得。

 

文档类型定义(Document Type Definitions

每一个XHTML文档都从文档类型定义开始的,文档类型定义定义了页面所使用的XHTML类型。尽管ASP.NET Web控件对XHTML是兼容的,但是ASP.NET并不会自动认为你要创建一个XHTML兼容的网页,因此,它不会自动产生文档类型定义。如果你要创建一个XHTML网页,则需要添加文档类型定义(doctype)。文档类型定义必须放置在网页的标记部分,紧随着页面指令(Page directive)之后。这样,文档类型定义将作为文档的第一行被渲染,而这正是所期望的结果。

下面的示例定义了支持XHTML1.1的网页:

<%@ Page Language="C#" AutoEventWireup="true"

CodeFile="TestPage.aspx.cs" Inherits="TestPage_aspx" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"

"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >

<head runat="server">

<title>Untitled Page</title>

</head>

<body>

<form id="form1" runat="server">

<div>

...

</div>

</form>

</body>

</html>

页面也为<html>元素定义了XML名字空间。这是XHTML所要求的另一细节,但ASP.NET并不会自动提供。

注意,在Visual Studio中创建网页时,为<html>元素定义了XML名字空间,并且为XHTML1.1添加了doctype。你可以修改doctype,甚至移除它。

如果你并不想支持全部的XHTML1.1标准,可以进行一些折衷。通用的方法是使用XHTML1.0 过渡,它增强了XHTML的结构规则,但允许使用被样式表替代和废弃的HTML格式化特征。下面doctype的示例:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

" http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd ">

XHTML 过渡文档类型(doctype)认为HTML中已经被废弃框架。如果需要创建框架页面,考虑使用XHTML1.0 框架集文档类型(frameset doctype):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">

记住,ASP.NET服务器控件使用任何一种doctype都能正常工作(即便浏览器只支持HTML也是一样)。在网页中采用什么层次的兼容标准完全取决于你。

注意,如果希望采用XHTML标准,可以使用MIME 内容类型application/xhtml+xml替代标准的text/html。这个变化是XHTML推荐的,用来帮助浏览器和其它应用区分传统的HTML网页和XHTML。不幸的是,本书付梓之时,Internet Explorer仍然不支持application/xhtml+xml内容类型(而其它现代的浏览器都提供了支持)。如果仍想实现这种改变,只要添加这个属性到页指令中就可以了: ContentType=“application/xhtml+xml”

 

XHTML确认(XHTML Validation

ASP.NET的核心控件遵循XHTML规则,但要确认网页是XHTML兼容的,需要对添加的任何静态内容进行确认,保证它们也遵循这些规则。Visual Studio内嵌的检查器可以帮助检查。在HTML源编辑工具栏的下拉列表中选择target标准。便如,如果选择了XHTML1.1Visual Studio标记了结构性的错误和废弃的标签(图3-3所示)。

在Visual Studio中检验XHTML1.1

3-3 Visual Studio中校验XHTML1.1

请记住,如果违反了XHTML规则,浏览器不会标记错误。要创建一个XHTML兼容的页面,可以使用Internet Explorer的智能感知,但这必须在网页源文件中进行,同时并不能保证最终的页面没有包含违反XHTML的内容。例如,你可能使用了第三方的控件,而此控件渲染的标签并不是XHTML兼容的。要对网页进行严格测试,需要使用第三方的检查器来请求网页,并且扫描所有错误。

一个很好的免费资源是W3C的校验服务 http://validator.w3.org.。只需要简单地将URL输入网页中,然后点击check。也可以上传一个文件来检查它,但这时必须保证上传的文件是最终的渲染页面,而不是.aspx源码。也可以通过IE查看页面渲染后的内容,点击视图—>查看源文件。

 

禁用XHTML渲染(Disabling XHTML Rendering

如果正在请求的浏览器支持HTML4.0或者更高版本,ASP.NET服务器控件会自动使用XHTML标记。但是,可能有一种不常见的情况,即你希望禁用XHTML兼容渲染。当客户端的JavaScript依赖于一些在XHTML中不允许的标签时,这种情况就会出现。为了解决这个问题,可以返回去使用ASP.NET1.1中的HTML渲染。

为了返回去使用纯HTML渲染,只需要将Web.config文件中xhtmlConformance元素的mode属性设置为legacy。另两种可选择的方案是transitional(默认)strict。选择最适合你的doctype的选项。下面是一个示例:

<system.web>

<xhtmlConformance mode="legacy" />

</system.web>

启用了过期的(obsolete)渲染后,ASP.NET控件不再使用任何XHTML优化,而这些优化在HTML4.01中并不要求严格兼容。例如,它们渲染了标准的HTML元素,如<br>,而不是正确的XHTML版本<br/>。但是,即便启用了过期的渲染,ASP.NET仍不会自动清除<html>标签中的名字空间,也不会移除doctype(如果已经存在于网页之中)。

注意,ASP.NET不保证非XHTML渲染将会在ASP.NET的未来版本中得到支持,所以对其的使用应该仅限于特殊场合。

 

Web窗体处理过程(Web Forms Processing Stages

在服务器端,对于ASP.NET Web窗体的处理是分阶段进行的。在每一个阶段,不同的事件被唤起。这允许在任何一阶段,向网页中插入处理流,并且进行你需要的响应。

下面列出了ASP.NET网页处理流中的主要阶段:

l         页面框架初始化

l         用户代码初始化

l         校验

l         事件处理

l         自动进行数据绑定

l         清除

请记住,对于每一次Web请求,这些阶段是独立进行的。图3-4显示了这些阶段的顺序。存在的阶段远不止列出这些,但是列出这些是在编写自己的ASP.NET控件时经常用到的,而且它们不会由页面直接处理。

ASP.NET页生命周期

3-4 ASP.NET页生命周期

后面的小节中,你将深入了解每个阶段,并且会对一个简单的Web页面示例进行测试。

 

页面框架初始化(Page Framework Initialization

这是ASP.NET创建网页的第一个阶段。这一阶段产生.aspx网页中标签定义的所有控件。如果页面不是第一次请求(也就是说是回传的页面),ASP.NET将反序列化视图状态信息并应用到所有控件。

在这一阶段,网页的Page.Init事件释放。但是,这个事件很少由网页进行处理,因为这一阶段尚未创建控件对象,也没有加载视图状态信息,执行页面初始化太早了点。

 

用户代码初始化

在这个处理阶段,Page.Load事件被释放。大多数网页处理这个事件,执行任何要求的初始化(如添充动态文本或配置控件)。

无论页面是首次被请求还是回传的请求的一部分,Page.Load事件总是会释放。幸运的是,ASP.NET提供了允许程序员区分首次加载和后续加载的方法。这为什么很重要?首先,由于视图状态是自动维护的,你必须在第一次页面加载时从动态数据源获取数据。而在回传页面中,除了根据根据视图状态信息存储控件属性外,不需要做任何其它事情。如果重新创建信息(例如,从数据库查询数据)的代价很大,这样做就可以大幅提升性能。其次,也有其它的场合,如编辑窗体和drill-down页面,需要在首次加载和后续加载的页面中显示不同的接口,这就需要对首次还是后续加载进行区分。

为了确定页面的当前状态,你可以检查静态的Page.IsPostBack属性,这个属性在网页第一次请求中被设置为false。示例如下:

if (!Page.IsPostBack)

{

// It's safe to initialize the controls for the first time.

FirstName.Text = "Enter your name here";

}

注意,IsPostBackPage类的静态属性。它总是基于当前的页面返回信息。也可以使用实例属性IsPostBack(如this.IsPostBack),返回相同的值,这种方法往往更受人偏爱。

记住,视图状态存储每个已变化的属性。Page.Load事件中对控件进行初始化也算作改变,因此接触到的任何控件值都将保存在视图状态中,这不必要地增大了网页的大小并且延长了传输的时间。为简化视图状态,并且保持页面尺寸较小,应避免在代码中对控件进行初始化设置。相反,在控件标签中设置属性(在源文件状态手工编辑标签或者使用属性窗口)。这样,这些细节不会保存在视图状态中。在代码中使初始化控件更为方便的时候,可以通过设置EnableViewStatefalse来禁用控件的视图状态,并且在Page.Load事件释放时初始化控件,无论当前的请求是不是回传的。

 

校验(Validation

ASP.NET引入了新的校验控件,能够自动校验其它用户输入控件,并且显示出错信息。这些控件在页面加载后其它事件发生前释放。但是,校验控件主要为那些大部分自供给(这意味着你不需要对校验事件进行响应)的控件服务。你可以检查页面在其它事件句柄里是否有效(使用Page.IsValid属性)。第4章详细讨论了校验器控件。

 

事件处理(Event Handling

现在,页面完全加载并且进行了校验。ASP.NET释放所有从最后一次回传以后发生的事件。大部分ASP.NET事件是下面两种类型之一:

l         立即响应事件(Immediate response events):包括单击提交按钮,或者点击在一个丰富的Web控件中的其它按钮、图像区域或者链接,通过调用_doPostBack() JavaScript函数触发回传。

l         变化事件(Change events):包括控件中的选择或者文本框中的文本发生的变化。如果AutoPostBack设置为true,这些事件就为Web控件立即释放。否则,在页面下次回传时释放事件。

你已经看到,ASP.NET事件模型与传统的Windows环境大为不同。在Windows应用中,窗体状态存放在内存中,程序连续不断地运行。这就是意味着你可以立即对事件进行响应。在ASP.NET中,所有的事件都分阶段发生,因此事件有时分批批处理的。

例如,假设有一个页面,页面上有一个提交按钮和一个文本框,它们并不自动回传。你改变文本框中的文本,然后点击提交按钮。此时,ASP.NET顺序唤起下列事件:

• Page.Init

• Page.Load

• TextBox.TextChanged

• Button.Click

• Page.PreRender

• Page.Unload

从这一点信息,本质上可以帮助你开发ASP.NET更为容易。对事件驱动模型,它有上层和下层。上层就是事件模型提供了更高层的抽象,它使你的代码保持清晰,方便维护状态。下层就是不需要考虑事件模型本身的竞争。这会使你设想并没有保持true(如期望将信息保存在成员变量中),或者设计结果不会很好执行(如在视图状态中存储大量信息)。(这一段翻译可能有问题

 

自动数据绑定(Automatic Data Binding

在第9章,你将学习数据源控件(ASP.NET2.0中提供),它将自动执行数据绑定进程。使用数据源控件时,ASP.NET在页面的生命周期中向数据源自动执行更新和查询。

本质上,存在两种类型的数据源操作。任何改变(插入、删除、更新)都在控件所有事件被处理后,但在Page.PreRender事件释放前执行。于是,在Page.PreRender事件释放后,数据源控件执行查询并将获得的数据插入到链接的控件。这个模型有直观的意义,因为如果查询在更新前执行,在网页中可能以过期的数据结束。然而,这个模型也引入了很必要的限制,即其它任务事件句柄都不能访问最近的数据,因为数据还没有获取。

这是网页生命周期的最后一站。以历史的观点来看,Page.PreRender事件是网页渲染成HTML之前的最后一个动作(尽管如先前了解那样,一些数据绑定工作可能在预渲染阶段后进行)。在预渲染(prerender)阶段,网页和控件都是可获得的,所以还可以执行收尾的工作,如在视图状态中存储附加的信息。

关于ASP.NET数据绑定的更多内容,参阅第9章。

 

清除(Cleanup

在生命周期的最后,网页渲染成HTML。渲染之后,开始执行清除,Page.Unload事件释放。此时,网页对象仍可用,但是最后的HTML已完成渲染,不能被更改。

不要忘记,.NET具有垃圾收集服务,它定期运行来释放那些不再引用的对象占用的内存。如果有任何未管理的资源需要释放,应当在清除阶段显性地执行,当然,之前就完成清除更好。当垃圾收集器收集页面时,Page.Disposed事件释放。至此,Web页面的整个流程完毕。

 

网页流程示例(A Page Flow Example

无论对工作原理解释多少次,但总不能尽如人意。为了探究竟,可以构建示例Web窗体测试来描述处理流程。本示例中不会描述校验,这个内容会在下一章讨论。

为了进行测试,首先创建一个Web窗体,命名为PageFlow.aspx。在Visual Studio中,从Web窗体工具栏拖动两个控件到设计区。这将产生服务器端的<form>标签和两个控件标签。第二步,选择标签。使用属性窗口,设置ID属性为lblInfoEnableViewState属性设置为false

完整的.aspx示例如下:

<%@ Page language="c#" CodeFile="PageFlow.aspx.cs"

AutoEventWireup="true" Inherits="PageFlow" %>

<html>

<head runat="server">

<title>Page Flow</title>

</head>

<body>

<form id="form1" runat="server">

<div>

<p>

<asp:Label id="lblInfo" runat="server" EnableViewState="False">

</asp:Label>>

</p>

<p>

<asp:Button id="Button1" runat="server"

Text="Button"></asp:Button>

</p>

</div>

</form>

</body>

</html>

下一步是添加事件处理器。完成之后,后置代码文件中会有5个事件处理器来响应不同的事件,包括Page.InitPage.LoadPage.PreRenderPage.UnloadButton.Click

页面事件处理器与其它控件事件处理器不同,不需要在标签中通过设置属性来进行连接。只要使用正确的方法名,就能自动调用页面事件处理器。PageFlow示例中的几个不同的页面事件处理器如下:

private void Page_Load(object sender, System.EventArgs e)

{

lblInfo.Text += "Page.Load event handled.<br />";

if (Page.IsPostBack)

{

lblInfo.Text +=

"<b>This is the second time you've seen this page.</b><br />";

}

}

private void Page_Init(object sender, System.EventArgs e)

{

lblInfo.Text += "Page.Init event handled.<br />";

}

private void Page_PreRender(object sender, System.EventArgs e)

{

lblInfo.Text += "Page.PreRender event handled.<br />";

}

private void Page_Unload(object sender, System.EventArgs e)

{

// This text never appears because the HTML is already

// rendered for the page at this point.

lblInfo.Text += "Page.Unload event handled.<br />";

}

每个事件处理器简单地将文本添加到labelText属性。代码添加这段文本时,也使用了<b><br/>(用于换行)HTML标签。除此之外,还需要创建多个分离label控件,并且为每个Label配置相关样式属性。

注意,在此示例中,EnableViewState属性设置为false。这保证在页面回传时文本被清除,文本只显示与最近的批处理的相对应的内容。如果保持EnableViewState属性为true,每回传一次,列表就会增长一些,它显示了从第一次请求以来的所有活动。

另外,需要为Button.Click事件连接事件处理器,如下所示:

protected void Button1_Click(object sender, System.EventArgs e)

{

lblInfo.Text += "Button1.Click event handled.<br />";

}

你可能已经注意到,Button.Click事件处理器要求与页面事件处理器的访问层次不同。页面事件处理器是私有的,而所有控件事件处理器是保护的。要理解这一区别,可以再去研究第2章介绍的代码模型。

页面处理器被代理(delegate)显性地捕获(hook up)。代理隐藏于代码设计器中。因为设计器代码仍被认为是你的类的一部分,它可以捕获任何方法,包括私有方法。控件事件处理器使用了不同的机制,即使用控件标签进行连接。绑定在流程的靠后的阶段中执行,即.aspx文件的标记和后置代码类合并到一起之后。这个合并后的类是由ASP.NET从后置代码派生一个新类而来。

这里有一点技巧。派生类应当具有访问页面中的事件处理器的能力,这样它们可以连接到正确的控件。派生类仅能够访问公有的(任何类都可以访问它们)事件处理器,或者保护的(派生类可以访问)。

提示,尽管对于页面事件处理器是私有的(private)是可接受的。ASP.NET2.0代码中约定所有事件处理器为保护的(protected),这是为了保持一致性和简单性。

3-5显示了点击按钮后的ASP.NET页面,点击按钮触发了回传和Button1.Click事件。注意,尽管这个事件导致了回传,Page.InitPage.Load仍然是第一次唤起。

ASP.NET操作顺序

3-5 ASP.NET操作顺序

 

将页面作为控件容器(The Page As a Control Container

现在,你已经学习了Web窗体处理过程。现在来靠近看看如何将服务器控件放入这个管道(pipeline)。为了渲染页面,Web窗体需要与它委托的控件(constituent control)进行合作。本质上,Web窗体渲染它自身,然后要求页面上的控件渲染它们自己。每一个控件都可以按顺序包含子控件,每个控件都对自己的渲染代码负责。当这些控件渲染自身时,页面将产生的HTML装配为一个完整的网页。这个过程起初看起来有点复杂,但它在创建丰富的网页接口时,能够提供极强的能力和灵活性。

ASP.NET最先创建一个网页时(响应一个HTTP请求),它检查.aspx文件。对于发现的每一个控件标签,它创建和配置一个控件对象,然后将该控件添加为网页的子控件。你可以检查Page.Controls集来查找网布上的所有子控件。

 

显示控件树(Showing the Control Tree

下面是一个查找控件的示例。每次它发现一个控件之后,代码使用Response.Write()命令将控件类类型和控件ID写到渲染的HTML网页后面,如下所示:

// Every control derives from System.Web.UI.Control, so you can use

// that as a base class to examine all controls.

foreach (Control control in Page.Controls)

{

Response.Write(control.GetType().ToString() + " - <b>" +

control.ID + "</b><br />");

}

// Separate this content from the rest of the page with a horizontal line.

Response.Write("<hr />");

注意,Response.Write()方法是从经典ASP延续过来的,在真正的ASP.NET Web应用中,你应当不再使用它。它有效地避开了导致接口混乱的Web控件模型,在ASP.NET创建标签来适应目标设备的能力和总是中断XHTML兼容性之间平衡。但是,在这个测试页面中,Response.Write()允许你写原始的HTML,而不需要产生任何额外的控件。这是用来分析网页上控件的完美技术,而不要求发布控件。

为了测试这段代码,将它添加到Page.Load事件处理器中。在这种情况下,渲染后的内容会在控件之前写到页面的顶部。但是,你可能会注意到一些异常行为,但仍可以运行它。例如,考虑图3-6中的Web窗体,它包含了几个服务器控件,其中一些使用Panel Web控件将一些服务器控件放入一个框体之中。也包含了两行静态文本。

具有多个控件的Web页

3-6 具有多个控件的Web

这个页面的.aspx标记代码如下:

<%@ Page language="c#" CodeFile="Controls.aspx.cs" AutoEventWireup="true"

Inherits="ControlTree" %>

<html>

<head runat="server">

<title>Controls</title>

</head>

<body>

<p><i>This is static HTML (not a web control).</i></p>

<form id="Controls" method="post" runat="server">

<div>

<asp:panel id="MainPanel" runat="server" Height="112px">

<p><asp:Button id="Button1" runat="server" Text="Button1"/>

<asp:Button id="Button2" runat="server" Text="Button2"/>

<asp:Button id="Button3" runat="server" Text="Button3"/></p>

<p><asp:Label id="Label1" runat="server" Width="48px">

Name:</asp:Label>

<asp:TextBox id="TextBox1" runat="server"></asp:TextBox></p>

</asp:panel>

<p><asp:Button id="Button4" runat="server" Text="Button4"/></p>

</div>

</form>

<p><i>This is static HTML (not a web control).</i></p>

</body>

</html>

运行页面,可以看到控件的完整列表。可以看到列表只命名了3个控件,如图3-7所示:

位于页面顶层的控件

3-7位于页面顶层的控件

ASP.NET使用控件对象创建了整个页面的模型,包括与服务器端内容无关的元素。例如,如果在页面上有一个服务器控件,ASP.NET将创建一个LiteralControl来代表控件之前的所有静态内容,并且创建另一个LiteralControl来代表之后的内容。根据静态内容的多少和在静态内容与其它控件之间怎样分隔,最终网页上可能有多个LiteralControl对象。

LiteralControl控件并不会提供多功能。例如,你不能设置样式相关的信息,如颜色和字体。它们也没有唯一的服务器端ID。但是,可以使用LiteralControlText属性来操作它的内容。下面的代码重写了前面的示例,使它能够检查Literal控件,并且如果存在的话,就转换基础的Control对象为LiteralControl类型,以便于能够解析相关的文本:

foreach (Control control in Page.Controls)

{

Response.Write(control.GetType().ToString() + " - <b>" +

control.ID + </b><br />);

if (control is LiteralControl)

{

// Display the literal content (whitespace and all).

Response.Write("*** Text: "+((LiteralControl)control).Text + "<br />");

}

}

Response.Write("<hr>");

这个示例仍然有问题。你现在对意外的新内容有了理解,但是否明白了丢失的内容呢?换句话说,页面上的其它控件对象丢失到哪里了呢?

为了回答这个问题,你需要理解ASP.NET的页面分级渲染。它仅仅直接渲染顶层的控件。如果这些控件包含了其它控件,那么它们提供Controls属性,通过这个属性来访问子控件。在示例页面中,和在所有ASP.NET Web窗体中一样,所有的控件都嵌入在<form>标签中。意思就是说你需要检查HtmlForm类的Controls集来获取页面上的服务器控件的信息。

但是,有时候工作起来并不是很直截了当的,因为没有对可以嵌入的控件的层数进行限制(获取控件就变得困难)。为了真正解决这个问题,显示页面上的所有控件,你需要创建递归程序来遍历整个控件树。

下面的代码展示了这个解决方案:

public partial class ControlTree : System.Web.UI.Page

{

protected void Page_Load(object sender, System.EventArgs e)

{

// Start examining all the controls.

DisplayControl(Page.Controls, 0);

// Add the closing horizontal line.

Response.Write("<hr/>");

}

private void DisplayControl(ControlCollection controls, int depth)

{

foreach (Control control in controls)

{

// Use the depth parameter to indent the control tree.

Response.Write(new String('-', depth * 4) + "> ");

// Display this control.

Response.Write(control.GetType().ToString() + " - <b>" +

control.ID + "</b><br />");

if (control.Controls != null)

{

DisplayControl(control.Controls, depth + 1);

}

}

}

}

3-8显示了新的结果,一个有层次结构的树显示了页面上和嵌入的所有控件。

页面上的控件树

3-8 页面上的控件树

 

页头(The Page Header

你已经看到,你可以使用runat = “server”属性来转换任何HTML元素为服务器控件,而且页面能够包含的HTML控件无数量限制。除了你添加的控件之外,Web窗体也包含了一个单独的HtmlHead控件,它提供了服务器端对<head>标签的访问。

前面的示例中显示的控件树并没有包含HtmlHead控件,因为没有将runat=“server”属性应用到<head>标签。但是Visual Studio默认总是将<head>标签放入服务器端控件,与以前版本的ASP.NET相反。

和使用其它服务器控件一样,可以使用HtmlHead控件来编程改变<head>标签内渲染的内容。不同之处是<head>标签与网页中所看到的真实内容并不对应。相反,它包含了其它细节,如标题,元数据标签(用于提供关键字,便于搜索引擎搜索),和样式参考。要改变这些细节,可以使用HtmlHead类中的成员集。这些成员包括:

l         标题(Title):这是HTML页面的标题,显示在浏览器的标题栏。可以在运行时修改标题。

l         样式表(StyleSheet):提供了IStyleSheet对象,描述头部定义的内嵌样式。也可以使用IStyleSheet对象的CreateStyleRule()RegisterStyleRule()方法来动态创建新的样式规则。

l         控件(Controls):可以使用控件集和HtmlMeta控件类来编程添加或移除元数据标签。

下面的示例动态设置了标题信息和元数据标签:

Page.Header.Title = "Dynamically Titled Page";

// Add metadata tags.

HtmlMeta metaDescription = new HtmlMeta();

metaDescription.Name = "description";

metaDescription.Content = "A great website to learn .NET";

Page.Header.Controls.Add(metaDescription);

HtmlMeta metaKeywords = new HtmlMeta();

metaKeywords.Name = "keywords";

metaKeywords.Content = ".NET, C#, ASP.NET";

Page.Header.Controls.Add(metaKeywords);

提示:HtmlHead控件在动态网页中很容易获得。例如,要创建一个数据驱动的站点,站点从数据库不断获取内容,当页面发出请求时,你可能想根据内容来改变关键词和页面的标题。

 

动态控件创建(Dynamic Control creation

使用Controls集,你可以通过程序创建一个控件,并且将其添加到网页。下面的示例产生了一个新控件,并且将它添加到了网页上的Panel控件:

protected void Page_Load(object sender, System.EventArgs e)

{

// Create a new button object.

Button newButton = new Button();

// Assign some text and an ID so you can retrieve it later.

newButton.Text = "* Dynamic Button *";

newButton.ID = "newButton";

// Add the button to a Panel.

Panel.Controls.Add(newButton);

}

你可以将这段代码放入任何事件处理器中执行。但是,由于页面已经创建,这段代码总是在控件集的后面添加新控件。本例中,意味着新的按钮将位于Panel控件的底部。

为了对动态添加的控件的位置有更多的控制,可以使用PlaceHolderPlaceHolder的唯一用途就是容纳其它控件。如果没有向PlaceHolderControls集添加任何控件,它在最终的页面上不会渲染任何内容。但是,Visual Studio对它有个默认的描述,使其在设计时看起来一个普通的标签,因此你可以精确放置它的位置。这样,你就可以在其它控件之间添加动态控件。

// Add the button to a PlaceHolder.

PlaceHolder1.Controls.Add(newButton);

使用动态控件时,必须记住:它们直到下一次传回时才会存在。ASP.NET不会重新创建一个动态添加的控件。如果想要多次重新创建一个控件,你应当在Page.Load事件处理器中执行控件创建。这可以带来额外的好处,即允许在动态控件中使用视图状态。尽管视图状态通常在Page.Load事件前存储,但如果你在Page.Load事件处理器中创建一个控件,ASP.NET会自动应用Page.Load事件处理器结束后的任何视图状态信息。这个过程是自动的。

如果稍后要与控件进行交互,应该给它指定唯一的ID。可以根据这个ID来从它所在容器的Controls集中获取控件。也可以使用递归查找逻辑(在控件树示例中已演示过)来查找控件,或者使用静态的Page.FindControl()方法,在整个页面中根据ID搜索控件。下面的示例演示了如何使用FindControl()方法查找动态添加的控件,然后将其移除:

protected void cmdRemove_Click(object sender, System.EventArgs e)

{

// Search for the button, no matter what level it's at.

Button foundButton = (Button)Page.FindControl("newButton");

// Remove the button.

if (foundButton != null)

{

foundButton.Parent.Controls.Remove(foundButton);

}

}

动态添加的控件可以处理事件。所需做的工作就是使用代理(delegate)程序来附加一个事件处理器。必须在Page.Load事件处理器中执行这个任务。正如前面了解到的一样,所有控件指定的事件都是在Page.Load事件之后释放。如果等候的时间晚了一点,事件处理器就在事件释放后才连接上,此时就不能进行任何响应。

// Attach an event handler to the Button.Click event.

newButton.Click += new System.EventHandler(this.Button_Click);

3-9演示了所有这些概念。它产生了一个动态按钮,当你点击这个按钮时,标签(label)里的文本被修改。另两个按钮允许你移除和重创建这个按钮。

处理动态添加的控件的事件

3-9 处理动态添加的控件的事件

与其它用户控件合并时,动态控件创建就特别强大(重用能够合并一组控件和HTML的用户接口块)。第14章将对用户控件进行详细介绍。

 

页面类(The Page Class

在前面,你已经研究了页面生命周期,学习了页面如何包含控件。应该指出的是,页面自身是作为控件对象类型实例化的。事实上,所有Web窗体事实上是ASP.NET页面类的实例,该类可以在System.Web.UI名字空间中找到。

注意到每个后置代码类显性地从System.Web.UI.Page类派生,你可能就知道了上面这一点。这意味着每个Web窗体都装配了大量的箱外(out-of-the-box)功能。静态的FindControl()方法和IsPostBack属性是到目前为止的两个示例。另外,你的代码从Page类派生会具有下面非常有用的属性:

. Session

. Application

. Cache

. Request

. Response

. Server

. User

. Trace

这些属性的多数对应固有的对象,这些对象在经典ASP web页面中也能够使用。但是,在经典ASP中,你只能通过一直可获得的内置对象来访问这些功能。在ASP.NET中,每个内置对象对应一个Page属性,展示了全功能的类的实例。

下一下节将对这些对些进行介绍。

 

SessionApplicationCache

Session对象是System.Web.SessionState.HttpSessionState类的实例。它用于存储任何用户指定类型的数据,这些数据用于多个网页请求间共享。Session对象提供了对一系列代表在那个会话期间的用户数据的名字/值(name/value)对的字典风格的访问。Session状态通常用于维护如用户名、用户ID,购物车或者不同的其它元素之类的信息,这些元素在用户不再访问网站上的页面是会被丢弃。

Application对象是System.Web.HttpApplicationState类的实例。就Session一样,它也是名字/值的数据字典。但是,这些数据对于整个应用是全局的。

最后,Cache对象是System.Web.Caching.Cache类的实例。它也存储全局信息,但是它提供了更多可扩展的存储机制,因为ASP.NET在服务器内存不足时能够移除对象。就其它状态集一样,它本质上是对象的名字/值集,但是你可以为每一项设置过期策略。

决定如果实现状态管理,是开发Web应用的一个关键的挑战。你可以在第6章了解关于所有这些状态管理类型的更多内容。

 

Request

Request对象是System.Web.HttpRequest类的实例。这个对象描述了HTTP请求的值和属性。它包含了所有URL参数和客户端发送的所有其它信息。大多数信息由更高层的抽象(如ASP.NET Web控件模型)包装Request对象提供。因此,它并不象在经典ASP应用中那样重要。但是,你可以使用Request对象查找客户端正在使用的浏览器,或者设置和检查Cookies

3-1描述了Request对象的一些常用属性

属性

描述

ApplicationPath PhysicalPath

ApplicationPath获取ASP.NET应用的虚拟目录(URL),PhysicalPath获得“真实”目录

AnonymousID

如果允许匿名访问,这唯一地标识了当前用户。如何使用新的匿名访问特征,请参阅第24

Browser

提供HttpBrowserCapabilities对象的链接,HttpBrowserCapabilities对象包含了描述不同浏览器特征的属性,如支持ActiveX控件、cookiesVBScript和框架

ClientCertitificate

是一个HttpClientCertificate对象,如果有安全认证,就获取当前用户的安全证书。

Cookies

获取在请求中发送的cookies集。第6章对cookies进行讨论。

FilePathCurrentExecutionFilePath

它们返回当前执行的页面的真实文件路径(相对于服务器)。FilePath返回开始执行进程的页面。它与CurrentExecutionFilePath是一样,除非你在没有使用重定向时,已经将用户传递到了一个新的页面(例如,使用Server.Transfer()方法)。在这种情况下,CurrentExecutionFilePath描述一个新的页面,并且FilePath指明原来的页面。

Form

描述回传到网页的窗体变量集。事实上,可以通过控件属性获取所有这些信息,而不是一定要使用这个变量集。

Headers, ServerVariables

提供HTTP头部和服务器变更的名字/值集。如果知道对应的头部或变量名,可以获得底层的信息。

IsAuthenticated, IsSecureConnection

如果用户被授权/用户通过SSL连接(安全Socket层),就返回true

IsLocal

如果页面是从当前计算机发出的请求,返回true

QueryString

提供随着查询串内的的参数。第6章描述了如何使用查询串在不同页面之间传递信息。

Url, UrlReferer

提供Url对象,描述网页的当前IP地址和用户来源的网页(也就是链接到这个页面的网页)

UseerAgent

描述浏览器类型的字符串。IE 为这个属性提供了MSIE

UserHostAddress, UserHostName

这两个属性获得远程客户端的IP地址和DNS名字。可以通过ServerVariables集来访问这个信息。但是,这个信息有时无法获得。

UserLanguages

提供了索引的字符串数组,列出客户端语言偏好列表。如果要使用多语言页面,这个属性非常有用。

3-1 HttpRequest属性

 

Response

Response对象是System.Web.HttpResponse类的实例,它描述Web服务器对客户端请求的响应。在经典的ASP中,Response对象是编程发送HTML文本到客户端的唯一方法。现在,服务器端已经内嵌了面向对象的方法来渲染自身。你所要做的就是设置它们的属性。因此,Response对象并不象中心角色那样重要。

HttpResponse也提供了一些重要的功能,即cookie功能和Redirect()函数。Redirect()方法允许将页面重定向到另一个页面。见下面的示例:

// You can redirect to a file in the current directory.

Response.Redirect("newpage.aspx");

// You can redirect to another website.

Response.Redirect("http://www.prosetech.com");

Redirect()方法是一个往返的过程。本质上,它发送消息到浏览器,指示它请求一个新的页面。如果想将网页定向到同一个站点的另一个页面,使用Server.Transfer()方法更为快捷。

提示,从一个页到另一个页面的另一个方法是交叉页面投递。使用这个技术,可以创建一个网页来将自身投送到另一个页面,它允许你高效地传递所有视图状态信息和任何控件的内容。第6章将介绍如何使用这个技术。

3-2 列出了常用的HttpResponse成员

成员

描述

BufferOutput

如果设置为true,页面在完成渲染后才发送到客户端,而不是分片发送。

Cache

引用HttpCachePolicy对象,允许配置输出缓存。第11章讨论缓存。

Cookies

随着response发送的cookies集。可以使用这个属性添加额外的cookies.

Expires, ExpiresAbsolute

可以使用这个属性来缓存网页渲染后的HTML,提升后续访问的性能。在11章可以了解到这个类型(输出缓存)的更多信息。

IsClientConnected

这个布尔值表明客户端是否仍在与服务器连接。如果不是,可能就要停止time-consuming操作。

Write(), BinaryWrite(), WriteFile()

这几个方法允许将文本或二进制内容直接写入response流。甚至可以将一个文件的内容写入。这些方法在ASP.NET中不再强调,也不应用于与服务器控件的连接。

Redirect()

这个方法将用户导航到应用的另一个页面或者不同的站点。

3-2 HttpResponse成员

 

Server

Server对象是System.Web.HttpServerUtility类的实例。它提供了多种有用的helper方法和属性,列于表3-3之中:

方法

描述

MachineName

该属性描述运行网页的计算机名。计算机名是Web服务用来在网络中对自身的标识。

CreateObject()

创建一个COM对象,该对象由progID标识。包括这个方法是为了后向兼容,因为使用.NETCOM交互的支持来与COM对象进行交互更为容易。COM互用提供了强类型交互。

GetLastError

获取最近遇到的错误产生的异常对象(如果没有错误,就是空引用)。这个错误必须发生在当前请求期间,而且还未被处理。这个属性在应用事件处理器中应用很广泛,用于检测错误条件(第5章有相关示例)。

HtmlEncode(), HtmlDecode()

将传统的字符串转变成合法的HTML字符串,然后返回。

UrlEncode(), UrlDecode()

将传统的字符串转换成合法的URL字符串,然后返回。

UrlEncodeToken(), UrlDecodeToken()

处理包含Base64编码数据的字节数组,其它与UrlEncode()UrlDecode()一样。

MapPath()

返回Web服务器上指定虚拟文件路径对应的物理文件路径。

Transfer()

在当前应用中将执行跳转到另一个网页。与Response.Redirect()方法相似,但它更快。该方法不能用于将用户导航到另一个Web服务器上的站点或者非ASP.NET页面(如HTML网页或者ASP网页)。

3-3 HttpServerUtility方法

Transfer()方法是在应用中将用户导航到另一个页面的最快方法。使用这个方法时,并没有进行一个轮回,相反,ASP.NET引擎简单地加载新的页面然后开始对其进行处理。因此,显示在客户端浏览器中的URL并不会发生改变。

// You can transfer to a file in the current web application.

Server.Transfer("newpage.aspx");

// You can't redirect to another website.

// This attempt will cause an error.

Server.Transfer ("http://www.prosetech.com");

MapPath()方法是另一个有用的Server对象方法。例如,假设你想从当前的虚拟目录中加载一个info.txt文件。不用对路径进行硬编码,你可以使用Request.ApplicationPath()来获得当前相关的虚拟目录,Server.MapPath()将其转换为绝对的物理路径。见下面的示例:

string physicalPath = Server.MapPath(Request.ApplicationPath + "/info.txt"));

// Now open the file.

StreamReader reader = new StreamReader(physicalPath);

// (Process the file here.)

reader.Close()

 

HTML URL编码

Server类也包含了一个方法,将普通的字符串转换为URL的一部分,或者在一个Web页面中显示。例如,假设你希望在网页中显示这段文本:

To bold text use the <b> tag.

如果你想将这个信息写入网页或者放置在一个控件中,你可能会得到这样的结果:

To bold text use the tag.

不仅文本<b>不会显示,而且浏览器将其描述为一个指令,将随后的文本加粗。为了防止这个自动的动作,你需要转换潜在的有问题的值为特殊的HTML替代符。例如,在最终的HTML页中,“<”变成“&lt;”,浏览器就会将其显示为“<”。表3-4列出了一些需要编码的特殊字符。

常用HTML实体

3-4 常用HTML实体

下面的示例使用Server.HtmlEncode()方法来解决编码的问题:

Label1.Text = Server.HtmlEncode("To bold text use the <b> tag.")

也可以使用HtmlEncode来对一些输入进行编码,但并不是所有的都可以,如果你想插入一个合并的文本,文本可能无效或者是HTML标签,如下例:

Label1.Text = "To <b>bold</b> text use the ";

Label1.Text += Server.HtmlEncode("<b>") + " tag.";

注意,一些控件通过自动编码标签来解决这个问题(如Label Web控件,它允许你自由地插入HTML标签)。例如,HTML服务器控件的基本设置包含了InnerText标签和InnerHtml标签。使用InnerText设置控件的内容时,任何非法的字符都转换成HTML替代符。但是,在设置包含了内嵌标签和编码字符的组合时,就不起作用了。

在从数据库获取值但又不能保证文本是有效的HTML时,使用HtmlEncode()方法显得尤为有效。如果在程序中要执行额外的操作或者需要对文本进行比较,可以使用HtmlDecode()方法将文本转换成普通形式。与此类似,UrlEncode()方法将文本转换为URL可以使用的形式,避免空格和其它特殊字符。希望将信息添加查询串时,往往会进行这步操作。

HtmlEncode()方法不将空格转换为非间断的空格就没有意义了。也就是说,如果你有一连串的空格字符,浏览器将只显示一个单独的空格。尽管这样HTML仍然有效,但它并不是期望的效果。为了改变这个行为,可以使用String.Replace()方法将空格替换为非间断空格。需要确认的是,你应该在对字符串编码之后(而不是之前)执行这一步操作,或者使用字符实体来替代非间断字符序列(&bps;),并将其当作普通文本对待。

// Encode illegal characters.

line = server.HtmlEncode(line);

// Replace spaces with nonbreaking spaces.

line = line.Replace(" ", "&nbsp;");

相似地,HtmlEncode()方法也不能将换行符转换为<br>标签。这意味着除非特别地插入<br>标签,否则硬回车将被忽略。

注意,对输入进行正确编码比正确显示数据更为重要。如果想要显示的数据嵌入了<script>标签,可以在客户端完成一段JavaScript代码执行。这个问题带来的危险,可通过ASP.NET请求的检验功能来防止。第27章对此进行了详述。

 

User

User对象描述了向Web服务器发出请求的用户的信息,它允许你测试用户的角色成员资格。

User对象通常实现System.Security.Principal.Iprincipal。特定的类依赖于认证类型。例如,可以基于IIS使用的Windows帐户信息对用户认证,或者通过一个专门的登录页面,进行基于cookie的认证。但是,仅仅在Web应用在执行严格限制匿名访问的认证时,User对象提供的信息才有用。

本书第4章讨论了安全方面的细节。

 

Trace

Trace对象是一个通常意义的跟踪工具,它是System.Web.TraceContext类的实例。它允许你将信息写入页面层级的日志。这个日志具有详细的计时的信息,不仅可以使用Trace对象来进行调试,也可以用它来监视性能和进行计时。另外,跟踪日志也显示了各种信息的汇编,组成了几个章节。表3-5描述了这些信息。

跟踪日志信息

3-5 跟踪日志信息

提示,跟踪是对Visual Studio调试的一个补充。在多数情况下,在编写一个Web应用时,调试是解决问题的最好方法。但当应用在服务器上运行时,跟踪是解决问题的更为简便的选择。跟踪提供了几个调试没有(至少没那么容易)的服务,如在视图状态中显示信息的数量和在服务器上执行页面所花的时间。跟踪不会因为构建的应用是在调试模式或释放模式而停止工作。

可以通过两种方法启用跟踪。可以在代码的任何位置设置Trace.IsEnable属性为true,如下所示:

Trace.IsEnable = true;

一般地情况下,这一过程是放在Page.Load事件处理器中的。另一个方法是在Page指令中使用Trace属性:

<%@ Page language="c#" CodeFile="PageFlow.aspx.cs" AutoEventWireup="true"

Inherits="PageFlow" Trace="true" %>

默认情况下,跟踪信息按产生的顺序列出来。另外,你也可以指定信息索引和分类,在Page指令中使用TraceMode属性来实现,如下所示:

<%@ Page language="c#" CodeFile="PageFlow.aspx.cs" AutoEventWireup="true"

Inherits="PageFlow" Trace="true" TraceMode="SortByCategory" %>

或者在代码中使用Trace对象的TraceMode属性:

Trace.TraceMode = TraceMode.SortByCategory;

3-10显示了前面演示的PageFlow示例中部分跟踪信息列表。

基本的跟踪信息

3-10 基本的跟踪信息

你也可以将自己的信息写入跟踪日志(作为跟踪日志的一部分,显示在Trace Information一节中),需要使用Trace.Write()或者Trace.Warn()方法。这两个方法是相同的,唯一的区别是Warn()用红色字母显示信息,以便于在列表中与其它信息相区别。

下面的示例中,当用户点击按钮时,程序将写入一段跟踪信息:

protected void Button1_Click(object sender, System.EventArgs e)

{

// You can supply just a message, or include a category label,

// as shown here.

Trace.Write("Button1_Click", "About to update the label.");

lblInfo.Text += "Button1.Click event handled.<br />";

Trace.Write("Button1_Click", "Label updated.");

}

当写入跟踪信息时,它们会自动发送到所有跟踪侦听者那里。但是,如果你对页面禁用了跟踪,信息会被忽略。跟踪信息自动用HTML进行编码,这意味着如<br/> <b>之类的标签作为文本显示,而不是描述为HTML

3-11显示了日志中的新输入。

写入自定义跟踪信息

3-11 写入自定义跟踪信息

提示,你不仅可以发送自定义跟踪信息,还可以创建事件处理器来接收任何跟踪信息。尽管这个技术使用得并不广泛,但你可以使用它来过滤开发中特定的感兴趣的信息并进行记录。要实现这一步只需要处理Trace.TraceFinished事件,它提供了一个TraceContext对象集代表每个跟踪信息。

 

应用跟踪(Application Tracing

默认情况下,跟踪是在逐页的基础上启用的。这样并不是很方便。某些情况下,你可能想收集跟踪统计到一个页面上以便于查看,ASP.NET支持应用层次的这种方法的跟踪。

为了启用应用层次的跟踪,需要修改Web.config配置文件。查找<trace>节并且使其启用,如下所示:

<configuration>

<system.web>

<trace enabled="true" requestLimit="10" pageOutput="false"

traceMode="SortByTime" localOnly="true" />

</system.web>

</configuration>

启用应用层次的跟踪时,在网页上看不到跟踪信息。相反,必须请求在Web应用的根目录下的trace.axd应用扩展,才可以查看跟踪信息。这个扩展并不对应真实的文件,相反,ASP.NET自动截取请求,并且列出最近收集的跟踪请求(图3-12所示),使你能够从本地计算机上发起请求,或者启用远程跟踪。可以通过点击View Details链接来查看任何请求的详细信息。

跟踪的应用请求

3-12 跟踪的应用请求

3-6列出了Web.config<trace>节里所有的跟踪选项。

跟踪选项

3-6 跟踪选项

 

使用开发帮手进行跟踪(Traceing with the ASP.NET Development Helper

如果使用了第2章介绍的ASP.NET Development Helper(下载:http://www.nikhilk.net/ASPNETDevHelperTool.aspx ),你就有了查看跟踪信息的新途径,在单独的窗口中查看它。当ASP.NET Helper在运行(同时在Web服务器和Web浏览器中)时,它自动从网页上移除跟踪信息。要访问它,可以在Page的检查框中不选择Hide Trace,或者点击Show Trakce链接。

3-13显示了这个方便的功能。

使用ASP.NET Development Helper跟踪信息

3-13 使用ASP.NET Development Helper跟踪信息

 

在另一个类中访问HTTP的背景(Accessing the HTTP Context in Another Class

在前面几节已经看到,Page类展示了许多有用的功能,允许你获取关于当前HTTP背景的信息。这些细节是通过Page类的属性来获得的。但是,如果想在另一个不是继承于Page的类中获取这些信息,应该怎么办呢?

幸运的是,还有另一种方法来访问所有HTTP背景信息。即使用System.Web.HttpContext类。这个类提供了静态的属性Current,它返回HttpContext类的实例,描述当前请求的所有信息。它提供了内置的ASP.NET对象作为属性。

例如,下面的代码显示了如何从另一个不是继承于Page的类来写入跟踪信息,该类由web页面使用并作为Web请求的一部分:

HttpContext.Current.Trace.Write("This message is from DB Component");

如果想执行多个操作,获取当前背景的引用并对其重用会更快一点:

HttpContext current = HttpContext.Current;

current.Trace.Write("This is message 1");

current.Trace.Write("This is message 2");

 

总结

在本章中,简单了解了ASP.NET页的详细的检查。学习了隐藏在可视场景背后的回传和视图状态的本质及其工作原理。也学习了服务器控件模型的基础知识,较深入地学习了System.Web.UI.Page类,还对如何使用跟踪进行了介绍。在下一章,将更深入地了解ASP.NET提供的web控件,构建复杂的页面。

 

原创粉丝点击