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

前言

要深入理解ASP.NET动态控件,首先就要深入理解整个ASP.NET对页面的处理过程,由你书写好一个ASPX文件(可能还有一个code-behind文件)到你在浏览器中看到的HTML页面,这中间到底发生了什么事。这其中的第一步就是解释ASPX文件并进行编译,也就是这篇文章要讨论的内容。

由于ASP.NET编译本身就是一个大话题,所以我决定在本系列文章把这个题目再细分成几篇文章来写。开头第一篇简单叙述编译过程中涉及的各个步骤,让大家了解ASPX中的声明性代码和C#/VB.NET代码如何合并在一起并编译成assembly。在这篇文章之后,再深入了解编译过程中的一些细节,看看一个ASPX中声明性定义的静态控件到底是如何运行起来的。

鸟瞰

开始讲编译过程了,首先大家来看两张图,这张是ASP.NET 1.x的编译流程图:

接下来这张是ASP.NET 2.0的编译流程图:

这两张图来自官方文档ASP.NET 2.0 的内部变化,大家要注意到代码嵌入(code-beside, inline)与代码隐藏(code-behind)的编译模式是不同的:代码嵌入仅进行一次编译,声明性代码与C#/VB.NET代码都一起编译到一个类里面;代码隐藏则将声明性代码与C#/VB.NET代码分开几次进行翻译/编译,这些代码之间是局部与局部(partial)的关系或是基类与派生类的关系。

着陆

我们现在着陆到图上的某一点,来看清一个编译步骤是如何执行的。

ASP.NET 1.x

图上引人关注的地方就是代码隐藏编译时存在两次的“继承自”关系。第一次继承是很好理解的,用过VS2002/2003的人都记得代码中明确声明本页面的类继承自Page类,那么第二次继承又是怎么来的呢?

先把上面的问题放一边,我们换一种思路来思考,重新想一想我们的C#/VB.NET代码有什么。如果我们在ASPX中放上了一个TextBox,那么两边的代码都会出现它的定义,ASPX代码是<asp:TextBox id="myTextBox" runat="server" />,C#代码是TextBox myTextBox = new TextBox();myTextBox.ID = "myTextBox";。然后我们在此TextBox的后面用HTML写上<div>Please write down something</div>,那么这段HTML仅在ASPX中存在定义,而不在C#代码中存在定义。

接下来我们将C#代码给编译了,然后用ASP.NET引擎运行它(确实能够如此运行,但这不是我们当前关心的事),你猜我们能够看到什么?我们应该能够看到一个TextBox。至于后面那段文字呢,聪明的你应该马上想到它没在C#代码中被定义的,所以不可能被看到。

现在我们明白到了,有一部分逻辑是仅仅在ASPX中有所定义,我们需要将它们添加到C#编译结果上。如何添加这部分的逻辑?ASP.NET选择了继承机制,从C#编译结果的那个类继承,然后在派生类中加入仅在ASPX中定义的逻辑。至于作为声明性语言的ASPX如何编译成MSIL,则属于下一篇文章讨论的内容,在这里就不解释了。

需要说明的是,这两次编译中的第一次必须手动进行的,例如在VS2002/2003中执行编译;第二次编译在运行时进行自动进行。因此改动了ASPX无需重新手动编译,而改动了C#/VB.NET代码则需要手动编译。

ASP.NET 2.0

