原文地址:http://blog.csdn.net/gatr/article/details/6804405
通俗理解页面生命周期
丽水市汽车运输集团股份有限公司信息中心 苟安廷
ASP.NET开发初学者经常问的一个问题是:为什么我一点分页按钮,表格里面的数据就没有了?其实,类似这样的问题,大多是没有对页面的生命周期(Page类的编程模型)正确理解。本文拟使用通俗易懂的描述,帮助初学者理解页面的生命周期,目的在于帮助初学者理解,因此,用词可能不严谨。
为便于后面的理解,我们需要先复习一些基础知识。
u 浏览器的能力
ASP.NET开发的软件属B/S软件,B就是Brower,即浏览器,也就是说,我们写的东西,最终是需要浏览器来展示的。我们可以这样认为,浏览器只认识3个东西:html、css和JavaScript,html主要用于描述控件的类别,如<inputid="Text1"type="text"/>表示一个文本框,css用于描述html控件的外观,当然,html也可以直接描述外观,但为了使html代码简单清晰,同时便于多个html控件共享外观,通常将外观描述部分分离出来,单独放到一个地方,这就是css。JavaScript是编程语言,用于对html控件进行操作。当然,借助插件,浏览器还可以认识视频、flash,甚至U盾等等,不过,这些都是插件的功劳,并不是浏览器与生俱来的,有关插件的相关知识超出了本文的范畴,建议参照其他相关资料。
既然浏览器只认识html、css和JavaScript,那么,我们在ASPX页面中布局的服务器控件、在cs代码中修改的东西等等,发送到客户端时,最终都必须转换成标准的html、css和JavaScript,而我们在网页上看到的光怪陆离的效果,其实,都是标准的html控件“模拟”出来的,为此,我们做一个试验,新建一个aspx页面,拖一个文本框上去,在aspx页面中,对应的代码(其他无关代码省略了)是:
<asp:TextBoxID="TextBox1"runat="server"></asp:TextBox>
运行后,在浏览器上点右键,查看源文件,发现浏览器收到的实际代码是:
<inputname="TextBox1"type="text"id="TextBox1" />
我们可以看到,服务器控件到了客户端,会被转换成标准的HTML控件,如果我们新建一个HTML文件,直接把服务器控件的描述(<asp:TextBox/>放进去,浏览器是不认的。
u 服务器事件
当我们点一下页面上的按钮,页面好像自动调用了服务器上的Click方法,同样,其他很多操作,如下拉框(设置AutoPostBack为True)的当前选择项更改时,都自动调用了后台对应的方法,貌似和普通C/S软件差不多,其实,这些也是“模拟”的,可以说,为了让B/S软件开发起来和C/S差不多,“模拟”功不可没,服务器真正收到的,只有一个事件:页面请求。
u 页面访问方式
我们知道,页面访问有两种方式:Get和Post,那么,到底该如何区分呢?我们来看两种情况:
1.直接输入网址,如http://www.lqjt.com,当然,还可以输入一些参数,如:http://www.lqjt.com?id=5,当我们直接在浏览器地址栏里面输入地址(包括带参数的地址),然后按回车就可以访问页面了,而此时,服务器收到的,仅仅是一个网址名称,这种轻量级的访问,就是我们所说的Get。
2.当页面已经打开时,假如页面上有一个按钮,我们点击该按钮,页面会回传,也就是重新读取页面,这时,浏览器就不是简单把网址发给服务器了,而是将当前页面中的内容作为附加信息,连同网址一起发送给服务器,也就是说,服务器收到的信息,包括网址和附加信息,这就是Post方式。
也就是说,Get方式收到的仅仅是网址,没有附加信息,而Post方式,除了网址,还要当前页面的相关内容信息,注意,网址后面的“?id=5”可以认为是网址的一部分,和Post访问中的附加信息是两回事,最直观的区别是,一个是直接在地址栏输入地址访问,另外一个是页面回传。
u 服务器端和客户端的顺序
总有人问:C#能否调用客户端的JavaScript啊,或者JavaScript能否调用C#的方法啊,其实服务器端和客户端是“接力棒”的关系,服务器端完成任务后,立即就释放了,也就是说,浏览器收到服务器返回的信息时,服务器端的页面已经不存在了,因此,这种互相访问是不存在的,或许你会说,为什么Ajax方法可以呢?需要说明的是,Ajax访问服务器时,访问的也是新建立的页面,而不是“刚才”的页面,浏览器接收到新页面后,刚才的页面内容全部扔了,也是典型的“喜新厌旧”,因此,你也不要指望新页面达到后,使用老页面的控件、变量等,如果需要保存变量,一般采用cookie。
有了上面的准备,我们开始进入正题。我们说了,服务器收到的,其实只有一个事件:页面请求,各种事件都是模拟的,而服务器返回的,是标准的HTML。当我们向服务器发送页面请求时,服务器新建一个页面,然后进行处理,处理完成后,把最终结果返回给浏览器,同时释放刚刚生成的页面,整个页面从诞生到消亡的整个过程,我们称之为页面生命周期,或者叫Page类编程模型,页面的生命周期是非常短暂的,每次请求,就产生一个独立的、全新的页面,本次请求结束后,立即被释放。因为每次都是全新的,因此,不要期望页面保存“上一次请求”的信息,这就是所谓的“无状态”。为了“模拟”C/S,ASP.NET将页面生命周期分成40多个阶段,不过常用也就几个,比如我们最熟悉的“Page_Load”阶段,每个阶段都有特定的目的,为了便于描述,我们假如整个服务器端就是一个方法,当然,并不是服务器上真的有该方法,而是我们希望用大家熟悉的C#来描述整个过程。
我们定义这样一个方法来描述整个页面生命周期:
///<summary>
///模拟页面生命周期
///</summary>
///<paramname="url">页面的网址</param>
///<paramname="Content">附加信息</param>
///<returns>浏览器能解析的标准HTML</returns>
publicstring AspWebService(stringurl,byte[] Content)
{
string strHTML ="";
return strHTML;
}
我们创建了上面一个公共方法来模拟整个页面生命周期,浏览器调用AspWebService方法,传入页面的地址(url)和当前页面的信息(Content),服务器创建新的页面,并完成各个阶段,最后,产生一个标准的HTML返回给浏览器,浏览器接收以后,把标准的HTML显示出来,下面,我们将几个关键的过程完成,看看最终页面是如何生成的。
当我们在地址栏里面输入网址并回车,页面请求就开始了,浏览器调用服务器的AspWebService方法。因为我们直接输入的地址,没有附加消息,属Get方式访问,Content当然就是null为此,我们需要声明一个变量,来表示是否有附加消息
bool IsPostBack;
if (Content ==null)
IsPostBack =false;
else
IsPostBack =true;
IsPostBack大家已经非常熟悉了,如果是第一次打开页面,IsPostBack为False,否则,为True,显然,第一次输入网站访问时,IsPostBack为False,为减少篇幅,把上面的代码写到一句话里面,完成后的方法为:
publicstring AspWebService(stringurl,byte[] Content)
{
bool IsPostBack=(Content ==null)? false: true;
string strHTML ="";
return strHTML;
}
接下来,读入aspx页面中的内容,并根据aspx页面中定义的控件,转换成对应的C#内存变量,这里,我们用一个字符串变量表示读入的aspx页面信息:
string strAspxLayout ="......<asp:ButtonID='Button1' runat='server' Text='Button' />....";
当然,一个aspx页面的内容非常多,我们只截取其中的一个按钮,前后的其他控件部分用省略号代替,根据aspx的布局,创建对应的变量:
Button btn =newButton();
btn.ID ="Button1";
btn.Text ="Button";
显然,上面的办法是将aspx页面中的控件“翻译”成C#能认识的控件,便于我们操作,现在的代码变成:
publicstring AspWebService(string url,byte[]Content)
{
bool IsPostBack=(Content ==null)?false:true;
string strAspxLayout ="…….<asp:ButtonID='Button1' runat='server' Text='Button' />……";
if (strAspxLayout.Length > 0)
{
//根据<asp:Button>创建按钮,其他控件类推
Button btn =newButton();
btn.ID ="Button1";
btn.Text ="Button";
}
string strHTML ="";
return strHTML;
}
控件创建好以后,将调用我们常用的第一个方法:Page_Init();该方法的具体用处我们后面会讲到。
接下来,根据是否是页面回传做必要的处理,因为我们本次是用Get方式访问的,因此,这一步我们暂时空着,等下次页面回传时再逐步完善,现在的代码如下:
publicstring AspWebService(string url,byte[]Content)
{
bool IsPostBack=(Content ==null)?false:true;
string strAspxLayout ="......<asp:ButtonID='Button1' runat='server' Text='Button' />....";
if (strAspxLayout.Length > 0)
{
Button btn =newButton();
btn.ID ="Button1";
btn.Text ="Button";
}
//调用常用的第一个方法
Page_Init(this,newEventArgs());
//如果是页面回传,进行必要处理,暂时空着
if (IsPostBack)
{
}
string strHTML ="";
return strHTML;
}
接下来,调用第二个方法,也就是我们最熟悉的方法:Page_Load,我们很多工作都是在Page_Load里面做的
//调用最常用的方法
Page_Load(this,newEventArgs());
接下来,看看有没有其他事件需要响应,比如button1_Click什么的现在的代码如下:
publicstringAspWebService(string url,byte[] Content)
{
bool IsPostBack=(Content ==null)?false:true;
string strAspxLayout ="......<asp:ButtonID='Button1' runat='server' Text='Button' />....";
if (strAspxLayout.Length > 0)
{
Button btn =newButton();
btn.ID ="Button1";
btn.Text ="Button";
}
Page_Init(this,newEventArgs());
if (IsPostBack)
{
}
//调用Page_Load方法
Page_Load(this,newEventArgs());
//如果有其他事件需要执行,在这里调用
Button1_Click(this,newEventArgs());
string strHTML ="";
return strHTML;
}
服务器端该做的事情差不多了,接下来,要准备发送到客户端了,前面说过,浏览器只认识HTML、CSS、JavaScript,“Button btn=new Button()”这样的显然是不行的,因此,我们要把当前内存中的服务器控件转换成浏览器能认识的标准HTML控件,比如,把按钮转换成<inputtppe=’submit’/>之类。该过程需要我们调用Render方法,在一个真实的页面中,我们可以重载下面的方法修改默认的HTML:
protectedoverridevoid Render(HtmlTextWriterwriter)
{
base.Render(writer);
}
如果没有重载,将使用默认的,在这里,我们就是要模拟Render方法,也就是说,我们要准备strHTML变量的内容,HTML其他部分我们忽略,这里示意一下按钮对应的HTML代码:
//准备浏览器能认识的HTML文本
string strHTML ="......<inputtype='submit' name='Button1' value='Button' id='Button1' />....";
内存中的控件和标准的HTML控件并不是可以完全互相转换的,在很多方面差别还很大,尤其是C#控件有大量属性,而HTML控件相对简单,在经过一系列操作(尤其是Page_Load和Button1_Click等方法处理后,或者动态添加、删除了部分控件),内存中的TextBox等控件已经面目全非了,前面说过了,B/S是无状态的,本次结束后,内存中的全部东西都会被释放,为了便于下次能“还原”到当前的样子,我们需要把内存中的控件等信息保存起来,这就是ViewState。例如一个文本框,有宽度、高度、文本内容、颜色等信息,我们把这些东西序列化,或者说,转换成一个字符串,然后和上面的html放一起,作为返回值的一部分。为此,我们创建一个隐藏域,值就是序列化以后的内容:
string strViewState ="<inputtype='hidden' name='__VIEWSTATE' id='__VIEWSTATE'value='/wEPDwUKLTExMjgzODMzMWRk+oeVKURRouiTt3dBl+gaw3M+9Ds=' />";
和上面的HTML组合起来:
strHTML += strViewState;
显然,strViewState中,value的内容就是当前页面各个控件(严格来说,用户可以在里面保存任何东西,比如变量值等等)转换而来的,我们把ViewState放到HTML里面,作为HTML的一部分,我们编写的模拟Render方法如下:
privatestring Render()
{
string strHTML ="......<inputtype='submit' name='Button1' value='Button' id='Button1' />....";
string strViewState ="<inputtype='hidden' name='__VIEWSTATE' id='__VIEWSTATE'value='/wEPDwUKLTExMjgzODMzMWRk+oeVKURRouiTt3dBl+gaw3M+9Ds=' />";
strHTML += strViewState;
return strHTML;
}
也就是说,生成的标准的HTML控件代码,是给浏览器看的,且用户可以更改(比如输入文字),而将内存中的控件序列化后,放隐藏域里面的目的是便于下一次还原,达到持久化的目的,且用户无法修改,但我们返回给客户端的只能是一个字符串,而当前内存控件很快要被释放,为此,我们将内存控件序列化后得到的字符串附加到前面的HTML中去,将HTML作为一个临时存放的场所。
然后在主程序里面调用上面的Render方法,得到的代码如下:
publicstringAspWebService(string url,byte[] Content)
{
bool IsPostBack=(Content ==null)?false:true;
string strAspxLayout ="......<asp:ButtonID='Button1' runat='server' Text='Button' />....";
if (strAspxLayout.Length > 0)
{
Button btn =newButton();
btn.ID ="Button1";
btn.Text ="Button";
}
Page_Init(this,newEventArgs());
if (IsPostBack)
{
}
Page_Load(this,newEventArgs());
Button1_Click(this,newEventArgs());
//准备浏览器能认识的HTML文本
string strHTML = Render();
return strHTML;
}
最后一步,很简单,就是把strHTML返回给客户端,然后再释放整个页面占用的内存,至此,第一轮的整个页面周期结束,浏览器得到strHTML后,呈现给用户,用户看到的就是漂亮的页面了。
前面讲的是Get方式访问页面的整个生命周期,其中,还留了一点没有做,下面我们来看第二种方式:Post方式是如何进行的,两种方式基本上一样,只不过Post要多一点点东西,就是我们上面未完成的。
现在,页面上已经有很多东西,用户输入完信息,点“提交”按钮,整个页面会被回传,新的页面生命周期又开始了,会重复上面的过程,不过,这次是按钮提交的,不是手工输入的网址,除了网址外,还包括当前页面的信息,比如文本框中用户输入的内容,下拉框当前选择等等,我们把这些保存在Content参数中,也就是说,这一次访问,Content是有内容的,内容就是当前客户端页面的全部信息,而不是null,我们来看看是如何进行的。
1. <![endif]>设置IsPostback变量,这一步完全一样
2. <![endif]>读入aspx页面内容,并转换成内存变量,这一步也是一样的
3. <![endif]>调用Page_Init方法,也一样
4. <![endif]>处理回传内容,也就是上面的if(IsPostBack)等待我们完成,现在我们就来完成
根据Content参数的内容,重新设置一下前面3步创建的控件,比如,页面设计时,默认的文本框内容是空的,现在,用户输入了内容,那么,就必须执行this.TextBox1.Text="用户输入的新内容",通过aspx页面的布局,结合客户端返回的值(保存在Content中),可以创建一个客户端当前页面的信息。
还记得前面的ViewState吗?那是保存上一次页面在服务器时的状态的,该变量也是在Content的一部分,我们把ViewState取出来,再根据ViewState新建一个页面,这时,我们是不是得到了前一次页面的样子了?
换句话说,我们根据Content中的内容,创建了一个客户端当前页面,根据ViewState还原了上一次服务器端的页面,这样,我们就有两个页面了,一个是当前页面,也就是经过客户端用户操作以后的页面,一个是上一次在服务器端的页面,也就是通过ViewState还原的页面,接下来,我们比较一下两个页面,假如文本框内容不一样,那么,我们就登记一个事件,TextBoxChanged事件,只不过,该事件不会马上执行,我们先记着,同样,也要记着可能产生的其他事件,比如下拉框当前选项改变等等,而Content中除了有控件当前状态等信息,还有一个信息就是,该页面回传是谁产生的,比如是按钮产生的,那么,也要登记一个Click事件,因此,这部分代码我们可以用下面的方法模拟:
if (IsPostBack)
{
Page page_Cur =this;
//根据Content还原成客户端的样子
this.TextBox1.Text ="new value";
//还原上一次页面的样子
Page page_Last =newPage();
string strViewState ="内容从Content中的ViewState隐藏域提取";
if (strViewState.Length > 0)
{
//根据strViewState创建控件并添加到page_Last中
}
//比较两个页面和根据事件源登记事件
foreach (Controlctlin page_Cur.Form.Controls)
{
foreach (Controlctl2inpage_Last.Controls)
{
if (ctl.ID == ctl2.ID)
{
//如果有变动,登记事件
}
}
}
}
后面的方法完全一样了。
最后,我们来看看整个模拟代码:
publicstringAspWebService(string url,byte[] Content)
{
//1.根据是否有附加信息,设置IsPostBack
bool IsPostBack=(Content ==null)?false:true;
//2.根据aspx页面布局,创建页面及控件
string strAspxLayout ="......<asp:ButtonID='Button1' runat='server' Text='Button' />....";
if (strAspxLayout.Length > 0)
{
Button btn =newButton();
btn.ID ="Button1";
btn.Text ="Button";
}
//3.调用Page_init方法
Page_Init(this,newEventArgs());
//4.如果是页面回传,根据Content恢复客户端页面,根据ViewState恢复上一次页面,并比较,然后登记事件
if (IsPostBack)
{
Page page_Cur =this;
this.TextBox1.Text ="new value";
Page page_Last =newPage();
string strViewState ="内容从Content中提取隐藏域";
if (strViewState.Length > 0)
{
//根据strViewState创建控件并添加到page_Last中
}
foreach (Controlctlin page_Cur.Form.Controls)
{
foreach(Control ctl2inpage_Last.Controls)
{
if (ctl.ID == ctl2.ID)
{
//如果有变动,登记事件
}
}
}
}
//5.调用Page_Load方法
Page_Load(this,newEventArgs());
//6.根据前面登记的事件,调用对应的方法
Button1_Click(this,newEventArgs());
//7.调用Render方法完成HTML字符串,HTML包含了ViewState
string strHTML = Render();
return strHTML;
}
下面,我们来讨论几个相关问题:
1. 数据绑定方式如下,为什么一点分页按钮,就没有数据了?
protectedvoid Page_Load(object sender,EventArgse)
{
if (!IsPostBack)
{
GridView1.DataSource =……;
GridView1.DataBind();
}
}
分析:前面说了,每次页面都是全新的,当页面回传时,IsPostBack为true,上面的代码就不会绑定数据了,当然是空的。
解决方法:把if(!IsPostBack)去掉。
2. 动态添加控件是不是在Page_Load方法里面?
分析:在页面的生命周期中,按照这样的先后顺序处理:读取ASPX中的控件、创建内存控件、调用Page_Init、恢复客户端控件和ViewState、调用Page_Load,显然,最合适的地方是Page_Init方法中,这样,和“原生”的控件没什么区别了。
3. 能否查看、修改发送给客户端的HTML
分析:显然,只需重载一下Render方法。
protectedoverridevoidRender(HtmlTextWriter writer)
{
StringWriter sw =newStringWriter();
HtmlTextWriter htmlw =newHtmlTextWriter(sw);
base.Render(htmlw);
htmlw.Flush();
htmlw.Close();
//这就是发送给客户端的内容,你可以随便加工,比如去掉多余的空格等等
string strConn = sw.ToString();
//这句话别忘了
Response.Write(strConn);
}
4. 能否减小ViewState以节约带宽
我们知道,ViewState保存了本次页面的信息,附加到html中,页面回传时,再由服务器处理(根据ViewState还原上一次页面的信息),仅仅用于服务器端,而客户端仅仅起临时保存用,但发送到客户端再回传到服务器,一个来回两次占用带宽,的确是一笔不小的开支,尤其是当控件很多时,甚至占到整个HTML内容的1/3以上,既然ViewState仅仅是用于服务器端,那么我们可以想办法让ViewState留在服务器上,具体思路是:
(1)在数据库中创建一个两列的表,一列是主键ID,一个列存放ViewState内容,ViewState生成以后(假如为string strViewState),我们拦截下来,用一个新的ID,把strViewState保存到数据库中,并用该ID替换strViewState返回,这样一来,HTML中的ViewState内容并不是真正的ViewState,而是刚刚生成的ID,体积当然非常小了。
(2)页面回传后,把客户端传回来的ViewState取出来,显然,这时得到的是第(1)步存放的ID而不是真正的ViewState,我们根据ID,从数据看里面查询出真正的ViewState,同时,顺便把ID对应的记录删除。
这样一来,就成功将真正的ViewState留在了服务器,不过,需要占用一定的服务器空间,也要记得清理过期的数据(只有页面回传时,上一次的数据才会被删除),这就是所谓的用空间换时间(用服务器空间换取网络传送时间),有人可能会说,服务器读写ViewState也是要时间的,但别忘了,IIS和数据库一般是同一台服务器,即使是多台,也在一个局域网内,速度很快,这个速度远远快于通过http协议传送页面内容,实现的关键步骤如下:
///<summary>
///将试图持久化到数据库中
///</summary>
///<paramname="state">本页面ViewState</param>
protectedoverridevoidSavePageStateToPersistenceMedium(object state)
{
//1.获取一个ID
string strViewState =……;
//2.将ID和state保存到服务器,注意state可以序列化成byte[],方法略
SaveState(strViewState, state);
//3.用ID替换到真正的ViewState
base.SavePageStateToPersistenceMedium(strViewState);
}
///<summary>
///载入通过数据库持久化的视图
///</summary>
///<returns>真正的ViewState</returns>
protectedoverrideobjectLoadPageStateFromPersistenceMedium()
{
//1.从ViewState中提取ID
string strViewState = (string)((Pair)base.LoadPageStateFromPersistenceMedium()).Second;
//2.根据ID查询真正的ViewState,同时将原来的从数据库删除,具体方法略
byte [] states=…..;
//3.返回真正的ViewState
return states;
}
5. Ajax貌似可以前台后台同时存在
Ajax访问页面和普通访问差别不是很大,关键是页面返回信息的处理方式不一样,普通页面信息返回后,浏览器把老的页面信息扔掉,显示新返回来的页面信息,而通过Ajax得到新的信息后,并不是由浏览器直接替换老的页面,而是交给JavaScript的一个函数(也就是回调函数),该函数根据返回的信息,做一些处理,然后对原来的页面进行有选择性的操作,就是所谓的“局部更新”,或者说,JavaScript回调函数取得了返回的字符串,然后根据该字符串做自己想做的事情,也可以什么都不做,直接扔掉。既然是通过回调函数处理得到的字符串,那么,字符串就不一定必须是HTML了,可以是任何字符串,回调函数收到后,可以进行任何处理,而没必要一定去新某个HTML控件,比如我们在服务器端这么处理:
protectedoverridevoid Render(HtmlTextWriterwriter)
{
string strName ="";
switch (Request["WorkNo"])
{
case"1001":
strName ="张三";
break;
case"1002":
strName ="李四";
break;
}
Response.Write(strName);
}
显然,重载了Render方法,根据工号返回对应的姓名,客户端收到以后,可以简单用alert(text)来提示一下,而页面内容不会改变,是不是有点WebService的味道?我们知道,网站可以压缩(主要针对页面、脚本等),而WebService返回的信息难以压缩,因此,需要返回大量数据时,用aspx页面代替WebService也是不错的思路。
6. 书籍推荐
要想对ASP.NET原理深入理解,掌握服务器控件开发是一种捷径,通过服务器控件的开发,可以弄明白服务器控件到底是如何工作的,也可以将常用的东西做成dll,供不同的页面、项目调用。不过,服务器控件开发难度较大,学习起来很吃力,需要很好的耐性,这里推荐两本书给大家参考,申明一下,本人和该书作者或出版社无任何关系,仅仅是因为本人阅读了这两本书后受益匪浅:
《道不远人--深入解析ASP.NET 2.0控件开发》
《庖丁解牛:纵向切入ASP.NET3.5控件和组件开发技术》
这两本书的讲解都很到位,互为补充,但刚阅读时非常晦涩难懂,必须要反复看才能明白,古人云:半部论语治天下,我想,这两本只要弄懂了前面几章,写ASP.NET软件就游刃有余了。