谈WebForms,以及一些ASP.NET开发上的经验

来源:老赵点滴 - 博客园
记得数年前,当ASP.NET刚出现时,天下间Web开发框架中似乎出现了一个“巨人”,WebForms这种似乎人人都能掌握的开发框架几乎瞬间流行起来。如果谁还在用传统ASP这种控制与表现混合的开发方式,似乎立即变得低俗了许多。于是乎许许多多人都学会了拖控件+绑定的方式,“Web开发人员”也越来越多,一片红火,好不热闹。

  风水轮流转,不知从什么时候开始Rails框架随着RoR忽的流行了开来,.NET社区也出现了Monorail,批判WebForms声音也慢慢多了起来。如今微软自己也推出了基于ASP.NET平台的MVC框架,很多WebForms的反对者似乎更加自信了:连微软自己都抛弃了WebForms,证明WebForms的确该退出历史舞台了,也听到了一些类似于“WebForms不适合Web开发已经是公认的事实”这样“无比肯定”的话。先不说微软推出MVC到底是不是意味着它抛弃了WebForms,单从那些MVC追捧者们“念念不忘”的WebForms的缺点上来看,我认为他们大部分只是在“跟风”,就和当年许许多多人追捧WebForms一样。

  不过我必须承认,我对ASP.NET MVC的了解仅限于Scott Gu博客上所写的内容,至今还没有下载过ASP.NET 3.5 Extensions CTP。而对于RoR和Monorail也仅限于一些资料和示例,从来没有写过一行代码。按照我的“标准”,我自己是没有资格评论MVC框架的优劣的。不过我还是想写这篇文章,因为我只会WebForms平反,而不会“贬低”MVC框架;我只是想证明WebForms的那些缺点到底真的是缺点,还是开发人员自身没有好好利用起这把利器。因此我将会根据我的经验,一一回应对WebForms比较常见的指责。如果措辞上有任何的不妥,也请大家多多包涵。

  我下面提到的做法,都是在经过实际开发过程检验的(例如开发人员与美工的合作),可能不是最佳,但是我认为还是不错的。

一、ViewState
  HTTP是无连接无状态的协议,因此ASP.NET中提出了ViewState的概念,这样数据被重新Post回页面时,页面(控件)的状态就能恢复,因此才有了很多丰富的功能,例如一些复杂的控件事件。但是ViewState带来的问题就是,如果使用不当,那么页面体积就会增加许多,网络中传输的数据太多自然会影响性能。

  但是ViewState真是必须的吗?我可以很负责任地说,在如今大部分Web应用的页面中,出现的几乎都是大量的链接,点击链接就会跳转到一个和当前页面完全无关的新页面,这样的话,页面上的ViewState又有什么用?因此我如果新建一个Web项目,做的第一件事情就是去Web.config中将enableViewState从全局关闭——同时关闭的还有enableSessionState,这也是影响性能的因素之一(stateless也便于做Web服务器层面的负载均衡)。

  有人曾经反驳我,关闭了ViewState,用WebForm还有什么意义?我的答案是:意义多的很。WebForm提供了控件模型,我能够使用“人人都能看懂和编写”的方式来设置或读取一个文本框里的值。我能轻松地响应不同按钮的事件来编写触发各种业务逻辑。这就是意义,WebForms的开发还是非常简单而清晰的(在一定程度上吧,不要“滥用”永远是正确的)。

  嗯?刚才不是说只有保持ViewState才能使用控件的事件吗?没有ViewState怎么从控件中重新获取状态呢?请注意我之前所说的是“复杂事件”。什么是复杂事件?TextBox的TextChange事件就是“复杂事件”,GridView的Command事件也是复杂事件,但是Button的Click事件就是“简单事件”;与此相对的,GridView里的每一行的数据每一个子控件的状态是“复杂状态”,而TextBox的Text属性则是“简单状态”。“复杂状态”和“复杂事件”需要ViewState,因为与之有关的这些“控件”是ASP.NET“无中生有”的,但是“简单事件”和“简单状态”基于页面中“必然”会提交的数据,它们自然能够还能够使用。在我的ASP.NET开发过程中,使用的几乎都是“简单事件”和“简单状态”,而印象中放弃“复杂事件”和“复杂状态”并没有给我带来任何的困扰。

  当某人送给我们10件礼物,而其中只有4件是我需要的,那么为什么不能简单地放弃其余6件,偏偏要去感谢只送给我们3件礼物的人而去指责前者呢?要知道他并没有恶意,那多余的6件也没有给我们造成任何困扰。

  但人就是那么奇怪。

