posts - 113, comments - 1464, trackbacks - 69, articles - 9
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

前言

在开始写这个系列的文章之时,我想着必须深入介绍背后的原理,然后将所有需要的背景知识呈现到读者眼前,不过我现在发觉这并不是好的写作方法,要写下去对我自己来说难度也不少。最近受到Infinities Loop发布TRULY Understanding Dynamic Controls (Part 4)的刺激,我决定继续写这个系列的文章,并且领悟到了更多读者需要的是对问题的一种较为易于理解的解释,而非一种严谨的解释,因为前者更有助于读者解决当前问题并在再次遇到类似问题时自行推导解决。

其实我在本系列的第一篇文章就已经明确了怎样的文章才让读者容易接受,现在是我自己误入歧途了,所以必须纠正过来。第一篇文章的结尾建议读者自己阅读控件开发有关的书籍,之后就能完整理解和解决这个问题。实际上这是一个比较不合理的建议,大多数人并不可能花时间看完一本厚厚的控件开发书籍(ASP.NET 2.0的比ASP.NET 1.x的要厚了不少)。我需要做的不是复述书上的观点,那也不是我想要做的事情,真正需要做的事情是将书里面的观点浓缩为一篇文章,要让读者能解决问题的,推理看起来符合常识以至于容易接受,然而又比严密推理省下一大多文字。好吧,就让我们按照这种思路去看看如何解决一些常见的动态控件问题。

问题分类

这是受到Infinities Loop启发的,我们首先要将问题分类,然后逐个击破。这里的分类将最常见也最容易解决的排到上面来,然后逐步深入讨论。在开发过程中,使ASP.NET程序员想到要用动态控件的情景通常有如下几种:

  1. 你需要呈现不确定数量的控件,但这些控件是同一类型的
  2. 你需要呈现不确定类型的控件
  3. 上述两个问题的综合或嵌套
  4. 你需要开发自己的Web控件

在对问题进行分类之后,我们就容易逐个去分析解决办法了。由于分类是按照难度逐步递增的,所以这对读者来说应该是较容易理解的。

不确定数量的同类型控件

如果你要在页面上显示一个调查问卷,问卷的题目来自数据库,而任何问题都只有“是”与“否”两个选项,你决定使用RadioButton提供选项。这时候动态创建控件的念头应该仅仅是一闪而过的,然后你就决定使用Repeater。

如果你在这时候没有想到Repeater,或者任何的TemplateControl,那么你就需要重新熟悉ASP.NET的内置控件了。很多时候我们用多了GridView,特别是一直都用BoundField的话,就很容易忘记世界上还有TemplateField这么一回事。

那么为什么Repeater在此会是一个好的选择呢?首先,连最近基本的foreach迭代循环它也帮我们做了,我们仅需要指定DataSource,然后执行一下DataBind(),它就帮我们动态为每一个数据项按照模板创建控件。其次,对于PostBack之后数据发生更新的情况它能应付自如。为了说明PostBack更新数据造成的影响,让我们再来看一个例子。

首先,我们直接在Session里存放一个string[],内容为{"apple", "boy", "cat", "dog"},然后我们需要将它们显示出来,每一个项目显示为一个LinkButton,点击之后就在数组中将它删除。我们都知道使用Repeater或者GridView搭配ObjectDataSource做这样简单的事情是绝对没问题的,但如果我们手动编写动态创建控件的过程呢?

按照大多数人所理解的ASP.NET逻辑,首先应该在Load这一阶段遍历数组,然后为每一个数据项创建一个LinkButton,最后把这一切都附加到页面上唯一的那个HtmlForm上面去。删除怎么做呢?LinkButton实现了IButtonControl,所以可以添加CommandArgument属性,我们就把字符串保存进去好了。在OnCommand的时候就通过此属性识别当前需要删除的字符串,然后从数组中删除,并且还要在HtmlForm中搜索对应的LinkButton然后把它移除。

