代码改变世界

ScriptPath属性的拙劣设计

2007-06-25 20:45 by Jeffrey Zhao, ... 阅读, ... 评论, 收藏, 编辑

背景

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对象使它指向一个外部脚本文件了。这正是我们需要的效果。

 

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

使用Live Messenger联系我