动态加载控件貌似给很多程序员都带来了困扰,经常收到这样的邮件,干脆就写下面这个示 例来演示如何解决那些常见的问题吧。
其实常见的问题通常有这样两个:
1. 通常他们都通过一个按钮来添加一个UserControl 并将它们加入PlaceHolder 容器的 Controls 中。然后页面上就会有一个另外一个按钮,这个按钮什么相关的事也没做,就是做了 一次回发。这样的情况动态添加的控件就不翼而飞了。
2. 今天收到了一封邮件说是要追加控件,和上面的情况看上去好像不一样,但实质就是同 一回事。
原因:
其实网上有很多帖子都不约而同地解释了这个问题,这里我还是不厌其烦地解释一下:
首先,要提到大家所熟知很多人一知半解的页面生命周期,以至于很多居然还停留在将 ASP.NET 和Winform 一样处理的层次上,因此就会有人试图将变量存在实例字段中,然后一如 既往地指望它能够用来共享数据,结果总是无功而返,以我所知这样的人居然还不在少数,当 然了,咱博客园的素质相对偏高,这种问题一般不在话下。事实上每次页面PostBack 都会从 Aspnet 线程池中返回一个空闲的用户线程,用于处理用户本次的请求。摆弄一下那种浏览器进 度条会动的控件基本也都算是回发事件了。两次回发之间可以当作没有什么关联的。但是你总 能看到很多控件等在回发之后还能保持状态比如文本框边上有个按钮。你填写完了文本后狂点 那个按钮,你会发现文本框中的文字还是你填写的那些而不会被清空。这就不得不说到 ViewState 这种神奇的双刃剑了。它的原理在MSDN 上讲的很清楚,找不到的留言或发邮件给我 我再慢慢给你找……
然后呢?还是查MSDN, 关键字“TemplateControl.LoadControl ” 我们在用PlaceHolder 中动态添加控件的时候就会用到这个方法了。我们注意到这里有一句:“ 在将控件加载到容器 控件时,该容器引发所添加控件的所有事件,直到所添加控件参与当前事件为止。但是,所添 加控件不参与回发数据处理。” 因为所添加的控件是不参与回发数据处理的,因此就会出现问 题1 中所遇到的按另一个按钮就消失的现象了。问题2 其实也是一样的问题,因为事实上它们 遇到的现象是一样的,只不过它的需求有所不同罢了。(可以理解成一个是i=1; 另一个是 i+=1; )
综上所述,问题的关键就是原本在页面加载的时候所有的控件初始化操作都应该完成,动态 加载将加载的过程延迟到了事件被触发之后,因此在页面回发后,因为会有一次新的页面加载 过程,显然这时候动态加载的控件是不存在的,但是用户预期的答案是显示已经加载的信息。 这时候如果可能我们最好在加载的过程中进行控件的重新加载和数据绑定。常见的方法中我们 呢通常通过LoadControl 来动态加载控件,因此只要在页面输出之前的所有事件节点上我们都 可以加载我们的控件。但是推荐的则是Init 事件。在Load 事件的时候进行数据绑定。解决:
既然问题的原因找到了,我们就应该解决它,现在关键就是在回发后PlaceHolder.Controls 的子集数量为0 ,也就是没有子控件,也就是很明显地控件跑没了。那么我们就应该在我们在 他们还在的时候将其存放起来。在经典的回发模型中,ViewState 通过将所有控件/ 其子控件 的各个属性字段等都存放到ViewState 中了,在最后Render 的时候都一并丢给了用户。数据包 括数据状态都一并发到了客户端,现在客户点击了一个能够引起回发的按钮或者下拉框按钮, 所有这些数据状态以及客户修改(也许没有修改,但我们假定客户篡改过了)的数据都传回客 户端。因为回发发生了,因此在加载数据的阶段IPostBackEventHandler 和 IPostBackDataHandler 接口所定义的方法(通常由服务器控件实现)都将被调用,然后就是一 系列的数据回填工作。用户的数据又被重新做成了新的ViewState 放在页面里面又丢给了客户 端。我曾经用一个比喻(相当拙劣的比喻,当时好像不是这样比喻的)是白衬衫(花花公子正 版)被蓝笔画后,送去洗衣店,人家新拿了一件一样的白衬衫(花花公子高仿),然后用蓝笔 划了一下还给你,事实上白衬衫不是你原来的那件了,但看上去还是无法分辨。因此我们这里 也可以用类似的办法来解决。但是真的可以吗?用ViewState 不仅有众所周知的性能问题,因 为ViewState 的存储介质(其实是指它的内容存储,可以理解成持久层)是页面,而页面是指 接受文本的一种载体(正如网页事实上都是文本一样的道理)因此会有序列化的问题。这就给 用户控件的开发带来了极大的不便。更关键的原因是不仅如此,因为UserControl 压根没有支 持序列化,因此你的控件即使精简到没有字段方法(就声明了个名字够精简了吧)再加上序列 化特性,只要你继承自UserControl ,就必然面临无法序列化的尴尬。况且它的性能问题确实 也很值得关注。和ViewState 有类似性质的常见的还有Session 和HttpContext.Current.Cache 等缓存,或者自己实现一个静态字典用于存储也是一个不错的选择。用它们是可以解决问题的 ,在下面的代码中将会用到。但这样的方案事实上是存在很多问题的。大家都知道Session 是 有超时时间的,默认长度也就是几十分钟,而且Session 也有诸多其他方面的限制,因此用它 来做容量如此之大的控件存储其实是非常不适合的。HttpContext.Current.Cache 是一个高级 的缓存对象,因为有完善的内部机制来限制其膨胀以及管理其内容,但也正因为这种管理比如 大小限制等原因会导致在生产环境中可能会遭遇严重的性能问题。缓存应该用来存取较小的常 用的数据,比如用户名/ 密码这样的常用数据,而不是这种大个头的东西。但是与ViewState 相似的性质让它们有了承担这份责任的义务。(家里的大人都死光了,孩子也只好来当家了) 这让我们想到了存储介质,事实上磁盘文件,数据库等都具有了同样的性质。另一条思路是来 自简单地加载思路,因为对动态添加的控件来说,它有一个很明显的特征,它是动态添加的。 因此既然可以在按钮事件处理程序中添加,同样也就可以在页面初始化事件处理程序中添加。 按照页面的生命周期动态添加最好写在Init 这时候理应做丰富的添加(不过不适合那种需要用 按钮添加的用户需求了)[ 另外一点有点郁闷的是在MSDN 中也是说应该在Init 而不是Load 中 动态添加,但是同样是在MSDN 的《如何:以编程方式创建ASP.NET 用户控件的实例》居然就用 了Load 事件来处理,因此这种区分对页面开发人员事实上并不是那么严谨的,事实上也不会出 现什么问题,因此也就没有人吹毛求疵了,而且Google 出来的答案估计90% 以上都是在Load 中写的,一传十十传百的结果可能这个数值还在上升,所以就更没必要计较了] 。刚刚打算帮 发邮件的兄弟直接找一个答案发现了有网友说在每个页面都要做判断搞加载,很烦很烦…… 所 以如果您的需求不是那种追求打开一个页面两天后再来点一下要追加或则重新加载控件的朋友 ,我的方案还是可以考虑的。当然如果你比较追求那种近乎变态的需求或者您的页面和淘宝有 一样大的访问量的话,不凡试试我的方案,更好的解释是,我的方案可以当作理解控件动态加 载原理解释的一个入口罢了。