Hello World
Spiga

ScriptPath属性的拙劣设计

2007-06-25 20:45 by 老赵, 5667 visits

背景

ExtenderControlBase类是开发AjaxControlTookit服务器端Extender组件的基础。ExtenderControlBase基于ASP.NET AJAX的Exnteder模型提供了许多方便开发人员的强大支持,能够在它的基础上开发Extender确实是一件非常容易的事情,这样我们就可以将更多的精力放在客户端Behavior的逻辑上了,那才是AjaxControlTookit组件的重点。

当基于ExtenderControlBase开发Extender控件时,我们一般总是先为服务器端和客户端的组件同时定义好属性,然后将经历几乎完全集中在客户端Behavior的开发上。在大多数情况下,存放客户端组件代码的JavaScript文件会嵌入到程序集中,然后使用ScriptResource.axd文件发送到页面上。如果我们改变了脚本文件并且想测试修改的效果,那么我们就必须重新编译那个程序集,这样网站引用的程序集将会更新,重新刷新页面时新的脚本文件就会被发送到客户端来。

ScriptPath属性以及它的拙劣设计

很显然,我们如果仅仅为了更新脚本文件而一次又一次地编译程序集会让人觉得异常繁琐,因此出现了ScriptPath属性。ScriptPath属性被定义在ExtenderControlBase类中,它的首要作用就是为开发Extender提供便利。我们可以在开发控件时设置这个属性为某个脚本文件的相对路径,这样页面将会加载这个脚本文件而不是使用程序集中的资源,由此避免了多余的编译。

那么有了ScriptPath属性生活就真的变得美好了呢?那还得看情况。在急于作结论之前还是先看看下面的代码吧。假设我们正在开发AutoCompleteExtender,这是我们正在使用的测试页面。

<asp:TextBox runat="server" ID="myTextBox" Width="300" />

<ajaxToolkit:AutoCompleteExtender runat="server" ID="autoComplete1" 
    TargetControlID="myTextBox" ... />
    
<br />
<asp:Literal ID="Literal1" runat="server"></asp:Literal>

大部分的属性被我省略了,因为我们只关心哪些脚本文件会被发送到客户端,所以我们使用下面的代码在页面上写下一系列资源标识。

protected void Page_Load(object sender, EventArgs e)
{
    List<string> identifiers = new List<string>();

    IEnumerable<ScriptReference> scriptReferences = 
        (this.autoComplete1 as IExtenderControl).GetScriptReferences();

    foreach (ScriptReference reference in scriptReferences)
    {
        string value = String.IsNullOrEmpty(reference.Assembly) ? 
            reference.Path + " (External)" : reference.Name + " (Assembly)";

        if (!identifiers.Contains(value))
        {
            identifiers.Add(value);
        }
    }

    StringBuilder sb = new StringBuilder();
    foreach (string refer in identifiers)
    {
        sb.AppendLine(refer + "<br />");
    }

    this.Literal1.Text = sb.ToString();
}

在浏览器中打开页面,我们来看一下页面上显示了什么。

  1. AjaxControlToolkit.Compat.Timer.Timer.js (Assembly)
  2. AjaxControlToolkit.Common.Common.js (Assembly)
  3. AjaxControlToolkit.Animation.Animations.js (Assembly)
  4. AjaxControlToolkit.ExtenderBase.BaseScripts.js (Assembly)
  5. AjaxControlToolkit.Animation.AnimationBehavior.js (Assembly)
  6. AjaxControlToolkit.PopupExtender.PopupBehavior.js (Assembly)
  7. AjaxControlToolkit.AutoComplete.AutoCompleteBehavior.js (Assembly)

这是页面使用AutoCompeteExtender所需资源的一个有序列表,请注意现在AutoCompleteExtender的ScriptPath为空。那么如果我们把它按照如下设置又会如何呢?

<ajaxToolkit:AutoCompleteExtender runat="server" ID="autoComplete1" 
    TargetControlID="myTextBox" ScriptPath="AutoCompleteBehavior.js" ... />

刷新页面之后您就会发现……

  1. AjaxControlToolkit.ExtenderBase.BaseScripts.js (Assembly)
  2. AutoCompleteBehavior.js (External)

