posts - 138, comments - 1881, trackbacks - 97, articles - 13
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

前言

ASP.NET的优点我说过很多次了,也就是各个控件独立负责自己内部的逻辑,这是一个好事情,因为它解决了原本ASP处理逻辑耦合度高的问题。然而这是需要代价的,那就是引入ASP.NET页面生命周期,随着控件的多层嵌套,应用的复杂度增加,我们再次陷入泥潭!

问题

其实这个文章题目我两个月前就写下了,可是一直没想写完它,直到今天我在这个泥潭中泡了几个小时,于是决定先从泥潭中跳出来把文章写完,再跳进去继续解决问题。问题是这样的:

  1. 使用MS AJAX 1.0 Beta2 + 2.0 CTP新建一个项目,同时在Bin中放上Beta2的AjaxControlToolkit.dll。
  2. 扔上一个Accordion,放置几个AccordionPane,设置一下CssClass。
  3. 在Page_Load中使用Page.LoadControl加载一个UserControl,然后添加到页面上。
  4. 接着发现UserControl内的控件无法正常触发事件,陷入泥潭中……

首先要说明,如果仅仅做第3步那个UserControl肯定正常运作,那意味着问题出在ScriptManager或Accordion中出现了问题。

正文

想知道到底是什么出问题了吗?先听我说说这个ASP.NET页面生命周期的问题吧。

由于生命周期按阶段划分,任务在不同阶段按部就班完成,所以我们的每一个操作都是阶段相关的,有些操作仅能在特定的阶段操作,有些操作在不同阶段执行会导致不同的结果。当然,MS希望尽量消除这些阶段间的差异,例如让一个操作在尽可能多的阶段中都能执行,并且尽可能减少在不同阶段中操作引发的不同结果。然而这不可能完全做到,例如我们都知道ViewState读写限制为仅能在某些阶段进行,于是依赖于ViewState的控件属性也就因此受到同样的限制。

控件属性读写受阶段限制,这很好接受,对吧?因为这仅仅是一层依赖关系。顺着依赖关系推广出去,情况会变得越来越复杂,限制的原因埋藏得越来越底层,接着我们发现复杂性这一问题在ASP.NET这种结构良好的体系中出现了,而消灭这种复杂性的银弹还没被发明。

作为控件或组件的开发人员,我们当然有义务消除阶段差异,让下游的开发人员面对更低的复杂性,而且我们也确实尽力去做了。控件的每一层封装,都包含着这种努力,并向上承诺尽可能低的阶段差异。然而为了让控件看起来简单易用,我们不可能将这些差异完整地记录在文档之中,我们尝试去隐瞒细节,控件被层层封装时我们都这样做。底层文档没告诉我的差异,我当然也没必要写到这一层的文档上去;底层文档提及了的差异,我尽力弥补了,即使弥补得不太好,也不写到这一层的文档上去。于是文档就好像神话传说一样随着世代相传而改变,最终没有人知道这个控件依赖于某些底层的阶段差异。

做过控件开发的人都知道,有时候我们必须根据实际情况采用不同的方式构建看起来一样的控件。例如最简单的数据控件都会存在是否PostBack的构建差异,如果是非PostBack,则需要在DataBind时构建并将数据保存到ViewState,如果是PostBack则根据ViewState直接构建,如果PostBack后又遇到了DataBind则需要清除原来的构建并重新根据新数据构建。再复杂一些的控件,还会分步骤构建,默认情况下为了消除使用方的阶段差异,部分构建步骤会尽可能靠前到Init时执行,而另外一部分构建步骤则尽可能推迟到PreRender时执行,中间部分则尽可能减少自己的变化以便使用方操作。然而事情不会那么简单,使用方的某些操作(通常是访问某个属性)如果依赖于某个构建步骤的完成,因此一旦这些操作出现,原本在PreRender才执行的特定构建步骤就要提前执行,当这样的操作在不同阶段进行多几次,构建步骤就已经散落在页面生命周期的各阶段。

构建步骤可能散落于页面生命周期的各阶段对于控件设计师来说是一个严峻的问题,这意味着他要保证任何一个构建步骤在任何一个阶段执行都是无差异的,当然这不可能做到,于是又要引入别的机制来减少这种差异,复杂性就此产生了,接下来随着复杂性的增加控件设计师越来越无法确保较低的阶段差异程度,这就到控件使用者遭殃了,如果控件使用者又再把控件封装,并且依然企图降低阶段差异程度,那么灾难也就发生了……

结果

我花了几个小时在泥潭中泡了几个小时,边泡边写这篇文章,问题当然已经有结果了。

如果Accordion设置了HeaderCssClass或者ContentCssClass,那就会出问题,但如果为AccordionPane都加上以上两个属性,又不会有问题了。这样的情况当然通过用Reflector查看这两个类的代码来解决,结果发现Accordion会检测每一个AccordionPane是否有设置这两个属性,如果没有就把AccordionPane的设置为和自己的一样。在AccordionPane被设置时,会调用this.EnsureChildControls(),这是一个会导致构建步骤提前执行的方法,于是控件构建的顺序就改变了,不仅仅Accordion内部的顺序改变了,整个Page的都改变了。由于控件的ID是按顺序自动分配的,包括我那个UserControl,构建顺序的改变意味着ID的改变,也就相当于整个控件树都改变了,事件当然不能正常触发。

