玩转C科技.NET

每天都在学习,每天都在退步 为什么?世界发展太快! 怎么办?加快学习速度! 如何做?关注.NET社区 进阶中……

导航

<2008年9月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

统计

公告

Subscribe to this feed
Contact volnet online!

MSN群MyMSDN技术讨论群
群号:www.msdn@hotmail.com
Windows Live Alerts
欢迎大家踊跃加入讨论任何与技术有关的问题。
————————————
欢迎给我发送邮件:
volnet@tom.com
[标题格式]:[TO玩转C科技]<您的用户名/匿名>[<主题>]
————————————

 
您可以直接Gmail联系我噢!(Gtalk/Mail)
开机自启动,天天都在线哦!

LiveMessenger:
<My Library>

与我联系

搜索

 

常用链接

留言簿(4)

我参与的团队

我的标签

随笔分类(119)

随笔档案(105)

文章分类(15)

文章档案(15)

相册

家园建设

最新随笔

积分与排名

  • 积分 - 150482
  • 排名 - 245

最新评论

阅读排行榜

评论排行榜

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

动态加载控件貌似给很多程序员都带来了困扰,经常收到这样的邮件,干脆就写下面这个示例来演示如何解决那些常见的问题吧。
其实常见的问题通常有这样两个:
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");
        }

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

posted on 2008-05-10 06:06 volnet(可以叫我大V) 阅读(3296) 评论(19)  编辑 收藏 所属分类: C# Controls

评论

#1楼 [楼主] 2008-05-10 06:24 volnet(可以叫我大V)      

吓我一跳,刚发上来就有人回复了,还是凌晨6点~原来是自己~~·
  回复  引用  查看    

#2楼  2008-05-10 08:08 Henry Liang      

看起来好像不对,因为添加控件信息到viewstate里面,是控件自己的事情,而不应该是让page来做。一般来说,封装一个控件,应该同时override SaveViewState()和LoadViewState()方法(具体名字记不清了,但是是这么个意思),而不应该是由page来控制。   回复  引用  查看    

#3楼  2008-05-10 09:49 求知无傲      

学习   回复  引用  查看    

#4楼  2008-05-10 10:01 狼Robot      

学习   回复  引用  查看    

#5楼  2008-05-10 10:36 jianyi      

楼主在胡闹吧?

解决这个问题,只要在每次页面加载(包括回发)的相应事件(越早越好)里每次都创建控件即可。当然楼主也是这个做的,但是: 把控件名放入Session或Cache里,这是对Session和Cache的误用! 用viewState即可,或者,控件名称可以写死的,页面动态加载那个控件一般是明确的。

  回复  引用  查看    

#6楼  2008-05-10 10:49 SZW      

每次在Page_Load里面不管是否回传都动态创建一下就行了,postback后很多时候找不到控件都是应为习惯性地把创建代码写在if(!IsPostbBack)里面了,放到外面来就能解决问题(根据Page_Load总是在按钮PostBack事件之前触发)。控件名如果可以确定,那么根本不用保存,如果是动态的(比如根据从数据库提取的信息而定)那么放在ViewState里面就可以了,若是实在数据量很大,不想造成传输之类的负担,我觉得还不如从新读取一遍,我以前就是这么做的。   回复  引用  查看    

#7楼 [楼主] 2008-05-10 14:39 volnet(可以叫我大V)      

@Henry Liang
你说的这种方法大多数做UserControl的用户是不会去做的,打死他们也不干的,当然了,包括我的文章中似乎也已经提及了,是用缓存来做简直就是胡闹,我提供的方式只能是用类似的方法就可以解决问题。我有一个Remove的方法,就是希望用户利用一些限制条件进行手动移除项,适合那种超短期的动态加载,而不是那种存在里面就什么都不做的方式了。用我的代码也可以直接使用ViewState的,只不过你得对用户控件做一下存取ViewState的方案了。
---------
而且你说的好像不对,这里要解决的问题不是用户控件自身的状态问题,而是容器的状态问题

@jianyi
@SZW
你们的方法在本文的细节中也有体现噢,我的解释是,这只是一个解释为什么丢失的方案,找出了问题的根源,解决就不难了。

-------------------------------------------------------
因为序列化到ViewState在对整个控件的时候基本是不现实的,除非你重写了×××,通常自定义控件存放在ViewState中的都是些系统默认的类型如string或者int等,因此将整个控件存ViewState不一定明智的。。。。。
  回复  引用  查看    

#8楼  2008-05-10 21:31 拼命郎      

这个还没说清楚啊,究竟是动态加载的控件内容没有回传到服务端,还是服务端在客户端回传后没有解析啊?   回复  引用  查看    

#9楼 [楼主] 2008-05-11 01:14 volnet(可以叫我大V)      

@拼命郎
不是动态加载控件内容是否回到服务端,也不是回传后解析的问题。而是页面回发后所没有引发的控件没有加载的问题。   回复  引用  查看    

#10楼  2008-05-12 16:21 镜涛      

mark   回复  引用  查看    

#11楼  2008-06-08 18:06 天启      

为什么要动态加载控件而不是控制visible属性?   回复  引用  查看    

#12楼 [楼主] 2008-06-09 02:03 volnet(可以叫我大V)      