嗨,我知道上一个列表中最后那个资源需要被外部文件所替换,那么其它哪些资源到哪里去了?很显然,现在结果使我们不得不自己手动地添加那些引用。ScriptPath的这个拙劣设计几乎使它成为了ExtenderControlBase中最没有用的属性了。

为什么会这样?

我们现在知道,一旦设置了ScriptPath属性之后有些资源引用就会消失,这是什么原因?以下就是我的简单说明:

AjaxControlTookit中所有的Extender将会和一系列的资源进行绑定。当一个Extender被放置在页面中时,所有的相关资源将会被发送到客户端。这些相关资源分为两种:“功能资源”和“辅助资源”(您不会在任何官方资料中找到这两个概念,因为这是我为了说明问题而自行提出的)。“功能资源”是那些直接用于实现组件功能的资源,将会使用ClientScriptResourceAttribute自定义属性在类上进行标记。“辅助资源”则是指那些可复用的,用于辅助实现组件功能的资源,它们将会使用RequiredScriptAttribute自定义属性标记在类上。RequiredScriptAttribute自定义属性会接受一个类型对象作为参数,这样这个类型上所有的相关资源将会被作为另一个控件的“辅助资源”。

这里还是让我们来看一下AutoCompleteExtender的定义:

[RequiredScript(typeof(CommonToolkitScripts))]
[RequiredScript(typeof(PopupExtender))]
[RequiredScript(typeof(TimerScript))]
[RequiredScript(typeof(AnimationExtender))]
[ClientScriptResource("AjaxControlToolkit.AutoCompleteBehavior",
    "AjaxControlToolkit.AutoComplete.AutoCompleteBehavior.js")]
public class AutoCompleteExtender : AnimationExtenderControlBase
{
    ...
}

通过上面的示例我们可以得出这样的结论:“AjaxControlToolkit.AutoComplete.AutoCompleteBehavior.js”是AutoCompleteExtender唯一的“功能资源”,而CommonToolkitScripts, PopupExtener, TimerScript, AnimationExtender这些类的所有相关资源都是AutoCompleteExtender的“辅助资源”。

几乎所有的组件都能被继承,所以我们还必须能够意识到这样一件事情:所有基类的相关资源也将会成为子类的相关资源。例如,从ExtenderControlBase类的定义中可以发现它拥有一个“功能资源”:

[ClientScriptResource(null, Constants.BaseScriptResourceName)]
public abstract class ExtenderControlBase : ExtenderControl, IControlResolver
{
    ...
}

由此我们很容易推断出,AjaxControlToolkit中所有的Extender组件都会把“AjaxControlToolkit.ExtenderBase.BaseScripts.js”作为自己的相关资源。

那么,如果我们设定了ScriptPath属性将会发生什么事情呢?最终的结果将会是,所有的与当前组件绑定的资源将会被忽略,也就是说只有ScirptPath指定的外部文件和绑定在基类上的资源会被发送到客户端。正是这点才让ScriptPath属性的境遇不甚理想。

我们该怎么做呢?

AjaxControlToolkit是一个开源的项目,因此我们可以将ScriptPath属性的相关实现修改为合理的状况。但是我更喜欢让它的开发团队来处理这件事情,因为在本地代码和官方发布的最新版本之间作同步总是一件让我感到头疼的事情。所以我在开发Extender时,会使用以下这种简单的做法。我们还是使用AutoCompleteExtender作为示例:

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);

    ScriptManager.GetCurrent(this.Page).ResolveScriptReference += 
        new EventHandler<ScriptReferenceEventArgs>(OnResolveScriptReference);
}

private static void OnResolveScriptReference(object sender, ScriptReferenceEventArgs e)
{
    ScriptReference script = e.Script;
    if (script.Name == "AjaxControlToolkit.AutoComplete.AutoCompleteBehavior.js")
    {
        script.Assembly = "";
        script.Name = "";
        script.Path = "AutoCompleteBehavior.js";
    }
}

