Spiga

ASP.NET 无法确保在注册的 JavaScript 内不存在重复定义

2007-01-28 00:13 by Cat Chen, 5987 visits, 收藏, 编辑

在ASP.NET 2.0中,我们使用Page.ClientScript属性(也就是一个ClientScriptManager对象)的一些名字以Register开头的方法注册客户端脚本,这是大家都知道的。

理论上应该如何避免冲突

先说说为什么要这样注册脚本,而不用Response.Write直接输出。举个例子,你用3个DropDownList做了一个输入日期的区域,分别代表年/月/日,然后你为了防止用户输入2007/02/31,所以你决定把这3个DropDownList做成级联的,也就是随着年和月的输入改变,日的可选项跟着改变。这时候你可以通过写一些JavaScript来实现级联,例如定义一个名为updateDateRange()的JavaScript函数负责更新DropDownList,然后直接把这些JavaScript放到C#的字符串里,并且使用Response.Write输出。这些代码用起来会很正常,直到有一次你的页面需要输入两个日期。

在需要输入两个日期的那个页面上,你把3个DropDownList复制粘贴了一遍,也把输出的JavaScript的那段代码复制粘贴了一遍,接着根据两处ID的不同做了相应的修改,结果有一组级联无法正常运行起来。你查看服务器端输出的HTML,接着恍然大悟——原来有两个updateDateRange()函数。于是你把updateDateRange()改为updateDateRange(yearControlClientId, monthControlClientId, dateControlClientId),同时把JavaScript删减为仅输出一遍,这时候无论哪组级联都使用同一个函数,它们根据调用时输入的DropDownList.ClientID来区分。

又有一天,你决定把这组级联封装为一个UserControl,做起来当然还是复制粘贴大法,也就是把3个DropDownList和JavaScript复制进UserControl,然后把UserControl的引用复制回原本的调用处。忙完之后,发现那个有两个日期输入的页面又出错了,原来updateDateRange(yearControlClientId, monthControlClientId, dateControlClientId)又被重复输出了,因为页面上放入了两个UserControl所以JavaScript被输出了两遍,并且没办法减少输出次数。

这时候你可以使用Page.ClientScript.RegisterClientScriptBlock解决问题,它通过type和key这两个参数确定脚本是否被重复注册,而被重复注册的脚本仅会输出一次。为什么要type和key两个参数呢?以前ASP.NET 1.x的同类函数只有key一个参数,这带来的问题是可能两个不同的控件设计时都使用了同一个key来注册自己的脚本,结果其中一个控件脚本的成功输出必然会抑制另一个控件脚本的输出。加上了type参数,各控件都用自己的类型作为标识,这样就能有效避免注册时冲突。

为何无法真正避免冲突

关于这个问题,我们先看看ASP.NET内部定义的JavaScript是以什么方式命名的。通常,private的全局函数或变量,命名都以双下划线开头,例如大家熟悉的__doPostBack,或者是WebPartManager在客户端使用的__wpm。而public或protected的全局函数或变量,一般就好像C#那样使用Pascal命名法。具体的例子,大家可以用Reflector看看System.Web.UI的资源中的那些js文件。

我们暂时就假设这种命名法是正确的,然后模仿着去在自己开发的控件中实践。事实上很多控件开发者也确实这样做了,比较多的专业控件中你都能看到双下划线开头命名的函数或变量,这至少可以避免和控件使用者在页面上注册的函数或变量冲突,因为在页面上注册的函数或变量通常都采用比较简单的命名法。

假如现在我们要做一个浮动上下文菜单,也就是当你的鼠标移动到某个HTML元素上时该浮动菜单自动出现,当鼠标离开元素并且也不在菜单上时,菜单自动消失。为了方便用户操作,我们允许用户鼠标移动过程中稍微离开菜单区域,所以定义当鼠标离开菜单区域若干时间后才让菜单消失,而这个时间在客户端保存在__disappearAfter变量中。这个控件看起来什么问题都没有,直到你把它和ASP.NET 2.0自带的Menu控件放在同一个页面上,因为Menu控件也有类似的功能,而且和我们的控件一样Menu控件选择了将时间变量保存在一个名为__disappearAfter的变量中。

现然,作为ASP.NET框架的使用者,框架没有声明这个变量的名字不允许使用,我用了有问题当然就可以认为是框架的错。Brad Abrams写了一本《.NET设计规范/Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries》,里面却完全没有提及JavaScript和CSS的规范,好像在ASP.NET中使用到的JavaScript与CSS都是琐碎的不能在琐碎的事情,所以完全不值得一提。

