asp.net

位置:IT落伍者 >> asp.net >> 浏览文章

ASP.NET Web Page应用深入探讨


发布日期:2021年05月19日
 
ASP.NET Web Page应用深入探讨

服务器脚本基础介绍

首先我们先复习一下Web服务器页面的基本执行方式

客户端通过在浏览器的地址栏敲入地址来发送请求到服务器端

服务器接收到请求之后发给相应的服务器端页面(也就是脚本)来执行脚本产生客户端的响应发送回客户端

客户端浏览器接收到服务器传回的响应对Html进行解析将图形化的网页呈现在用户面前

对于服务器和客户端的交互通常通过下面几种主要方式

Form这是最主要的方式标准化的控件来获取用户的输入Form的提交将数据发送给服务器端处理

QueryString通过在Url后面带参数达到将参数传送给服务器这种方式其实跟Get方式的Form是一样的

Cookies这是一种比较特殊的方式通常用于用户身份的确认

ASPNet简介

传统的服务器脚本语言如ASPJSP等编写服务器脚本的方式大同小异都是在Html中嵌入解释或编译执行的代码由服务器平台执行这些代码来生成Html对于这类似的脚本页面的生存周期实际上很简单就是从开头至末尾执行完所有的代码当然用Java编写的Servlet可以编写更复杂的代码但是从结构上看和JSP没什么区别

ASPNet的出现打破了这种传统ASPNet采用了CodeBehind技术和服务器端控件加入了服务器端的事件的概念改变了脚本语言编写的模式更加贴近Window编程使Web编程更加简单直观但是我们要看到ASPNet本身并没有改变Web编程的基本模式只是封装了一些细节提供了一些易用的功能使代码更容易编写和维护从某种程度上来说将服务器端执行的方式复杂化了这就是我们今天要讨论的主体ASPNet Web Page的生存周期

ASPNet请求处理模式

我们说ASPNet的Web Page并没有脱离Web编程的模式所以它仍然是以 请求>接收请求>处理请求>发送响应 这样的模式在工作每一次与客户端的交互都会引发一次新的请求所以一个Web Page的生命周期是以一次请求为基础的

当IIS收到客户端的请求的时候会将请求交给aspnet_wp这个进程来处理这个进程会查看请求的应用程序域是否存在如果不存在则会创建一个然后会创建一个Http运行时(HttpRuntime)来处理请求这个运行时为当前应用程序提供一组 ASPNET 运行时服务(摘自MSDN)

HttpRuntime在处理请求的时候会维护一系列的应用程序实例也就是应用程序的Global类(globalasax)的实例这些实例在没有请求的时候会存放在一个应用程序池中(实际上应用程序池由另一个类来维护HttpRuntime只是简单的调用)每接收到一个请求HttpRuntime都会获取一个闲置的实例来处理请求这个实例在请求结束前不会处理其他的请求处理完毕之后它又会回到池中一个实例在其生存期内被用于处理多个请求但它一次只能处理一个请求(摘自MSDN)

当应用程序实例处理请求的时候它会创建请求页面类的实例执行它的ProcessRequest方法来处理请求这个方法也就是Web Page生命周期的开始

Aspx页面与CodeBehind

在深入了解页面的生命周期之前我们先来探讨一些Aspx与CodeBehind之间的关系

<%@ Page language=c# Codebehind=WebFormaspxcs Inherits=MyNamespaceWebForm %>

相信使用过CodeBehind技术的朋友对ASPX顶部的这句话应该是非常熟悉了我们来一项一项的分析它

Page language=c# 这个就不用多说了吧

Codebehind=WebFormaspxcs 这一句表示绑定的代码文件

Inherits=MyNamespaceWebForm 这句非常重要它表示页面继承的类名称也就是CodeBehind的代码文件中的类这个类必须从SystemWebWebControlsPage派生

从上面我们可以分析出实际上CodeBehind中的类就是页面(ASPX)的基类到这里可能有些朋友要问了在编写ASPX的时候完全是按照ASP的方式在Html中嵌入代码或者嵌入服务器控件没有看到所谓的影子啊?

这个问题实际上并不复杂各位使用ASPNet编程的朋友可以到你们的系统盘\WINDOWS\MicrosoftNET\Framework\<版本号>\Temporary ASPNET Files这个目录下这个下面就放了所有本机上存在的ASPNet应用程序的临时文件子目录的名称就是应用程序的名称然后再下去两层(为了保证唯一ASPNet自动产生了两层子目录并且子目录名称是随机的)然后我们会发现有很多类似yfygjhcdllxeunjudll这样的链接库以及komeebpcsfalckavcs这样的源文件实际上这就是ASPX被ASPNet动态编译后的结果打开这些源文件我们可以发现

