Saturday, October 09, 2004

 

动态控件的状态问题

By 思归
From http://blog.joycode.com/saucer/archive/2004/10/20/35927.aspx
http://blog.joycode.com/saucer/archive/2004/10/21/36124.aspx
http://blog.joycode.com/saucer/archive/2004/10/26/36466.aspx

在论坛上,动态控件好象是永久的话题。大家都知道要动态控件起作用,PostBack时需要重新生成或装载(LoadControl),而且需要深入了解其状态的变化过程。

有个同事另谋高就,要离开我们工作的地方了。我给她出了一道出门考题,同时也叫其他手下一起参加。这题目是这样的:

下面两页差别很小,就是一句语句的前后次序有所不同,但PostBack后显示效果有所不同,请解释为什么显示效果不同,并且解释正确显示的那页(你知道是哪页,对么?)中Response.Write的输出结果

TestDyn1.aspx:
html>
body>
form id="form1" runat="server">


静态:





动态:
/form>
/body>
/html>
script language="C#" runat="server">
void Page_Load(Object sender, EventArgs e)
{
DropDownList ddlDynamic = new DropDownList();
ddlDynamic.ID = "ddlDynamic";

form1.Controls.Add(ddlDynamic);

if (!IsPostBack)
{
for (int i=1; i <=3; i++)
ddlDynamic.Items.Add(new ListItem(i.ToString(), i.ToString()));
}



if (IsPostBack)
{
Response.Write("[Page_Load]静态:" + ddlStatic.SelectedIndex + "
");
Response.Write("[Page_Load]动态:" + ddlDynamic.SelectedIndex + "
");
}
}

void Button_Click(Object sender, EventArgs e)
{
DropDownList ddlDynamic = (DropDownList)form1.FindControl("ddlDynamic");
Response.Write("[Button_Click]静态:" + ddlStatic.SelectedIndex + "
");
Response.Write("[Button_Click]动态:" + ddlDynamic.SelectedIndex + "
");
}
/script>



TestDyn2.aspx:
html>
body>
form id="form1" runat="server">


静态:





动态:
/form>
/body>
/html>
script language="C#" runat="server">
void Page_Load(Object sender, EventArgs e)
{
DropDownList ddlDynamic = new DropDownList();
ddlDynamic.ID = "ddlDynamic";

if (!IsPostBack)
{
for (int i=1; i <=3; i++)
ddlDynamic.Items.Add(new ListItem(i.ToString(), i.ToString()));
}



form1.Controls.Add(ddlDynamic);


if (IsPostBack)
{
Response.Write("[Page_Load]静态:" + ddlStatic.SelectedIndex + "
");
Response.Write("[Page_Load]动态:" + ddlDynamic.SelectedIndex + "
");
}
}

void Button_Click(Object sender, EventArgs e)
{
DropDownList ddlDynamic = (DropDownList)form1.FindControl("ddlDynamic");
Response.Write("[Button_Click]静态:" + ddlStatic.SelectedIndex + "
");
Response.Write("[Button_Click]动态:" + ddlDynamic.SelectedIndex + "
");
}
/script>

要理解第一个问题,即,对于一个动态生成的DropDownList对象,为什么先添加ListItem,后调用父控件的Controls.Add,其状态并没有保存,其关键在于理解TrackViewState的调用以及动态控件加入父控件的Controls后的阶段“追赶”过程。

如果你在文档里查询Control的TrackViewState方法描述,其中说到,只有调用该方法后,view-state的变化才会存到服务器控件的StateBag对象里去,这样才会在下一次的PostBack后的LoadViewState中恢复到原来状态。

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemwebuicontrolclasstrackviewstatetopic.asp

大家都知道每个控件一般都会经历如下几个阶段 (抄自《Developing Microsoft ASP.NET Server Controls and Components》 一书第九章)

http://www.microsoft.com/mspress/books/5728.asp

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

在页面里declared的服务器控件,譬如,例子中的ddlStatic,其TrackViewState方法是在Init阶段后面调用的,其后的变化将保存到StateBag里去,但其前的变化不会保存。如果你用Paul Wilson的ViewState Parser查看该例的ViewState,你是看不到其状态的。但假如在此之后,你改变其状态,那么这些状态也许就会保存到ViewState去(取决于该对象是否override了SaveViewState)。

http://www.wilsondotnet.com/Demos/ViewState.aspx

那动态控件呢?其一开始是新建对象,处于原始状态,当它被加到父控件的Controls里时,父控件会根据其当前的control阶段来调用该子控件的一些方法,让子控件赶上父控件的control阶段 (这些方法可以从上个贴里leighsword和microhelper贴的Control的AddedControl方法里看到,在此就不重复了,而且也不用看那些方法)。为什么要这样呢?这应该跟整个页面的生命周期有关吧。

打个比方,不是很恰当,但凑合着吧,这好象是复制一个人后,让他快速经历婴儿,童年,少年,青年,。。。直至赶上被复制人目前的阶段为止。

但大概来讲,当我们在Page_Load里调用form1.Controls.Add()时,父控件form1处于Load阶段(上面的第六行),它就会调用下拉框的一些方法让它经过Init->Load状态,其中的一个结果是在Init后面调用了TrackViewState,DropDownList的父类ListControl, override了TrackViewState,在其中调用了Items(ListItemCollection类)对象的TrackViewState。其结果是,如果你在form1.Controls.Add()之后改变动态DropDownList控件的Items的话,那些ListItem就会被保存下来,因为ListItemCollection对象 override 了 SaveViewState() 。而在form1.Controls.Add()之前添加的ListItem则不会被保存下来。

其实解决TestDyn2.aspx中的问题有个现成的答案,即去除 if (!IsPostBack):

DropDownList ddlDynamic = new DropDownList();
ddlDynamic.ID = "ddlDynamic";
for (int i=1; i <=3; i++)
ddlDynamic.Items.Add(new ListItem(i.ToString(), i.ToString()));

form1.Controls.Add(ddlDynamic);

这样,跟ddlStatic一样,每次都生成ListItem对象

第二个问题,即为什么动态生成的DropDownList控件在PostBack后在Page_Load里其选择的项没有被设置,再供大家研究。

上次的第二个问题是,为什么动态生成的DropDownList控件,在PostBack后,在Page_Load里其选择的项没有被设置。

拿TestDyn1.aspx为例,如果你在第一个(静态)下拉框里选择2,在第二个(动态)下拉框里选择3,然后按Click Me按钮,你的输出是这样的

[Page_Load]静态:1
[Page_Load]动态:0
[Button_Click]静态:1
[Button_Click]动态:2

不管你选什么,第二项总是
[Page_Load]动态:0

即,动态下拉框的选项在Page_Load没有被正确设置,但在Button的Click事件里被正确设置了。

大家知道,表单控件(TextBox, CheckBox, DropDownList, ListBox,....) 的输入值或被选状态与ViewState无关,而是在Load Postback Data阶段被设置的,因为它们都实现了IPostBackDataHandler接口。

上次说到动态控件被加入父控件的Controls集合时,会通过阶段“追赶(catch-up)”过程来赶上父控件当前的阶段,如果你仔细看一下前一个贴里leighsword和microhelper贴的Control的AddedControl方法,你将看到

control.InitRecursive(control1);
...
control.LoadViewStateRecursive(obj1);
...
control.LoadRecursive();
...

并没有涉及Load Postback Data。那么这个阶段是什么时候被执行的呢?如果你参考Reflector(也可以参考上一个贴的2个回贴)里System.Web.UI.Page的ProcessRequestMain()方法,在去掉了那些Trace语句后是这样的:

base.InitRecursive(null);

if (this.IsPostBack)
{
this.LoadPageViewState();

//注意,这里是._requestValueCollection
this.ProcessPostData(this._requestValueCollection, true); //第二个参数表明是否是在Load前调用的
}

base.LoadRecursive();

if (this.IsPostBack)
{

//注意,这里是._leftoverPostData,即,尚未被处理的PostData
this.ProcessPostData(this._leftoverPostData, false);

this.RaiseChangedEvents();

this.RaisePostBackEvent(this._requestValueCollection);

}

base.PreRenderRecursiveInternal();

this.SavePageViewState();

base.RenderControl(this.CreateHtmlTextWriter(this.Response.Output));


ProcessPostData会根据Request.Form里每对名字/值,看是否有实现了IPostBackDataHandler接口的对应名字的控件,有的话,就会调用该控件的LoadPostData方法,譬如DropDownList的LoadPostData是这样的

bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection)
{
string[] textArray1 = postCollection.GetValues(postDataKey);
if (textArray1 != null)
{
int num1 = this.Items.FindByValueInternal(textArray1[0]);
if (this.SelectedIndex != num1)
{
this.SelectedIndex = num1;
return true;
}
}
return false;
}

从上面可见,ProcessPostData在Load前被执行了一次,在Load后又会被执行一次。看上去有点怪,但这正是系统给你的方便,允许你在Load里动态生成控件,并让那些实现了IPostBackDataHandler接口的控件获取用户输入的值或选择的状态。

在我们当前的情形下,我们的动态控件是在Load里生成的,错过了第一次ProcessPostData,所以在Page_Load里其选项还没有被正确设置,但第二次ProcessPostData让其获取了用户输入的值或选择的状态,所以在Button的Click事件里被正确设置了。

这也意味着,如果我们的表单控件是在Load之后生成的,譬如你的控件是在PreRender事件里生成的,

void Page_PreRender(Object sender, EventArgs e)
{
DropDownList ddlDynamic2 = new DropDownList();
ddlDynamic2.ID = "ddlDynamic2";

form1.Controls.Add(ddlDynamic2);

if (!IsPostBack)
{
for (int i=1; i <=3; i++)
ddlDynamic2.Items.Add(new ListItem(i.ToString(), i.ToString()));
}
else
Response.Write("[Page_Load]动态2:" + ddlDynamic2.SelectedIndex + "
");
}

那么尽管它可以恢复ViewState,但因为它错过了2次ProcessPostData机会,它不可能获取用户输入的值或选择的状态。同时这些控件也不会触发Changed Events 与 Postback Events。 当然,你尽可以使用Request.Form来获取用户输入的值或选择的状态,但这跟我们的讨论无关。

所以,如果你需要产生动态控件,而且需要获取用户设置的输入值或触发Changed Events 与 Postback Events事件的话,最好在Load阶段或之前生成。

关于ASP.NET里Page事件及其次序的细节,请参考MVP Paul Wilson的文章
Page Events: Order and PostBack

http://aspalliance.com/articleViewer.aspx?aId=134&pId=

也请参考

The ASP.NET Page Object Model

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/aspnet-pageobjectmodel.asp

Page Life-cycle in ASP.NET

http://weblogs.asp.net/eporter/archive/2003/07/15/10109.aspx



<< Home

This page is powered by Blogger. Isn't yours?