深入理解 ASP.NET 动态控件

来源:互联网 发布:考勤机如何修改数据 编辑:程序博客网 时间:2024/05/17 08:28
 深入理解 ASP.NET 动态控件

(Part 1 - 感性认识)  

正如我在《我喜欢的教材与我讨厌的教材》中所说的,我讨厌那种标题之后直入理论部分并开始写“定理1、定理2、定理3”的做法,所以在我自己的文章也绝对不会这样写。我认为感性认识是理性认识不可缺乏的基础条件,所以在很理论性的解释ASP.NET页面生命周期之前,先通过一些大家可能都遇到过的例子给大家一个感性认识。

动态控件遇到的第一类问题就是跨页面生命周期时无法自动保存,你必须每次手动创建。举个简单的例子,例如现在我有一个DropDownList,有三个ListItem,值分别是"0", "1", "2",在我设置了AutoPostBack之后,我希望SelectedIndexChanged时根据我选择的ListItem数值动态创建相应数量的TextBox,简单的代码如下:protected void dropDownList_SelectedIndexChanged(object sender, EventArgs e){  for (int i = 0; i < dropDownList.SelectedIndex; i++)  {    TextBox dynamicTextBox = new TextBox();    this.Form.Controls.Add(dynamicTextBox);  }}需要解释一下的是,直接用dropDownList.SelectedIndex是为了省事,因为ListItem的值本身也就是从0开始的顺序整数。

测试一下我们这个小小的ASP.NET程序有没有问题,结果当然是没问题的,你选择了哪个数值就真的会有相应数量的TextBox出现,好简单哦!我们再扔一个Button到页面上看看又会怎样,这时候你就会发现如果通过点击Button导致PostBack,那么动态创建的TextBox就没掉了,看起来事情并不如我们期望的那么简单。

“我们已经知道这个问题啦,快点给出解决方案啦”——如果你急需要一个解决方案,请直接看本篇文章的最后几段。我知道很多人是因为当前有一个棘手的问题才来翻看这类文章的,但我也不能因此而忽视了另外一部分人的需求——他们希望由浅入深地了解这个问题,并且得到解决方案的同时得到完整解释。

接下来我们继续来看第二类问题,动态创建控件的事件触发不正常。我们又来写一段简单代码:protected void Page_Load(object sender, EventArgs e){  TextBox dynamicTextBox = new TestingTextBox();  dynamicTextBox.ID = "DynamicTextBox"  dynamicTextBox.Text = "InitData"  dynamicTextBox.TextChanged += new EventHandler(dynamicTextBox_TextChanged);  this.Form.Controls.Add(dynamicTextBox);}void dynamicTextBox_TextChanged(object sender, EventArgs e){  this.Trace.Write("DynamicTextBox", "TextChanged");}由于用到了Trace,测试的时候别忘记把Trace打开哦。

我们再扔一个LinkButton到页面上,目的仅仅是为了触发PostBack,然后看看事件是否正常。奇怪的事情发生了,在修改TextBox的值之前,无论怎么点那个LinkButton,一切都非常正常,TextChanged事件确实不发生。修改了TextBox的值之后点LinkButton,事情也还正常,TextChanged事件发生了。但之后就出问题了,无论你是否修改了TextBox的值,TextChanged总是在每一次PostBack时都被触发。

这个问题很怪异对吗?事件既非完全不触发,也非总是触发。其实答案隐藏在我之前那篇《深入理解 ViewState》里面,去读一读那篇文章,或许你自己也能够解释为什么会这样。

动态创建的控件或许还存在第三类、第四类问题,在此就不一一列举了。我相信被动态控件问题困扰过的ASP.NET程序员绝对不少,而未遇到过此类问题的程序员看到上述两个问题也未必能给出解决方案和正确解释。