public class WebForm_aspx MyNamespaceWebForm SystemWebSessionStateIRequiresSessionState

这就印证了我们前面的说法ASPX是代码绑定类的子类它的名称是ASPX文件名加上_aspx后缀通过研究这些代码我们可以发现实际上所有aspx中定义的服务器控件都是在这些代码中生成的然后动态产生这些代码的时候把原来在ASPX中嵌入的代码写在了相应的位置

当某个页面第一次被访问的时候Http运行时就会使用一个代码生成器去解析ASPX文件并生成源代码并编译然后以后的访问就直接调用编译后的dll这也是为什么ASPX第一次访问的时候非常慢的原因

解释了这个问题我们再来看另一个问题我们在使用代码绑定的时候在设计页面拖一个控件然后切换到代码视图就可以直接在Page_Load中使用这个控件了既然控件是在子类中产生的那为什么在父类中可以直接使用呢?

实际上我们可以发现每当用VSNet拖一个控件到页面上代码绑定文件中总是会类似这样的添加一个声明

protected SystemWebWebControlsButton Button

我们可以发现这个字段被声明成protected而且名字与ASPX中控件的ID一致仔细想一想这个问题就迎刃而解了我们前面提到ASPX的源代码是被生成器动态生成和编译的生成器会产生动态生成每一个服务器控件的代码在生成的时候它会检查父类有没有声明这个控件如果声明了它会添加类似下面的一句代码

thisDataGrid = __ctrl

这个__ctrl就是生成该控件的变量这时候它就把控件的引用赋给了父类中相应的变量这也是为什么父类中的声明必须为protected(实际上也可以为public)因为要保证子类能够调用

然后在执行Page_Load的时候因为这时候父类的声明已经被子类中的初始化代码赋了值所以我们就可以使用这个字段来访问对应的控件了解了这些我们就不会犯在代码绑定文件中的构造器里使用控件造成空引用的异常的错误了因为构造器是最先执行的这时候子类的初始化还没有开始所以父类中的字段是空值至于子类是什么时候初始化我们放到后面讨论

页面生存周期

现在回到第三个标题中讲到的内容我们讲到了HttpApplication的实例接收请求并创建页面类的实例实际上这个实例也就是动态编译的ASPX的类的一个实例上一个标题中我们了解到ASPX实际上是代码绑定中类的子类所以它继承了所有的protected方法

现在我们来看看VSNet自动生成的CodeBehind类的代码以此来开始我们对页面生命周期的探讨

#region Web Form Designer generated code

override protected void OnInit(EventArgs e)

