是否是ASP.NET的CheckBoxList的Bug?
2010-04-29 02:22 横刀天笑 阅读(2443) 评论(11) 编辑 收藏 举报缘起
今天其他项目的同事碰到一个bug,封装的一个控件有些问题。先就描述一下这个控件。
控件是从ASP.NET自身的CheckBoxList派生而来的,然后扩展一些功能,控件最后样式如下图所示:
点击展开按钮后,在控件下方显示一个浮动层,里面放着一个CheckBoxList:
(暂时没有控件的真实截图,暂且对付着看吧,中间有黑点的表示选中)
给该控件扩展了一个事件,当点击展开的时候触发该事件,回发到服务器端,从数据库里读取数据,然后决定哪些值选中。代码示例:
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的那位同学却没有这么干,也就造成了这种情况~
此文写的及其零乱,仅仅是个人记录。后面有时间会再仔细的加工整理一番,现在请各位看官多多见谅。