这时候你应该看看OnLoad中的代码是否记得为每一个控件的ID属性赋值,否则就会出问题了。页面一开始生成的结构应该是这样的:(左侧的是控件的ID,右侧是控件显示的字符串)

ctl01 ("apple")
ctl02 ("boy")
ctl03 ("cat")
ctl04 ("dog")

我们点击"boy",页面进行PostBack,然后Load生成同样的控件树,之后OnDelete删除ctl02,所以输出的控件树应该是这样的:

ctl01 ("apple")
ctl03 ("cat")
ctl04 ("dog")

我们这次点击"cat",页面又在PostBack,但接着Load生成的控件树就不同了:

ctl01 ("apple")
ctl02 ("cat")
ctl03 ("dog")

必须留意到控件的ID属性重新编号了,然而ASP.NET仅仅知道我们点击了ctl03,所以触发的ctl03的OnCommand,根据现在的ctl03的CommandArgument属性,删除了"dog"字符串。这就是所谓的问题了,无指定ID的控件会自动按顺序分配ID,因此ID具有了不确定性。

如果在OnCommand的时候,调用HtmlForm的Controls.Clear(),是否就能移除所有控件并且让ID重头开始编号呢?实验结果表明上述删除过程中第一次PostBack后会生成这样的控件树:

ctl05 ("apple")
ctl06 ("cat")
ctl07 ("dog")

也就是说,移除确实是移除了,然而ID编号没有重置,而是继续编号。那么Repeater是怎么做到的呢?为什么直接使用Repeater就没有任何问题呢?这个下一篇文章再说,我们现在专心来把问题逐个击破,现在你记住这种情况选择Repeater或者其他更高级的数据控件就是了。

不确定类型的控件

在面对此类问题的时候,首先问问自己控件的数量,如果数量不多,直接通过设置控件的Visible属性解决问题就是了。这也就是说,把可能要显示的控件都声明为Visible="false",然后在代码中判断当前应该将哪个显示出来。

如果控件比较多,然而还是能分组的,同一时间仅仅显示其中的一组,那么你应该考虑使用MultiView,这样你的工作将会轻松不少。事实上,能够使用MultiView解决的,都应该优先考虑使用MultiView解决,这比起自己控制哪一个控件显示哪一个控件隐藏要方便多了。其实MultiView所做的,也就是帮你控制控件的显示与隐藏。

这样做的性能如何呢?我们关注两方面的问题,一方面是服务器端执行的资源消耗,另一方面是传输的带宽消耗。我们先来看看服务器端执行的资源消耗吧,我们最常见的消耗应该就是数据控件操作数据库时的消耗了。在ASP.NET 1.x时代,我们没有数据源控件,所以必须手动进行DataBind(),这也就是说如果不手动执行DataBind()的话就不会进行任何数据操作,因此只要我们记得在数据控件不显示的时候也不要让它执行DataBind()就是了,那样就不会有性能损失。在ASP.NET 2.0当中,使用数据源控件的话数据控件是会自动DataBind()的,这时候会造成控件隐藏时的资源消耗呢?事实上是不会的,数据控件即使已经定义了DataSourceID属性,它也仅仅在自己第一次可见时才进行自动DataBind()。如果数据控件的状态是隐藏的(包括使用MultiView隐藏),它就不会自动进行DataBind()。因此,在ASP.NET 2.0中使用数据源控件以及MultiView之后其底层过程还是和ASP.NET 1.x手动操作的一样,就是少写一些代码而已。

我们接着来看看带宽消耗如何,因为隐藏的控件不输出任何的HTML,因此带宽消耗就是指ViewState了。控件隐藏后,ViewState是不变的,因此隐藏控件确实比完全不加载控件造成了更多的资源消耗,换取的是该控件的状态得以保存。一般来说,简单控件隐藏后多出来几十字节的ViewState是可以忽略不计的,整个页面中HTML缩进所需的空格也都几十上百字节了;但如果是复杂控件,拥有大量的ViewState,这时候你真的应该考虑动态加载了。