上面我们解释ASP.NET 1.1的代码隐藏编译时也提到了其中的问题,一个TextBox控件要在两边同时声明,这明显违反了DRY(Don't Repeat Yourself)原则。ASP.NET 2.0为了解决这个问题而引入了新的机制。

所谓的新机制就是C#代码中的那个partial关键字,大家可能都习惯了它的存在,但有没有人曾经想过一个这样的Page继承类的其他partial在哪里呢?如果你在VS2005中作一次项目内搜索,就会发现这个类的其它partial是不存在的,这时候你就该去看看官方文档(例如我上面给出那个)。官方文档会告诉你,另外一个partial就是ASPX,它们会好像两个普通的partial文件那样合并编译,所以在ASP.NET 2.0中我们仅需要一次合并编译就解决了所有问题。然后我要告诉你,官方文档所说的是错误的,ASP.NET 2.0的编译还是好像ASP.NET 1.1那样,只不过根据ASPX中的控件定义生成对应C#定义的工作由IDE转交给了ASP.NET编译器,至于细节你可以去参考我之前写的两篇文章:《ASP.NET 2.0 解决了 Code-Behind 需要控件声明同步的问题》与《ASP.NET 2.0 的编译模型并非完全像 MS 说的那样》。

在ASP.NET编译器捡起了定义同步这项工作后,整个编译过程就都在它的职责范围内了,不再好像ASP.NET 1.x那样先由C#/VB.NET编译器负责隐藏代码的编译,再由ASP.NET编译器负责二次编译。既然ASP.NET编译器同时负责两次编译,那就能够省去第一次编译手工进行的麻烦,编译工作都由它在运行时负责就好了。

下一步

现在我们已经对整个编译过程有了了解,大多数编译步骤都很容易理解,无非是叫C#/VB.NET编译器出来做些本职工作,只有一个除外:仅在ASPX中声明的逻辑是如何被编译为MSIL的,因为我们将此作为下一步深入理解的目标,并在下一篇文章中讨论。

问题与实验

这里有一些简单的问题或者是小实验,通过它们可以加深大家对文章的理解,大家可以将答案直接写在文章评论中。

  1. 我在Web应用的根目录新建了一个用户控件MyUserControl.ascx,隐藏文件中定义类名称为MyUserControl,我现在需要在页面上动态加载此用户控件,请问以下哪种方法正确?为什么?(提示:ASCX的编译方式与ASPX类似)
    1. this.Page.Controls.Add(new MyUserControl());
    2. this.Page.Controls.Add(this.Page.LoadControl("~/MyUserControl.ascx"));
  2. 在讨论ASP.NET 1.1编译的时候,我说到可以直接运行隐藏代码编译出来的类,并且说应该能看到一个TextBox。事实上这个TextBox可能也无法看到,不过我手上没有VS2002/2003,所以没办法验证。大家有兴趣的话,可以自己去动手做一下实验看看那个TextBox到底是否会出现。在实验之前,让我先说说如何让隐藏代码编译结果直接运行:
    1. 打开MSDN,找到IHttpHandler这个条目,然后看看它的示例代码,以及如何在web.config中配置一个路径使用特定的IHttpHandler。
    2. 由于Page类本身实现了IHttpHandler,所以隐藏代码编译后的Page继承类也一定是IHttpHandler,在web.config中配置一个使用IHttpHandler的路径,并指向你要测试的隐藏代码类。
    3. 在浏览器中访问你配置的路径,你就能够看到纯隐藏代码编译后的执行结果。

Feedback

#1楼    回复  引用    

2006-11-05 21:04 by ocean2000[匿名] [未注册用户]
hehe,有种豁然开朗的感觉,期待你的下篇文章。继续关注

#2楼    回复  引用  查看    

2006-11-05 21:15 by Dflying Chen      
非常不错!

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

2006-11-05 23:07 by Cat Chen      
@Dflying Chen
首次尝试在文章的最后加上“问题与实验”部分,因为我觉得作为深入理解系列的文章就应该鼓励读者自己往更深层次去探索。如果读者都是看完了也就看完了,这样的收获不会太大,最多就是需要用到的时候想起来,然后在bookmark内做一次搜索找回这篇文章,这没办法让人对问题有深入的理解,文章也成了应急时的工具。

我会在这个系列中继续发几篇有“问题与实验”部分的文章,然后发一张意见反馈专贴了解大家对这种写作风格的看法,如果大家都不乐意积极思考而更喜欢拿来主义的话,那我就再换别的写作风格吧。

#4楼    回复  引用  查看    

2006-11-05 23:59 by Dflying Chen      
@Cat Chen
写Blog是给自己看的,也没有必要考虑太多,保持自己的风格就好。
当然我还是很喜欢针对读者写的。

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

2006-11-06 00:42 by Cat Chen      
@Dflying Chen
我这个blog在开始写的时候就考虑作为一个个人的小小KB或者是FAQ。

在各大ASP.NET论坛上,每天你都能看到很多人遇到各种不同的问题,有一部分最终是要用ViewState或其它的控件底层原理来解释,如果每次都从头解释就需要打很多很多的字。所以我就想做一个自己的KB,解答问题时先将基础解释以链接的形式给出,再补充针对该问题特有的解释,这样我才能有效地针对该问题思考,而不是思考之前先将一些熟悉的文字默写一遍。

在blog方面,我的想法是要增强作者读者之间的互动,我希望能够有多一些评论是提出新问题的,然后我们能够一起再去研究。或者简单点说,要好像英文技术社区那么活跃,大家都有自己的观点,而不是读者就简单地接受作者的观点,这样才能达到互相促进的效果。

#6楼    回复  引用  查看    

2006-11-06 02:10 by Jeffrey Zhao      
@Cat Chen
说的好,我们需要的是交流,而不是简单的写和看。
我在这点上作的蛮失败的。

#7楼    回复  引用  查看    

2006-11-06 08:39 by buliangdedeng      
支持

#8楼    回复  引用  查看    

2006-11-06 09:58 by adonio      
你的那个类似LIVE.com的东东可不可以提供一个可以演示的下载..我现在很需要这个东西..看过Dflying的使用ASP.NET Atlas实现拖放(Drag & Drop)效果,但是一直没有实现如何保存,对这个比较需要,先谢过了

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

2006-11-06 12:59 by Cat Chen      
@adonio
那个东西我也仅仅是做到和Dflying的例子差不多,不过继承了DragDropList和ITemplate,做了一些扩展。详细你可以看我的《Microsoft Ajax Beta1 - 边学边用边补充》系列的文章,事例代码里面也有的了。

我现在没想好的一件事是拖动后的位置以何种数据结构保存到Profile,在我没有一个很好的idea之前我把它先放一边了。我有想过用纯客户端Object来表述,保存到服务器端时就存JSON String,然后不在服务器端读取,但这意味着用户每次重新打开时都要在客户端启动Profile读取(Live.com可能也是这样),之后再进行客户端布局。但Google Homepage做得更好,第一次的布局由服务器端提供,我在想这个如果做,如果用服务器端控件支持的话,复杂性就加大了不少。

#10楼    回复  引用  查看    

2006-11-06 13:19 by newsjobs      
支持,对.net内部机制的讲解一直是我在找的,关注,另外也想问是Cat chen
是怎么研究出来这些原理性的知识?还有为什么微软的官方文档会出现你所说的错误?

#11楼    回复  引用  查看    

2006-11-06 13:51 by newsjobs      
我用的是.net2003 在你的第一个试验中,开头的this.Page应改为Form1
是第二个才有显示控件,可是我不明白啊,原因及原理请赐教,呵呵

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

2006-11-06 13:57 by Cat Chen      
@newsjobs
用Reflector来看ASP.NET的源代码,很动手做实验。前者对思维能力可能有一定要求,因为你要在不能动态调试一段代码的情况下去理解这段代码,比较难跟踪的是通过Reflection执行的代码,virtual函数有时候也比较容易让人迷失,因为你要清楚当前真正执行的是哪一个派生类的override版本。后者则是人人都能做到的,只是你是否能够有心思去设计实验,遇到无法解释的问题时如何向一个更好的实验方案来探索未知项,这和解方程组或者追踪电路故障点差不多。

例如你想知道为什么静态的TextBox和在Page_Load创建的动态TextBox不同,是不是ViewState引起的,那么你可以继承TextBox,然后override一些你想跟踪的事件,在里面执行Trace.Write。之后你就可以根据Trace来看看两个TextBox的行为有什么不同。可能一次两次Trace你还无法得到合理的解释,多做几次改进你总能从实验中得出一个让人满意的结论。

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

2006-11-06 14:08 by Cat Chen      
@newsjobs
官方文档那个,MS自己或许不会承认是错误,而叫做“比较好听的解释”。就好像Windows上除了IP还有计算机名,MS有时候把那个叫做“比较好听的名字”。

至于这样做的动机,也就是MS成功的根本——“易用性”。MS不希望开发人员知道太多,希望尽可能实现“开盒即用”,所以有必要对大家隐瞒细节。隐瞒细节这个没什么不好,我们用别人做好的服务器端控件,不用管它怎么写的,它要输出JavaScript可是我可以不懂JavaScript,这很好。只不过MS有时候也会踩过界,变成了隐瞒真相,这是为了吸引更多的程序员到MS的平台上来——看看我们的ASP.NET 1.0 WebForm概念多好,好象WinForm那样简单;看看我们的ASP.NET 2.0编译模型多好,双重继承编译变成了直接合并编译。

当你明白到这种事情时有发生,也就习惯了去面对。对于有一定深度的文章,按照可信程度来排,MSDN是最差的,通常都过多隐瞒,甚至存在误导性;在MSDN上的专栏作家发表的好一些,这些文章能说真话,但是能在MSDN专栏上发表意味着不可能太深入;最具有探索性的文章往往来自MSDN之外的一些个人blog,有时候是.NET领域的知名贡献者(例如一些MVP),有时候则是一些你听都没听过的人写的。

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

2006-11-06 14:12 by Cat Chen      
@newsjobs
谢谢你指出的错误,我一时忘记了很多控件在Render时会检查自己是不是在HtmlForm内,如果不是则抛出异常。

至于解释,先看看有没有其它人愿意写出来,如果没有我迟点回复你(或者在本系列的下一篇写上)。

#15楼    回复  引用  查看    

2006-11-06 17:01 by newsjobs      
感动中,非常感谢这么详细的回复,也相信cat chen能写出更好的文章,继续关注

#16楼    回复  引用    

2006-11-27 09:48 by isMe [未注册用户]
第一个问题应该是用第2种方式,因为采用第一种方式的话asp.net运行时是没有加载MyUserControl.ascx中的内容的,不知道是不是这样:)

