动态控件的状态问题 (续2)

2004-10-26 by 开心就好

上次的第二个问题是,为什么动态生成的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
<font color="#ff0000">this.ProcessPostData(this._requestValueCollection, true);</font> //第二个参数表明是否是在Load前调用的
}

base.LoadRecursive();

if (this.IsPostBack)
{

//注意,这里是._leftoverPostData,即,尚未被处理的PostData
<font color="#ff0000"> this.ProcessPostData(this._leftoverPostData, false);</font>

 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 + "\<BR>");
}

那么尽管它可以恢复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

也请参考

The ASP.NET Page Object Model

Page Life-cycle in ASP.NET


Comments