在提供问题的解决方案之前首先要说明一点,作为ASP.NET程序员的你需要在某一时刻某一地方让控件动态出现时,就立即在该处写代码动态创建并添加控件,这往往都是错误的做法。正确的做法是向后退三步再抬头看,这时候你看到的就不是你要让控件动态出现的那一个准确的时刻和地方,你应该看到ASP.NET页面生命周期的全貌,接着你就应该清楚你的代码该加去哪里了。

好了,是时候给出最直接的解决方案了,唯一的解决方案就是让你看清楚ASP.NET页面生命周期的全貌,而其中最佳的入门方式就是学习控件设计。虽然上面把动态控件说成一个复杂的问题,然而大家天天都在用动态控件,只不过动态控件已经被封装到一个静态控件里了。例如复杂的GridView控件,它会自动根据每一列的性质来生成对应控件,如果是模板列还要分析模板中的内容来生成模板中定义的控件,这些控件都算是动态控件,为什么PostBack不会让他们自动消失,为什么为它们添加的事件从来不会错误触发,在你学习完控件设计之后就会一清二楚。

关于控件设计,我推荐大家买Wrox(乐思)的书来看,是以控件设计为主题的那两本,不会很厚,很快能看完。如果你在使用的是ASP.NET 1.x,或者你一定要看中文版的书,那么ASP.NET服务器控件高级编程将是一本很适合你的书。至于ASP.NET 2.0的则有Professional ASP.NET 2.0 Server Control and Component Development,英文版今年8月才发布,根据清华出版社的惯例至少要等半年才可能有对应中文版。

既然连解决方案都给出了,这个系列的文章继续写下去还有什么意义吗?书上能给你的只是一个临摹着去做就不会出错的模式,以及一个听起来很合理的解释。到底为什么临摹这种模式去做就符合ASP.NET的大模式(主要是编译模型和页面生命周期),ASP.NET的大模式到底是怎样的,这就是我接下来要写的东西。

Part 2 - ASP.NET 支持

在上一篇中,我们知道了HTTP属性与客户端缓存的关系,现在就可以着手用ASP.NET来控制这种缓存。需要注意的是,ASP.NET的Cache是用于服务器端缓存的,所以和我们正在讨论的事情完全无关,我们在这里要讨论的是如何通过HTTP属性控制客户端缓存。

页面缓存

在ASP.NET中,如果你需要添加HTTP属性,可以使用HttpResponse.AppendHeader方法,例如在Page的代码中直接执行Response.AppendHeader。HttpResponse.AddHeader方法是与之等效的,不过仅用于与ASP代码兼容,所以我的建议你最好不要使用。通过AppendHeader方法,你可以将上述Last-Modified属性和ETag属性写入返回中。

接着我们考虑如何从请求中读上述属性然后判断如何返回。我们可以使用HttpRequest.ServerVariables读取请求中的属性,然后和当前的值比较,如果比较结果表明内容无变化,我们就可以设置HttpResponse.StatusCode为304,然后返回空内容;如果比较结果表明内容变化了,那就还是按一般的方式完成整个返回。

这很麻烦,对吧?所以ASP.NET内置了HttpCachePolicy类,让我们可以直接控制有关属性,我们可以通过HttpResponse.Cache访问此类的实例,而如果在Page中我们可以直接通过Reponse.Cache访问它。这个类的使用方式在MSDN中有详细的描述,所以我就不再解释了。由于它的实现也依靠上述HTTP属性,所以使用AppendHeader控制上述属性时,就会破坏掉HttpCachePolicy中的设置(如果你设置了的话)。因此直接使用AppendHeader与通过HttpCachePolicy间接控制这两个方法中,同一时间最好仅用其中的一个,如果你需要灵活性就使用前者,如果你需要简单设置就是用后者。

资源缓存

ASP.NET内置了Cache和HttpCachePolicy,这让Page的缓存已经足够方便,所以让我们来看一看非Page该怎么缓存。事实上资源文件(例如js和css)的最大可能请求数量比Page要多得多,因为一个Page通常链接几个资源文件。

编译嵌入资源