@天启
其实能用Visable的场景都尽量不推荐大家用动态加载控件,因为动态加载控件需要自己管理更大一部分的生命周期,但这显然并不那么乐观。很多场景我们不可能预知我们要使用哪些控件,这就给开发带来的一定的未知性,因此这时候通常会采用动态加载控件的方式而不是使用Visable属性
  回复  引用  查看    

#13楼  2008-06-10 08:42 天启      

@volnet(可以叫我大V)
原来如此,受教了。   回复  引用  查看    

#14楼  2008-07-05 16:21 Forrest_Hu [未注册用户]

我想请问 ContainerManager是在哪个类库里面的。。。   回复  引用    

#15楼 [楼主] 2008-07-06 02:45 volnet(可以叫我大V)      

@Forrest_Hu
这个是下载代码中提供的一个类   回复  引用  查看    

#16楼  2008-08-09 19:46 昏天黑地 [未注册用户]

其实这个问题就像搂主说的,是在后来的回发中没有再一次创建同样的控件造成的。一般地,用户控件特定的内容可以使用ControlState在回发之间保存。ViewState用来保存容器的状态是比较合理的。但使用ViewState存在一个动态加载控件时机的问题。
我在这里简单按发生先后次序列出几个主要的页面事件:
PreInit
Init
InitComplete
PreLoad
Load
(controls events, such as someButton.Click event)
LoadComplete
比较正宗的加载动态控件的事件似乎是在Init时,因为这样在后续的处理中可以借助asp.net页面处理的机制自动完成动态加载的控件的值的回填。就是说,asp.net会处理ViewState和ControlState,一遍能够把动态加载的控件的值正确地设置到控件的属性中。这样在Load事件里就能够读到控件的值了。这在某些情形下还是有用处的,因为可能会依赖某个控件的取值而使用不同的数据绑定逻辑来初始化页面。比如说在业务系统根据中不同的销售方式可能会造成可选的促销品列表发生变化。
但要在Load(确切地说是PreLoad)之前动态加载控件还存在一些困难。经常地,我们会把需要动态加载的哪些控件的一些标记性的信息放在ViewState里。但这里存在一个asp.net在何时加载ViewState的问题。事实上,asp.net页面会两次尝试加载ViewState(至少目前根据实验得到的证据表明是这样的)。第一次是发生在InitComplete以后,第二次是在Load事件处理完成以后。至于为什么这样做我们只能猜测。一个事实是这样做我们在控件的事件处理程序中(比如someButton.Click)是可以通过控件的属性取得控件的取值的。然而依赖asp.net页面自己的处理过程,在Init阶段是无法得到ViewState的值的。于是我们就无法知道应该动态加载哪些控件。但如果把动态加载控件放在Load中,我们就无法在Load中通过控件的属性获取控件的取值,从而无法完成复杂的数据绑定初始化的逻辑。
我再把这个问题直观地表达在下面:
PreInit
Init (此时asp.net页面处理过程尚未加载ViewState,因此无法获得我们在需要动态加载哪些控件的信息)
InitComplete
<---- asp.net页面处理过程在这里尝试加载ViewState
PreLoad
Load
<---- asp.net页面处理过程在这里再次尝试加载ViewState
控件的事件处理程序 (在这里可以获取控件的值)
LoadComplete
...

这样一来问题变成了我们如果要使用ViewState来保存动态加载控件的信息,则只能在Load阶段加载控件。而这样又无法完成复杂的数据绑定逻辑。所以为了解决问题,很多人选择使用Session等其它机制来保存要动态加载哪些控件的信息。这种解决方案不是不可以,但有一些弊端。其一是增加了服务器端的开销;其二是Session有过期的问题;其三是Session不是局限于某个页面的,有可能存在Session中Key的冲突从而存在造成某些问题的风险。
而使用其它的机制比如说用某个hidden来存储该加载哪些控件的信息,而在Init阶段使用Request.Form来获得hidden的内容。这种方式也总让人感觉不大美妙。
实际上要解决这个问题并没有什么太美妙的方法。asp.net 2.0提供了一个扩展机制 - PageStatePersister。PageStatePersister只是一个抽象基类,如果要从页面的__VIEWSTATE中获取,则可以使用HiddenFieldPageStatePersister类。使用这个机制可以在你想要的时候手动为页面加载ViewState。这样就可以在要动态加载控件前手动加载ViewState了。关于这个扩展机制,我在这里提供一些链接供大家参考。
http://aspnetresources.com/blog/page_state_persisters_overview.aspx
http://aspnet.4guysfromrolla.com/articles/011707-1.aspx
  回复  引用    

#17楼  2008-08-29 16:31 肥虾米 [未注册用户]

我加载自定义控件的时候碰到一个问题,控件加载不进来,这是为什么??每次运行到"CType(Me.AxReceiptXFS1, System.ComponentModel.ISupportInitialize).EndInit()"的时候就报"动态链接库(DLL)初始化失败"这个错,楼主帮我下啊,是一个ocx控件,而且我已经注册过了   回复  引用    

#18楼 [楼主] 2008-08-30 00:32 volnet(可以叫我大V)      

@肥虾米
可否描述详细点,不是太理解您的意思   回复  引用  查看    


标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-05-11 01:12 编辑过


相关链接:
 
使用Live Messenger联系我
关闭