2006年11月24日星期五

从 ASP 到 ASP.NET (Part 3 - 后记)

首先要说明,题目原本是《从熟练的ASP程序员到熟练的ASP.NET程序员》,不过我觉得太长了所以删减了。这篇是后记,不再会提及任何技术细节,需要说的只是如何到达“熟练”甚至是“精通”的境界。

使用造就熟练

在ASP.NET 2.0发布一年后才来发表这个系列的文章,距离ASP.NET 1.0发布已经有4年了,可能很多人都会觉得太晚了。其实还不算太晚吧,因为我们真正在讨论的是“熟练”而不是“入门”。或许标题为《从ASP到ASP.NET》的文章和书籍在2002年就已经泛滥了,但在那个年代敢说自己到得了“熟练”水平的人可能非常少。很多人都在2002年开始入门,然而真正坚持了4年下来的人,或许才觉得自己有资格称之为“熟练的ASP.NET程序员”。

实际上有经验的程序员都知道,从学习一门新技术到熟练使用都是需要经过3~5年的,没有捷径可走,无论你是多么的高手,或者这门技术声称多么的简单而且容易上手,你都无法用几个月的时间到达熟练使用的水平。所以,如果想尽快成为一名熟练的ASP.NET程序员,就要多使用ASP.NET,并且避免以使用ASP的方式来使用ASP.NET,这样才能摸索出一套真正适用于自己的ASP.NET使用方法来。

需求推动技术

很多人通过看书跨入了ASP.NET的门槛,然而却不知道下一步该怎么做。各种“高级”技术让入门者眼花缭乱,如果拿一个PetShop或者CommunityServer来解剖的话会发现有太多的东西值得深入了解了,而这些都是无底洞,然而如果都是浅尝则止的话又好像学不到什么。

这时候你需要记住一个原则,是需求推动技术,而不是反过来。这个原则在商业上适用,在学业上也适用。我们来看一个很具体的例子,就是在中国读计算机系的大多数本科生是怎么应付学业内的各种需要编写程序的作业的。作为学生,学习过面向对象程序设计和软件工程这两门课程是一回事,然而怎么做作业却是另外一回事。在做作业时,因为复用的需求很低,所以对象级的复用是几乎不需要的,顶多就是用到函数级的复用,如果一个函数能够多次复用,他们已经欢呼函数级复用的伟大。至于面向对象程序设计和软件工程,那是用来考试的知识,考完也就忘了。

你当然不希望你在学习ASP.NET途中积累的知识好像上面说的那样很快就遗忘了,所以一定要基于需求而去学习,而不是别人说什么有用就把什么拿起来生吞活剥一番。所以,如果你入门之后不知道需要接着学什么,那么先别急着学,而是尝试用现有的知识去做点什么,凭你的个人爱好去发挥,直到你发现你现在的知识无法应付了,你也就知道下一步该学什么了。现实中的每一项技术,都是基于之前的技术无法满足某个需求而发明的,如果你想忽略这个历史过程而直接把历史上各阶段出现的技术都学懂,那么你将无法把知识记牢。只有当你重复这一历史过程的时候,你才能够真正的掌握到你所需要学习的知识,同时因为这些技术已经被发明了,你需要做的仅仅是拿资料来看,所以学习的效率其实并不低。

总结

正如每一个人类胚胎的发育都重新经历了人类进化的过程一样,成为熟练的ASP.NET程序员也必须重复部分前人所走过的路。为什么ASP不够好而需要发明ASP.NET,为什么要为ASP.NET发明那么多分枝技术,这些都是需要了解的,如果你有兴趣和我一起了解这些知识,欢迎订阅Cat in dotNET

2006年11月20日星期一

Blog Refactoring (Volume 2)

我的blog refactoring差不多完成了,现在持续更新中的blog主要有3个。

Cat in Chinese

原本Cat's Life已经转移地址,并重命名为Cat in Chinese(feed)。如果您以前订阅Cat's Life的feed,那么请更新订阅地址为新的Cat in Chinese的feed

在这个blog,我会继续按照原来的风格写,发一些古怪的甚至完全不值得别人注意的想法。有些可能是非常短篇的,想到什么自己觉得有价值的东西就马上写下来;有些可能比较长,和现在的深入理解系列文章一样,是写很久很久才release一篇出来的。

和以前一样,这个blog的feed是聚合了我的del.icio.us,如果您关注我的收藏的话请订阅此feed。

Cat in English

Cat in English(feed)已经开张啦,我会开始在里面发一些英文文章,开头可能的一些文章比较无聊,因为我想不到有什么一定要用英文发的,但以后可能逐步会多发一些技术文章。其实这个英语blog的主题还没定下来,要看之后会有些什么读者,以及他们的口味如何吧,可能英文读者的口味和中文读者相去甚远,那我就不能用中文的风格来写作了。