我们先来看看编译控件是如何缓存资源的。系统自带的很多控件都是带有资源的,因为他们需要这些小图片、脚本或样式来确保它们的正常运行,这些资源编译时选择为嵌入到dll中,之后无论控件发布到哪都会附带有这些资源。这些嵌入到dll中的资源以特定的形式引用,在控件呈现为HTML代码时就成了WebResource.axd开头链接,例如:<script  src="/WebResource.axd?d=7wVzVzBOs3_HEjhM5umRSQ2&amp;t=632962899860156250"  type="text/javascript"></script>WebResource.axd注册为由AssemblyResourceLoader处理,这个IHttpHandler专门负责从dll中将资源文件提取出来,然后返回给客户端。

留意WebResource.axd后面的两个参数,d是资源的标示,它表明了当前请求的是哪个资源;t是该dll最后编译的时间戳,如果dll重新编译了t就会跟着改变,这就让浏览器知道这是一个新的URL,不应该再使用原来的缓存。

需要强调的是,这并非是一个具有兼容性的做法,它只能确保资源更新时缓存过期,但不能确保没更新的资源成功缓存。根据RFC2616,浏览器操作分为安全与不安全两类,GET和HEAD应该是安全的,因为除了获取信息它们不对外界造成任何影响;POST、PUT以及DELETE是不安全的,因为它们对外界造成影响,所以你刷新POST后的页面时浏览器会提示你是否确认再次提交数据。RFC2616中提到,对于安全操作除非服务器端显式声明过期,否则客户端有权直接取缓存来显示,因为无论客户端是从服务期端取还是从缓存取都应该是不对外界造成任何影响的,然而有一种情况除外——就是当URL中存在QueryString时。

当URL中存在QueryString时,这个请求被认为是可能对外界造成影响的,所以当客户端进行这个请求时必须通过服务器端完成,也就是不允许使用缓存。RFC2616如是说了,但并非每一个浏览器都如此做了。IE和Firefox违反RFC2616对有QueryString的URL进行缓存,而Opera和Safari则遵守此规矩每次重新获取内容。也就是说,ASP.NET的这种资源地址在Opera和Safari中是决不会被缓存的,例如你的ASP.NET应用在MasterPage使用了ASP.NET AJAX的ScriptManager,那么打开每个页面时有关的脚本文件都要从新下载。

非编译嵌入资源

如果我们当前在写一个ASP.NET网站,有些资源是直接以文件形式存在的,不是编译嵌入到dll中的,那么我们就没办法享受上述系统提供的便利了,但我们可以自己实现类似的机制,并避免上述某些浏览器不缓存资源的问题。详细的实现方式将在本系列文章的下一篇中讨论,如果你不想错过其中的精彩内容,请订阅Cat in dotNET。

(Part 3 - 页面生命周期)

前言

在上一篇文章中,承诺了这一篇开始讲解释器的,不过看来要按着一个大框架来写文章不那么容易,没仔细推研究过就写出来的内容似乎很应付式。所以我决定恢复我原来的写作习惯,我觉得哪部分的内容已经成熟了,那就把它release出来,没成熟的就继续留在我的draft里面。这次要讲的是页面生命周期,动态控件对此关注的当然是动态与静态控件在生命周期中加载的差别。

一般加载

虽然一般加载过程已经被说过很多次了,但我在这里还要说,希望能把每一个阶段的特点描绘出来,让大家加深印象。