#17楼    回复  引用    

2006-12-22 11:40 by kai[匿名] [未注册用户]
很明显,第一个问题要用第二种方式
LoadControl就是标准的动态载入控件方法,它同时载入ascx和behind代码
运行时由ASP.NET编译器一次性编译ascx和它的behind代码。

#18楼    回复  引用    

2007-06-01 10:24 by yyy [未注册用户]
您好,看了你的文章很有启发
但是最近在项目里遇到一个很棘手的问题,
是关于自定义控件动态加载用户控件,
项目中有个页面使用了自定义控件,
但是运行以后发现在控件里定义的事件处理函数不响应,
看了网上一些文章,初步认定是因为创建的控件刷新时根本获得不了正确的值和事件,动态控件经常需要两次创建。
但是却找不到解决方案,请问你能给出点提示吗?

#19楼    回复  引用  查看    

2007-06-14 13:30 by 阿多斯      
然后我们在此TextBox的后面用HTML写上<div>Please write down something</div>,...至于后面那段文字呢,聪明的你应该马上想到它没在C#代码中被定义的,所以不可能被看到。

怎么可能!
难道Html代码在浏览器中居然看不到???换句话说,我们去掉div标签,只在页面上写Please write down something这几个单词,通过浏览器访问页面,会看不到麽??

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

2007-06-14 16:42 by Cat Chen      
@阿多斯
我的意思是,纯html内容并不包含在你的C#代码中,引起你去思考到底这段html保存到哪里了,从而理解为什么new一个UserControl是不可行的。这和是否存在div标签无关,我们讨论的是任何html代码。

#21楼    回复  引用  查看    

2008-03-25 00:24 by netkey码      
看过支持一下!

#22楼    回复  引用    

2008-03-27 15:22 by orichisonic [未注册用户]
真的是技术无止精,本来以为自己对webform很精通,读了你的文章,看来还有待提高