此blog将于近期开通email订阅,如果需要email订阅的话请关注首页的更新。另外现在有一个feed是将Cat in Chinese和Cat in English聚合到一起的,那就是托管在xFruits的Cat's Collection

Cat in dotNET

Cat in dotNET(feed)本来是我用于专门存档.NET类文章的blog,在Cat in Chinese中所有关于.NET技术的文章都会在此处存档(纯幻想型的除外)。从这个星期起,这个blog将不仅仅是存档了,我会为它引入两个全新的栏目。

在我阅读了DflyingJeffrey关于针对读者市场的讨论之后,我决定多发一些针对入门者的文章。这些文章都会是短篇,读者读起来轻松,确保不会让读者在feed reader中滞留着一些长篇而又unread的文章。这些文章会归到两个新增栏目中,每个栏目每个星期会发一篇,栏目简介如下:

  • Most Practical - 最佳实践,这会是一个类似FAQ的栏目,我会帖一个很常见的问题,然后给出一个简单可行的解决方案。在这里我注重的是实在可行的解决方案,而不是其中的原理和奥秘,不过实现方式则会倾向于我个人喜欢的风格,也就是能划分为局部的逻辑尽量封装为控件,避免代码散落在Page中。
  • Random Clippings - 随机剪报,有点类似Dflying的英文技术文章推荐系列,不过我一次只会推荐两篇文章。如果用MSDN WebCast的技术等级来看的话,第一篇文章的技术等级会是100或200的,如果您有一定的基础则文章中所描述的技术肯定会是您已通过中文文章了解过的,通过阅读英文文章可以慢慢锻炼自己的英文阅读能力,这和学校里的阅读练习差不多,只不过练习阅读的文章是专业技术类的。第二篇文章的技术等级会是300或400的,我会选择推荐一些有趣的技术文章,或许看完了您不觉得从其中学到了什么,但可以开阔视野。

Most Practical将于每周二或周三发表,大家工作途中遇到什么问题可以过来碰碰运气看看有没有好的解决方案。Random Clippings将于每周五或周六发表,这是考虑到大家可能要到周末放假了才有时间来读读英文技术文章,或者练习一下英文技术文章的阅读能力。

最后,要感谢Dflying和Jeffrey在聊天中给我的一些建议,以及在他们的文章中给我带来的灵感。还要感谢所有读者的关注,如果您希望长期关注这两个新的栏目,那么请订阅Cat in dotNET

2006年11月19日星期日

