Page Life Cycle 之二: ViewState
ViewState用于记录同一个Page的不同请求之间保存和还原服务器控件的视图状态。那么它到底能存储什么信息呢?主要有两部分组成:
1 程序员通过调用ViewState[""]存储的信息
2 通过编程改变的控件状态
ViewState的使用
Asp.net内置控件的状态值都是通过ViewState存储的,如Textbox的Text属性值、TextMode属性等
public virtual string Text
{
get
{
string str = (string) this.ViewState["Text"];
if (str != null)
{
return str;
}
return string.Empty;
}
set
{
this.ViewState["Text"] = value;
}
}
[Themeable(false), WebSysDescription("TextBox_TextMode"), DefaultValue(0), WebCategory("Behavior")]
public virtual TextBoxMode TextMode
{
get
{
object obj2 = this.ViewState["Mode"];
if (obj2 != null)
{
return (TextBoxMode) obj2;
}
return TextBoxMode.SingleLine;
}
set
{
if ((value < TextBoxMode.SingleLine) || (value > TextBoxMode.Password))
{
throw new ArgumentOutOfRangeException("value");
}
this.ViewState["Mode"] = value;
}
}
也可以使用ViewState存储自己想要存储的信息,如ViewState["no"] =this.no
注:ViewState记录的信息只用于同一个页面的请求间使用,不同页面之间不起作用
ViewState作用时间
在Page的生命周期中,并不是任何时候都可以访问ViewState。必须在InitComplete事件开始才能访问,在PreInit及Init事件内不起作用。可以显示的调用TraceViewState()函数来启动ViewState。
(
bool ret;
ret = ViewState.IsItemDirty("item"); //returns false
ViewSTate["item"] = "1";
ret = ViewState.IsItemDirty("item"); //returns false
base.OnInit(e);
}
在Init方法里,即使给ViewState赋值,也无法使用。
{
bool ret;
ret = ViewState.IsItemDirty("item"); //returns false
ViewState["item"] = "1";
ret = ViewState.IsItemDirty("item"); //returns true
}
在InitComplete事件里,由于调用了TrackViewState(),在ViewState内保存的项就会被保存(项被标记为Dirty)
ViewState 和 PostBack
控件的ViewState功能可以开启也可以关闭,如果关闭,是否在post过程中就不会保存任何信息呢。下面是test code
void btnSubmit_Click(Object sender, EventArgs e)
{
lblMessage.Text = "Goodbye everyone";
lblMessage1.Text = "Goodbye everyone";
txtMessage.Text = "Goodbye everyone";
txtMessage1.Text = "Goodbye everyone";
}
</script>
<form id="form1" runat="server">
<asp:Label runat="server" ID="lblMessage" EnableViewState =true
Text="Hello World"></asp:Label>
<asp:Label runat="server" ID="lblMessage1" EnableViewState =false
Text="Hello World"></asp:Label>
<asp:Textbox runat="server" ID="txtMessage" EnableViewState =true
Text="Hello World"></asp:Textbox>
<asp:Textbox runat="server" ID="txtMessage1" EnableViewState =false
Text="Hello World"></asp:Textbox>
<br />
<asp:Button runat="server" Text="Change Message" ID="btnSubmit"></asp:Button>
<br />
<asp:Button ID="btnEmptyPostBack" runat="server" Text="Empty Postback"></asp:Button>
</form>
由两个Label、两个Textbox、两个Button组成。点击“Change Message”,执行btnSubmit_click事件,将所有控件的值被置为“Goodbye everyone”,这与预期结果一样。然后,点击“Empty PostBack”引发空回送,EnableViewState=false的控件(一个Label、一个Textbox)是否不保存前一个状态(Goodbye everyone)呢。结果是Label的值恢复为Hello World,而Textbox的值保留了前一个值“Goodbye everyone”。
其原因是在于asp.net 2.0新增的事件LoadPostbackData,在此事件中,实现了IPostBackEventHandler的控件会使用回送数据重置控件的状态。有以下三个重要点:
1 此阶段的数据不使用ViewState中获取的,而是从回送数据表中获取的
2 只把回送数据分发给实现了IPostBackEventHandler的控件,如Textbox、Checkbox
3 由于此事件发生在LoadViewState事件之后,此时会重写控件来自ViewState的值
TextBox的LoadPostBackDat
a()方法定义如下:
protected virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
base.ValidateEvent(postDataKey);
string text = this.Text;
string str2 = postCollection[postDataKey];
if (!this.ReadOnly && !text.Equals(str2, StringComparison.Ordinal))
{
this.Text = str2;
return true;
}
return false;
}
其中postDataKey为控件的主要标识符,postCollection为发送到服务器的名/值对的集合。首先进行验证操作,然后根据控件的标识从键值对集合中取出回送数据,如果允许更改,则更改Text属性值,这样从用户传回来的值就被重新设置到Textbox控件上,用户看到的还是提交前的数据。
子控件的ViewState
每个控件一般会经历如下几个阶段:
1。Instantiate
2。Initialize
3。Begin Tracking View State
4。Load View State (postback only)
5。Load Postback Data (postback only)
6。Load
7。Raise Changed Events (postback only, optional)
8。Raise Postback Events (postback only, optional)
9。PreRender
10。SaveViewState
11。Render
12。Unload
13。Dispose
对于Page对象,是在InitComplete()事件中开启ViewState的,而Page内的control则是在Init后开启的。test code如下;
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="Label1" runat="server" Text="StaticText"></asp:Label>
<asp:Button ID="Button1" runat="server" Text="PostBack"
onclick="Button1_Click" />
</div>
</form>
</body>
</html>
/********************************************************************/
protected void Page_PreInit(object sender, EventArgs e)
{
if (!IsPostBack)
{
Label1.Text = "dymaticText";
}
}
protected void Page_Init(object sender, EventArgs e)
{
if (!IsPostBack)
{
Label1.Text = "dymaticText";
}
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
Label1.Text = "dymaticText";
}
}
protected void Button1_Click(object sender, EventArgs e)
{
}
在PreInit、Init、及Load事件中,修改Label1的Text属性值。下面将测试这三个事件,哪些会使ViewState保存通过program修改的控件值。
注释掉PreInt、Init中代码,保留Load事件,由于在Load事件在InitComplete事件之后,所以毫无疑问,ViewState会保存对Label1控件的修改,将"symaticText"值保存在"__VIEWSTATE"隐藏字段中,当按下Button按钮回送页面时,在LoadViewState阶段,会从ViewState中提取dymaticText,并将值赋值给Label1控件的Text属性。最终显示在页面的是"dymaticText"字符串。
注释掉Init、Load,保留PreInit事件,由于PreInit事件在InitComplete事件之后,所以在此阶段更改的数据不会保存,回送后最终显示在页面的是设计时赋的值"StaticText".
注释掉PreInit、Load,保留Init事件。Init事件在InitComplete事件之前,但是回送后显示在页面的也是“dymaticText”。其原因如下:
由Page Life Cycle 之一: Overview一节知道,InitComplete不是递归事件,它只能有Page对象启动,Control控件不包含此事件。Control是在Init事件中开始track ViewState的,而Page的Init是递归事件,在此阶段page对象会调用子控件的Init事件处理函数, 所以从Page.Init开始,就可以保存Page的子控件的视图状态了。这就解释了测试代码的结果。
动态子控件 和 ViewState
可以在运行时根据需要动态的加载控件到Page对象,如下面代码那样:
Page.Controls.Add(lbl);
因为动态控件是在运行时加的,它不包含在编译好的类中,所以控件树不包含动态控件。必须在Page的每一次加载过程中运行上面的代码,以示它能插入到控件树中,并最终显示到页面。对动态控件的初始化操作或添加操作都不能包含在!IsPostBack条件语句中。
此外,动态控件一旦被添加到控件组,就会执行生命周期的“追赶”过程。也就是说,它会把它错过的事件依次执行一遍。比如,如果add操作是在Page_Load事件中,当添加到Controls中后,它会依次执行Init、LoadVewState、LoadPostBackData,直至追赶到Page目前执行的Load事件。原则上,用户可以在Render之前的任意阶段添加动态控件,但一般推荐在PreInit、Init阶段,因为最好在启用ViewState的InitComplete之前把控件添加到控件树上,以保存其视图状态。
<script language="C#" runat="server">
void Page_Load(Object sender, EventArgs e)
{
DropDownList ddlDynamic = new DropDownList();
ddlDynamic.ID = "ddlDynamic";
form1.Controls.Add(ddlDynamic); //(1)
if (!IsPostBack)
{
for (int i=1; i <=3; i++)
ddlDynamic.Items.Add(new ListItem(i.ToString(), i.ToString()));
}
//form1.Controls.Add(ddlDynamic); //(2)
if (IsPostBack)
{
Response.Write("[Page_Load]静态:" + ddlStatic.SelectedIndex + "<BR>");
Response.Write("[Page_Load]动态:" + ddlDynamic.SelectedIndex + "<BR>");
}
}
void Button_Click(Object sender, EventArgs e)
{
DropDownList ddlDynamic = (DropDownList)form1.FindControl("ddlDynamic");
Response.Write("[Button_Click]静态:" + ddlStatic.SelectedIndex + "<BR>");
Response.Write("[Button_Click]动态:" + ddlDynamic.SelectedIndex + "<BR>");
}
</script>
<body>
<form id="form1" runat="server">
<asp:Button id="btn" runat="server" Text="Click Me" OnClick="Button_Click" />
<br/>
静态: <asp:DropDownList id="ddlStatic" runat="server">
<asp:ListItem Text="1" Value="1" />
<asp:ListItem Text="2" Value="2" />
<asp:ListItem Text="3" Value="3" />
</asp:DropDownList>
<br/>
动态:
</form>
</body>
</html>
上面的测试代码中,form1.Controls.Add操作放在(1)、(2)两个不同的地方,执行结果却大不一样。
代码(1):执行结果与预期相同。首次执行时,动态下拉框填充了数据,分别选择第二项和第三项,然后点击按钮回送,页面上显示:
[Page_Load]动态:0
[Button_Click]静态:1
[Button_Click]动态:2
并且两个下拉框中保存了上次选择的项。
代码(2):首次执行时,动态下拉框填充数据。分别选择第二项和第三项,然后点击按钮回送,页面显示:
[Page_Load]动态:-1
[Button_Click]静态:1
[Button_Click]动态:-1
没有保存动态下拉框的选项,并且动态下拉框中没有数据。
根据前面的原理我们可以分析出,其原因如下:
1 填充下拉框的代码包含在 !IsPostBack代码内,回送时自然不会填充控件;
2 执行fomr1.Controls.Add(ddlDynamic)代码时,DropDownList实例对象会依次执行Init、LoadViewState、LoadPostbackData。把这句话放在填充数据的前面,对control所作的更改才能保存在ViewState中。而如果放在后面,只是把控件放入控件树,并没有保存对它的更改记录,在回送时,ViewState中没有它的记录。如果把 !IsPostBack去掉,就能够填充控件了。
3 去掉 !IsPostBack,虽然可以填充数据,但执行结果如下:
[Page_Load]静态:1
[Page_Load]动态:0
[Button_Click]静态:1
[Button_Click]动态:2
在PageLoad阶段没能设置PostBack值,但在Button_Click阶段却正确设置了。其原因是什么呢?
Page.ProcessRequestMain函数定义如下(经删减后的部分),从它的定义中我们可以看到一个页面所经历的全过程。
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint)
{
HttpContext context = this.Context;
if (this.PageAdapter != null)
{
this._requestValueCollection = this.PageAdapter.DeterminePostBackMode();
}
else
{
this._requestValueCollection = this.DeterminePostBackMode();
}
this.PerformPreInit();
this.InitRecursive(null);
this.OnInitComplete(EventArgs.Empty);
this.LoadAllState();
this.ProcessPostData(this._requestValueCollection, true);
this.OnPreLoad(EventArgs.Empty);
this.LoadRecursive();
this.ProcessPostData(this._leftoverPostData, false);
this.RaiseChangedEvents();
this.RaisePostBackEvent(this._requestValueCollection);
this.OnLoadComplete(EventArgs.Empty);
this.PreRenderRecursiveInternal();
this.PerformPreRenderComplete();
this.SaveAllState();
this.OnSaveStateComplete(EventArgs.Empty);
this.RenderControl(this.CreateHtmlTextWriter(this.Response.Output));
}
从定义看出,Page在Load和LoadComplete之间还包含三个阶段:
ProcessPostData second try:在Load之后,处理未处理过的PostBack data。这种方式允许用户在Load方法内加载动态控件,并获取用户的输入数据。
RaiseChangedEvents:服务器控件通知asp.net状态发生了更改
RaisePostBackEvents:进行了加载并发出了更改通知后,在预呈现发生前触发此事件。通知引起回发的服务器控件,使其处理传入的回发事件。如Button控件的Click事件,从而进入Click事件处理程序
根据上面的分析,可以知道最后一个问题的原因了:在Load前的ProcessPostBackData阶段,由于还未对动态DropDownList填充数据,无法给它赋予post data(这里是SelectedIndex),所以在Load里无法获取用户所选的项。但在Load之后,数据已填充完成后,就可以在第二个ProcessPostBackData中设置post data了,Button_Click是在第二次ProcessPostBackData之后的RaisePostBackEvent中触发的,所以在Click事件里就能够正确获取用户所选项了。
接下来仔细查看两个方法的定义:
internal void RaiseChangedEvents()
{
if (this._changedPostDataConsumers != null)
{
for (int i = 0; i < this._changedPostDataConsumers.Count; i++)
{
Control control = (Control) this._changedPostDataConsumers[i];
if (control != null)
{
IPostBackDataHandler postBackDataHandler = control.PostBackDataHandler;
if (((control == null) || control.IsDescendentOf(this)) && ((control != null) && (control.PostBackDataHandler != null)))
{
postBackDataHandler.RaisePostDataChangedEvent();
}
}
}
}
}
private void RaisePostBackEvent(NameValueCollection postData)
{
if (this._registeredControlThatRequireRaiseEvent != null)
{
this.RaisePostBackEvent(this._registeredControlThatRequireRaiseEvent, null);
}
else
{
string str = postData["__EVENTTARGET"];
bool flag = !string.IsNullOrEmpty(str);
if (flag || (this.AutoPostBackControl != null))
{
Control control = null;
if (flag)
{
control = this.FindControl(str);
}
if ((control != null) && (control.PostBackEventHandler != null))
{
string eventArgument = postData["__EVENTARGUMENT"];
this.RaisePostBackEvent(control.PostBackEventHandler, eventArgument);
}
}
else
{
this.Validate();
}
}
}
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void RaisePostBackEvent(IPostBackEventHandler sourceControl, string eventArgument)
{
sourceControl.RaisePostBackEvent(eventArgument);
}
RaiseChangeedEvent事件中每一个状态发生了更改的控件(实现了IPostBackDataHandler接口)都执行RaisePostBackChangedEvent,通知asp.net该控件状态执行了更改。 在此例中是DropDownList空间内容发生更改,DropDownList控件的实现内容如下:
protected virtual void RaisePostDataChangedEvent()
{
if (this.AutoPostBack && !this.Page.IsPostBackEventControlRegistered)
{
this.Page.AutoPostBackControl = this;
if (this.CausesValidation)
{
this.Page.Validate(this.ValidationGroup);
}
}
this.OnSelectedIndexChanged(EventArgs.Empty);
}
该事件的调用引发DropDownList控件的SelectdIndecChanged事件,如果用户定义了该事件的处理程序,则进入处理程序继续执行。
RaisePostBackEvent则是调用引起回送的控件(实现了IPostBackEventHandler接口)的RaisePostBackEvent事件,从而进入回送事件处理程序。Button控件的RaisePostBackEvent实现如下:
protected virtual void RaisePostBackEvent(string eventArgument)
{
base.ValidateEvent(this.UniqueID, eventArgument);
if (this.CausesValidation)
{
this.Page.Validate(this.ValidationGroup);
}
this.OnClick(EventArgs.Empty);
this.OnCommand(new CommandEventArgs(this.CommandName, this.CommandArgument));
}
通过触发RaisePostBackEvent进入Button的Click事件处理程序。
参考:
http://www.codeproject.com/KB/aspnet/ASPViewStateandPostBack.aspx
http://www.codeproject.com/KB/aspnet/aspnetviewstatepagecycle.aspx
http://blog.joycode.com/saucer/archive/2004/10.aspx