二、性能
  WebForms的一个重要特点就是一个强大(很多情况下也是“复杂”的代名词)的组件模型。这个组件模型包含一个叫做“生命周期”的玩意儿,也就是这个玩意儿被不少人指责为性能杀手。这个复杂的生命周期的确在很多时候只是“无谓”地一遍遍执行,似乎的确造成了“浪费”,但是这真的到了“杀手”级别了吗?

  如果您认为这个组件模型为性能杀手,不如编写一个内置1000个动态Button控件的页面,然后部署到服务器上,我保证运行的飞快。1000个不够的话那么可以试试看3000、5000甚至10000个控件。您哪张页面上控件的数量会比这个还多?但是您多少页面的性能会比它高?也有文章说“尽可能少的使用服务器端控件,最多使用HTML控件加上runat=server”,这更加没有理由了:一个加了runat=server的HTML控件,它已经变成了服务器端控件了。而普通的HTML最后在控件树中仅仅被作为普通的文本而处理,在控件树中是用一个Literal保存其中的“字符”。至于具体内容是什么,ASP.NET根本不会关心。

  造成性能问题的原因多种多样,在对性能问题进行探索和优化之前,一定要找准性能瓶颈是什么,才能对症下药。如果从某些层面上讲,将公共部分提取成新的方法,会造成执行上多一次call指令的执行,性能也就“降低”了,但是我相信没有人会因此将同样的代码到处复制。在我们接触到的Web应用中,性能瓶颈大都是在数据库访问上(或者外部Service访问,等等),多执行一次数据库查询操作可能就能抵得上内存中1亿次引用拷贝。我相信,如果一个ASP.NET应用程序的性能不高,几乎不可能是因为组件模型或生命周期造成的问题。

  既然Web应用瓶颈大都在数据库访问上,那么一般该如何解决这个问题呢?最直接的方式应该是优化数据库的查询,但是最关键的可能还是缓存。君不见每个谈到Web应用性能优化的讲座都将Cache放在数一数二的位置上,因为这的确是最有效的优化方式之一。在一个并发较高的Web应用中,对一些数据进行1分钟的缓存也能带来相当可观的性能提高。其他的方式可能还有生成静态页面(没有比这访问速度更快的了),异步调用(例如一篇刚发布的文章,在数分钟后才能被搜索到也没有关系,那么何必一定要同步地、即时地写入数据或者创建索引呢?)、分离不同作用的服务器(可以为不同服务器进行有针对性的配置,例如分离图片服务器),做Web服务器端的负载均衡(stateless的重要性由此可见),对数据库进行纵向切割(加快内存中载入的数据量可以提高查询性能,并且纵向切割后能够使用多台数据库服务器分担压力),横向切割(sharding,将数据分置在不同的数据库中,以此可以通过scale out来扩展减少每台服务器的负载,提高性能),作数据冗余或Master-Slave(稍稍降低写操作的性能而提高读取数据的性能,普通Web应用大都“读取”远多于“写入”)等等。

  当然我上面提到的都是应用程序实现和架构方面的东西,事实上开发一个高性能Web应用还涉及到硬件/软件/操作系统等多方面,这里就不多解释了(其实这方面我也还在探索过程中)。其实我在这里想说的仍然是,开发高性能Web应用程序的关键大都与具体所用的实现技术无关:只要“实现”正确,做法大都相同,无论Sql Server/Oracle,Windows/Linux还是ASP.NET/RoR,其本质都差不多。Ruby和C#的性能相差十倍(存疑,求证),不还是能够开发出高性能的Web应用吗?

