zoukankan      html  css  js  c++  java
  • 是否是ASP.NET的CheckBoxList的Bug?

    缘起

    今天其他项目的同事碰到一个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的那位同学却没有这么干,也就造成了这种情况~

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

  • 相关阅读:
    搭建个人Spring-Initializr服务器
    “不蒜子”统计总访问人数脚本
    基于Hazelcast及Kafka实现的分布式锁与集群负载均衡
    虚拟机部署hadoop集群
    程序员、黑客及开发者之间的区别
    今日校园自动登录教程
    逆向DES算法
    来自穷逼对HttpCanary的蹂躏
    今日校园提交签到和查寝-Java实现
    JS 判断数据类型方法
  • 原文地址:https://www.cnblogs.com/yuyijq/p/1723441.html
Copyright © 2011-2022 走看看