ASP.NET 中的自定义脚本回调

来源:互联网 发布:材料成本核算软件 编辑:程序博客网 时间:2024/04/28 07:49

Dino Esposito

下载本文中的代码: CuttingEdge0501.exe (161KB)

*
本页内容
受 GridView 控件的启发 受 GridView 控件的启发 控件脚本回调基本知识 控件脚本回调基本知识 CallbackValidator 控件 CallbackValidator 控件 该控件如何工作 该控件如何工作 构建控件 构建控件 解决状态问题 解决状态问题 JavaScript 文件作为嵌入式资源 JavaScript 文件作为嵌入式资源 小结 小结

ASP.NET 客户端回调代表着一种简洁而绝佳的方法,它可以在不发布和刷新当前页的情况下执行服务器端代码。我在 2004 年 8 月和 12 月的 Cutting Edge 专栏中讨论了 ASP.NET 回调,当时是从对服务器进行后台回调、向相关页发送输入数据以及接收响应的呈现页的角度对它们进行了讨论。然后,响应字符串由合适的客户端进行处理,并且通常通过动态 HTML (DHTML) 对象模型和嵌入到页面中的回调 JavaScript 函数来操作呈现的页面内容。

尽管回调的这种用法已经让人非常激动了,但它们还可以执行更多的任务。脚本回调机制也可以为服务器控件添加高级功能。通过实现几个接口,任何自定义控件都会被赋予脚本回调功能,以便使用后台往返来收集服务器数据以及更新用户界面 — 这就是本月我要讲述的主题。

受 GridView 控件的启发


如果您读过我最近写的一篇功能文章 ASP.NET 2.0 GridView,您就会了解 GridView 控件无需刷新整个页面就可以显示新的记录页。实际上,GridView 控件提供了一个基于 ASP.NET 脚本回调进行分页和排序的高级引擎。新页面的数据是在后台下载的,用户看不到。在数据到达客户端之后,这些数据将立即由 JavaScript 函数收集,并用于更新当前视图。

分页和排序回调并不是 100% 的客户端回调解决方案(如果您需要一个纯粹的客户端实现,请参阅 2004 年 2 月 Jeff Prosise 在 Wicked Code 专栏中发表的文章)。GridView 的分页和排序回调是按需工作的,它只下载需要的数据,而不会将整个数据源都下载到客户端上。您仍然要付出一个往返的代价,但是能够保证得到最新的数据,即使这些数据最近已经在服务器上更新过。

自从发现 ASP.NET 控件可以支持脚本回调功能之后,我感到非常兴奋,同时也促使我赶紧找出构建自己的脚本回调的方法。

顺便提一句,GridView 并不是唯一一个支持类似功能的 ASP.NET 2.0 控件。其他视图控件(如 TreeView、DetailsView 和 FormView)也能以其他方式提供相同的功能。作为使用具有回调功能控件的开发人员,您不需要处理服务器端代码,也不用担心编写以及在宿主页中嵌入 JavaScript 代码的问题。该控件可以完成一切操作,它展示了一个直观的编程模型,您可以通过该模型控制脚本回调机制。

返回页首返回页首

控件脚本回调基本知识


ASP.NET 脚本回调机制由两个关键元素组成:响应用户操作的服务器端代码,以及客户端上处理服务器端事件所生成结果的 JavaScript 回调代码。在页面回调自身的情况下,正如我在前面提到的文章中所述的那样,您可以在执行对用户不可见的回发的页面按钮中附加一些 ASP.NET 生成的脚本代码。因为该请求的目标是当前页,所以该页会发布到自身,这与它在一个普通回发事件中的行为方式相似,只是页面的生命周期缩短了。该页必须实现 ICallbackEventHandler 接口,以便可以调用一个具有预定义签名的方法,来为客户端生成结果。

那么,当控件触发带外调用时,该方案又有什么不同呢?在这种情况下,“不可见”回发的目标 URL 是承载该调用方控件的页面的 URL。该控件必须实现 ICallbackEventHandler 才能提供为客户端生成某些结果的方法。同样,该控件负责在承载页中插入处理结果和刷新该页所需的任何 JavaScript 代码。

具有回调功能的控件只是一个实现 ICallbackContainer 和 ICallbackEventHandler 接口的控件,两个接口都各有一个方法。ICallbackContainer 接口具有的方法可以返回触发远程调用的脚本代码;ICallbackEventHandler 接口则提供了在调用期间执行的服务器端代码。ICallbackEventHandler 也是一个具有回调功能的页面必须实现的接口。一个实现回调接口的自定义控件示例的声明如下面的代码所示:

public class CallbackValidator : WebControl,     INamingContainer, ICallbackContainer, ICallbackEventHandler

在 ICallbackContainer 接口的实现中,您可能需要放入一个对该页 GetCallbackEventReference 方法的调用,以获得一个可启动服务器事件的正确 JavaScript 调用。稍后我再讲述这些内容。

返回页首返回页首

CallbackValidator 控件


为了解具有回调功能的服务器控件,我们来看一个具有 ASP.NET 脚本回调功能的自定义验证器控件示例。在 ASP.NET 中,验证控件用于检查并验证网页中定义的窗体域的输入。验证器是一个服务器控件,它是从 BaseValidator 类继承的,而该类又是从 Label 继承的。

每个验证控件都引用一个位于该页其他位置的输入控件。当页面要提交时,任何受监视服务器控件的内容都会传递到该验证器,以进行进一步处理。每个验证器都执行一种不同类型的验证。例如,CompareValidator 控件使用比较运算符(如小于、等于或大于)将用户的输入与一个固定值进行比较。RangeValidator 确保用户输入位于某个指定范围内,而 RegularExpressionValidator 只在匹配某个常规表达式定义的模式时才验证用户输入。

通常,验证都在服务器上发生。然而 ASP.NET 还为大多数验证控件提供了一个完整的客户端实现,并允许用户为其余验证控件编写自定义客户端脚本。这就使得具有 DHTML 功能的浏览器(如 Microsoft?Internet Explorer 4.0 和更高版本)在用户点击或单击受监视输入域之外的位置后,能够立即在客户端上执行验证。在很多情况下,客户端验证足够强大,可以检测出许多重大错误并通知用户。例如,RequiredFieldValidator 控件可验证给定域不能保留为空。无需回发到服务器即可验证当前值。

如果客户端验证打开,则在所有输入域均包含有效数据之前,该页不会回发。为了运行安全代码,以及防止恶意和秘密的攻击,您还是应该在服务器上验证数据;服务器端验证始终由验证器控件执行,即使同时要执行客户端验证也是如此。另外,并非所有类型的验证都能在客户端上完成。实际上,如果您需要针对数据库进行验证,则没有别的选择,只能回发到服务器。而这也正是发生问题的地方。

常规回发涉及整个页面。上载整个视图状态,处理整个页面,生成、下载和呈现同样的大型响应。如果您能够向服务器发出经过优化的带外请求,并只检查验证之下的控件的状态,那岂不是很好?

在 ASP.NET 中,没有这样的控件。那么我们就来编写一个这样的控件吧,我将其命名为 CallbackValidator。CallbackValidator 是一个自定义 ASP.NET 2.0 控件,我构建这个控件的目的是为了演示控件可以如何实现对承载页的带外调用,以及如何在服务器上自行处理事件。

在我开始着手此项目时,实际上并没有如此雄心勃勃的目标:我原先的目标只是修改 CustomValidator 标准控件。对于该记录,CustomValidator 控件采用了以编程方式定义的验证逻辑来检查用户输入的有效性。如果预先不知道要检查的值,则应该使用此方法。CallbackValidator 控件的最初意图是提供一种方法,以便在不回发整个页面的情况下执行服务器端验证。我意识到无需太多的额外努力,就可以拥有一个类似于自定义按钮的控件,这个控件可以在不回发整个页面的情况下在服务器上对许多输入域进行验证,而此时我的修改工作已经完成了一半。这个行为就是 CallbackValidator 控件的全部。

在我深入讲述该控件的精髓之前,我们先来看一下图 1。该页面上的 Submit 按钮只会按照普通的方式将所有值发布到服务器上。实际上,这些值将在客户端上进行处理,如果所有这些值都需要传递,那么该控件就会将其传递到服务器上,在该服务器上,所有控件输入都将使用服务器端验证代码(如果有的话)进行验证。Validate 按钮会触发一个对 Web 服务器的带外调用,并只验证指定的输入控件。在它返回时,您就会知道哪些值已经通过了服务器的验证。例如,在图 1 中,您将在尝试提交其余数据之前了解到是否已经采用了该用户 ID。


图 1 带有具有回调功能验证的输入窗体


图 2 显示了该页面的源代码。正如您可以看到的那样,它包含了一个 HTML 服务器窗体、一些文本框(每个文本框都绑定到一个标准的验证控件)以及该自定义 CallbackValidator 控件的一个实例。此控件实际上负责创建并显示 Validate 按钮。

返回页首返回页首

该控件如何工作


该 CallbackValidator 控件从 WebControl 继承,并实现了 INamingContainer 接口。另外,它还实现了 ICallbackContainer 和 ICallbackEventHandler 接口,以便获得回调支持。

ICallbackContainer 接口需要方法 GetCallbackScript 按照下列方式声明:

string GetCallbackScript(IButtonControl buttonControl, string argument)

GetCallbackScript 采用两个参数。第一个是对预期要触发回调的页面控件的引用。第二个参数(字符串)表示调用方希望传递给方法以帮助构建输出的任何上下文。从名称可以看出,GetCallbackScript 方法使用 JavaScript 函数调用来准备和返回字符串,以便附加到指定的按钮控件来触发远程调用。

该按钮控件参数使您能够精确地指定要对控件 UI 中的哪个按钮进行 JavaScript 调用。该示例 CallbackValidator 控件只有一个可单击按钮;而 GridView 控件则具有很多可单击按钮,每个按钮都用于页导航或标头中的一个链接按钮。在 ASP.NET 2.0 中,所有充当窗体中按钮角色的控件都需要实现一个新的接口 — IButtonControl。该接口在图 3 中进行了详细说明,它是由下列 Web 控件实现的:Button、LinkButton 和 ImageButton。HTML 按钮控件不能实现该接口。请注意,在 Microsoft .NET Framework 1.x 中,IButtonControl 接口仅对于 Windows?Forms 按钮控件存在(尽管成员集合完全不同)。

具有回调功能的控件所需的第二个接口是 ICallbackEventHandler — 在支持脚本回调的页面上也需要这个接口。该接口由一个方法组成:

string RaiseCallbackEvent(string eventArgument)

该方法以字符串的形式接收输入值,执行一些服务器端的工作,再以字符串的形式返回响应。此处非常重要的一点是,输入和输出数据都可以打包为字符串进行传递;而该字符串真正的内容和格式则由编码人员决定。

在我讨论 CallbackValidator 控件的实现之前,让我们先来看一下图 4,该图说明了该控件如何适应页面中处理 ASPX 资源请求的 HTTP 处理程序。CallbackValidator 控件看起来就像一个附加了某些脚本代码的按钮。该脚本代码就是它的 GetCallbackScript 方法所返回的内容。单击该按钮 (Validate) 时,它会激发后台回发以发送视图状态、当前输入值,以及两个名为 CALLBACKPARAM 和 CALLBACKID 的自定义字符串。前者包含了 RaiseCallbackEvent 在 GetCallbackScript 方法的正文中创建时的输入值,CALLBACKID 则负责识别处理服务器事件的服务器端对象。从 ASP.NET 运行库提取请求的页面 HTTP 处理程序一旦位于服务器之后,它就会尝试查找具有该 ID 并实现 ICallbackEventHandler 的控件。如果成功,则会调用该控件的 RaiseCallbackEvent 方法,并将其输出返回到客户端。如果 CALLBACKID 的目标为该页,该 HTTP 处理程序就会查看该页是否实现该接口,然后按照通常方式继续。

返回页首返回页首

构建控件


CallbackValidator 是一个合成控件,其用户界面由一个简单的按钮组成。通过添加一些属性来设置该按钮的样式(链接、按、图像或其他什么内容),您可以轻松地扩展该控件的这方面内容。要在按钮上显示的文本由 ButtonText 属性设置。一个名为 ControlsToValidate 的集合属性会收集回调期间要在服务器上测试的所有页验证器的 ID。该属性被实现为 StringCollection 类型,它在开始时是空的。图 5 中的代码只允许您在运行时添加控件 ID,而通常情况下,这是在 Page_Load 事件中完成的:

void Page_Load(object sender, EventArgs e) {   CallbackValidator1.ControlsToValidate.Add("valUserId");   CallbackValidator1.ControlsToValidate.Add("valEmail");}

请注意,该集合类不会保持它的视图状态内容。因此,在传入请求时,您必须总是重新对其进行初始化。另外还要注意,您应该在该集合中添加验证控件而不是输入控件。在远程调用过程中,CallbackValidator 控件会针对关联的控件调用 Validate 方法,并存储客户端回调的响应。CallbackValidator 控件与现有的验证器协同工作,使得一个带外调用就能够对它们全部进行测试;它实际上并不是一种新型的验证器。

正如您在图 5中看到的那样,CallbackValidator 控件会创建一个 Button 控件,并向其 OnClientClick 属性附加一些代码。OnClientClick 是 ASP.NET 2.0 中引入的一个新属性,用于向 HTML onclick 事件添加 JavaScript 调用。在 ASP.NET 2.0 中,下面两行代码的作用完全相同:

button.Attributes["onclick"] = js; // ASP.NET 1.x; still works in 2.0button.OnClientClick = js; // ASP.NET 2.0

与该验证按钮相关联的代码是通过一个包装在 ICallbackContainer 接口中的特定方法获取的。请注意,截止到 Beta 1,ICallbackContainer 接口的使用并不是必须的,但是使用它确实有助于保持代码的简洁。在上个月的示例中,我没有使用该接口,甚至在讨论脚本回调的 MSDN Beta 1 文档中根本没有提到该接口。尽管如此,从脚本回调获益的 ASP.NET 控件(大多为 GridView)都会实现该接口。使用 ICallbackContainer 接口的唯一组件就是该控件本身,这就意味着您可以轻松编写一个具有回调功能但不使用该接口的控件。

GetCallbackScript 返回的 JavaScript 函数调用来自以下语句:

Page.GetCallbackEventReference(    this, args, "CallbackValidator_UpdateUI", "null"));

第一个参数 (this) 表示当前控件,它在发出的请求中设置 CALLBACKID 域。第二个参数 (args) 为服务器端 RaiseCallbackEvent 方法的输入字符串。第三个参数为 JavaScript 回调函数的名称,用于处理 RaiseCallbackEvent 方法的输出。最后,第四个参数在这里为空,它表示一个作为回调函数上下文传递的 JavaScript 对象。

CallbackValidator 控件必须确保 JavaScript 回调是在承载页中定义的,并且必须决定该 args 参数的格式和内容。RaiseCallbackEvent 实现只需要来自其客户端调用方的一种类型的信息:要测试的验证器列表。下面是将所有验证器 ID 串联到一个由竖杠分隔的字符串中的代码:

int i = 0;StringBuilder sb = new StringBuilder("");foreach (string s in ControlsToValidate) {   if (i>0) sb.Append("|");   sb.Append(s);   i++;}string args = String.Format("'{0}'", sb.ToString());

绑定到图 2 中的 Validate 按钮的 JavaScript 代码示例可能与以下代码类似:

WebForm_DoCallback(    'ctl00$PageBody$CallbackValidator1',    'valUserId|valEmail',    CallbackValidator_UpdateUI,    null,    null);

请注意,CallbackValidator 控件的 ID 是由服务器生成的,以便可以唯一标识页面上的每个控件。

返回页首返回页首

解决状态问题


正如图 4 中显示的那样,回发的请求中包含一些输入域。除了前面提到的 CALLBACKID 和 CALLBACKPARAM 域之外,该请求还包含了其他一些输入域。更准确地说,它包含了窗体中的所有输入域,以及两个特定于该回调操作的输入域。换句话说,视图状态是与这些输入域(文本框、下拉列表等)的当前值一起回发的。

在检查请求的回调状态并找出哪个对象(Page 还是控件)要调用 RaiseCallbackEvent 之前,HTTP 页面处理程序会还原视图状态和发布的值。根据这点,您可能会猜想 RaiseCallbackEvent 方法中定义的代码将以一种一致和更新的状态执行。特别是,您可能会猜想 CallbackValidator 控件的 RaiseCallbackEvent 方法所调用的所有验证器都将测试它们所绑定的输入控件的当前值。但是,从本部分的标题中可以看出,实际上并不是这样。请看一下图 6 中的 RaiseCallbackEvent 代码。

该方法会检索验证器控件,并调用它的 Validate 方法。该方法执行其任务(具体取决于验证器的类型)并将 IsValid 属性设置为 True(有效)或 False(无效)。接下来,RaiseCallbackEvent 方法会构建它对客户端页面的响应。返回字符串是一个以竖杠分隔的字符串集合,每个字符串的格式如下:

controlID:valid (0/1):message:tooltip

第一个标记为该控件的客户端 ID,它是由于 Master Pages 和命名容器而考虑创建的完全限定 ID。第二个标记为 0 或 1,具体取决于 IsValid 的值。第三个标记为验证器在一个完整回发后要显示的消息。这与验证器的 Text(默认)或 ErrorMessage 属性相对应。如果这两个属性都为空,我还强制了一个 * 字符串。按照设计,Text 预期会包含一些只将域标记为无效的文本,而 ErrorMessage 则提供更为详细的错误说明。如果 CallbackValidator 的 ShowDetailedInfo 属性为 true,我就会使用 ErrorMessage 字符串作为一个工具提示,如图 7 所示。


图 7 验证错误消息


那么与状态相关的讨论在哪里呢?这个机制纸上谈兵还行,但是涉及到真正的值就不行了。进行调试时,我意识到验证测试的结果完全不可靠。例如,User ID 文本框被设计为接受除“Dino”和空字符串之外的任何内容。但是,它只对于常规回调运行正常,如果使用回调验证时就不行了。有些位置设置得很好的断点则显示所有文本框都保持了它们的原始值,而忽略了在尝试验证之前键入的内容。这个问题与视图状态无关,而是发布值的问题。页机制像平时一样运行正常;只是不能接收来自客户端且我认为正确的值。

如果您滚动查看使用 ASP.NET 回调的页的 HTML 源代码,就会发现在加载页时会调用一个名为 WebForm_InitCallback 的 JavaScript 函数。此函数是 ASP.NET 2.0 基础架构的一部分,它是通过 WebResource.axd 系统处理程序插入页面的。看一下此函数的源代码是否运行良好。(有关如何获得该代码的详细信息,请参阅上个月的专栏。)基本上,WebForm_InitCallback 在加载页时构建 POST 请求的正文。该页的正文是一个字符串(其名称为 __theFormPostData),其中填满了视图状态的内容以及窗体中的所有输入域。该代码是正确的;但是执行的时间不是我所预期的!这些输入域的内容是在加载时收集的,但是在进行回发时没有使用用户提供的值进行更新。这就是服务器状态看起来不正确的原因。为了解决此问题,我只是在开始带外调用之前重复了对 WebForm_InitCallback 的调用(请参见图 6)。请注意,这实际上是预期的行为,而不是什么错误。针对系统在带外调用之前调用 WebForm_InitCallback 的讨论是面向这样的情况的 — 即,用户希望在最初从服务器下发的数据上下文中执行回调。

返回页首返回页首

JavaScript 文件作为嵌入式资源


在该专栏的最后,我来讨论一个非常好用的技术,它大大简化了 ASP.NET 2.0 自定义控件中的 JavaScript 代码插入。该技术在 ASP.NET 1.x 中也能使用。其理念很简单:在常规 JS 文件中写入您的 JavaScript 代码,并将其作为嵌入式资源添加到项目中。(在 Visual Studio?.NET 属性窗口中设置 Build Action 属性)。另外,还要为资源名称添加该组件的命名空间作为前缀。接下来,当您需要在代码中使用该脚本时,请执行 EmbedScriptCode 方法在图 6中所作的操作。您使用一点技巧,就可以在代码维护和可读性方面收获很多。

返回页首返回页首

小结


ASP.NET 脚本回调是一个功能非常强大的功能,可以节省每个页更新的时间。尽管发出的是服务器请求,但是整个页在浏览器窗口中保持不变。其优点是双重的。首先,用户以为无需回发,会继续读取或使用该页。第二,只有页的一些部分会刷新(使用 HTML 对象模型)。在 2004 年 8 月和 12 月专栏中,我深入讲述了该基本功能的内容。本月我讲述了如何在自定义控件中实现回调,以在所构建的控件中提供更大的灵活性。