总的来说,面对这类问题时首先判断显示隐藏控件的逻辑是否复杂,控件本身是否复杂。如果是比较简单的情况,则直接使用MultiView解决就是了。如果是复杂的情况,那就应该考虑自己使用控件将此逻辑封装在内,而不是直接在页面上暴露这些复杂性。关于封装控件的问题,在下一篇文章中再讨论,因此我们继续看下一类问题。

既不确定类型也不确定数量的控件

有时候我们面对前面两类问题都有清晰的思路,但是面对复合问题就感觉很混乱了。例如还是一个调查问卷的显示,数据来自XML,问题类型包括单选和多选,每一道问题的选项个数也不确定,这时候怎么办呢?foreach嵌套foreach,外层迭代问题内层迭代选项,逐个CheckBox/RadioButton来生成?

这时候我们需要的是把问题分而治之逐个击破的思想。既然是上述两类问题的嵌套,我们就应该能够通过嵌套对应的解决方案来实现。对于这个调查问卷的例子,我们可以用Repeater来迭代问题,先把这个定下来,再考虑模板里面怎么做。模板里面需要显示的是一个不确定类型的问题,因此模板里面放一个MutliView,把问题类型的表达式绑定到其ActiveViewIndex属性上,例如单选题就是0多选题就是1。然后MultiView里面的两个View各自嵌套一个Repeater,第0个Repeater迭代选项并显示为RadioButton,第1个Repeater迭代选项并显示为CheckBox。就这样就完成了,我们没写任何一行后台代码,也没有动态创建任何控件。

然后我们来分析一下这个解决方案的性能。对比起动态创建控件,它所使用的控件确实是多了一倍,因为一道问题同时创建了两组选项,一组单选一组多选,只不过其中一组被隐藏了。然而隐藏掉的那一组唯一的服务器端资源消耗就是创建以及绑定,它们不输出任何的HTML,因为它们的值不会被改变所以也不会输出任何的ViewState,并且它们也不会触发任何事件,因此在对性能没有特别要求的情况下这样的性能损失还是可以接受的。至少,这比起你自己去研究ASP.NET页面生命周期然后自己写一大段代码来实现动态加载控件要好多了。

问题与实验

本系列上一篇文章的问题与实验一直没有解答,现在给出参考答案如下:

  1. 为Page增加一个ShowCheckBox的属性:
    bool ShowCheckBox {
      get { return (ViewState["ShowCheckBox"] == null) ? false : (bool)ViewState["ShowCheckBox"]; }
      set { ViewState["ShowCheckBox"] = value; }
    }
    在OnLoad的时候检测ShowCheckBox属性,如果为true则添加上该CheckBox控件。在Button的OnClick事件中,设置ShowCheckBox为true,并添加上CheckBox。记得这两处创建的CheckBox必须拥有一致的ID属性。
  2. 这是为了让ICallbackEventHandler的处理模型符合页面生命周期的模型。虽然Callback发生的时候,页面生命周期已经与PostBack不同,然而ICallbackEventHandler还是让Callback模仿了PostBack的页面生命周期。RaiseCallbackEvent相当于PostBack的Raise PostBackEvent阶段,GetCallbackResult相当于PostBack的PreRender阶段。前者负责事件响应,后者负责生成返回客户端的HTML代码。

这次想和大家讨论的问题是,你觉得你是完美主义者吗?面对上面的调查问卷需求,你会选择我所说的Repeater套MultiView再套Repeater的做法,从而避免写任何一行后台代码,还是会选择自己封装一个控件动态创建所有控件,避免任何不必要的性能损失?

最后,如果你喜欢本系列文章,并且不希望错过下一篇关于控件开发的文章,欢迎订阅我的blog:

在下一篇文章中,我将会介绍Repeater等数据控件是如何工作的,为什么它们能够轻松应对动态创建控件的各种情况,我们如何学习这些控件的设计模式并运用到我们的开发当中。

Feedback

#1楼    回复  引用    

2007-09-11 07:42 by 怪怪 [未注册用户]
Infinities Loop的文章, 关键是有几个重要的点提的非常好, 就是老手都有可能忽略. 其实动态控件这块, 不用讲太深, 深了普通人看不懂也用不着, 真是老手呢基本也都自己摸索清楚了, 再重复一遍由于内容太多, 反而把那些需要谨慎处理的点给埋没在大段文字之中了~~

支持一下~~

#2楼    回复  引用    

2007-09-11 08:22 by imtc [未注册用户]
何苦倚赖复杂控件呢?

#3楼    回复  引用    

2007-09-11 08:24 by bluebirdzx [未注册用户]
是翻译的?不错,内容不错,我感觉没有几个人会踏踏实实的仔细看,呵呵,我们都习惯直接用现成的,对于一些技巧 原理性的不怎么去探求他。

#4楼    回复  引用  查看    

2007-09-11 08:59 by 1-2-3      
头一次听说Infinities Loop。好像挺有用的。

#5楼    回复  引用  查看    

2007-09-11 09:41 by Clark Zheng      
@bluebirdzx
不完全是翻译的,有很强的总结性

#6楼    回复  引用  查看    

2007-09-11 11:19 by 快乐老鼠Jerry      
拿来主义,也应该先声明一下啊。

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

2007-09-11 11:25 by Cat Chen      
@bluebirdzx
不完全是翻译的,但参考了Infinities Loop的文章。

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

2007-09-11 11:26 by Cat Chen      
@1-2-3
Infinities Loop仅仅是一个blog的名称。

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

2007-09-11 11:28 by Cat Chen      
@快乐老鼠Jerry
如果“拿来”是指我参考Infinities Loop这一点,我已经在文章开头说明了。然而因为我不是直接翻译他的文章,所以我只说参考自而不说原文自,并且也没必要获取原作者的授权。

#10楼    回复  引用    

2007-09-12 11:21 by 杯子 [未注册用户]
你的blog我进不了,希望能能到,能加QQ42405873吗?
我对这些比较感兴趣,希望能有进一步的更深了解!
谢谢~~~

#11楼    回复  引用  查看    

2007-09-12 12:59 by From Ocean      
@cat
既然你说到问卷,有个相关的实现方法想请你指点一下。
对应问卷我主要有4个表,其中问卷,问题,选项3个表是针对问卷维护的,还有一个答题表是存储使用者回答问卷的信息。
对于形成一张问卷,我用的是用repeater来绑定问卷的问题信息,而在repeater的模版项中添加一个panel,用来存放查询数据库后生成的选项控件(我是itemdatabound事件查询该问题有哪些选项从而动态生成)。但是这样的实现会导致需要多次访问数据库,所以我自己写了个缓存类来缓存查询后得到的数据信息,不过效率不是太高。不知道你对这样的实现方法有什么建议,以便能改进。
3ks

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

2007-09-12 14:59 by Cat Chen      
@杯子
你是指Chinese.CatChen.biz无法访问吗?另外我很少用QQ,MSN或者GTalk用得比较多,联系方式你可以PM我。

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

2007-09-12 15:12 by Cat Chen      
@From Ocean
你是用DataReader?DataSet?或者别的数据访问方法?如果是DataSet的话,是不会有连续访问的问题的,因为DataSet是和数据库无连接的。

#14楼    回复  引用  查看    

2007-09-12 22:03 by From Ocean      
@cat
因为我数据访问是本公司的一个orm框架,对于这样的需要跨表查询,我是转换成dataset存储的,莫非你是说存储在viewstate或者session里面?

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