没想到我的文章引起了那么大的反应,看来最近MVC框架的确是一个热门话题。正如上一篇文章开始所说的,我不会对MVC框架有任何“贬低”,任何技术滥用都有问题,所以任何东西都会有所谓的Best Practice(去MSDN的Patterns & Practice栏目看看就知道了)。我写这几篇文章,是想说明,很多WebForms的缺点是被夸大了。WebForms的确有缺点,但是我们完全可以避开,并且仅仅利用到WebForms给我们带来的便利。这其实也是一种Best Practice。相信以后MVC框架也一定会出现这样的东西。

  在上一篇的评论中,有朋友说我是“边缘”的ASP.NET程序员,大概是因为我用WebForms但是抛弃ViewState、PostBack和GridView这种“重要”的东西吧。我不知道这样是不是“边缘”,但是我的确抛弃了不少东西。例如还有ADO.NET中的DataSet/DataTable——但是我们还有DataReader呢!抛弃了微软提供的一部分东西,我们其实可以发现,.NET里的基础组件一个都不少,用起来也会得心应手。

  去其糟粕,取其精华。

三、生成丑陋的HTML,难以进行样式控制
  在ASP.NET的WebForms刚出现时,各种“演示”看上去真的很美。这个特点微软至今还保留着,各微软技术大会上的演示真的让人感到心潮澎湃。在我看来,那些“激素大会”更是一种推广策略,而并没有将目光集中在技术细节的本身。所以微软的东西似乎总是有入门容易提高难的“毛病”。开发人员被“宠坏”了,上一篇文章中有位朋友说这就是“穷人的孩子早当家”,还是有一定道理的。在.NET环境下我们就像是官宦子弟,不过这并不能成为我们习惯于“吃喝嫖赌”的理由。我们要合理利用富裕的环境带给我们的资源,但是要适当地抛弃一些不好的东西。

  好像说了几句废话,现在进入正题。说到WebForms则不得不提丰富的控件,基于ASP.NET平台的第三方控件提供商似乎比其他平台下的总合还要多,这也是产业阿。在微软提供的控件里,GridView(或者1.1里的DataGrid)是被“演示”次数最多的,也似乎是最强大(还是差不多等同于“复杂”)的。有了GridView之后,很多开发人员就习惯于朝页面上拖个控件,然后再设计器里点点鼠标,设设样式,最后绑定一个DataTable上去。哗,好一个表格就此诞生。然后我们还可以响应其事件,对某一行数据进行编辑,删除。太轻松了,这世界一下子变得美好了起来。不过由于GridView过于“强大”,它几乎成为了ASP.NET初学人员的必修课,当对于Web开发都不明就里的初学者都习惯于GridView、GridView、GridView时,它的滥用也就不可避免了。于是乎,做表格,用GirdView,做列表,也用GirdView(不就是个只有一列的表格嘛)。

  可惜的是,GridView有很多毛病。如果要使用GridView的高级功能(添加、修改、删除等等),ViewState必不可少,再加上很多开发人员在绑定数据时没有经过筛选,导致ViewState变得无比庞大(例如显示一个文章列表,明明只需要ID、标题,创建时间等基本信息,却用一个“令人无比愉悦”的SELECT * FROM Article语句把文章内容也选择了出来并绑定在GridView上,ViewState中也就不得不包含几千几万字的多余数据)。

  不过现在想说的,倒是它生成出的HTML。

  GridView在客户端生成的HTML是个<table />,可惜这个<table />里东西太多了,的确显得很丑陋。不过更关键的在于,这个表格的样式实在难以控制。虽说GridView里从标题到单元格到低部都能够进行样式的定义,但是灵活度还是远远不够。再加上GridView生成的HTML死板而不符合Web标准(例如不会生成<th />标签,用ControlAdaptor进行改造的做法不算),但是优秀的样式开发人员大都是坚定的Web标准支持或推广者,ASP.NET对于标准支持不佳,难以控制样式的说法满天飞扬。

  我想为WebForms喊冤。不过首先我会打倒以GridView为首的复杂控件(包扩DataList、FormView等等)并狠狠踩上几脚。有人说,当抛弃了GridView之后,用WebForms还有什么意义?其实类似的话也不断在我说要抛弃ViewState和(复杂)的PostBack时出现。如果您觉得抛弃了这些东西WebForms就失去意义的话,那么我想说,ViewState、PostBack、GridView远不是WebForms的全部。我认为,Control模型(或者说组件化模型)才是WebForms的关键。而这个模型的“基础”是绝对优秀的。下面我会进行一些展示,虽然这些展示我觉得是基础中的基础。

  首先我们先来看一下最常用的用户控件的表现吧(DemoControl.ascx):

