前言
ASPNET的优点我说过很多次了也就是各个控件独立负责自己内部的逻辑这是一个好事情因为它解决了原本ASP处理逻辑耦合度高的问题然而这是需要代价的那就是引入ASPNET页面生命周期随着控件的多层嵌套应用的复杂度增加我们再次陷入泥潭!
问题
其实这个文章题目我两个月前就写下了可是一直没想写完它直到今天我在这个泥潭中泡了几个小时于是决定先从泥潭中跳出来把文章写完再跳进去继续解决问题问题是这样的:
使用MS AJAX Beta + CTP新建一个项目同时在Bin中放上Beta的AjaxControlToolkitdll
扔上一个Accordion放置几个AccordionPane设置一下CssClass
在Page_Load中使用PageLoadControl加载一个UserControl然后添加到页面上
接着发现UserControl内的控件无法正常触发事件陷入泥潭中……
首先要说明如果仅仅做第步那个UserControl肯定正常运作那意味着问题出在ScriptManager或Accordion中出现了问题
正文
想知道到底是什么出问题了吗?先听我说说这个ASPNET页面生命周期的问题吧
由于生命周期按阶段划分任务在不同阶段按部就班完成所以我们的每一个操作都是阶段相关的有些操作仅能在特定的阶段操作有些操作在不同阶段执行会导致不同的结果当然MS希望尽量消除这些阶段间的差异例如让一个操作在尽可能多的阶段中都能执行并且尽可能减少在不同阶段中操作引发的不同结果然而这不可能完全做到例如我们都知道ViewState读写限制为仅能在某些阶段进行于是依赖于ViewState的控件属性也就因此受到同样的限制
控件属性读写受阶段限制这很好接受对吧?因为这仅仅是一层依赖关系顺着依赖关系推广出去情况会变得越来越复杂限制的原因埋藏得越来越底层接着我们发现复杂性这一问题在ASPNET这种结构良好的体系中出现了而消灭这种复杂性的银弹还没被发明
作为控件或组件的开发人员我们当然有义务消除阶段差异让下游的开发人员面对更低的复杂性而且我们也确实尽力去做了控件的每一层封装都包含着这种努力并向上承诺尽可能低的阶段差异然而为了让控件看起来简单易用我们不可能将这些差异完整地记录在文档之中我们尝试去隐瞒细节控件被层层封装时我们都这样做底层文档没告诉我的差异我当然也没必要写到这一层的文档上去;底层文档提及了的差异我尽力弥补了即使弥补得不太好也不写到这一层的文档上去于是文档就好像神话传说一样随着世代相传而改变最终没有人知道这个控件依赖于某些底层的阶段差异
做过控件开发的人都知道有时候我们必须根据实际情况采用不同的方式构建看起来一样的控件例如最简单的数据控件都会存在是否PostBack的构建差异如果是非PostBack则需要在DataBind时构建并将数据保存到ViewState如果是PostBack则根据ViewState直接构建如果PostBack后又遇到了DataBind则需要清除原来的构建并重新根据新数据构建再复杂一些的控件还会分步骤构建默认情况下为了消除使用方的阶段差异部分构建步骤会尽可能靠前到Init时执行而另外一部分构建步骤则尽可能推迟到PreRender时执行中间部分则尽可能减少自己的变化以便使用方操作然而事情不会那么简单使用方的某些操作(通常是访问某个属性)如果依赖于某个构建步骤的完成因此一旦这些操作出现原本在PreRender才执行的特定构建步骤就要提前执行当这样的操作在不同阶段进行多几次构建步骤就已经散落在页面生命周期的各阶段
构建步骤可能散落于页面生命周期的各阶段对于控件设计师来说是一个严峻的问题这意味着他要保证任何一个构建步骤在任何一个阶段执行都是无差异的当然这不可能做到于是又要引入别的机制来减少这种差异复杂性就此产生了接下来随着复杂性的增加控件设计师越来越无法确保较低的阶段差异程度这就到控件使用者遭殃了如果控件使用者又再把控件封装并且依然企图降低阶段差异程度那么灾难也就发生了……
结果
我花了几个小时在泥潭中泡了几个小时边泡边写这篇文章问题当然已经有结果了
如果Accordion设置了HeaderCssClass或者ContentCssClass那就会出问题但如果为AccordionPane都加上以上两个属性又不会有问题了这样的情况当然通过用Reflector查看这两个类的代码来解决结果发现Accordion会检测每一个AccordionPane是否有设置这两个属性如果没有就把AccordionPane的设置为和自己的一样在AccordionPane被设置时会调用thisEnsureChildControls()这是一个会导致构建步骤提前执行的方法于是控件构建的顺序就改变了不仅仅Accordion内部的顺序改变了整个Page的都改变了由于控件的ID是按顺序自动分配的包括我那个UserControl构建顺序的改变意味着ID的改变也就相当于整个控件树都改变了事件当然不能正常触发
最后的解决方案当然是为我那个UserControl指定ID我花了那么多个小时才发现自己做了件蠢事一早打开Trace来看控件树就应该能发觉UniqueID的变化
总结
虽然这个问题看起来不是一个太好的例子因为一打开Trace就应该能找到问题的来源但实际上它却正好揭示了ASPNET框架内部的蝴蝶效应(Butterfly Effect)——随着复杂度的增加任何一个细微的改变都会导致全局上的巨大变化在设计ASPNET的时候MS可能也在想着解耦在简单的情况下这东西确实也解耦然而在复杂的情况下却正好背道而驰这真的是很讽刺