最后的解决方案当然是为我那个UserControl指定ID。我花了那么多个小时才发现自己做了件蠢事,一早打开Trace来看控件树就应该能发觉UniqueID的变化。

总结

虽然这个问题看起来不是一个太好的例子,因为一打开Trace就应该能找到问题的来源,但实际上它却正好揭示了ASP.NET框架内部的“蝴蝶效应(Butterfly Effect)”——随着复杂度的增加,任何一个细微的改变都会导致全局上的巨大变化。在设计ASP.NET的时候,MS可能也在想着解耦,在简单的情况下这东西确实也解耦,然而在复杂的情况下却正好背道而驰,这真的是很讽刺。

Feedback

#1楼   回复  引用  查看    

2006-11-11 02:34 by Jeffrey Zhao      
说得很对阿,所以“Trick”这个东西往往也会带来这些东西。有时候解决一个复杂问题往往会用到所谓的“Trick”,的确不错,但是带来的问题会在后面接踵而来,使用、维护等等……
//其实ControlToolkit不用Reflector的,可以直接看代码啊。

#2楼   回复  引用  查看    

2006-11-11 09:25 by 自適應軟件......      
對阿,有源碼的啊!
不過作者理解問題真的很透澈,受益,值得學習!

#3楼   回复  引用  查看    

2006-11-11 11:03 by Dflying Chen      
架构越复杂,则掌握、合理运用所花费的时间就越长。
在编写ASP.NET应用程序时,这些都是非常重要的需要考虑的东西,不能随随便便写出来就好了。Accordion就是一个反面的教材。

#4楼[楼主]   回复  引用  查看    

2006-11-11 12:43 by Cat Chen      
@Jeffrey Zhao
@自適應軟件......
其实用Reflector是单纯为了方便吧,例如你看到this.EnsureChildControls的时候一点击就知道它调用的是Control.EnsureChildControls,这比在源代码中看到后再在Reflector确认要方便一点点吧。

#5楼[楼主]   回复  引用  查看    

2006-11-11 13:01 by Cat Chen      
@Dflying Chen
我想这不仅仅是Accordion的问题吧,而是ASP.NET自动分配ID时的问题。

在两个月前写下这个题目时,我也没想到能够遇到这样的问题的,我的想法也仅仅是任何一个控件设计师都必须做到“爱护环境”——尽量减少对环境的依赖性以及尽量减少对环境的影响,如果一定要对环境造成影响则必须请求上级来为你进行,由上级来统一仲裁。

而这次的问题已经超出了“破坏环境”的讨论范围,变成了“蝴蝶效应”。我们站在设计师的角度想想,你会认为一只蝴蝶扇一下翅膀也算“破坏环境”吗?肯定不会,而且我们也确实常在自己的控件中“扇翅膀”,这很正常。

#6楼[楼主]   回复  引用  查看    

2006-11-11 17:10 by Cat Chen      
现在遇到一个更加无厘头的问题:在IE7中正常的PostBack,在FF1中竟然提示无法通过EventValidation。

#7楼   回复  引用    

2006-11-11 18:16 by cw[未注册用户]
在Page_Load中使用Page.LoadControl加载一个UserControl,然后添加到页面上。

上面这种用法似呼不对! 应该让控件自动去加载的...

#8楼[楼主]   回复  引用  查看    

2006-11-11 19:34 by Cat Chen      
那个无厘头问题的来源是FF,不知道是否因为刚刚进行了自动升级没重启FF,反正整个页面就算都注释掉只剩下一个LinkButton也不正常,但重启FF后就正常了。

#9楼[楼主]   回复  引用  查看    

2006-11-11 19:38 by Cat Chen      
@cw
这样动态加载是没错的,在Page_Load这样做也不存在任何已知问题。你可以在MSDN查阅TemplateControl.LoadControl,它附带的例子也就是在Page_Load中用此方法动态加载一个UserControl然后添加到页面上。

这样做也是很正常的,ASP.NET设计时就考虑到用户可能需要在Load阶段动态添加控件,而由于UserControl的特殊性所以提供了LoadControl方法。

#10楼   回复  引用    

2006-12-04 10:01 by xtgswmj[未注册用户]
很早以前就有这种问题了,对动态控件的载入某些ASP.NET控件必须指明控件的ID,否则出错,我在做这方面的项目时出现过这样的问题.在网上有相关问题的解答(也就是动态控件载入后事件不响应)

#11楼[楼主]   回复  引用  查看    

2006-12-04 10:12 by Cat Chen      
@xtgswmj
没有学过ASP.NET控件开发的人犯这些小错误是经常有的,但Accordion作为官方推荐的控件库收录的控件,其制作者至少有点专业水平吧。如果一个已经熟悉ASP.NET控件开发的人也很容易犯这样的错误,那应该是ASP.NET的框架存在问题了。

#12楼   回复  引用    

2007-06-01 10:57 by yyy[未注册用户]
您好,看了你的文章很有启发
但是最近在项目里遇到一个很棘手的问题,
是关于自定义控件动态加载用户控件,
项目中有个页面使用了自定义控件,
但是运行以后发现在控件里定义的事件处理函数不响应,
看了网上一些文章,初步认定是因为创建的控件刷新时根本获得不了正确的值和事件,动态控件经常需要两次创建。
但是却找不到解决方案,请问你能给出点提示吗?



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 557257




相关文章:

相关链接: