ASP.NET 自定义控件 - 客户端脚本生成






4.67/5 (42投票s)
2003年4月8日
9分钟阅读

236132

3907
创建能够封装客户端脚本的控件,这些控件易于重用,并能在不产生频繁服务器调用的开销的情况下提供动态行为。
引言
ASP.NET 新的状态管理和回发(postback)功能确实非常令人兴奋。它们为开发人员提供了生产动态网页的全新机制。编写自己的自定义控件的能力将这种能力提升到了一个新的水平,允许你编写具有自定义功能的控件,通过简单地定义自定义标签,类似于任何其他 HTML 元素,就可以在多个页面中轻松重用。页面布局的实现者不再需要了解编写客户端代码以获得流行起来的动态行为的所有细节。然而,开发人员需要注意一些陷阱。ASP.NET 倾向于以服务器为中心的(server-heavy)设计。网络流量会急剧增加,因为每个客户端事件都可能导致一次与服务器的往返。这些频繁的服务器往返所产生的许多效果,可以通过几个简单的 JavaScript 函数轻松实现。与服务器的调用应尽量少,尽可能多地在客户端完成。通过使用自定义控件生成客户端脚本,我们可以利用客户端的动态 HTML(Dynamic HTML),同时仍然在布局和逻辑之间提供一定程度的分离。
客户端脚本生成
从自定义控件生成脚本的目标之一是允许开发人员创建控件并指定其行为,然后发布供他人使用,而无需了解代码的工作原理。我们希望封装控件的实现,并将 HTML 渲染与与之配合工作的脚本紧密耦合,以减少与传统 Web 组件重用方法(例如复制粘贴和包含文件)相关的潜在故障点。最直接的脚本生成方法是在控件的 `Render` 方法中编写脚本以及控件本身(请参见下面的代码)。
namespace Spotu
{
public class HelloWorld : Control
{
protected override void Render (
HtmlTextWriter writer
)
{
writer.Write(@"
<script>
function HelloWorld()
{
document.all('_msg').innerText = 'Hello World';
}
</script>");
writer.Write("<button onclick='javascript:HelloWorld()'>"
+ "Click Me</button>");
writer.Write("<div id=_msg></div>");
}
}
}
下面的代码块展示了一个使用 `HelloWorld` 类进行客户端脚本生成的页面示例。
<%@ Page language="c#" %>
<%@ Register Namespace='Spotu'
TagPrefix='spotu'
Assembly ='helloworld' %>
<html>
<body>
<form runat='server'>
<spotu:HelloWorld runat='server'/>
</form>
</body>
</html>
这种方法有效,并且解决了允许开发人员编写一个自定义控件,供他人将其用于页面以提供动态功能而无需回发到服务器的初始问题。然而,它并不优雅,并且存在一些缺点,最显著的是我们无法在页面中多次包含此控件,这样做会导致创建具有相同 ID 的多个 div。即使我们为本例中的元素唯一命名,它仍然效率低下,因为每次引用此控件时都会写入 JavaScript。这可能会产生大量开销,将相同的脚本为控件的每个实例传输到客户端。
我们需要一种方法来让控件生成脚本,但只生成一次,即使同一页面上使用了该控件的多个实例。幸运的是,微软的开发人员考虑到了这一点,并提供了一种注册脚本块的方法,以确保我们只写一次脚本的一部分,方法是使用 `Page.RegisterClientScriptBlock` 方法。此方法接受两个参数:一个标识脚本块的 ID,以便 `Page` 类知道忽略注册相同代码块的任何其他请求;以及一个包含要注册的脚本的字符串。注册脚本块的最佳位置是在控件的 `Init` 事件处理程序中。要利用此事件,请重写 `Control` 类的 `OnInit` 方法。考虑到这一点,`HelloWorld` 示例可以重写为如下所示:
using System;
using System.Web;
using System.Web.UI;
namespace Spotu
{
public class HelloWorld : Control
{
protected override void OnInit(EventArgs e)
{
string strCode = @"
<script>
function HelloWorld(id)
{
document.all(id).innerText = 'Hello World';
}
</script>";
Page.RegisterClientScriptBlock("Spotu_HelloWorld",
strCode);
}
protected override void Render(HtmlTextWriter writer)
{
writer.Write("<button onclick='javascript:HelloWorld(\""
+ this.UniqueID + "\")'>"
+ "Click Me</button>");
writer.Write("<div id='" + this.UniqueID
+ "'></div>");
}
}
}
这种方法好多了,但仍然存在问题。如果正在注册的脚本很长,或者在生成脚本时涉及大量计算、数据访问等,那么当控件创建这个巨大的脚本块时,我们仍然会因为脚本最终被丢弃(因为它已经被注册)而遭受性能损失。一旦脚本块被注册,我们就可以使用 `Page.IsClientScriptBlockRegistered` 方法来测试它。为了提高 `HelloWorld` 控件的性能,我们会在 `OnInit` 方法中包含调用,如下所示:
protected override void OnInit(EventArgs e)
{
if (!Page.IsClientScriptBlockRegistered("Spotu_HelloWorld"))
{
string strCode = @"
<script>
function HelloWorld(id)
{
document.all(id).innerText = 'Hello World';
}
</script>";
Page.RegisterClientScriptBlock("Spotu_HelloWorld",
strCode);
}
}
从自定义控件使用客户端脚本生成提供了一种干净的、封装好的方法来在 Web 页面中实现动态行为,同时还能保护页面设计者无需了解产生所需效果的细节。开发人员现在可以自由地专注于如何让控件完成您想要的工作,而不会被放在哪里,或者被营销人员催促移动控件、添加新控件或删除旧控件所困扰。通过将这种方法与设计器集成相结合,具有动态行为的控件可以轻松定制并在多个页面中重用,开发人员的交互很少或没有,并且没有服务器端包含或复制粘贴代码重用的陷阱。
缓存
有些人可能会问:我如何缓存此脚本,使其不必每次都下载?毕竟,客户端脚本往往相当静态,不必在每次加载网页时都下载。
有几种选项可以缓存控件的输出。ASP.NET 的方法是利用输出缓存。输出缓存有无数种选项,但大多数选项将设置缓存的责任放在了进行表示的人身上,他们通过使用 `.aspx` 页面中的指令和标志来实现。此外,缓存整个页面可能不是期望的效果。有些页面极其动态。在这种情况下,理想情况是只缓存控件,或控件的某个部分。ASP.NET 确实为此提供了一些支持,但这种支持主要保留给用户控件(`.ascx` 文件),这无法提供我们想要的重用性。
对于提供生成脚本的自定义控件,我们可能需要考虑使用外部脚本文件。正如我们已经注意到的,大多数脚本不经常更改,或者根本不更改,并且可以轻松地在客户端缓存。与其直接从自定义控件编写脚本,不如将其放在外部脚本文件中,然后只编写一个包含 `src="..."` 属性的 `
using System;
using System.Web;
using System.Web.UI;
using System.Collections.Specialized;
namespace Spotu
{
public class Calculator : Control, IPostBackDataHandler
{
const string sc_strStyleClass = "calcButton";
private string _strNumButton;
private string _strOpButton;
private string _strScriptSrc;
private string _strStyleHref;
private string _strSavedValue;
private int _intCalcValue = 0;
// Custom property for explicitly setting the location
// of the script file
public string ScriptSrc
{
get { return _strScriptSrc; }
set { _strScriptSrc = value; }
} // End ScriptSrc
// Custom property for explicitly setting the location
// of the stylesheet file
public string StyleSrc
{
get { return _strScriptSrc; }
set { _strScriptSrc = value; }
} // End StyleSrc
#region IPostBackDataHandler
// LoadPostData gets call when the 'save' button
// rendered by this control is clicked
public virtual bool LoadPostData (
string postDataKey,
NameValueCollection values
)
{
_strSavedValue = "Saved Value: "
+ values[UniqueID + "_display"];
return false;
} // end LoadPostData
// Needed to implement IPostBackDataHandler
public virtual void RaisePostDataChangedEvent()
{
} // End RaisePostDataChangedEvent
#endregion
// Loads the state of the control from the
// viewstate managed by .NET
protected override void LoadViewState (
object savedState
)
{
_strSavedValue = savedState as string;
} // End LoadViewState
// Saves the state of the control
protected override object SaveViewState()
{
return _strSavedValue;
} // End SaveViewState
// Init event handler, called to initialize any state
// in the object before the viewstate is restored.
protected override void OnInit (
EventArgs e
)
{
_strNumButton = string.Format("<button "
+ "onclick='javascript:g_{0}.EnterNumber(this.innerText);'"
+ " class='{1}'>", this.UniqueID, sc_strStyleClass);
_strOpButton = string.Format("<button "
+ "onclick='javascript:g_{0}.OnOperator(this.innerText);' "
+ "class='{1}'>", this.UniqueID, sc_strStyleClass);
if (_strScriptSrc == null)
{
_strScriptSrc = Context.Request.ApplicationPath
+ "/includes/calc.js";
}
if (_strStyleHref == null)
{
_strStyleHref = Context.Request.ApplicationPath
+ "/includes/calcStyle.css";
}
string strScriptBlock = "<script src='"
+ _strScriptSrc
+ "'></script>";
Page.RegisterClientScriptBlock("Spotu_Calculator",
strScriptBlock);
string strStyle = "<link rel='stylesheet' "
+ "type='text/css' href='"
+ _strStyleHref
+ "'></link>";
Page.RegisterClientScriptBlock("Spotu_Calculator_Style",
strStyle);
} // End OnInit
// Load Event Handler. Retrieve the value posted in the
// display field of the calculator so we can keep the
// state of the display regardless of how the form is
// submitted
protected override void OnLoad (
EventArgs e
)
{
if (Page.IsPostBack)
{
_intCalcValue =
Int32.Parse(Context.Request.Form[UniqueID
+ "_display"]);
}
} // End OnLoad
// Render out the control
protected override void Render (
HtmlTextWriter writer
)
{
string strHtml = string.Format(@"
<script> var g_{0} = new Calc('{0}_display'); </script>
<table>
<tr colspan='*'>
<input type='text'
name='{0}_display'
readonly=true
value={4}>
</input>
</tr>
<tr><td>{1}7</button></td>
<td>{1}8</button></td>
<td>{1}9</button></td>
<td>{2}/</button></td>
<td>
<button
class='{3}'
onclick='javascript:g_{0}.OnClear();'>
C
</button>
</td>
</tr>
<tr><td>{1}4</button></td>
<td>{1}5</button></td>
<td>{1}6</button></td>
<td>{2}*</button></td>
</tr>
<tr><td>{1}1</button></td>
<td>{1}2</button></td>
<td>{1}3</button></td>
<td>{2}-</button></td>
</tr>
<tr><td>{1}0</button></td>
<td></td>
<td>{1}.</button></td>
<td>{2}+</button></td>
<td>
<button
class='{3}'
onclick='javascript:g_{0}.OnEqual();'>
=
</button>
</td>
</tr>
</table>", UniqueID,
_strNumButton,
_strOpButton,
sc_strStyleClass,
_intCalcValue);
writer.Write(strHtml);
writer.Write("<INPUT type='submit' name='"
+ this.UniqueID + "' value='Save'></INPUT>");
writer.Write("<H3 id='" + UniqueID + "_savedVal'>"
+ _strSavedValue + "</H3>");
} // End Render
}
}
calculator.aspx
<%@ Page %>
<%@ Register Namespace='Spotu'
TagPrefix='spotu'
Assembly ='calc' %>
<html>
<body>
<form runat='server'>
<spotu:Calculator runat='server'/>
<hr>
<spotu:Calculator runat='server'/>
</form>
</body>
</html>
计算器的 JavaScript 源文件
function Calc(dispId)
{
this.intCurrentVal = 0;
this.intLastNum = 0;
this._op = "";
this.bEqual = false;
this.displayId = dispId;
this.EnterNumber = function(num)
{
if (this.bEqual)
this.OnClear()
if (this.intLastNum != 0)
this.intLastNum += num;
else
this.intLastNum = num;
document.all(this.displayId).value = this.intLastNum;
}
this.ComputeValue = function()
{
switch (this._op)
{
case '+':
this.intCurrentVal = Number(this.intCurrentVal)
+ Number(this.intLastNum);
break;
case '-':
this.intCurrentVal -= this.intLastNum;
break;
case '*':
this.intCurrentVal *= this.intLastNum;
break;
case '/':
this.intCurrentVal /= this.intLastNum;
break;
default:
this.intCurrentVal = this.intLastNum;
}
document.all(this.displayId).value = this.intCurrentVal;
}
this.OnOperator = function(op)
{
if (!this.bEqual)
this.ComputeValue();
this.bEqual = false;
this.intLastNum = 0;
this._op = op;
}
this.OnEqual = function()
{
this.ComputeValue();
this.bEqual = true;
}
this.OnClear = function()
{
this._op = "";
this.intCurrentVal = 0;
this.intLastNum = 0;
this.bEqual = false;
document.all(this.displayId).value = this.intCurrentVal;
}
}
计算器按钮的样式表
.calcButton
{
width=25;
}
检查代码
需要注意的一点是,引用定义计算器按钮样式的样式表与脚本块注册一样,都位于 `OnInit` 方法中。注册客户端代码块不限于“脚本”本身。这里的样式表是外部的,允许设计者通过修改 `.css` 文件来修改按钮的外观和感觉。另一种允许页面设计者更改计算器外观和感觉的方法是实现自定义属性,或者更好的是,实现具有子属性的自定义属性将它们分组在一起(例如:Font-Style、Font-Size 等)。这种方法似乎有些局限性,因为设计者只能更改您公开的属性。有了样式表,设计者就可以获得标准 HTML 元素使用时可用的所有选项,而这些选项否则将不可用,因为他们无法直接访问自定义控件生成的 HTML 元素,也无法为其应用类或样式。
在控件渲染时会写入一个脚本块,而不是包含在 `.js` 文件中。这使得计算器控件的多个实例可以在同一页面上使用。从 `Control` 类继承的 `UniqueID` 属性用于区分控件。`UniqueID` 属性是一个唯一的标识符,用于标识页面中控件的实例。
样式表和外部脚本文件的位置默认在虚拟应用程序根目录下的 `/includes` 目录。但是,提供了两个自定义属性,允许设计者覆盖这些文件的位置。
通过使用控件的 `UniqueID` 作为提交按钮的名称,我们可以确保只有在单击该控件的“保存”按钮时,才会调用我们控件的 `LoadPostData` 方法。如果我们使用控件的 `UniqueID` 为文本框命名,那么我们将保存页面上所有控件的计算数字,而不管如何将数据提交到服务器。这个例子有点牵强,如果您确实想减少服务器负载,可以修改“保存”按钮,使其不将表单回发到服务器,而是执行 Web 服务调用。
结论
使用自定义控件生成客户端脚本可以带来巨大的好处。自定义控件的外观和行为将与其他用 ASP.NET 编写的控件类似,易于重用,并能保护页面设计者无需了解代码的工作原理。通过使用客户端脚本来创建动态行为,您可以极大地提高单个页面的响应速度以及网站的整体性能,方法是显著减少与服务器的调用次数。为脚本使用外部文件有利有弊。优点包括利用浏览器缓存和易于自定义。缺点包括部署更复杂,包括生产环境和设计时环境。
下载次数
下载并解压演示项目到虚拟应用程序的根目录。`calculator.aspx` 文件应位于虚拟应用程序的根目录中,`calc.js` 和 `caclStyle.css` 文件应位于虚拟应用程序下的 `/includes` 目录中,`calc.dll` 文件应位于虚拟应用程序下的 `/bin` 目录中。