{ // // CODEGEN该调用是 ASPNET Web 窗体设计器所必需的

// InitializeComponent()baseOnInit(e)}

/// <summary> /// 设计器支持所需的方法 不要使用代码编辑器修改/// 此方法的内容

/// </summary>

private void InitializeComponent()

{ thisDataGridItemDataBound += new SystemWebUIWebControlsDataGridItemEventHandler(thisDataGrid_ItemDataBound)

thisLoad += new SystemEventHandler(thisPage_Load)}

#endregion

这个就是使用VSNet产生的Page的代码我们来看这里面有两个方法一个是OnInit一个是InitializeComponent后者被前者调用实际上这就是页面初始化的开始在InitializeComponent中我们看到了控件的事件声明和Page的Load声明

下面是从MSDN中摘录的一段描述和一个页面生命周期方法和事件触发的顺序表

每次请求 ASPNET 页时服务器就会加载一个 ASPNET 页并在请求完成时卸载该页页及其包含的服务器控件负责执行请求并将 HTML 呈现给客户端虽然客户端和服务器之间的通讯是无状态的和断续的但是必须使客户感觉到这是一个连续执行的过程

这种连续性假象是由 ASPNET 页框架页及其控件实现的回发后控件的行为必须看起来是从上次 Web 请求结束的地方开始的虽然 ASPNET 页框架可使执行状态管理相对容易一些但是为了获得连续性效果控件开发人员必须知道控件的执行顺序控件开发人员需要了解在控件生命周期的各个阶段控件可使用哪些信息保持哪些数据控件呈现时处于哪种状态例如在填充页上的控件树之前控件不能调用其父级 下表提供了控件生命周期中各阶段的高级概述有关详细信息请点击表中的链接

阶段 控件需要执行的操作 要重写的方法或事件初始化 初始化在传入 Web 请求生命周期内所需的设置请参阅处理继承的事件 Init 事件(OnInit 方法)

加载视图状态 在此阶段结束时就会自动填充控件的 ViewState 属性详见维护控件中的状态中的介绍控件可以重写 LoadViewState 方法的默认实现以自定义状态还原 LoadViewState 方法处理回发数据 处理传入窗体数据并相应地更新属性请参阅处理回发数据

注意 只有处理回发数据的控件参与此阶段 LoadPostData 方法 (如果已实现IPostBackDataHandler)

加载 执行所有请求共有的操作如设置数据库查询此时树中的服务器控件已创建并初始化状态已还原并且窗体控件反映了客户端的数据请参阅处理继承的事件 Load 事件(OnLoad 方法)

发送回发更改通知 引发更改事件以响应当前和以前回发之间的状态更改请参阅处理回发数据

注意 只有引发回发更改事件的控件参与此阶段 RaisePostDataChangedEvent 方法(如果已实现 IPostBackDataHandler)

处理回发事件 处理引起回发的客户端事件并在服务器上引发相应的事件请参阅捕获回发事件

注意 只有处理回发事件的控件参与此阶段 RaisePostBackEvent 方法(如果已实现 IPostBackEventHandler)

预呈现 在呈现输出之前执行任何更新可以保存在预呈现阶段对控件状态所做的更改而在呈现阶段所对的更改则会丢失请参阅处理继承的事件 PreRender 事件(OnPreRender 方法)

保存状态 在此阶段后自动将控件的 ViewState 属性保持到字符串对象中此字符串对象被发送到客户端并作为隐藏变量发送回来为了提高效率控件可以重写 SaveViewState 方法以修改 ViewState 属性请参阅维护控件中的状态 SaveViewState 方法呈现 生成呈现给客户端的输出请参阅呈现 ASPNET 服务器控件 Render 方法处置 执行销毁控件前的所有最终清理操作在此阶段必须释放对昂贵资源的引用如数据库链接请参阅 ASPNET 服务器控件中的方法

Dispose 方法卸载 执行销毁控件前的所有最终清理操作控件作者通常在 Dispose 中执行清除而不处理此事件 UnLoad 事件(On UnLoad 方法)

从这个表里面我们可以清楚的看到一个Page从装载到卸载之间调用的方法和触发的时间接下来我们就深入的对其进行一些分析

看了上面的表细心的朋友可能要问了既然OnInit是页面生命周期的开始而我们在上一讲中谈到控件在子类中被创建那么在这里实际上在InitializeComponent方法中我们已经可以使用父类中声名的字段了那么就意味着子类的初始化更在这之前?

在第三个标题中我们讲到了页面类的ProcessRequest才是真正意义上的页面声明周期的开始这个方法是由HttpApplication调用的(其中调用的方式比较复杂有机会单独撰文来讲解)一个Page对请求的处理就是从这个方法开始通过反编译Net类库来查看源代码我们发现在SystemWebWebControlsPage的基类SystemWebWebControlsTemplateControl(它是页面和用户控件的基类)中定义了一个FrameworkInitialize虚拟方法然后在Page的ProcessRequest中最先调用了这个方法在生成器生成的ASPX的源代码中我们发现了这个方法的蹤影所有的控件都在这个方法中被初始化页面的控件树就在这个时候产生

接下来的事情就简单了我们来逐步分析页面生命周期的每一项

初始化

初始化对应Page的Init事件和OnInit方法

如果要重写MSDN推荐的方式是重载OnInti方法而不是增加一个Init事件的代理这两者是有差别的前者可以控制调用父类OnInit方法的顺序而后者只能在父类的OnInit后执行(实际上是在OnInit里面被调用的)

加载视图状态

这是个比较重要的方法我们知道对于每次请求实际上是由不同的页面类实例来处理的为了保证两次请求间的状态ASPNet使用了ViewState

LoadViewState方法就是从ViewState中获取上一次的状态并依照页面的控件树的结构用递归来遍历整个树将对应的状态恢复到每一个控件上

处理回发数据

这个方法是用来检查客户端发回的控件数据的状态是否发生了改变方法的原型

public virtual bool LoadPostData(string postDataKey NameValueCollection postCollection)

postDataKey是标识控件的关键字(也就是postCollection中的Key)postCollection是包含回发数据的集合我们可以重写这个方法然后检查回发的数据是否发生了变化如果是则返回一个True如果控件状态因回发而更改则 LoadPostData 返回 true否则返回 false页框架跟蹤所有返回 true 的控件并在这些控件上调用 RaisePostDataChangedEvent(摘自MSDN)

这个方法是SystemWebWebControlsControl中定义的也是所有需要处理事件的自定义控件需要处理的方法对于我们今天讨论的Page来说可以不用管它

加载

加载对应Load事件和OnLoad方法对于这个事件相信大多数朋友都会比较熟悉用VSNet生成的页面中的Page_Load方法就是响应Load事件的方法对于每一次请求Load事件都会触发Page_Load方法也就会执行相信这也是大多数人了解ASPNet的第一步

Page_Load方法响应了Load事件这个事件是在SystemWebWebControlControl类中定义的(这个类是Page和所有服务器控件的祖宗)并且在OnLoad方法中被触发

很多人可能碰到过这样的事情写了一个PageBase类然后在Page_Load中来验证用户信息结果发现不管验证是否成功子类页面的Page_Load总是会先执行这个时候很可能留下一些安全性的隐患用户可能在没有得到验证的情况下就执行了子类中的Page_Load方法

出现这个问题的原因很简单因为Page_Load方法是在OnInit中被添加到Load事件中的而子类的OnInit方法中是先添加了Load事件然后再调用baseOnInit这样就造成了子类的Page_Load被先添加那么先执行了

要解决这个问题也很简单有两种方法

) 在PageBase中重载OnLoad方法然后在OnLoad中验证用户然后调用baseOnLoad因为Load事件是在OnLoad中触发这样我们就可以保证在触发Load事件之前验证用户

) 在子类的OnInit方法中先调用baseOnInit这样来保证父类先执行Page_Load