在上面的代码片段中,我们响应了ScriptManager的ResolveScriptReference事件,这样我们就可以修改某个特定的ScriptReference对象使它指向一个外部脚本文件了。这正是我们需要的效果。

 

点击此处查看此文英文版本

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

16 条回复

  1. asboy
    *.*.*.*
    链接

    asboy 2007-06-25 21:39:00

    是不是说 路径设置麻烦 郁闷
    ajaxpro 它会在页面产生这些
    <script type="text/javascript" src="/ajaxpro/prototype.ashx"></script>
    <script type="text/javascript" src="/ajaxpro/core.ashx"></script>
    好象没碰到什么问题 也是js文件做资源吧

  2. 多拉A梦[未注册用户]
    *.*.*.*
    链接

    多拉A梦[未注册用户] 2007-06-25 21:41:00

    博主已经同时用中英文写文章了
    呵呵,看来是在实现理想ing。。。

  3. 老赵
    admin
    链接

    老赵 2007-06-25 22:00:00

    @asboy
    您没有明白我的意思。:)

  4. 老赵
    admin
    链接

    老赵 2007-06-25 22:00:00

    @多拉A梦
    定期还是写点英文比较好。:)

  5. Anthan
    *.*.*.*
    链接

    Anthan 2007-06-25 22:06:00

    是不是他设计之初就没想着让大家去扩展他,留下一个鸡肋般的东西好让大家又没什么话说,呵呵,不过确实没开发过AjaxControlTookit,等真正用到的时候希望能够改进啊。

  6. 老赵
    admin
    链接

    老赵 2007-06-25 22:09:00

    @Anthan
    有一个很有趣的印象,就是我记得以前是正确的阿……莫非我之前没有用RequiredScriptAttribute?

  7. asboy
    *.*.*.*
    链接

    asboy 2007-06-25 22:09:00

    晕 AjaxControlToolkit 没用过呵呵
    刚才没详细看看

  8. 老赵
    admin
    链接

    老赵 2007-06-25 22:12:00

    @asboy
    :)

  9. Clingingboy
    *.*.*.*
    链接

    Clingingboy 2007-06-25 23:44:00

    恩,遇到过这个问题,当时我也这么想,一遍遍的编译.外部资源的话则有依赖性.不过可以用firefox,不一定套死在ie上的.

    说到底,所有的逻辑都在脚本里.后台只是为了方便设置几个属性和事件而已.关键还是js

  10. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2007-06-25 23:45:00

    ScriptPath的问题应该在于它设计时没有考虑继承吧?也就是说,没办法指定ScriptPath对于最后哪几层继承有效。很有可能一个控件0~3层都是第三方编译好的,而4~5层是你自己写的,每一层都有自己的ClientScriptResource,那么如何能“方便调试”呢?想通过一个属性解决问题,这不可能吧。

  11. Clingingboy
    *.*.*.*
    链接

    Clingingboy 2007-06-25 23:48:00

    @Cat Chen
    那也可以继承后再套用元数据吧,方便是方便了,调试又不方便了

  12. 老赵
    admin
    链接

    老赵 2007-06-26 01:08:00

    @Clingingboy
    嗯?这个和IE还是FireFox有什么关系呢?

  13. 老赵
    admin
    链接

    老赵 2007-06-26 01:09:00

    @Cat Chen
    不是,父类所有的资源都加载了,而自己的资源如果设了ScriptPath就不会被加载了。我觉得其实应该仅仅是ClientScriptResourceAttribute标记的东西不会被加载。

  14. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2007-06-26 10:11:00

    @Jeffrey Zhao
    我的意思是,这样涉及还是不方便啊。例如4~5层都是我设计的,我希望同时改变4~5层的ScriptPath就做不到了吧?

  15. 老赵
    admin
    链接

    老赵 2007-06-26 10:45:00

    @Cat Chen
    这倒也是……

  16. 分类信息网[未注册用户]
    *.*.*.*
    链接

    分类信息网[未注册用户] 2007-06-26 15:15:00

    您在为AjaxControlTookit开发Extender控件时使用ScriptPath属性吗?ScriptPath属性的拙劣设计几乎使它成为了ExtenderControlBase类中最没有用的属性了。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我