<%@ Control Language="C#" AutoEventWireup="true" %>

<%= "Hello World!" %>

  然后将它放在页面里:

<div>
<uc1:DemoControl ID="DemoControl1" runat="server" />
</div>

  在浏览器里打开页面会发现如下的代码:

<div>
Hello World!
</div>

  多干净的代码,我甚至连多余的空格都没有去除。还有一个例子就是Master Page,<asp:ContentPlaceHolder />也不会产生任何多余的代码。这说明了使用用户控件搭出的WebForms页面,是不会出现多余的“脏”代码的。如果您在观察那些基础控件,TextBox,CheckBox(不设Text属性),Panel等等,亦或是加上runat=server的HTML元素,无一例外(当然客户端ID的确还是比较长,关于这个问题我会在以后的文章进行讨论)。

  那么肮脏的Tag是哪里来的呢?当然是以GridView为首的复杂控件。那么如果我们要生成批量数据,又该怎么办呢?现在来看看Repeater的表现吧,就以最常见的无序列表为例:

<asp:Repeater runat="server" ID="rptItems">
<HeaderTemplate>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li>
<img src="<%# Eval("ImagePath")) %>" alt="<%# Eval("Title") %>" />
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>

  还是在浏览器里察看HTML(我这里就不贴出来了),一行多余的代码也没有。

  Repeater是ASP.NET 2.0中我最喜欢用的控件,它的功能很简单,把ItemTemplate和AlternatingItemTemplate的内容返回生成在页面上,并且将HeaderTemplate和FooterTemplate的内容显示在头尾。除此之外——没了。但是这已经足够了,对于绑定控件来说,还需要什么呢?这里面每一行代码都由我们自己编写,想定义样式也易如反掌,我们对于HTML的控制没有任何损失。

  另外,有些开发人员总认为ASP.NET中的DataTable绑定的方式让我们无法写出建模良好的代码。就算使用ObjectDataSource,在控制上也会有诸多不便。但这个也是一种误解,我们完全可以将领域模型中的对象绑定到视图里的控件上。首先是ASPX的内容:

<asp:Repeater runat="server" ID="rptItems">
<HeaderTemplate>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li>
<img src="<%# this.GetImagePath(Container.DataItem) %>"
alt="<%# Eval("Title") %>" />
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>

  然后是Code Behind:

public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
this.rptArticles.DataSource = this.GetArticles();
this.rptArticles.DataBind();
}