2007-09-12 22:14 by Cat Chen      
@From Ocean
如果用Repeater等控件,它也是存储在ViewState里,不过存储格式不用你操心。如果你自己做,也应该存储在ViewState而非Session中,但你需要自己设计按什么格式存放,然而设计良好的话就可能比Repeater节省一些ViewState空间,因为它的是通用方案。

#16楼    回复  引用  查看    

2007-09-12 22:24 by From Ocean      
@cat
请原谅我的无知,据我知道viewstate存储的是简单加密(或者是加工过的)的数据,不知道你说的设计格式存放是什么意思?能否再深入点,举个例子更好。
我平时用viewstate存放都是直接viewstate[] = ds;但是另外如果这样的数据放在viewstate里面了,会影响页面的呈现速度的,那么节省viewstate空间就能相对的减少这样的影响,不过对于减少viewstate空间,我还是没有什么头绪,以前我记得似乎有压缩viewstate的文章,不知道你提到的按格式存放是否指的是这个?
谢谢老大的回复。

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

2007-09-12 22:39 by Cat Chen      
@From Ocean
你把整个DataSet存放到ViewState,和仅仅把显示信息必要的几个列放到ViewState,这肯定有体积上的差别吧?其实我说的就是这个。

#18楼    回复  引用  查看    

2007-09-12 23:08 by From Ocean      
@cat
你说的这个我知道,不过我没有在前台上面来做,我在查询选择字段的时候就已经筛选了的,得到的dataset已经是我需要的数据。不知道你还有什么建议了?不要怕太高深我不懂,我也不是一个只动嘴不动手的人:)

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

2007-09-13 00:06 by Cat Chen      
@From Ocean
这样的话,我觉得已经可以了,你按照你认为正确的去做吧,过度讨论的话还不如自己动手实验,想几种不同的做法,然后对比一下,数据最能说明问题。

#20楼    回复  引用  查看    

2007-09-13 09:39 by From Ocean      
◎cat
谢谢你的耐心指导。我再好好思考下。

#21楼    回复  引用    

2007-09-20 13:31 by Mr.LJ [未注册用户]
继续看

#22楼    回复  引用    

2007-09-20 18:38 by 杯子 [未注册用户]
两个都进不了!!
我很少用msn的
那我就多来这里看看吧,希望你能,在这里也贴一下后续内容!!
谢谢~~~

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

2007-09-20 19:15 by Cat Chen      
@杯子
也可以。

#24楼    回复  引用    

2007-10-15 01:01 by 锦瑟 [未注册用户]
动态控件的问题算讲完了吗?我怎么觉得说了一大通原理,第一章提出的问题到现在还没有彻底解决呢?

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

2007-10-15 09:03 by Cat Chen      
@锦瑟
还没讲完,敬请继续关注。

#26楼    回复  引用    

2007-11-21 18:30 by 小E [未注册用户]
唉~兄弟~何必这么麻烦???
"既不确定类型也不确定数量的控件"里用了MulitView和View
repetaer+RadioButtonList+CheckBoxList
动态设置一下RBL和CBL的Visible属性
就完了啦`
还用那么多View和MulitView干嘛呢

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

2007-11-21 19:21 by Cat Chen      
@小E
我只是以此为例子而已,如果用到的是更复杂的控件,更复杂的控件显示逻辑,就不是现有控件通过动态修改属性能完成的了。

#28楼    回复  引用    

2007-11-21 23:23 by 小E [未注册用户]
唉`
呵呵`
我只是就事说事了~
我也是大四了
呵呵`
在你Blog里发现不少写得不错嘞东东
兄弟研究得比较深啊`

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

2007-11-22 00:08 by Cat Chen      
@小E
呵呵……是的,我们都希望无论什么情景都能有类似的控件把问题简单化,但往往发现需求变成了要我们自己开发这样一个控件,因此还是必须面对这类知识。

#30楼    回复  引用  查看    

2008-03-27 13:29 by guojin      
你的文章写的风格有点诡异哦,乱得很,看了半天没看出你要说个什么