事实上,既然ASP.NET允许一个页面上不同的控件设计者引入不同的JavaScript和CSS,就必须提供一种方法去管理潜在的命名冲突。如果是JavaScript,我们可以考虑使用ASP.NET AJAX的namespace来避免冲突,下一代代号为Orcas的Visual Studio和ASP.NET将内置ASP.NET AJAX支持,所以其内置控件所使用的JavaScript应该也会有namespace,这样就有有效降低冲突概率。至于CSS命名冲突,暂时没有好的解决放案,只能依赖控件设计者的习惯了,你可以考虑为你的控件根元素附上一个namespace以示区分,这样也算是降低冲突概率的一个办法。

最后,如果你希望阅读更多类似的ASP.NET主题文章的话,欢迎订阅Cat in dotNET (Feed: http://feeds.feedburner.com/CatChen/dotNET)或Cat in Chinese (Feed: http://feeds.feedburner.com/CatChen/Chinese)。

Add your comment

11 条回复

  1. #1楼 Jeffrey Zhao      2007-01-28 02:17
    嗯,JavaScript。
    就像除了privilege方式外做不到真正的“私有”一样,prototype的扩展方式里的_方式也做不到真正私有。
     回复 引用 查看   
  2. #2楼[楼主] Cat Chen      2007-01-28 11:21
    @Jeffrey Zhao
    是否真正private还不是问题,问题是可能导致冲突。现在暂时没碰到什么冲突,是因为大家的控件都没有用太多的JavaScript,但如果越来越多人在自己的控件中引入JavaScript,那冲突的可能性就会增加了。
     回复 引用 查看   
  3. #3楼[楼主] Cat Chen      2007-01-28 21:22
    @Jeffrey Zhao
    例如GridView对应的客户端类就是是GridView,而不是Sys.UI.GridView,如果大家都如此命名自己控件的客户端类,很容易就会造成冲突。希望Orcas开始所有控件的客户端都开始使用namespace吧。
     回复 引用 查看   
  4. #4楼 Jeffrey Zhao      2007-01-28 21:41
    @Cat Chen
    这就是namespace出现的原因,呵呵。
     回复 引用 查看   
  5. #5楼[楼主] Cat Chen      2007-01-28 22:05
    @Jeffrey Zhao
    不过CSS没有namespace哦,例如现在CSS Friend ControlAdapter就强制了一些控件的CSS命名,如果另外一些控件开发者也在控件内强制命名CSS,那就有冲突的潜在性了。

    事实上我觉得任何强制的CSS都是不应该的,应该允许暴露属性让用户选择CSS名称,同时也不强制应用任何CSS规则。例如HyperLink,如果用于图片它就会以style的方式直接给border-width赋值为0。虽然通常我们都希望border-width为0,而浏览器默认为1,但这样赋值为0就很容易给设计人员造成麻烦。我上次设计好应用之后开始styling,就发现无论如何都无法改变HyperLink图片的border-width,结果发现它强制赋值了,而我的赋值要覆盖它的就必须加!important声明。
     回复 引用 查看   
  6. #6楼 Jeffrey Zhao      2007-01-28 23:01
    @Cat Chen
    理论上Namespace也无法解决问题。万一一个类库定义了A.B这个space,另一个类库定义了A这个namespace里的B类呢?
    CSS依靠制定规范还是可以在一定程度上避免冲突了。就是说,不在CSS里使用单个类名之类,一定要使用多个类名,例如“.Sys_UI_GridView .title”。
    其实这也类似一个Namespace,而且最好,第一个“CSS类名”就是namespace转化而来的。
     回复 引用 查看   
  7. #7楼 浪子      2007-01-29 13:02
    这些问题都是比较晕的,我最近还碰到Callback中因为js局部变量和全局变量冲突导致的异步错误问题,连微软自己都不可避免额。

    在Callback的脚本中,ms用了for(i=0;i<...;i++),
    结果晕死我,有人在自己的方法内用了变量i,但是跟ms一样,忘了var,结果都变成window.i ,互相覆盖了,导致异常:“ __pendingCallbacks[...].async 为空或不是对象”....

    :(
     回复 引用 查看   
  8. #8楼 Jeffrey Zhao      2007-01-29 22:58
    @浪子
    忘了加var?这个问题相当严重阿……
     回复 引用 查看   
  9. #9楼 浪子      2007-01-30 08:39
    @Jeffrey Zhao
    是啊,这个可是相当的严重阿!
     回复 引用 查看   
  10. #10楼 daijun[未注册用户]2007-10-20 11:39
    if (!Page.ClientScript.IsClientScriptBlockRegistered(this.GetType(), "myName"))
    {
    Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "myName", info, true);
    }

    这样不行?
     回复 引用   
  11. #11楼[楼主] Cat Chen      2007-10-21 17:39
    @daijun
    这能够从服务器端避免重复注册,然而无法避免客户端代码的冲突。

    例如A公司和B公司都提供客户端的Timer对象,如果它们的名字就是Timer,而非A.Timer与B.Timer,这样冲突也就出现了。
     回复 引用 查看