protected string GetImageUrl(object dataItem)
{
return "http://img.jeffz.net/" + (dataItem as Article).ImagePath;
}
}

  GetArticles方法的返回值可以是List<Article>或任何一个实现了IEnumerable<Article>接口的类型,这个样在ItemTemplate中访问Container.DataItem就会得到这个Article对象。我们在Code Behind类里定义一个protected方法,由于aspx最终会被编译为Code Behind类的子类,因此我们就可以在ASPX页面中调用GetImageUrl方法。在方法内部我们可以将object类型的DataItem转换成Article类型的对象,然后就可以做任意的处理了。

  非常方便,非常灵活,还有什么可挑剔的呢?。

  有。比如我们想要使用每行10个元素,最后一行如果不足就让右边空着的方式来展示,Repeater可能就难以实现了(其实不能这么说,按标准应该还是使用无序列表,用样式来进行控制,Repeater完全够用)。ASP.NET 1.1和2.0可能我们会使用DataList控件,只要把RepeaterColumns属性设为10即可。可惜DataList生成的元素要么是<table />,要么是<span />和<br />,让人头疼。但是在ASP.NET 3.5中又多了一个ListView控件,使用这个控件进行展示可以分组循环,可以指定容器,真可谓无比强大。重要的是ListView和Repeater一样,所有的HTML都由自己控制,一个多余的字符都没有。

  有了Repeater和ListView,真可谓打遍天下无敌手,任何页面的展示方式都不在话下。

  我们再走个极端吧,我们来看下面的呈现方式:

<ul><% foreach (Article article in this.GetArticles())
{ %>
<li>
 <img src="<%= "http://img.jeffz.net/" + article.ImagePath %>"
alt="<%= article.Title %>" />
</li>
<% } %>
</ul>

  上面的例子在ASPX页面中使用了内联的foreach语句进行显示。这样生成代码无论从干净还是自定义角度来说,都可以的让任何开发方式“无地自容”。但关键是,这个方式……为什么……恩,没错,说的难听,这是原始的ASP的开发方式;说的好听,这是“先进”ASP.NET MVC框架中模板的写法。这是不是很讽刺?但是这个的确是事实。Rick Strahl大牛也在他的Blog中写到:“我还记得在早些时候,那些ASP.NET的疯狂追随者们是多么不屑使用内联的脚本标签,或者使用显式的URL而不是PostBack来进行开发的做法,而其中的一些人现在又毫不犹豫地张开双臂去迎接MVC框架,这难道不讽刺吗?”而且由于ASP.NET MVC的特性(从Controller传递到View的只是一个ViewData对象),因此真正ASP.NET MVC的“模板”的写法还要多一次Cast,也就是类似于如下的写法(当然ASP.NET MVC可以使用强类型的ViewData,这样就免去了这样强行的Cast):