发送回发更改通知

这个方法对应第步的处理回发数据如果处理回发数据返回True页面框架就会调用此方法来触发数据更改的事件所以自定义控件的回发数据更改事件需要在此方法中触发

同样这个方法对于Page来说没有太大的用处当然你也可以在Page的基础上自己定义数据更改的事件这当然也是可以的

处理回发事件

这个方法是大多数服务器控件事件引发的地方当请求中包含控件事件触发的信息时(服务器控件的事件是另一个论题我会在不久将来另外撰文讨论)页面控件会调用相应控件的RaisePostBackEvent方法来引发服务器端的事件

这里又引出一个常见的问题

经常有网友问为什么修改提交后的数据并没有更改

多数的情况都是他们没有理解服务器事件的触发流程我们可以看出触发服务器事件是在Page的Load之后也就是说页面会先执行Page_Load然后才会执行按钮(这里以按钮为例)的点击事件很多朋友都是在Page_Load中绑定数据然后在按钮事件中处理更改这样做有一个毛病Page_Load永远都是在按钮事件之前执行那么意味着数据还没来得及更改Page_Load中的数据绑定的代码就先执行了原有的数据又赋给了控件那么执行按钮事件的时候实际上获得的是原有的数据那么更新当然就没有效果了

更改这个问题也非常简单比较合理的做法是把数据绑定的代码写成一个方法我们假设为BindData

private void BindData()

{ //绑定数据}

然后修改PageLoad

private void Page_Load( object senderEventArgs e )

{ if( !IsPostBack )

{ BindData() //在页面第一次访问的时候绑定数据}

最后在按钮事件中

private Button_Click( object senderEventArgs e )

{ //更新数据BindData()//重新绑定数据}

预呈现

最终请求的处理都会转变为发回服务器的响应预呈现这个阶段就是执行在最终呈现之前所作的状态的更改因为在呈现一个控件之前我们必须根据它的属性来产生Html比如Style属性这是最典型的例子在预呈现之前我们可以更改一个控件的Style当执行预呈现的时候我们就可以把Style保存下来作为呈现阶段显示Html的样式信息

保存状态

这个阶段是针对加载状态的我们多次提到请求之间是不同的实例在处理所以我们需要把本次的页面和控件的状态保存起来这个阶段就是把状态写入ViewState的阶段

呈现

到这里实际上页面对请求的处理基本就告一段落了在Render方法中会递归整个页面的控件树依次调用Render方法把对应的Html代码写入最终响应的流中

处置

实际上就是Dispose方法在这个阶段会释放占用的资源例如数据库连接

卸载

最后页面会执行OnUnLoad方法触发UnLoad事件处理在页面对象被销毁之前的最后处理实际上ASPNet提供这个事件只是设计上的考虑通常资源的释放都会在Dispose方法中完成所以这个方法也变成鸡肋了

我们简单的介绍了页面的生存周期对于服务器端事件的处理做了不太深入的讲解今天主要是想大家了解页面执行的周期对于服务器控件的事件和生存期我会在后续在写一些文章来探讨

这些内容是我在学习ASPNet的时候对Page研究的一些心得具体的细节没有很详细的探讨更多的内容请大家参考MSDN但是我举了一些初学者常犯的错误和出现错误的原因希望可以给大家带来启发

上一篇:在Asp.net MVC中使用Repeater

下一篇:asp.net文件操作类