玩转C科技.NET

从学会做人开始认识这个世界!http://volnet.github.io

导航

动态加载控件_常见问题解决之道

动态加载控件貌似给很多程序员都带来了困扰,经常收到这样的邮件,干脆就写下面这个示例来演示如何解决那些常见的问题吧。
其实常见的问题通常有这样两个:
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中写的,一传十十传百的结果可能这个数值还在上升,所以就更没必要计较了]。刚刚打算帮发邮件的兄弟直接找一个答案发现了有网友说在每个页面都要做判断搞加载,很烦很烦……所以如果您的需求不是那种追求打开一个页面两天后再来点一下要追加或则重新加载控件的朋友,我的方案还是可以考虑的。当然如果你比较追求那种近乎变态的需求或者您的页面和淘宝有一样大的访问量的话,不凡试试我的方案,更好的解释是,我的方案可以当作理解控件动态加载原理解释的一个入口罢了。

我的例子,因为代码比较多,我就贴出如何调用的部分(也就是“如何用”的代码)源码可以在后面的链接中下载。

扩展性:虽然是为我那位邮友给出的答案,但是还是考虑了扩展性,我们可以尝试扩展用磁盘文件、网络、或者数据库的方式来作为存储介质,当然,您必须为此实现部分接口。局限性,因为有存储介质一说,因此不同容器托管方面不允许同时使用多种存储介质,否则将会出现两个集合,因此就带来了另一个扩展性,您可以自行实现扩展存储之间的数据同步,不过做此之前提醒您一下,不同的存储介质可能存在不同的存储能力,比如Session有大小限制,而数据库简直就是容量大王,这些数据之间的同步可能会引发新的问题,另者就是这样的同步除了看上去很酷之外并没有什么好处,将数据乱存的结果可能导致程序显得混乱,更尴尬的是数据同步所白白消耗掉的性能。当然如果您只是练练手的话您确实可以这么做,做完记得告诉我一下,哈哈,我也想不劳而获。哈哈。下面贴一下代码就不多做解释了,因为如果你理解了上面这些,看懂那些代码就不可能有问题了。

    public partial class _Default : System.Web.UI.Page
    
{
        
public ContainerManager.ContainerManager cm = new ContainerManager.ContainerManager();

        
protected void Page_Load(object sender, EventArgs e)
        
{
            
//重载控件(HttpContext.Current.Cache作为存储介质)
            cm.ReloadControls(HttpContext.Current.Cache, "PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls);
        }


        
protected void btnInsertDynamicUserControl_Click(object sender, EventArgs e)
        
{
            
//Control c1 = LoadControl("DynamicUserControl.ascx");
            
//PlaceHolder_DynamicUserControlContainer.Controls.Add(c1);

            
int displayCount;
            
int.TryParse(txtNumber.Text, out displayCount);
            
if (displayCount == 0)
            
{
                
//追加控件(Session作为存储介质)
                Control c1 = LoadControl("DynamicUserControl.ascx");
                cm.AppendControl(
this.Session, "PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls, c1);
            }

            
else if (displayCount == 1)
            
{
                
//追加控件(HttpContext.Current.Cache作为存储介质)
                Control c1 = LoadControl("DynamicUserControl.ascx");
                Control c2 
= LoadControl("WebUserControl.ascx");
                cm.AppendControl(HttpContext.Current.Cache, 
"PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls, c1);
                cm.AppendControl(HttpContext.Current.Cache, 
"PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls, c2);
            }

            
else
            
{
                
//常见的动态加载控件后点击其他回发事件就导致控件丢失
                PlaceHolder_DynamicUserControlContainer.Controls.Clear();
                Control c1 
= LoadControl("DynamicUserControl.ascx");
                PlaceHolder_DynamicUserControlContainer.Controls.Add(c1);
                cm.CacheControls(HttpContext.Current.Cache, 
"PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls);
            }

        }


        
protected void btnUnloadStorage_Click(object sender, EventArgs e)
        
{
            cm.Remove(HttpContext.Current.Cache, 
"PlaceHolder_DynamicUserControlContainer");
        }

    }

源码地址:https://files.cnblogs.com/volnet/WebAppPlaceHolder.zip

关于事件丢失

BUG1:因为我们知道控件通常都是使用EventHandlerList Events的Events进行存取事件的,也就是说控件和Page的耦合度太高了。这样即便控件从缓存中取回后,仍然无法从Events中获得事件,这样就无法正确地载入控件事件了。(注意:因为事件是一种引用,而重新加载后,被引用的内容不在了,当然就成为没有事件了!)

Fixed1:失败!首先上述理由中,即使不用Events,使用public event AEventHandler RaiseEvent,虽然事件可以正确触发了,但是在响应事件的时候,将不能调用页面生命周期的大部分实例,如HttpResponse对象。然后是Events的问题,因为它不可能被重新访问,所以我们的代码将无法正确调用这些处理程序。在Fixed的过程中,我将页面的this.Events也存起来,并在再次加载的时候(PreLoad中)将其放回去,事件被重新找回了(简易的Session加载示例)。使用了本文提供的管理器后,Page的Events并没有丢失,也就没用上面的重新找回了,但是在控件存储后返回的控件的Events为空,我用了反射将其填满,但仍然无效!囧!所以暂时没有更好的办法……改进中……

有兴趣的朋友可以下载一起研究:

https://files.cnblogs.com/volnet/WebAppPlaceHolder0.2.zip
https://files.cnblogs.com/volnet/WebAppDynamicControlEventsLost.zip

关于这个程序!

其实原文也不止一次提到,这不是一个用于生产的高效的程序,它的目的旨在你能够正确理解动态载入控件丢失的“灵异”现象的本质的一篇文章。即使我让它可以完美支持持久化了,它仍然被认为是高消耗的!至于这个问题,大家能够尽量不用“动态加载”就不用,否则就遵循“重新生成控件”->“恢复控件状态”的思路就可以了。

posted on 2008-05-10 06:06  volnet(可以叫我大V)  阅读(8576)  评论(24编辑  收藏  举报

使用Live Messenger联系我
关闭