一般加载分为以下几个主要阶段(粗体标出的阶段的特殊性后面解释):

  1. Init - 初始化,是否为动态控件就以此为分界,Init之前加入到控件树的控件其处理过程就和ASPX中静态声明的一致,因为静态控件也就是在Init前加入的。
  2. LoadViewState - 加载ViewState。
  3. ProcessPostData - 处理PostData,倒不如说是加载PostData,因为此阶段控件多数仅加载PostData,顺便判断PostData是否有改变,别的处理不在此阶段作。
  4. Load - 加载,让ASP.NET程序员尽情发挥创意的地方,包括如何糟蹋ASP.NET这个框架。
  5. ProcessPostData Second Try - 第二次尝试处理PostData,和第一次所做的一样,不过第一次执行时已在控件树上的控件不会受到第二次打扰。
  6. Raise ChangedEvents - 冒泡Changed类事件,这里指的是由于PostData变更而引起的Changed类事件。
  7. Raise PostBackEvent - 冒泡PostBack类事件,除了Changed类以外的所有事件都在这里引发。
  8. PreRender - 预呈现,这名字不怎么好记,改为“末日审判”或许会好一些,因为作为上帝的程序员在这里判决每一个变量的最终值。
  9. SaveViewState - 保存ViewState,判决执行的阶段,变量最终值在此保存,判入地狱的变量无权进入ViewState这个天堂并从此消失。
  10. Render - 呈现,可能是生命周期中最无法解耦的一个阶段。
  11. Unload - 卸载,有加载自然有卸载,但其实没有多少人知道它的存在。

这11个主要阶段可以简单分为3大步骤:

  1. 加载数据:LoadViewState, ProcessPostData, ProcessPostData Second Try
  2. 处理数据:Raise ChangedEvents, Raise PostBackEvent
  3. 保存数据:SaveViewState

这3大步骤构成了ASP.NET页面处理体系,其中第2步的处理数据是基于事件冒泡的形式,也正是ASP.NET比ASP先进的地方。ASP.NET把是否处理以及如何处理分离开来了:控件内部的逻辑决定是否处理,如果要处理就触发事件;控件外部的逻辑决定如何处理,仅当事件触发时才会被执行。

追赶加载

与其说动态加载,不如说追赶加载,因为动态加载的过程包含追赶加载,这是和静态加载的主要区别。每一个控件内部都保存着它当前的加载进度,也就是它到达了上述的哪一个阶段,当我们执行Control.Controls.Add方法来将一个控件添加到另一个控件中时,父控件就会检查子控件的加载进度,如果子控件的加载进度比自己的慢了,就会要求子控件追赶上来,所以叫做追赶加载。

在上面11个主要阶段中,用粗体标出的阶段就是追赶加载时必须补回执行的阶段,而其他则是追赶加载时错过了就忽略的阶段。正是由于有一些阶段不被包括在追赶加载中,所以如果我们的控件要使用到这些阶段,就必须保证在这些阶段之前加载。也就是说,如果控件要处理PostData,包括加载PostData及根据PostData触发事件,则必须赶上ProcessPostData Second Try,这意味着它必须在Load的时候加载。否则一旦错过ProcessPostData Second Try,一个控件将在PostBack中表现得和非PostBack时一样,完全不知道有PostData这回事。

结论

其实结论已经说了,在此再强调一遍:如果你的控件要能成功触发事件,必须在Load阶段加载,如果在Load阶段之后(例如另一个控件的事件中)加载,那么此控件的事件无法正常触发。

问题与实验

先解答上次的问题与实验:

  1. this.Page.Controls.Add(this.Page.LoadControl("~/MyUserControl.ascx"));是正确的做法。ASCX与ASPX的编译方式是类似的,MyUserControl类只是一个中间过程,仅包含C#代码的编译结果,不包含ASCX的逻辑。而使用Page.LoadControl方法获得的类才是一个UserControl的最终编译结果,包含了ASCX的逻辑。
  2. 这个实验我自己也没去做过,有兴趣的朋友可以自己做一下看看结果如何。

然后是本次的问题与实验。

  1. 如果要求页面上有一个Button,点击后出现一个CheckBox,要这个CheckBox能够正常触发CheckChanged事件,应该怎么做?注意,不要使用隐藏控件的方法,因为隐藏控件所生成的HTML和ViewState是要占用空间的,我希望这个CheckBox在Button被点击之后才在页面生命周期里出现。
  2. 为什么ICallbackHandler在Beta2中仅有RaiseCallbackEvent一个事件,而到了正式版中被拆分为RaiseCallbackEvent和GetCallbackResult两个事件?(提示:这和页面生命周期的阶段划分有关)
 
原创粉丝点击