代码改变世界

是否是ASP.NET的CheckBoxList的Bug?

2010-04-29 02:22  横刀天笑  阅读(2426)  评论(11编辑  收藏  举报

缘起

今天其他项目的同事碰到一个bug,封装的一个控件有些问题。先就描述一下这个控件。

控件是从ASP.NET自身的CheckBoxList派生而来的,然后扩展一些功能,控件最后样式如下图所示:

image

点击展开按钮后,在控件下方显示一个浮动层,里面放着一个CheckBoxList:

image

(暂时没有控件的真实截图,暂且对付着看吧,中间有黑点的表示选中)

给该控件扩展了一个事件,当点击展开的时候触发该事件,回发到服务器端,从数据库里读取数据,然后决定哪些值选中。代码示例:

   1: public class DropDownListEx : CheckBoxList
   2: {
   3:     public event EventHandler<EventArgs> Expanded;
   4:  
   5:     protected virtual OnExpanded(EventArgs e)
   6:     {
   7:         if(Expanded != null)
   8:             Expanded(this,e);
   9:     }
  10:     
  11:     protected override void OnPreRender(EventArgs e)
  12:     {
  13:         base.OnPreRender(e);
  14:         //注册回发,为扩展事件
  15:         if (this.Page != null && this.Enabled)
  16:             this.Page.RegisterRequiresPostBack(this);
  17:     }
  18:  
  19:     protected override bool LoadPostData(string postDataKey, NameValueCollection postCollection)
  20:     {
  21:         //判断按钮是否点击了,模拟的
  22:         if (postCollection["data"] == "click")
  23:         {
  24:             //触发Expanded事件
  25:             OnExpanded(new EventArgs());
  26:             return true;
  27:         }
  28:         else
  29:             return base.LoadPostData(postDataKey,postCollection);
  30:     }
  31: }

下面是Expanded事件处理器代码示例:

   1: this.DropDownListEx1.Expanded += new EventHandler<EventArgs>(DropDownListEx1_Expanded);
   2:  
   3: private void DropDownListEx1_Expanded(object sender,EventArgs e)
   4: {
   5:     //全部选中
   6:     foreach(ListItem item in this.DropDownListEx1.Items)
   7:         item.Selected = true;
   8: }

表面看这段代码好像没什么问题,貌似也“一直”工作的很好,但是有用户突然发现,最后一个复选框“值3”,如果原来没有选中,即使在Expanded事件里,将所有复选框都选中,但回发完成后这最后一个依然是未选中状态。

最后调试发现,一个现象,DropDownListEx的LoadPostData多次调用,调用的顺序(语言不准确)是:值1,值2,值3,控件自身,值3

在控件自身这里我们触发Expanded事件,然后选中所有的复选框,奇怪的是值3这个居然会调用两次,就是因为这最后一次,把Expanded事件处理器的Selected=true又给覆盖掉了。解决这个bug倒是很容易,只需要这个事件在最后触发就行了,要么加个计数器让最后的值3不调用(不太优美),不过大家应该还记得,和LoadPostData同属一个接口的还有一个方法:RaisePostDataChangedEvent。该方法会在LoadPostData方法返回为true的时候调用,那么我们只需要将代码稍微改成这样就解决了这个bug了:

   1: protected override bool LoadPostData(string postDataKey, NameValueCollection postCollection)
   2: {
   3:     if (postCollection["data"] == "click")
   4:         //返回true就ok了,剩下的交给RaisePostDataChangedEvent方法吧
   5:         return true;
   6:     else
   7:         return base.LoadPostData(postDataKey,postCollection);
   8: }
   9: protected override void RaisePostDataChangedEvent()
  10: {
  11:     OnExpanded(new EventArgs());    
  12: }

RaisePostDataChangedEvent方法会在所有的LoadPostData方法执行完毕后执行,所以上面的bug也不复存在了。

问题是解决了,但心里总有一个疑问,为什么最后一个复选框总会出现两次呢?这个还得从LoadPostData是谁调用的开始说起。

谁调用LoadPostData

在ASP.NET中,最后都会终结到IHttpHandler接口的ProccessRequest(HttpContext context)方法上,Page类实现了IHttpHandler接口,所以对于aspx页面来说,入口点就是那个ProccessRequest方法。查看代码不难发现最后归结到ProcessRequestMain方法上。Page的整个生命周期,以及一切的事件,比如Init啊,Load啊,什么的都是从这一条线上来的。

在这中间就调用了一个ProcessPostData方法,LoadPostData方法就是从这里调用的,那看来要查看为什么LoadPostData多调用一次的入口点就在这里了:

   1: private void ProcessPostData(NameValueCollection postData, bool fBeforeLoad)
   2: {
   3:     if (this._changedPostDataConsumers == null)
   4:     {
   5:         this._changedPostDataConsumers = new ArrayList();
   6:     }
   7:     if (postData != null)
   8:     {
   9:         foreach (string str in postData)
  10:         {
  11:             if ((str == null) || IsSystemPostField(str))
  12:             {
  13:                 continue;
  14:             }
  15:             Control control = this.FindControl(str);
  16:             if (control == null)
  17:             {
  18:                 if (fBeforeLoad)
  19:                 {
  20:                     if (this._leftoverPostData == null)
  21:                     {
  22:                         this._leftoverPostData = new NameValueCollection();
  23:                     }
  24:                     this._leftoverPostData.Add(str, null);
  25:                 }
  26:                 continue;
  27:             }
  28:             IPostBackDataHandler postBackDataHandler = control.PostBackDataHandler;
  29:             if (postBackDataHandler == null)
  30:             {
  31:                 if (control.PostBackEventHandler != null)
  32:                 {
  33:                     this.RegisterRequiresRaiseEvent(control.PostBackEventHandler);
  34:                 }
  35:             }
  36:             else
  37:             {
  38:                 if ((postBackDataHandler != null) && postBackDataHandler.LoadPostData(str, this._requestValueCollection))
  39:                 {
  40:                     this._changedPostDataConsumers.Add(control);
  41:                 }
  42:                 if (this._controlsRequiringPostBack != null)
  43:                 {
  44:                     this._controlsRequiringPostBack.Remove(str);
  45:                 }
  46:             }
  47:         }
  48:     }
  49:     ArrayList list = null;
  50:     if (this._controlsRequiringPostBack != null)
  51:     {
  52:         foreach (string str2 in this._controlsRequiringPostBack)
  53:         {
  54:             Control control2 = this.FindControl(str2);
  55:             if (control2 != null)
  56:             {
  57:                 IPostBackDataHandler handler2 = control2._adapter as IPostBackDataHandler;
  58:                 if (handler2 == null)
  59:                 {
  60:                     handler2 = control2 as IPostBackDataHandler;
  61:                 }
  62:                 if (handler2 == null)
  63:                 {
  64:                     throw new HttpException(SR.GetString("Postback_ctrl_not_found", new object[] { str2 }));
  65:                 }
  66:                 if (handler2.LoadPostData(str2, this._requestValueCollection))
  67:                 {
  68:                     this._changedPostDataConsumers.Add(control2);
  69:                 }
  70:                 continue;
  71:             }
  72:             if (fBeforeLoad)
  73:             {
  74:                 if (list == null)
  75:                 {
  76:                     list = new ArrayList();
  77:                 }
  78:                 list.Add(str2);
  79:             }
  80:         }
  81:         this._controlsRequiringPostBack = list;
  82:     }
  83: }

注意后面的foreach(string str2 in this._controlsRequiringPostBack)

这个就是多次调用LoadPostData的循环,而this._controlsRequiringPostBack又是怎么得到的呢?

如何得到_controlsRequiringPostBack

这个得看LoadAllState方法:

   1: private void LoadAllState()
   2: {
   3:     object obj2 = this.LoadPageStateFromPersistenceMedium();
   4:     IDictionary first = null;
   5:     Pair second = null;
   6:     Pair pair2 = obj2 as Pair;
   7:     if (obj2 != null)
   8:     {
   9:         first = pair2.First as IDictionary;
  10:         second = pair2.Second as Pair;
  11:     }
  12:     if (first != null)
  13:     {
  14:         this._controlsRequiringPostBack = (ArrayList) first["__ControlsRequirePostBackKey__"];
  15:         if (this._registeredControlsRequiringControlState != null)
  16:         {
  17:             foreach (Control control in (IEnumerable) this._registeredControlsRequiringControlState)
  18:             {
  19:                 control.LoadControlStateInternal(first[control.UniqueID]);
  20:             }
  21:         }
  22:     }
  23:     if (second != null)
  24:     {
  25:         string s = (string) second.First;
  26:         int num = int.Parse(s, NumberFormatInfo.InvariantInfo);
  27:         this._fPageLayoutChanged = num != this.GetTypeHashCode();
  28:         if (!this._fPageLayoutChanged)
  29:         {
  30:             base.LoadViewStateRecursive(second.Second);
  31:         }
  32:     }
  33: }

this._controlsRequiringPostBack = (ArrayList) first["__ControlsRequirePostBackKey__"];

这里的first是控件状态,而second是视图状态。

既然是控件状态,那我们去看看保存控件状态的地方。

SaveAllState方法

   1: if ((this._registeredControlsThatRequirePostBack != null) && (this._registeredControlsThatRequirePostBack.Count > 0))
   2: {
   3:     if (dictionary == null)
   4:     {
   5:         dictionary = new HybridDictionary();
   6:     }
   7:     dictionary.Add("__ControlsRequirePostBackKey__", this._registeredControlsThatRequirePostBack);
   8: }

哦,原来键值为__ControlsRequirePostBackKey__的控件状态实际上就是_registeredControlsThatRequirePostBack啊,而_registeredControlsThatRequirePostBack又是怎么得来的呢?

   1: [EditorBrowsable(EditorBrowsableState.Advanced)]
   2: public void RegisterRequiresPostBack(Control control)
   3: {
   4:     if (!(control is IPostBackDataHandler) && !(control._adapter is IPostBackDataHandler))
   5:     {
   6:         throw new HttpException(SR.GetString("Ctrl_not_data_handler"));
   7:     }
   8:     if (this._registeredControlsThatRequirePostBack == null)
   9:     {
  10:         this._registeredControlsThatRequirePostBack = new ArrayList();
  11:     }
  12:     this._registeredControlsThatRequirePostBack.Add(control.UniqueID);
  13: }

这个方法是Page提供的一个公共方法,哪个控件想注册回发就得调用一下。一般控件都会在PreRender方法里干这个注册的事儿,那控件的PreRender方法是怎么调用的呢?

   1: internal virtual void PreRenderRecursiveInternal()
   2: {
   3:     if (!this.Visible)
   4:     {
   5:         this.flags.Set(0x10);
   6:     }
   7:     else
   8:     {
   9:         this.flags.Clear(0x10);
  10:         this.EnsureChildControls();
  11:         if (this._adapter != null)
  12:         {
  13:             this._adapter.OnPreRender(EventArgs.Empty);
  14:         }
  15:         else
  16:         {
  17:             this.OnPreRender(EventArgs.Empty);
  18:         }
  19:         if ((this._occasionalFields != null) && (this._occasionalFields.Controls != null))
  20:         {
  21:             string errorMsg = this._occasionalFields.Controls.SetCollectionReadOnly("Parent_collections_readonly");
  22:             int count = this._occasionalFields.Controls.Count;
  23:             for (int i = 0; i < count; i++)
  24:             {
  25:                 this._occasionalFields.Controls[i].PreRenderRecursiveInternal();
  26:             }
  27:             this._occasionalFields.Controls.SetCollectionReadOnly(errorMsg);
  28:         }
  29:     }
  30:     this._controlState = ControlState.PreRendered;
  31: }

上面的代码实际上就是在整个页面的控件树上递归的调用PreRenderRecursiveInternal()方法,最后调用控件树每个节点的OnPreRender方法,该方法里将有注册回发的代码。

好,这里就介绍到这里,我们再回过头来看看CheckBoxList是怎么实现的:

CheckBoxList的实现

我原本以为CheckBoxList

里会有很多CheckBox,但查看其源代码后发现只有一个,原来这里使用了原型设计模式。生成的那么多复选框都只是那一个CheckBox Render出来的。

比如在CheckBoxList的PreRender方法里注册回发的时候:

   1: protected internal override void OnPreRender(EventArgs e)
   2: {
   3:     base.OnPreRender(e);
   4:     this._controlToRepeat.AutoPostBack = this.AutoPostBack;
   5:     this._controlToRepeat.CausesValidation = this.CausesValidation;
   6:     this._controlToRepeat.ValidationGroup = this.ValidationGroup;
   7:     if (this.Page != null)
   8:     {
   9:         for (int i = 0; i < this.Items.Count; i++)
  10:         {
  11:             this._controlToRepeat.ID = i.ToString(NumberFormatInfo.InvariantInfo);
  12:             this.Page.RegisterRequiresPostBack(this._controlToRepeat);
  13:         }
  14:     }
  15: }

 上面代码中的_controlToRepeat就是那仅有的一个CheckBox。大家可以看到CheckBoxList注册回发就是遍历其Items,然后注册。

如果我们查看CheckBoxList的Controls属性发现,其Count为1,实际上就是那仅有的一个CheckBox,再回到上一节介绍的内容,在CheckBoxList的OnPreRender方法执行完毕,也就是把几个checkbox都注册回发了,然后还会调用CheckBoxList的子控件的PreRenderRecursiveInternal()方法,就是这里的仅有的那个CheckBox,经过CheckBoxList的OnPreRender方法调用后,我们发现该CheckBox的ID就等于最后一个复选框的ID,在ChexkBox的OnPreRender方法里,它又会把自己再注册一次,这样也就出现了文章开头那一幕:会对最后一个复选框调用两次LoadPostData。

疑为bug

这里的问题就是在CheckBoxList里,已经为所有的复选框注册了回发,但是因为递归调用PreRender,又会调用CheckBoxList子控件的PreRender方法,致使最后一个复选框注册两次。

 

后记 

 在这里不难发现Control的PreRenderRecursiveInternal()方法是internal virtual的,允许程序集内的子类覆盖,从Control派生的类,应该根据自己的实际情况,选择是否覆盖该方法,而这里的CheckBoxList就是其中一个,CheckBoxList无需再调用子控件的PreRender方法来再次注册会发,所以它应该覆盖Control的PreRenderRecursiveInternal方法,但是开发CheckBoxList的那位同学却没有这么干,也就造成了这种情况~

此文写的及其零乱,仅仅是个人记录。后面有时间会再仔细的加工整理一番,现在请各位看官多多见谅。