<ul><% foreach (Article article in (ViewData["Articles"] as List<Article>))
{ %>
<li>
 <img src="<%= "http://img.jeffz.net/" + article.ImagePath %>"
alt="<%= article.Title %>" />
</li>
<% } %>
</ul>

  这里就要谈到ASP.NET MVC了。其实光从View上来看,我不知道这算不算是进步(当然其实我不介意内联的写法,有时候反而更清晰)。ASP.NET MVC如果光从View(模板)方式来看,有点回到了当年的ASP方式(当然,还是补充了一些Helper)。ASP.NET MVC的特点在于M-V-C的分离,在于业务逻辑的触发方式,在于URL Routing的驱动方式,而不是模板化的写法。如果要说MVC在View层面比ASP.NET清晰,我是肯定不会同意的,因为我完全可以在WebForms的ASPX页面中“写成”ASP.NET MVC类似的方式。不知道您是否同意,至少我认为,使用WebForms内Repeater或ListView的写法,不会输给ASP.NET MVC的模板。而且事实上,在ASP.NET MVC中作为View使用的aspx页面里,我们完全也可以放置Repeater,然后再Page_Load方法里写代码,这更证明了,在View方面WebForms和ASP.NET MVC其实是非常相似的。

  刚才说了,ASP.NET MVC的重要特点,在于M-V-C之间的分离,非常清晰,而且Model和Controller之间能够进行独立的测试。但是WebForms也可以做到这一点,我在后面的文章中还会谈一下WebForms里如何使用MVC来做到清晰、分离和单元测试等等。而且,我似乎觉得某些WebForms中能够做到的东西在MVC框架里却无法或者很难实现。可能是我对于MVC的了解程度还不够多的缘故吧,等我先研究了MVC框架之后再说。:)

  讲到这里,您心中是否有了答案,WebForms究竟能否写出清晰美观的HTML?在WebForms控制样式是否困难?抛弃GridView,我们并没有什么损失,因为现在的网页中又有多少会用到GridView的功能呢?

  目前老赵在公司使用WebForms开发时会与一个专职的前台开发人员配合。他是我认识的最好的前台开发人员,编程能力强,经验丰富,对于各种浏览器中脚本和样式的差异和问题可谓了如指掌,只是在这之前他只用过PHP,并没有接触过ASP.NET WebForms。但是仅仅向他解释了几个控件生成HTML的方式,或者如何从服务器端获得ClientID之后,我们之间的配合就非常轻松顺畅了。无论是开发人员先写放控件然后由他进行修饰,还是他先写静态样式页面我们再进行替换,都工作的非常顺利。我现在已经能够专注于业务的实现,架构的设计,而彻底从页面样式的“泥潭”中解放了出来,最多编写一下简单的JavaScript代码。分工的明确,也使得我们的工作效率得到了相当的提高。

  所以最后我给大家的建议就是,尽可能地使用“纯粹”的服务器端控件。例如使用Repeater和ListView,其实写出优秀的页面非常容易。当然,即使这么做,还是会有一些缺点的,例如:

  例如Repeater的ItemCommand事件已经无法使用了,因为它会用到ViewState。但是我们开发的大部分的页面甚至连PostBack都不需要,这又有什么关系呢?

  使用服务器端控件虽然能让我们对于HTML有十足的控制,但是我们无法避免在客户端生成一个不规则的ID。这一点理论上可能会影响页面样式的开发,但是就我那前台开发人员同事的话来说,除了一些在客户端进行布局的DOM元素之外,几乎不会使用ID来控制样式。就我们工作的结果来看,不规则的ID也并没有造成任何的影响。

四、生成复杂的ID难以使用JavaScript操作
  我在上一篇文章的最后提到了,虽然使用WebForms我们能够对于页面上的HTML属性和样式等进行自由的定制和控制,但是有一点是毋庸置疑的,我们没有办法(正常的办法吧,Hack不算)让服务器端控件在客户端生成一个简单的ID。例如,一个TextBox控件,在服务器端的ID是txtUserName,但是最终在客户端生成的ID可能是LoginForm_txtUserName,因为它被放在一个ID为LoginForm的NamingContainer中。

  有了组件模型,就出现了大量控件。控件最主要的目的之一就是复用,而复用的一个特点就是应该高度内聚,而不依赖于外部环境。因此,为了使组件内部的服务器控件最终生成的客户端ID能够在页面上唯一,WebForms引入了NamingContainer这个概念。在NamingContainer中的服务器端控件最终在客户端生成的ID,会使用NamingContainer的“客户端ID”作为前缀。如此“递归”的做法保证了服务器控件在客户端的ID唯一。

  Web 2.0在业界风卷残云般的势头至今还未停歇,与其有密切相关的AJAX技术也被广泛使用。AJAX技术从根本上讲,是一种在浏览器中使用JavaScript实现的技术,因此使用JavaScript操作DOM元素的情况非常多见。在非WebForms的页面中我们可以编写如下的代码:

<input type="text" id="textBox" />

