2007年9月9日星期日

深入理解 ASP.NET 动态控件 (Part 4 - 解决问题)

前言

在开始写这个系列的文章之时,我想着必须深入介绍背后的原理,然后将所有需要的背景知识呈现到读者眼前,不过我现在发觉这并不是好的写作方法,要写下去对我自己来说难度也不少。最近受到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等数据控件是如何工作的,为什么它们能够轻松应对动态创建控件的各种情况,我们如何学习这些控件的设计模式并运用到我们的开发当中。

没有评论:

发表评论