深入理解 ASP.NET 动态控件 (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两个事件?(提示:这和页面生命周期的阶段划分有关)

如果想讨论或者公布答案的,可以直接在文章评论中进行。如果想知道详细的分析,敬请期待本系列文章的下一篇,通过订阅Cat in dotNET将确保你不会错过本系列的后继文章。

2006年11月17日星期五

从 ASP 到 ASP.NET (Part 2 - 忘记什么)

前言

上一边讲到ASP程序员迁移到ASP.NET时,应该顺应Web开发的潮流学习Web Standards,应该为了更好的理解ASP.NET而学习OOP,然而学习这些知识之后并不代表你就是一个合格的ASP.NET程序员了,因为你仍被ASP的思想所束缚,接下来我要告诉你如何解决这些束缚。

比喻

首先我们要看看ASP.NET是如何“确保”你被继续禁锢在ASP的思想内的。假如我把ASP比作洗衣板,而把ASP.NET比作洗衣机,那么ASP.NET这台洗衣机就实在有点太过“多功能”了,因为你可以选择:

  • 把衣服扔进去,然后把水倒进去,接着伸手进去按照老方式把衣服洗干净;
  • 又或者使用洗衣机的自动进水功能取代上述倒水步骤;
  • 还可以从洗衣机内侧把它独有的增强型洗衣板拉出来,以获得比老洗衣板更洁净的效果;
  • 甚至进行手洗机洗混合洗,总有一种混合洗方式能同时满足你洗衣服的欲望与对洁净衣服的需求。

这台洗衣机有一个严重问题,就是想尽办法诱惑你把手伸进去,而你需要做的仅仅是把洗衣机盖上然后管好你自己的双手。

坚持原则

“干净衣服与人手勿进”——这是你应该贴在洗衣机上的标签。

在这里我必须假设你已经把洗衣机的使用手册翻烂了,其实我的意思是你已经将上一篇中说明一个ASP.NET程序员必须学习的东西都学好了。这时候你已经了解了ASP.NET的运行方式,那就必须避免不符合这种运行方式的操作。例如一个ASP.NET处理程序是立体的,那么你就要拒绝去执行那些平板的操作。

一开始你肯定会非常不适应,例如为什么洗衣机洗的衣服不如手洗的干净,又或者为什么要我记着复杂的洗衣编程设定。然而这问题不是出在ASP.NET身上,而是出自于你对它的了解还不够深入,所以你不知道如何让它完美的视线你的目标,同时少费功夫。这个过渡阶段最需要的是坚持,或许一开始你会发现ASP.NET能实现的功能真的和你的目标有一定差距,但只要你不是急功近利的去完成目标,而是仔细摸索ASP.NET其中的奥秘,那么总有一天你会发现无论多古怪的需求你总能提供一个简洁的ASP.NET解决方案。

假装的ASP.NET程序员

这部分内容本来应该属于trouble-shooting的,你有兴趣的话或许可以看看自己是否属于某一类trouble:

  • 坚决不使用WebControl,仅在必要的情况下使用HtmlControl;
  • 在ASP.NET 2.0中坚决不使用DataSource控件,使用手工DataBind的方法;
  • 使用Response.Write输出脚本或调试信息,而不使用ClientScript和Trace;
  • 直接从Request.Form读取数据,而不在LoadPostData时从NameValueCollection中读取。

如果你命中上面任何一类trouble,其原因都是没有好好坚持ASP.NET的使用原则,而尝试用ASP的方式解决问题。解决途径就是拒绝继续使用ASP的方式,然后深入了解ASP.NET的内在运行机制,从而选择一个正确的ASP.NET式解决方案。

长期关注

最后,如果你希望更多的关注ASP.NET运行机制方面的资料更新,你可以直接订阅Cat in dotNET ,这样你将不会错过任何一篇的更新。

2006年11月16日星期四

10 年后我还会用 .NET 吗?

现在看着那些“学院派的老人家”在用MFC,感觉总是怪怪的。明明非常简单的一个东西,用WinForm完全能取代MFC的写法,而且不会由于采用WinForm而显得隐藏了什么底层(要讲解底层的话会用Win32API写),为什么偏偏要用MFC呢?

或许这就是惯性的问题吧,当你对一样东西有了深入的了解后,就好比作了长期的投资,变得很难撤出来。之后即使有一样更好的东西放在你眼前,要放弃原本的投资迁移到新投资项目上也不容易。

其实别人用MFC还是用WinForm与我无关,我关心的是我10年后会怎样,我会变得一样保守吗?会一样抱着.NET不放不乐意接受新事物吗?我觉得这样的事情最好不要发生在我身上,否则我会觉得生活十分没聊的。

平时写写ASP.NET的深入理解系列,属于自己真正花时间去了解一样东西了然后把其中的乐趣分享给大家,但如果真的把自己绑死在ASP.NET上就不好了。现在我忙于手头上的学习任务和一个ASP.NET项目,完结了这个ASP.NET项目后我会去学学Ruby on Rails,看看其中有什么值得学习和思考的地方,或者以后再用ASP.NET的时候就可以把那些优秀的思维方式搬过来用。

2006年11月12日星期日

在 catch 块内进行 throw 的多种方式

参考了throw; vs. throw ex; Here's the difference!我才知道在catch块内的throw;和throw ex;是有区别的,以前都不知道可以直接写throw;呢。

这两者的主要不同在于输出的stack trace上:

  • 如果你直接使用throw;,那么stack trace就和根本不存在这个catch块的时候一样,显示错误根源是真正抛出异常的地方。
  • 如果你使用throw ex;,那么stack trace就认为你catch到的异常已经被处理了,只不过处理过程中又抛出新的异常,这时候stack trace就把throw ex;当作错误根源了。

 显然,后者会让stack trace的信息量少了,增加了追踪错误来源的难度,所以最好不要这样做。如果你要进行catch,然后你又要让异常继续冒泡,除了throw ex;以外你还有另外一个选择:
try
{
  MethodThatThrowsException();
}
catch (Exception ex)
{
  throw new Exception("oops!", ex);
}

这时候,你就将原本的异常封装进了新抛出的异常中,而stack trace会自动认为内部异常是导致当前异常的原因,也就会把内部异常的stack trace也递归显示出来。

2006年11月11日星期六

ASP.NET - 解决一个大难题的同时引入另一个更大的难题

前言

ASP.NET的优点我说过很多次了,也就是各个控件独立负责自己内部的逻辑,这是一个好事情,因为它解决了原本ASP处理逻辑耦合度高的问题。然而这是需要代价的,那就是引入ASP.NET页面生命周期,随着控件的多层嵌套,应用的复杂度增加,我们再次陷入泥潭!

问题

其实这个文章题目我两个月前就写下了,可是一直没想写完它,直到今天我在这个泥潭中泡了几个小时,于是决定先从泥潭中跳出来把文章写完,再跳进去继续解决问题。问题是这样的:

  1. 使用MS AJAX 1.0 Beta2 + 2.0 CTP新建一个项目,同时在Bin中放上Beta2的AjaxControlToolkit.dll。
  2. 扔上一个Accordion,放置几个AccordionPane,设置一下CssClass。
  3. 在Page_Load中使用Page.LoadControl加载一个UserControl,然后添加到页面上。
  4. 接着发现UserControl内的控件无法正常触发事件,陷入泥潭中……

首先要说明,如果仅仅做第3步那个UserControl肯定正常运作,那意味着问题出在ScriptManager或Accordion中出现了问题。

正文

想知道到底是什么出问题了吗?先听我说说这个ASP.NET页面生命周期的问题吧。

由于生命周期按阶段划分,任务在不同阶段按部就班完成,所以我们的每一个操作都是阶段相关的,有些操作仅能在特定的阶段操作,有些操作在不同阶段执行会导致不同的结果。当然,MS希望尽量消除这些阶段间的差异,例如让一个操作在尽可能多的阶段中都能执行,并且尽可能减少在不同阶段中操作引发的不同结果。然而这不可能完全做到,例如我们都知道ViewState读写限制为仅能在某些阶段进行,于是依赖于ViewState的控件属性也就因此受到同样的限制。

控件属性读写受阶段限制,这很好接受,对吧?因为这仅仅是一层依赖关系。顺着依赖关系推广出去,情况会变得越来越复杂,限制的原因埋藏得越来越底层,接着我们发现复杂性这一问题在ASP.NET这种结构良好的体系中出现了,而消灭这种复杂性的银弹还没被发明。

作为控件或组件的开发人员,我们当然有义务消除阶段差异,让下游的开发人员面对更低的复杂性,而且我们也确实尽力去做了。控件的每一层封装,都包含着这种努力,并向上承诺尽可能低的阶段差异。然而为了让控件看起来简单易用,我们不可能将这些差异完整地记录在文档之中,我们尝试去隐瞒细节,控件被层层封装时我们都这样做。底层文档没告诉我的差异,我当然也没必要写到这一层的文档上去;底层文档提及了的差异,我尽力弥补了,即使弥补得不太好,也不写到这一层的文档上去。于是文档就好像神话传说一样随着世代相传而改变,最终没有人知道这个控件依赖于某些底层的阶段差异。

做过控件开发的人都知道,有时候我们必须根据实际情况采用不同的方式构建看起来一样的控件。例如最简单的数据控件都会存在是否PostBack的构建差异,如果是非PostBack,则需要在DataBind时构建并将数据保存到ViewState,如果是PostBack则根据ViewState直接构建,如果PostBack后又遇到了DataBind则需要清除原来的构建并重新根据新数据构建。再复杂一些的控件,还会分步骤构建,默认情况下为了消除使用方的阶段差异,部分构建步骤会尽可能靠前到Init时执行,而另外一部分构建步骤则尽可能推迟到PreRender时执行,中间部分则尽可能减少自己的变化以便使用方操作。然而事情不会那么简单,使用方的某些操作(通常是访问某个属性)如果依赖于某个构建步骤的完成,因此一旦这些操作出现,原本在PreRender才执行的特定构建步骤就要提前执行,当这样的操作在不同阶段进行多几次,构建步骤就已经散落在页面生命周期的各阶段。

构建步骤可能散落于页面生命周期的各阶段对于控件设计师来说是一个严峻的问题,这意味着他要保证任何一个构建步骤在任何一个阶段执行都是无差异的,当然这不可能做到,于是又要引入别的机制来减少这种差异,复杂性就此产生了,接下来随着复杂性的增加控件设计师越来越无法确保较低的阶段差异程度,这就到控件使用者遭殃了,如果控件使用者又再把控件封装,并且依然企图降低阶段差异程度,那么灾难也就发生了……

结果

我花了几个小时在泥潭中泡了几个小时,边泡边写这篇文章,问题当然已经有结果了。

如果Accordion设置了HeaderCssClass或者ContentCssClass,那就会出问题,但如果为AccordionPane都加上以上两个属性,又不会有问题了。这样的情况当然通过用Reflector查看这两个类的代码来解决,结果发现Accordion会检测每一个AccordionPane是否有设置这两个属性,如果没有就把AccordionPane的设置为和自己的一样。在AccordionPane被设置时,会调用this.EnsureChildControls(),这是一个会导致构建步骤提前执行的方法,于是控件构建的顺序就改变了,不仅仅Accordion内部的顺序改变了,整个Page的都改变了。由于控件的ID是按顺序自动分配的,包括我那个UserControl,构建顺序的改变意味着ID的改变,也就相当于整个控件树都改变了,事件当然不能正常触发。

最后的解决方案当然是为我那个UserControl指定ID。我花了那么多个小时才发现自己做了件蠢事,一早打开Trace来看控件树就应该能发觉UniqueID的变化。

总结

虽然这个问题看起来不是一个太好的例子,因为一打开Trace就应该能找到问题的来源,但实际上它却正好揭示了ASP.NET框架内部的“蝴蝶效应(Butterfly Effect)”——随着复杂度的增加,任何一个细微的改变都会导致全局上的巨大变化。在设计ASP.NET的时候,MS可能也在想着解耦,在简单的情况下这东西确实也解耦,然而在复杂的情况下却正好背道而驰,这真的是很讽刺。

2006年11月9日星期四

愿饥饿与你们同在!

上个星期二回去和Benny讨论设计方案,他指出了一个问题——你的这个设计既帮助用户共享信息,同时也提供信息过滤机制,这不很矛盾吗?相当于你一边创造信息,一边销毁信息。当时正在吃饭,于是我想到了一个解释:

人什么时候要去获取信息,这基本上和肚子饿了就回去找东西吃一样,饿的时候会饥不择食,饱的时候你送给他都不要。在这个问题上,Google站对了立场,它不塞信息给你,因为人人都有饱的时候,饱了你就无法逼他吃,在一般情况下你无法知道一个人是否饥饿,所以它不做任何推服务。Google只做拉服务,因为当你饿的时候主动找上它,证明你很需要食物,它塞信息给你,你乐意接受的程度就很高,这时候只要它做得比同类型的其他服务提供商要优秀一点,你自然会找它。甚至有些领域你是只能选择Google的,这就好像本地只有一家Chinese Food Restaurant一样,你不能不选它,除非你做好了思想准备接受更坏的结果。

所以,要做信息供应商,我们的目标要很明确——瞄准饥饿的用户,看看用户哪方面不容易吃饱的就狠攻那个方面。当然,还有另外一种方案,就是在一个未知领域通过创造需求,让过上温饱生活的人把他现在的盘中餐倒掉然后去你那里抢信息。“操纵消费者”,这不是广告公司常做的事情吗?不用管那些客户原本的需求和消费能力如何,只要能操纵他们对你的产品有需求,他们自然会努力赚钱并且努力消费。

从 ASP 到 ASP.NET (Part 1 - 学习什么)

前言

首先要告诉大家,文章标题是我“恶意删改”了,原本是《从熟练的ASP程序员到熟练的ASP.NET程序员》。

从ASP迁移到ASP.NET的程序员肯定不少,我就是其中一个,然而要从熟练的ASP程序员转变为熟练的ASP.NET程序员并不容易,这不仅仅要求你学习非常多的新东西,还要求你丢弃非常多的旧东西。对于没学过ASP的人来说,或许这还容易些,因为他们本来就做好了苦学的准备,也没多少需要丢弃;对于熟练的ASP程序员来说则比较痛苦了,因为原本期望自己原来的知识都可以平滑过渡轻松用上ASP.NET,结果发现现实与期望的差距是那么的大。

在发现这个差距之后,没有人应该停下来然后倒退回去ASP的时代,知难而上把ASP.NET用到和ASP一样熟练才是我们的目标,因此才有了这个系列的文章。在系列的第一篇里,我们先来讨论ASP程序员缺了什么,什么是应该优先补上的,只有把这些知识补上了,我们才能够把自己称作ASP.NET程序员。

Web Standards / Web标准

做Web应用首先要懂做Web,现在提倡的是Web Standards,其所涉及的XHTML、CSS、JavaScript是一定要懂的。很多ASP程序员可能已经熟悉老式的表格排版方式,但这是应该被丢弃的东西。很多宣扬Web Standards的文章都给出了不少表格排版的坏处,那些我就不多说了,我要说的是不使用Web Standards对ASP.NET程序员最致命的一个坏处。由于MS也向Web Standards靠拢了,所以ASP.NET 2.0被设计为兼容Web Standards的,这时候所有的控件都被设计为语义与表现分离。如果你不遵守此分离规则去分工,那么随着你和你的美工轮流编织这张Web,最终有一天这张Web就会把你和你的美工给绑死。

要学好XHTML+CSS的设计,不仅仅需要观念上的转变,还需要开发工具上的更换。很多人无法适应Web Standards的设计观念,是因为他们还在用老工具,于是总是觉得用旧观念设计更方便效果更好。因为我相信,只有当你适应了新的工具,体验到新工具带来的便利和高效,你才会乐意接受观念上的转变。

说到开发工具,我假设熟练的ASP程序员都能够完全脱离WYSIWYG的编辑器而以纯文本方式编写XHTML和CSS,因为XHTML+CSS的开发要求你和你的美工都具有这样的能力。以往美工可以安乐地对着Photoshop,这是他们最习惯使用的工具,操作起来有精确性的同时又可视化,他们可能有一半的时间是用眼睛思考的。然而现在改用CSS就没这样的好事情了,能够好像Photoshop那样设计CSS的软件还没有诞生,修改任何一条CSS规则都会应用到所有页面上,至于每一个页面哪些元素会匹配这条CSS规则这需要美工用脑袋记着,不再是可视化。虽然改一下CSS规则然后看一下几个页面的预览这也是一种选择,然而这比Photoshop中调整参数时的即时预览要差多了,所以让美工学会在脑袋里进行预览是很重要的,这样才能写出好的CSS来。

如果要推荐一些工具的话,我会选择Visual Studio 2005 + Expression Web Beta 1,前者开发人员自己用,后者是美工用来设计或修改Web页面用的。

OOP / 面向对象程序设计

OOP可以说是ASP.NET的基础,没有OOP就没有ASP.NET控件这个概念,也就没有了ASP.NET与ASP最巨大的差别。

从最原始的CGI开始,Web应用开发者无非就在设计着这样一种逻辑——根据输入的Request生成输出的Response,大多数情况下两者都是平板的纯文本字符串,除非设计上传/下载文件。ASP引入了Request和Response对象,让处理稍微显得立体了一些,你不再需要手动分析Request文本,它能够帮你将提交上来的Form、QueryString、Cookies等参数提取出来供你使用。Session和Application对象的引入让你在不了解细节的情况下进行特定目的的存储,Server对象的引入则为你提供了很多有用的函数。

ASP遇到的最大问题是,立体的Request提供出来的数据却是平板的,整个处理过程也是平板的。那就说,在处理过程中的任何一个步骤,都可以访问任何一个Request数据项,然后把结果输出到Response中,这导致程序代码的耦合度很高。如果输出的Response有问题,你没办法明确指出处理过程中的哪一段应该对它负直接责任。

ASP.NET尝试通过引入控件的概念来解决这个问题。每一个控件都是一个独立的逻辑单元,它仅仅对自己内部的逻辑负责,并且尽可能减低对外部环境的依赖性。控件不再像普通的ASP逻辑那样它可以乱访问Request和Response,它的能力应该受到限制:

  • 一个控件仅仅应该读取它生成的HTML元素提交回来的数据,否则应该考虑通过其他控件的属性来获取,而不是从Request获取。详细说明如下:
    • IPostBackDataHandler和IPostBackEventHandler就是为控件处理自己生成的HTML有关的参数与事件而设计的。
    • 如果控件要获取的数据来自子控件,则应该通过子控件的属性获取。
    • 如果控件要获取的数据来自外部控件,则应该请求父控件或环境帮忙获取。
  • 一个控件生成的HTML应该是环境无关的,也就是无论其他控件生成怎样的HTML都不会和此控件生成的HTML冲突。详细说明如下:
    • Render用于生成本控件的HTML。
    • 如果控件要生成的HTML存在可能引起冲突的情况,则应该请求父控件或环境处理。例如最常见的生成脚本,为了避免同一段脚本多次输出就应该向ClientScriptManager注册脚本,然后让它来觉得脚本的输出。

当然,上面这些规则你喜欢怎么违反都行,没有人规定你一定要这样做的。但只有遵守了这些规定,你才算得上是一个ASP.NET程序员,否则就仅仅是一个使用着ASP.NET框架的ASP程序员。

要遵守这些规则,首先要把OOP学好,这样你才会明白为什么要遵守以及如何去遵守。因为规则是死的,而我们面对的情况可能是灵活多变的,当面对一个新的情形时应该选择如何设计呢?显然你不一定能够从上面的规则中找到一条来参考,这时候你的OOP思想及价值观就起决定性作用了。

HTTP协议

HTTP协议其实没什么好说的,一个熟练的ASP程序员必须懂的东西,而且可能从你学习ASP的那天起它就没改变过。只不过对于ASP程序员来说,这东西是透明的,因为我们直接使用Request,这和直接处理HTTP协议没太大的区别。但是到了ASP.NET,Request已经被隐藏起来了,你应该避免使用它,这时候你就需要重视HTTP协议了,否则底层通讯发生了什么你完全不知道。

总结

虽然看起来我只列了3个学习要点,但我们的目标是熟练,所以每一样你都至少用上一年半载才算学到点东西,这一点儿都不简单。

本系列的下一篇将讨论“忘记什么”,如果你明白了“学习什么”,却发现学习进度不理想,那就证明你有些包袱没有抛下了。

2006年11月5日星期日

深入理解 ASP.NET 动态控件 (Part 2 - 编译过程)

前言

要深入理解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. 在浏览器中访问你配置的路径,你就能够看到纯隐藏代码编译后的执行结果。

2006年11月4日星期六

Microsoft Ajax Beta1 - 边学边用边补充 (Part 4 - $create)

在Beta1之前,我们可以使用Xml-Script定义对象,当然也可以用JavaScript的老方式来定义对象,不过用过后者的人肯定会觉得这种方式不太方便,因为大多数Control和Behavior都需要手动调用initialize后才能正常运作。例如CTP中的Button,不执行initialize就不会绑定DomElement的click事件,它自身的click事件也就不能被正常触发。还有beginUpdate和endUpdate的初次运行也由Xml-Script的解释器负责,在JavaScript中你就需要自己负责了。

在Beta1里面,终于有一样东西可以让你方便地定义对象,同时不需要自己负责这些琐碎的必要执行项目,那就是$create指令,它的参数列表是$create(type, properties, events, references, element)。从名称上看这几个变量都很好解释,但实际上不是那么简单,现在逐个来说:

type

你要创建的对象类型,必须是继承自Sys.Component的,例如Sys.UI.Control或者Sys.UI.Behavior。注意这里说的是继承自,也就是你不能用$create创建一个Sys.Component,这应该是一个bug。

properties

你要为新建对象所提供的初始属性。对于属性名称,设置时会先确定其get方法和set方法是否存在,如果都存在就直接用set方法赋值。如果不是呢?这就要分支判断了,我不想用文字解释代码,所以我用例子解释可能有哪些分支:

  • get方法不存在,要设置的是公开的成员变量,那么{Property:"Value"}的赋值行为就是instance["Property"]="Value"。
  • get方法不存在,则可能是公开子对象深层次赋值。例如处理{Sub:{Property:"Value"}}的赋值行为就是instance.Sub.set_Property("Value")。如果是更多层的公开子对象,程序也能为你递归处理。
  • get方法存在而set方法不存在,传入的是Array,则通过get方法获取原本的Array,然后进行Array覆盖。
  • get方法存在而set方法不存在,则也可能是深层次赋值。同样是{Sub:{Property:"Value"}},这种情况赋值行为就是instance.get_Sub().set_Property("Value")。更深层次的递归调用赋值也是当然的。

这些例子的思路很不严谨,仅仅是为了让大家明白到没有get方法或没有set方法时可能会是什么情况。要严谨的就自己去看Sys$Component$_setProperties函数,参照着上述分析很容易看明白。

events

你要为新建对象提供的初始化事件。这个解释起来就简单多了,$create中仅仅检查事件名称对应的add方法是否存在,存在就添加,不存在或者要添加的不是function就抛出异常。

references

这个参数暂时没有任何资料有提到过,但是从代码看得出是用于设置对其他Component的引用,这些引用实质上也是一种属性。和properties不同,reference传入的属性值不是真正要赋值的实例,而是Component的名称,然后通过$find找到这个Component的实例并用set方法赋值。$find找不到实例或者set方法不存在都会导致抛出异常。如果大家还记得Xml-Script的写法,那就会想到Component之间交叉引用是很正常的,如果你在Application的initialize阶段进行$create,那么$create将享受和Xml-Script等同的待遇,也就是允许交叉引用。你可以按你喜欢的顺序用$create声明Component,即使先声明的在reference中提到后声明的名称没问题,因为Application的initialize阶段结束时这些reference才真正赋值。但是过了这个阶段就没有这种待遇了,reference中提到的名称必须都是已存在的Component,否则将引发异常。

element

你要新建的对象关联的DomElement。如果你创建的是Sys.UI.Control或Sys.UI.Behavior的子类,则必须由element,否则必须没有element。和type一样存在的bug就是,如果你想创建一个Sys.UI.Control,那么你自己会要关联一个DomElement,然后一个异常被抛出来提醒你不应该有element。

说完了这5个属性,是不是发觉这看起来简单的$create其实一点都不简单呢,这就是为了保证$create能够获得和Xml-Script一致的效果。它们的效果真的一致吗?书写起来不是Xml-Script更美观吗?Jeffrey向来支持Xml-Script,有了$create之后我选择支持$create,因为你将发现它的书写方式并不比Xml-Script要丑陋。

大家可以先看看Dflying使用ASP.NET Atlas实现拖放(Drag & Drop)效果(下),那个声明DragDropList和DraggableItem的Xml-Script看起来不错吧。然后来看看我的使用$create的DragDropList与DraggableItem声明,是不是层次感也不比Xml-Script差?Xml-Script的层次感来自于XML,而$create的层次感来自于JSON,因为那些properties、events、references都是使用JSON表达式书写的。

2006年11月2日星期四

中文“正义”一词可以是矛也可以是盾

在中国人眼里,好像是不会说因为追求正义的处理结果所以要去法院的。去法院通常都是解决问题的一个步骤,因为计算结果显示这步如此走对自己有好处。而这还不是最让人觉得奇怪的,让人觉得奇怪的是在某些时候会有人拉住你说——这事情为什么要去法院解决?过去的事情就过去了,显然这步你如此走对你和对另一方都没有好处。

这意味着其实我们并不需要真正意义上的正义,是吗?“正义必胜”中用到“正义”这个词,有时候可以把它用作矛刺伤别人,有时候也可以把它用作盾用于保护自己,但它永远不可能成为追求,也不可能长期背在肩上,这是因为我们负担不起,当我们需要握手和拍肩膀的时候矛和盾就必须先扔到一边。

把 IE7 装上了,不知道是好是还是坏事

奇怪的事情连续发生,昨天中午在麦当劳要了一对麦辣鸡翅,结果发现两块都是中翅。因为WGA Crack有点过时了,所以跑去9down弄了一个新的,结果发现IE7的WGA Check也能绕过了,成功把IE7装上了。

其实装IE7真的不知道是不是好事情,因为它和IE6一样是quirk兼buggy的,而且quirk和buggy的方面和IE6还不同,还有待大家去探索并寻找解决问题的hacks。装上IE7后,意味着我在IE7中测试的网页在别人的IE6中不一定能够获得的效果,然而我在同一台机上面又不可能有多一个IE6,所以要做好IE6的兼容性就不容易。

2006年11月1日星期三

Microsoft Ajax Beta1 - 边学边用边补充 (Part 3 - ITemplate)

首先,使用ITemplate的例子大家可以在Dflying那里找到一些:

这些例子当中,ITemplate都不是显式声明的。我们仅仅是在xml-script中制定了以某一个DomElement为基础生成一个ITemplate,但是ITemplate不是Control,到底ITemplate是如何生成的呢?

在PreviewScript.js中,我们可以看到唯一一个实现了ITemplate的类:Template(全称为Sys.Preview.UI.Template),它有一个parseFromMarkup的静态方法,这个方法用于解释xml-script中的<layoutTemplate />段落,然后根据其中的layoutElement属性给出的DomElement生成Template的实例。

如果要单纯用JavaScript指定基于某个DomElement生成ITemplate,那该怎么做呢?在官方论坛上有一张名为How to implement the Sys.UI.ITemplate interface programmatically的帖子,里面有人给出了在CTP中可行最简单的答案,放到Beta1中代码应该是这样的:
var layoutElement = $get("layoutElement");
var templateInstance = Function.emptyMethod;
templateInstance.createInstance = function() {
return {instanceElement:layoutElement};
}
这样你就获得了一个名为templateInstance的对象,它基于HTML中一个id为"layoutElement"的节点。templateInstance可以用于DragDropList.set_dropCueTemplate,虽然它连ITemplate都没继承。

给出了这个例子以后,大家肯定就要问为什么可以这样做。其实客户端的ITemplate和服务器端的ITemplate是很类似的,它们的功能都是关联到一个声明性模板,并且在特定时刻将声明性模板的内容转换为树提供给上级容器使用。只不过客户端的ITemplate在createInstance时生成DomElement树;服务器端的ITemplate则在InstantiateIn时生成控件树(这个以后在《深入理解 ASP.NET 动态控件》里再分析)。要生成DomElement树这就再简单不过了,只需要直接从整个DOM中摘取你想要的节点不就是咯,所以就有了上述解决方案。

不过这不是一个太好的解决方案,首先它没做到继承自ITemplate,如果ITemplate的使用方先检查你给的对象是否继承真的自ITemplate,那么templateInstance对象就无法通过检查。其次它直接返回DOM上面的节点,而createInstance会被调用多次,无论是单个调用方还是多个调用方的情形,调用方都会期望每次调用返回一个新的DomElement树,然而templateInstance每次给的都是同一个,这就可能造成一些奇怪的问题。

为了解决这两个问题,我们需要设计一个新的Template类,实现ITemplate并且保证createInstance时每次都返回新的树(使用cloneNode),完整的代码我放在这里:继承自Control的简单Template

最后就是提问时间,你觉得这个Template类继承自Control是好还是不好?好与不好我都各列举一个原因作为范例:

  • 好的地方是它采用类似Control的构造函数,可以直接用$create来从DomElement创建Template。
  • 不好的地方是它破坏了Template的语意,因为从语意上来说Template和Control应该是互斥的。一个东西是Template就不应该是Control,它的功能是为容器提供DomElement/Control,它的角色应该是provider。

那么你觉得到底是好还是不好呢?