<script language="javascript" type="text/javascript">
document.getElementById("textBox").value = "Hello World!";
</script>

  但是由于NamingContainer的缘故,我们在使用WebForms的服务器端的控件时就可能无法通过textBox在客户端获得文本框(生成的元素)。为了解决这个问题,服务器端的控件模型提供了一个ClientID属性,通过这个属性,我们就可以在服务器端得到控件最终在客户端的ID。例如,如果上面的代码放在一个用户控件里的话,就一定必须写成如下形式:

<%@ Control Language="C#" AutoEventWireup="true" %>

<asp:TextBox runat="server" ID="textBox" />

<script language="javascript" type="text/javascript">
document.getElementById("<%= this.textBox.ClientID %>").value = "Hello World!";
</script>

  此时,当控件被放到页面上之后,它在客户端生成的代码则会是:

<input name="DemoControl1$textBox" type="text" id="DemoControl1_textBox" />
<script language="javascript" type="text/javascript">
document.getElementById("DemoControl1_textBox").value = "Hello World"!;
</script>

  请注意<input />元素的name和id,它们都留下了NamingContainer的痕迹。由于我们在页面上使用了<%= %>标记直接输出了服务器控件的ID,这样在客户端的JavaScript代码也就可以正确访问到服务器端<asp:TextBox />对应的客户端<input />元素了。

  这种在设计器很难预测的客户端ID,就是使用WebForms时所谓的“客户端ID污染”。

  接下来我们不妨来看一个略为复杂点的例子:

<%@ Control Language="C#" AutoEventWireup="true" %>

<asp:TextBox runat="server" ID="textBox" />

<script language="javascript" type="text/javascript">
var counter = 0;

function increase()
{
 document.getElementById("<%= this.textBox.ClientID %>").value = (counter++);
window.setTimeout(increase, 500);
}

increase();
</script>

  上面这段JavaScript代码的作用是每500为一个计数器加1,并且显示在文本框上。随着项目的发展,页面上复杂的JavaScript代码会越来越多,于是我们就会想办法将其转移到js文件中并且在页面上引用它们。使用js文件的好处很多,便于进行代码管理是一方面,但是最重要的好处之一还是对于性能的提高。如果JavaScript代码完全写在页面上,这样每次加载页面都需要下载这些JavaScript代码,而js文件可以缓存,这样客户端只需要在第一次加载时下载这个文件就可以了。减少了客户端与服务器之间数据通信的大小,也就加快页面加载的速度,提高了性能。

  不过问题就此出来了:为了能够正确引用到页面上的某个服务器控件生成的DOM元素,我们就必须在页面中使用<%= %>标记来输出控件的ClientID,但是<%= %>无法写在js文件中,这可怎么办?于是很多人着急了起来,我也不时会收到此类问题,似乎很难找到合适的解决办法。于是“客户端ID污染”似乎也就成了一个使用WebForms时非常严重的问题。

  有些朋友会说:“这个没有问题啊,仔细观察ClientID的组成方式能够很容易找到规律的。”服务器控件的ClientID是由自身ID和它所在的NamingContainer“树”来共同决定的,因此在理论上我们也完全可以在设计器得到“已经放置在页面中”的某个服务器控件的客户端ID,并将其写进JavaScript代码中。话虽如此,的确没错,但是这个解决方案实在不好,因为它违背了控件的重要特性:“复用”。作为一个控件来说,它可能会被放在任意的NamingContainer树下,也就是说,它的客户端ID在不同的环境中并不固定。另外,如果控件上层NamingContainer树中有任何一个的服务器端ID被修改的话,js文件中使用的ID就需要进行改变,这样实在不利于的维护,随着项目增大,此类问题会愈发明显。

  那么我们究竟该怎么做呢?

  在设法解决这个问题之前,我们先来思考一下这个问题。如果我们没有使用WebForms进行开发,就在普通的页面上编写代码,那么我们对于上面的功能会如何将其提取到js文件中呢?嗯,就直接在代码中通过textBox这个ID来获得DOM元素吧。那么好,请您先回答我以下几个疑问:

  为什么要写textBox而不是其他ID呢?
  如果其他页面上有个同样需要实现的功能,而那个文本框的id是txtCounter,那么该怎么作呢?
  如果一张页面上有两个文本框需要显示这样的计数器,那么又该怎么做呢?
  上面的几个疑问其实只反应了一件事情,那就是这个计数器的复用性实在太差。什么叫做好的复用性呢?那么我们来看一下一个典型的示例,MaskedEditExtender。我们来看看它是怎么做的:

<ajaxToolkit:MaskedEditExtender
TargetControlID="TextBox1"
Mask="9,999,999.99"
MessageValidatorTip="true"
OnFocusCssClass="MaskedEditFocus"
OnInvalidCssClass="MaskedEditError"
MaskType="Number"
InputDirection="RightToLeft"
AcceptNegative="Left"
DisplayMoney="Left"
ErrorTooltipEnabled="True" />

  MaskedEditExtender的第一个属性TargetControlID,就可以决定了究竟是为哪个文本框添加效果,然后效果的样式可以由MaskType和Mask决定,获得焦点的样式和输入错误的样式可以由OnFocusCssClass和OnInvalidCssClass属性决定,连字符输入的顺序都可以定制。

  这就是复用:爱怎么用,就怎么用。爱给谁用,就给谁用。想什么时候用,就什么时候用。

  要复用,一般总需要组件化或模块化,内部实现通用的功能,而具体的信息应该由外部传入。例如我们上面的计数器就应该进行改造(用到了MS AJAX Lib里的Function.createDelegate方法):

function Counter(textBoxId, interval)
{
this._counter = 0;
this._textBox = document.getElementById(textBoxId);
this._interval = interval;
}
Counter.prototype =
{
run : function()
{
 this._textBox.value = (this._counter ++);
 window.setTimeout(
 Function.createDelegate(this, this.run), this._interval);
}
};

  现在这个技术器的复用性已经有质的飞跃了,因为我们可以随意指定一个客户端的文本框进行显示,并且可以自由地设置计数器增长的间隔时间。于是我们在WebForms页面中就可以写如下的代码了:

<asp:TextBox runat="server" ID="textBox1" />
<asp:TextBox runat="server" ID="textBox2" />

<script language="javascript" type="text/javascript">
new Counter("<%= this.textBox1.ClientID %>", 500).run();
new Counter("<%= this.textBox2.ClientID %>", 1000).run();
</script>

  现在WebForms客户端ID污染已经不构成问题了吧!

  其实解决客户端ID污染的做法用一句话就能说清:“将不变的部分提取至js文件,将变化的部分(例如服务器控件的客户端ID)留在页面中”。但是我在这里将它上升到组件化的高度,因为它能让我们开发出更优秀的客户端程序。组件化的客户端编程方式较之传统的零散function的做法,更有利于代码的管理,并且增强了复用性和可维护性。有人说,客户端ID污染问题使脚本代码很难做到“内聚”——可能他的意思是将脚本代码提取到js文件中吧——但是我认为,这种污染“迫使”我们使用组件化的方式进行客户端开发,而这种组件化或者模块化的做法恰恰提高了代码的内聚性。

  不过,似乎组件化的编程方式会写更多的代码,不是吗?从理论上来说,可能的确是。不过需要注意的是,我上面提出的例子非常简单,简单到了其中的一半代码是用于“组件化”编程的“骨架”上。而对于一个略为复杂的功能来说,例如一个通用的表单验证组件,或者客户端级联组件,增加的这点“骨架”还算得了什么呢?

  这也算是一种因祸得福吧。

 

 

TraceBack:http://dotnet.csdn.net/page/8e1b1ffd-b3e4-4296-9f28-a91ace74fc7b

posted @ 2008-06-17 14:10  my favorite  阅读(246)  评论(0编辑  收藏  举报