Saturday, October 16, 2004

 

Working with Client-Side Script

1.
Working with Client-Side Script

By Scott Mitchell
From
http://msdn.microsoft.com/asp.net/using/building/web/default.aspx?pull=/library/en-us/dnaspp/html/clientsidescript.asp

Chinese version

Translated By hiyaolee
From http://blog.csdn.net/hiyaolee/archive/2004/10/14/136899.aspx

摘要:尽管 ASP.NET 在服务器上执行其大多数操作,但是某些操作在客户端进行处理可能会更好。Scott Mitchell 说明了 ASP.NET 页面和控件如何添加客户端代码。



本页内容
简介
创建基类作为添加客户端脚本的基础
从代码隐藏类添加客户端脚本
根据对用户操作的响应执行客户端代码
实现常用客户端功能
小结
相关书籍

简介
当使用动态的、基于 Web 的脚本技术时,与传统 ASP 或 PHP 类似,开发人员必须对客户端和服务器间的逻辑、暂时和物理分隔有着敏锐的理解。例如,对于触发服务器端代码执行的用户操作,使用传统 ASP 的开发人员必须明确地使用户的浏览器将请求返回到 Web 服务器。创建这样的交互可能会轻易地占用大量开发时间,并且导致不易读的代码。

Microsoft ASP.NET 通过使用 Web 窗体,有助于减轻将用户事件绑定到特定服务器端代码执行的负担,这就模糊了客户端和服务器间的界线。使用 ASP.NET 和最少的工作,开发人员就可以快速地创建如下的网页,它具有大量的交互式用户界面元素按钮、下拉列表等,而这些都基于最终用户的操作,可以选择性地运行服务器端代码。例如,利用 ASP.NET 添加下拉列表,只要选定的下拉列表项目更改则执行某些操作,您只需添加 DropDownList Web 控件、将其 AutoPostBack 属性设置为 True,然后为该下拉列表创建一个 SelectedIndexChanged 事件处理程序。如果利用传统的 ASP 完成上述任务,则会要求编写许多复杂的 HTML、客户端 JavaScript 和服务器端脚本代码;利用 ASP.NET,则为您提供了必要的脚本代码和服务器端事件模型。

尽管在执行客户端操作时,ASP.NET 中的 Web 窗体极大地简化了运行服务器端脚本,但是,如果误用这种功能可能会导致无法接受的性能。尽管 Web 窗体隐藏了所涉及的复杂性,每次需要执行服务器端代码时,最终用户的浏览器必须通过重新提交窗体,将请求返回到 Web 服务器。当提交窗体时,所有窗体字段(文本框、下拉列表和复选框等)必须同时返回它们的值。此外,页面的视图状态也被返回到 Web 服务器。总而言之,每次回发网页时,几千字节的数据将需要潜在地发送回 Web 服务器。于是,经常回发可能很快就会导致 Web 应用程序不可使用,尤其是对于那些仍然使用拨号连接的用户。通过将功能推到客户端可以降低经常回发的需要。

注 ASP.NET Web 窗体发出一个标题为 VIEWSTATE 的隐藏窗体字段,它包含 Web 窗体中 Web 控件已更改状态的基于 64 位编码的表示。根据出现的 Web 控件,视图状态的范围可以从几十字节到几万字节。要学习有关视图状态的更多知识,请查阅我的文章 Understanding ASP.NET View State。

http://msdn.microsoft.com/library/en-us/dnaspp/html/viewstate.asp

利用传统的 ASP,添加数据驱动、自定义客户端脚本非常简单,但并不是非常易读。例如,要在传统的 ASP 中显示根据某个 ID 字段加载 URL 的弹出窗口,您可以使用 语法来插入 ID 字段的值,在适当的客户端脚本中进行输入。ASP.NET 允许您利用 Page 类中的各种方法,创建这种数据驱动的客户端脚本。

本文分析了向 ASP.NET 网页添加客户端脚本的技术。客户端脚本是运行在访问者浏览器中的脚本代码,如其名字所示。我们将看到如何完成常见的客户端任务,例如显示警告、确认框和弹出窗口。(客户端脚本窗体字段验证的一个主要用途可能与 ASP.NET 主题有点不相关,因为验证程序 Web 控件提供了随取随用的客户端窗体验证。)本文的重点在于插入客户端脚本的服务器端类、方法和技术;我们不会详细地分析实际的客户端脚本,因为该信息涉及了围绕 Web 的众多其他文章和站点。

返回页首
创建基类作为添加客户端脚本的基础
传统的 ASP 和 ASP.NET 之间的主要差别之一在于各自技术的编程模型。ASP 页面是原子的、程序上的脚本,解释每个页面的访问。然而,ASP.NET 完全是面向对象的编程技术。所有 ASP.NET 网页都是带有属性、方法和事件的类。所有网页直接或间接地派生自 System.Web.UI 命名空间中的 Page 类,Page 类包含了 ASP.NET 网页的基本功能。

面向对象编程的概念之一就是继承。继承使您可以创建一个扩展其他类功能的新类。(如果类 B 继承类 A,也可以说扩展了 A;类 A 被称为基类。)当使用代码隐藏模型来创建 ASP.NET 网页时,可以非常清楚地看到代码隐藏类继承了 Page 类:

Public Class WebForm1
Inherits System.Web.UI.Page

...
End Class

通过使您的代码隐藏类继承 Page 类,它自动接收在 Page 类中继承的功能,例如 Request、Response、Session 和 ViewState 对象以及常见事件,如 Init、Load、Render 等等。我们将在本文中看到,如果您需要可用于所有 ASP.NET 网页的某个常见功能,一种方法是创建派生自 Page 类并具有完成这些所需增强功能的其他方法和属性的类。然后,要使 ASP.NET 网页利用这些增强功能,我们只需更新页面代码隐藏类中的 Inherits 语句,以使用扩展 Page 类的类。

在本文中,我们将创建一个名为 ClientSidePage 的类,它派生自 Page 类并提供额外的方法以协助执行常见的客户端任务。通过让代码隐藏类继承 ClientSidePage,而不是继承 Page,添加脚本代码将会像调用方法和传送几个参数那样简单。具体说来,该类包括用于下列用途的方法:

• 显示模式客户端对话框。

• 在页面加载时将焦点设置到特定的窗体字段。

• 使用模式确定对话框来确定用户是否希望回发该窗体。

• 显示弹出窗口。


在我们深入研究 ClientSidePage 类之前,首先让我们分析一下 Page 类中的有关方法,以便将客户端脚本插入到网页中。在我们讨论这些 Page 方法后,我们将开始利用 ClientSidePage 类扩展它们的功能,并且查看如何将所有内容结合在一起以及如何在 ASP.NET 网页中使用扩展的类。

返回页首
从代码隐藏类添加客户端脚本
所有 ASP.NET 网页必须直接或间接地派生自 System.Web.UI 命名空间中的 Page 类。Page 类包含正常运行的网页所要求的方法、属性和事件的基本集合。在该类的众多方法中,一些方法旨在将客户端脚本插入到生成的 HTML 中。这些方法从代码隐藏类调用,因此可以用于发出数据驱动的客户端脚本。用于发出客户端脚本的相关 Page 类方法如下所示。

该基类派生自 System.Web.UI.Page 类,因此可以通过从代码隐藏类直接调用 Page 类的公共方法来访问它们。

注 要访问 Page 类的方法,您可以直接键入方法名,或者通过输入 MyBase.(对于 Microsoft Visual Basic .NET)、this. (对于 C#)或者 Page.(对于 C# 或 Visual Basic .NET),利用 Microsoft Visual Studio .NET 中的 IntelliSense 来实现。如果使用 Visual Basic .NET 作为选择的编程语言,请确保将 Visual Studio .NET 配置为不 隐藏高级成员,否则将无法看到这些客户端脚本方法。(要显示高级成员,请转到 Tools | Options | TextEditor | Basic,然后清除 Hide advanced members 复选框。)

RegisterClientScriptBlock(key, script)
在 Web 窗体已呈现的 form> 元素之后,在包含于 Web 窗体中的任意 Web 控件之前,RegisterClientScriptBlock 方法会添加一个客户端脚本块。key 输入参数允许您指定与该脚本块相关联的唯一的密钥,而 script 参数包括要发出的完整的脚本代码。(这个 script 参数应该包括实际的 script> 元素,同时还包括客户端 JavaScript 或 Microsoft VBScript。)

在通过 ASP.NET 网页的代码隐藏类发出客户端脚本时,通常情况下,key 参数的值就不是非常重要了。简单地选择一个说明性的密钥值。在通过自定义编译的服务器控件插入客户端脚本代码时,key 参数就更加适用。编译的控件有可能需要一组客户端函数。一个页面上服务器控件的多个实例可以共享这些公用客户端脚本函数,因此对于整个页面而言,这些函数只需要发出一次即可,不需要每个控件实例发送一次。例如,验证控件利用客户端代码来增强用户体验。如果页面上存在任意验证控件,这种客户端代码就必须存在,但是如果存在多个验证控件,那么全部控件都可以使用这个单个的共享函数的集合。

通过为脚本块提供一个密钥,利用公用客户端函数集合构建控件的控件开发人员可以检查所要求的公用函数集合是否已经被页面上控件的另一个实例添加。如果已添加,它不需要重新添加公用脚本。要检查脚本块是否已经使用相同的密钥添加,请使用 IsClientScriptBlockRegistered(key) 方法,它将返回布尔值,表示带有相同密钥的脚本块是否已经进行了注册。需要注意的是可以在不首先检查它是否注册的情况下添加脚本块。如果尝试利用已经注册的密钥添加脚本块,添加的脚本块将被忽略,并且原来的脚本将保持分配到该密钥。

注IsClientScriptBlockRegistered 方法在以下两种情况下尤为有用。第一,当添加相似但又独特的脚本块时它很方便,您需要确保每个脚本块都给予唯一的密钥。本文稍后分析的代码说明了“is registered”方法的实用性。第二个用途就是当构建需要某个公用脚本的控件时,尤其是如果特别的生成该脚本。通过使用 IsClientScriptBlockRegistered 方法,可以确保对页面上服务器控件的所有脚本通用的脚本在每次页面加载时只生成一次,而不是对页面上的每个控件实例都生成一次。

RegisterClientScriptBlock 方法对于添加客户端脚本非常有用,该脚本不依赖于 Web 窗体内出现的任意窗体字段。该方法的常见使用就是显示客户端警告框。例如,设想您具有一个带有一些 TextBox Web 控件和一个“Save”按钮的网页。TextBox 控件可能会具有来自数据库的特殊值。设想该页面允许用户修改这些值并通过单击“Save”按钮提交他们所做的更改。当单击“Save”时,网页将会回发,并且会触发按钮的 Click 事件。您可以为更新数据库的事件创建一个服务器端事件处理程序。要使用户知道他们的更改已经保存,您可能希望显示一个警告框“Your changes have been saved”。通过将以下代码行添加到按钮的 Click 事件处理程序中,可以实现这个任务:

RegisterClientScriptBlock("showSaveMessage", _
"script language=""JavaScript""> _
alert('Your changes have been saved.'); _
/script>")

上述代码将会在页面的 form> 内添加指定的脚本内容,但是在该窗体的内容前。当在用户浏览器中生成页面时,他们将会看到根据页面加载显示的客户端警告框,如图 1 所示。

form method="post" ...>
script language="JavaScript">
alert('Your changes have been saved.');
/script>
...
/form>



图 1. 客户端 JavaScript 的结果

注 上面示例中一个潜在地不需要的副作用就是,警告框将会在浏览器接收到 form> 标记后立即显示。在用户单击警告框的“OK”按钮之前,浏览器将挂起网页的呈现。这意味着用户在单击“OK”之前,将看到一个空白的浏览器页面。如果希望在显示警告框之前完全显示该页面,您可以使用 RegisterStartupScript 方法(我们将在下面进行讨论),在 form> 元素的结尾处插入 JavaScript。

RegisterStartupScript(key, script)
RegisterStartupScript 方法与 RegisterClientScriptBlock 方法非常相似。主要的区别在于发出客户端脚本的位置。在 form> 元素开始后,在窗体的内容前,记住用 RegisterClientScriptBlock 发出脚本。另一方面,RegisterStartupScript 在窗体的结尾 处、在所有窗体字段后,添加指定的脚本。使用 RegisterStartupScript 来放置与呈现的 HTML 元素交互的客户端脚本。(稍后我将研究根据页面加载将焦点设置到窗体字段的示例;要完成这个操作,您将要使用 RegisterStartupScript 方法。)

与 RegisterClientScriptBlock 相似,由 RegisterStartupScript 添加的脚本块需要一个唯一的密钥值。同样,该密钥值主要由自定义控件开发人员使用。并不奇怪,还有一个 IsStartupScriptRegistered(key) 方法,它返回布尔值,表示带有指定密钥 的脚本块是否已经进行了注册。

注 有关使用 RegisterStartupScript 和 RegisterClientScriptBlock 来创建自定义编译的服务器控件的详细信息,请阅读我以前的文章: Injecting Client-Side Script from an ASP.NET Server Control.

RegisterArrayDeclaration(arrayName, arrayValue)
如果需要创建带有某些设置值的客户端 JavaScript Array 对象,请使用该方法向特定的数组添加值。例如,当使用 ASP.NET 网页中的验证控件时,就会构建 Array 对象 (Page_Validators),以包含对页面上验证控件集合的引用。当提交窗体时,就会枚举该数组以检查各种验证控件是否有效。

要将值 1、2 和 3 添加到名为 FavoriteNumbers 的客户端 Array 对象,要使用以下服务器端代码:

RegisterArrayDeclaration("FavoriteNumbers", "1")
RegisterArrayDeclaration("FavoriteNumbers", "2")
RegisterArrayDeclaration("FavoriteNumbers", "3")

这段代码会发出以下客户端脚本:

script language="javascript">

/script>

请注意,被传递的每个数组值都必须是字符串;但是,呈现的客户端脚本将 Array 对象的值设置为字符串的内容。也就是说,如果希望创建带有字符串值“Scott”和“Jisun”的 Array,要使用以下代码:

RegisterArrayDeclaration("FavoriteFolks", "'Scott'")
RegisterArrayDeclaration("FavoriteFolks ", "'Jisun'")

请注意,第二个输入参数是包含 'Scott' 和 'Jisun' 的字符串 - 文本由单撇号分隔。这会显示以下客户端脚本:

script language="javascript">

/script>

RegisterHiddenField(hiddenFieldName, hiddenFieldValue)
在传统的 ASP 中,通常需要将各种信息从一个页面分发到另一个页面。完成这个操作的常用方法就是使用隐藏窗体字段。(隐藏窗体字段是不显示的窗体字段,但是它的值会根据窗体的提交而发送。创建隐藏窗体字段的语法是 。)在 ASP.NET 中,通过自定义隐藏窗体字段传递信息的需要会极大地减少,因为页面中的控件状态会自动保持。但是,如果您发现需要创建自定义隐藏窗体字段,可以通过 RegisterHiddenField 方法来完成。

RegisterHiddenField 方法接受两个输入参数。隐藏字段的名称和值。例如,要创建带有名称 foo 和值 bar 的隐藏窗体字段,请使用以下代码:

RegisterHiddenField("foo", "bar")

这会在页面的 form> 元素中添加隐藏窗体字段,如下所示:

form name="_ctl0" method="post" action="test.aspx" id="_ctl0">

...
/form>

理解客户端元素是如何呈现的
Page 类包含负责呈现在上面讨论的方法中注册的客户端脚本的两个 internal 方法:OnFormRender 和 OnFormPostRender。(标记为 internal 的方法只能被相同程序集中的其他类调用。因此,无法从 ASP.NET Web 应用程序中的代码隐藏类调用 Page 的 internal 方法。)这两个方法都在 HtmlForm 类的 RenderChildren 方法中进行调用。位于 System.Web.UI.HtmlControls 命名空间中的 HtmlForm 类表示 Web 窗体;也就是说,ASP.NET 网页中的服务器端窗体form runat="server">.../form> 在页面的实例化阶段中作为 HtmlForm 类的实例加载。

因为由 Page 类的众多方法注册的客户端脚本是在 OnFormRender 和 OnFormPostRender 方法中呈现的,并且因为这些方法只能由 HtmlForm 类进行调用,所以通过这些方法以编程方式添加的客户端脚本,只有在网页包含 Web 窗体时才会呈现。也就是说,通过上面讨论的任意方法以编程方式添加的任意脚本元素,在 ASP.NET 网页包含服务器端窗体(form runat="server">.../form>)时只会在页面的最后标记中发出。

通过首先添加开始 form> 元素,会呈现 ASP.NET 网页上的 Web 窗体。之后,会调用 Web 窗体的 RenderChildren 方法,它包含三行代码:

Page.OnFormRender(...)
MyBase.RenderChildren(...)
Page.OnFormPostRender(...)

对 Page 类的 OnFormRender 方法的调用会添加以下标记:

• 通过对 RegisterHiddenField 进行调用而添加的任意隐藏窗体字段。

• 隐藏窗体字段中名为 __VIEWSTATE 的基于 64 位编码的视图状态。

• 通过对 RegisterClientScriptBlock 进行调用而添加的任意脚本块。


Web 窗体的 RenderChildren 方法中的第二行代码调用基类的 RenderChildren 方法,它会在 Web 窗体中呈现内容。在呈现所有窗体的内容后,对 Page 类的 OnFormPostRender 方法进行调用,这将会添加以下客户端内容:

• 由 RegisterArrayDeclaration 方法添加的任意 Array 对象。

• 通过对 RegisterStartupScript 进行调用而添加的任意脚本块。


最后,在 Web 窗体的 RenderChildren 方法完成后,则会呈现关闭窗体标记 ()。图 2 以图表形式说明了这个呈现过程。

注 图 2 假设您有些熟悉 ASP.NET 页面的生命周期。如果您对学习更多有关页面生命周期的知识感兴趣,请考虑阅读 Understanding ASP.NET View State,将重点放在标题为“The ASP.NET Page Life Cycle”的节上。



图 2. ASP.NET 中呈现的页面

分析脚本块的呈现顺序
乍看 Page 类的注册方法,在网页中呈现的已注册元素的顺序好像是对应于它们被添加到代码中的顺序。也就是说,设想 ASP.NET 网页的代码隐藏类中有以下两行代码:

RegisterClientScriptBlock("showSaveMessage", _
"script language=""JavaScript"">var name='" & _
someDataDrivenValue & "'; /script>")
RegisterClientScriptBlock("showSaveMessage", _
"script language=""JavaScript"">alert('Hello, ' + name + '!');
/script>")

当发现页面呈现以下客户端脚本块(假设 someDataDriveValue 的值为 Sam)时,您不要太奇怪:

script language="JavaScript">var name='Sam';/script>
script language="JavaScript">alert('Hello, ' + name + '!');/script>

访问该页面的用户将会看到一个警告框显示“Hello, Sam!”。

基于这个测试,您可能会认为这样的事实始终成立:在 HTML 页面中发出脚本块的顺序就是在服务器端代码中为它们指定的顺序。但是,这是一个不正确的假设,并且可以导致页面中断。例如,设想前面添加的脚本块在 HTML 页面中以相反的顺序发出。那么,您将会得到:

script language="JavaScript">alert('Hello, ' + name + '!');/script>
script language="JavaScript">var name='Sam';/script>

这将会显示警告框“Hello, !”,因为变量 name 尚未分配值。显然,有些时候发出脚本块的顺序非常重要的。

Page 类的注册方法 - RegisterClientScriptBlock、RegisterStartupScript、RegisterArrayDeclaration 和 RegisterHiddenFields - 全部将提供的脚本内容写入到内部 HybridDictionary 中。HybridDictionary 是在 System.Collections.Specialized 命名空间中发现的数据结构,设计用于在有很多项目未知的字典中存储项目。对于小型的项目集合,ListDictionary 是最有效的数据结构,但是对于较大的字典,Hashtable 会更有效。HybridDictionary 分离差异 - 它通过使用 ListDictionary 存储项目来开始。当 ListDictionary 添加了它的第九个项目后,HybridDictionary 会从使用 ListDictionary 切换到使用 Hashtable。

尽管这种方法对于性能而言非常理想,但是如果您使用几个脚本块而且脚本块的顺序非常重要,那么它可能会带来严重的破坏。这是因为,ListDictionary 维护元素被添加的顺序,而 Hashtable 没有进行维护。因此,如果您将八个或更少项目添加到任意一个特定的注册方法中,项目将会以它们被添加的顺序发出。但是,如果添加了第九个项目,脚本发出的顺序看起来将是随机的。

注ListDictionary 使用 linked list 存储它的元素,而 Hashtable 将它的元素存储在数组中,该数组的内容按照字符串键的哈希值进行排序。有关链接列表和哈希表的完整讨论已经远远超出了本文所讨论的范围。有关详细信息,包括对其性能的分析,请考虑阅读 An Extensive Examination of Data Structures,尤其是 Part 2 和 Part 4。

如果您计划出现如下情况:可能会存在使用特殊注册方法添加的多于八个客户端元素,并且元素的出现顺序比较重要,那么您可能希望研究一下 Peter Blum 的免费的 RegisterScripts 库。对于所发出客户端元素的顺序,RegisterScripts 提供了更大的控制能力,并且还提供了无需手动添加 script> 标记的选项,当利用 RegisterClientScriptBlock 或 RegisterStartupScript 方法包括客户端脚本时,您必须添加此标记。

返回页首
根据对用户操作的响应执行客户端代码
对于插入页面加载时运行的客户端代码而言,Page 类的注册方法非常理想,但是在很多情况下,我们希望根据对最终用户操作的响应来运行代码。例如,我们可能希望当用户单击按钮时显示确认对话框,或者当下拉列表的选定项目发生变化时调用特殊的客户端 JavaScript 功能。

HTML 元素具有您可以点击的大量客户端事件,并且当触发事件时可以执行客户端代码。所要求的标记只是在 HTML 元素的标记中作为一个属性。例如,要在单击某个按钮时显示警告框,可以添加以下代码:

value="Click me to see an alert box!"
onclick="alert('Here it is!');" />

要在客户端事件触发时运行客户端代码,可以将适当的属性添加到 HTML 元素中。对于 Web 控件,可以借助于编程方式使用 Attributes 集合添加客户端属性。例如,设想您具有一个 TextBox Web 控件,只要呈现的文本框获得焦点,您就希望它突出显示为黄色。要完成上述操作,您要将 TextBox Web 控件的呈现 HTML 添加如下所示的代码:

onfocus="this.style.backgroundColor='yellow';"
onblur="this.style.backgroundColor='white';" />

要完成这个标记,我们可以借助编程方式通过 Attributes 集合设置 TextBox Web 控件的 onfocus 和 onblur 客户端属性,如下所示:

TextBoxControl.Attributes("onfocus") = "this.style.backgroundColor='yellow';"
TextBoxControl.Attributes("onblur") = "this.style.backgroundColor='white';"

将客户端代码与客户端事件结合在一起的这种技术通常用于提供丰富的、交互式的用户体验。稍后,我们将会在本文中说明如何使用这种技术来显示基于用户操作的确认对话框。

返回页首
实现常用客户端功能
在我们研究 ASP.NET 方法(涉及动态地将客户端脚本添加到网页)后,让我们将注意力转移到这个知识。本文的其余部分重点讲述常用客户端任务,例如显示警告框、确认框、弹出窗口等等。具体说来,我们将创建包含一组方法的类,这组方法可用在 ASP.NET 项目中,从而快速、便捷地提供这样的功能。

我们将要分析的、贯穿本文剩余部分的 Visual Basic .NET 代码可以在本文的代码下载中获得。

显示警告框
一个常用的客户端要求就是显示警告框。警告框是一个客户端模式对话框,通常用于为最终用户提供某些重要的信息。警告框的示例如图 1 所示。警告框是通过客户端 JavaScript alert 函数来进行显示的,该函数接受一个单个的参数,即要显示的消息。显示警告框相当简单和直接;实际上,本文前面已经显示了一个示例。

为了使页面开发人员尽可能简单的显示警告框,让我们创建一个名为 ClientSidePage 的类,其中包含一个名为 DisplayAlert(message) 的方法。这个类将会继承 Page 类。想要利用这些客户端 helper 方法的页面开发人员需要使他们的代码隐藏类继承这个 ClientSidePage 类,而不是继承默认的 Page 类。以下代码显示了带有其第一个方法 DisplayAlert 的 ClientSidePage 类。

Public Class ClientSidePage
Inherits System.Web.UI.Page

Public Sub DisplayAlert(ByVal message As String)
RegisterClientScriptBlock(Guid.NewGuid().ToString(), _
"script language=""JavaScript"">" & GetAlertScript(message) & "/script>")
End Sub

Public Function GetAlertScript(ByVal message As String) As String
Return "alert('" & message.Replace("'", "\'") & "');"
End Function

End Class

请注意,这个类派生自 System.Web.UI.Page 类。DisplayAlert 方法只是使用 RegisterClientScriptBlock 方法,在警告框中显示提供的 message。由于这个方法可能会由单个页面多次调用,每个调用将使用其密钥的 GUID(是“全局唯一标识符”的首字母缩写)。传递到 alert 函数的字符串会使用撇号分隔,message 中的任意撇号都必须进行转义(JavaScript 将撇号转义为 \')。

要将这段代码用于 ASP.NET Web 应用程序中,您需要将新类添加到 ASP.NET 应用程序中。在 Visual Studio .NET 中,右键单击解决方案资源管理器中的 ASP.NET Web 应用程序项目名,然后选择添加新类。然后,剪切上述代码并将其粘贴到该类中。接下来,在要利用该代码的 ASP.NET 网页中,您需要修改代码隐藏类,以便它从 ClientSidePage 类而不是从 Page 中继承。以下代码显示了派生自 ClientSidePage 并使用 DisplayAlert 方法的示例代码隐藏类。

Public Class WebForm1
Inherits ClientSidePage

Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
DisplayAlert("Hello, World!")
End Sub

...
End Class

请注意,ClientSidePage 类不仅具有可以生成完整客户端 script> 元素的 DisplayAlert 方法,还具有可以返回客户端脚本无 script> 标记的 GetAlertScript 方法。第二个方法可用于您希望基于某个客户端事件显示警告的情况中。例如,如果您希望在特定文本框接收到焦点的任意时间显示警告,可以将以下代码添加到服务器端代码隐藏类中:

TextBoxControlID.Attributes("onfocus") = GetAlertScript(message)

将焦点设置为页面加载时的窗体字段
您是否注意到当访问 Google 时,焦点自动设置在搜索文本框呢?这一点小的“功能”使得搜索 Google 更快 - 在访问 Google 时您不必再花费时间移动鼠标,然后单击文本框。更确切的说,您只需在页面加载时键入即可。将焦点设置到窗体字段(可以是文本框、单选按钮、复选框或下拉列表)只要求几行客户端 JavaScript 代码。让我们将方法添加到 ClientSidePage 类,该类将在页面加载时自动向指定的 Web 控件中添加焦点。这种方法需要发出如下所示的客户端脚本:

script language="JavaScript">
function CSP_focus(id) {
var o = document.getElementById(id);
if (o != null)
o.focus();
}
/script>

... Form fields ...

... Form fields ...

script language="JavaScript">
CSP_focus(id of element to focus);
/script>

客户端函数 CSP_focus 接受字符串参数,窗体字段的 ID 被设置为焦点,并且从 DOM 中检索 HTML 元素。然后,调用检索元素的 focus() 函数。在网页的底部,在指定所有窗体字段后,我们需要调用 CSP_focus 方法,该方法在想要设置焦点的窗体字段的 ID 中传递。

下面的方法 GiveFocus(Control) 使用 RegisterClientScriptBlock 和 RegisterStartupScript 方法来生成所需要的客户端脚本。

Public Sub GiveFocus(ByVal c As Control)
RegisterClientScriptBlock("CSP-focus-function", _
"script language=""JavaScript"">" & vbCrLf & _
"function CSP_focus(id) {" & _
" var o = document.getElementById(id); " & _
"if (o != null) o.focus(); " & _
"}" & vbCrLf & _
"/script>")

RegisterStartupScript("CSP-focus", _
"script language=""JavaScript"">CSP_focus('" & _
c.ClientID & "');/script>")
End Sub

要从其代码隐藏类继承 ClientSidePage 的 ASP.NET 网页中使用 GiveFocus 方法,可以简单地在 Page_Load 事件处理程序中调用 GiveFocus,并传递应该在页面加载时设置其焦点的 Web 控件。例如,要将焦点设置为 TextBox Web 控件 TextBoxControl,请使用以下 Page_Load 事件处理程序:

Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
GiveFocus(TextBoxControl)
End Sub

打开弹出窗口
尽管弹出窗口作为广告发布者的工具在 Internet 上早已臭名昭著,但是很多 Web 应用程序还是因为使用弹出窗口而得到了好处。例如,您想要某个页面在 DataGrid 中显示数据库项目的列表,同时带有可以编辑每个特定项目的链接。不再使用 DataGrid 的内联编辑功能,您可能希望在用户选择编辑 DataGrid 时打开弹出窗口,其中弹出窗口包含带有可编辑 DataGrid 字段的文本框列表。(您希望这么做的一个原因在于可能存在大量的可编辑字段,但是您只想显示 DataGrid 中最适当的字段,因此要消除使用 DataGrid 的内置编辑功能的可能性。)

要显示弹出窗口,请使用 JavaScript 函数 window.open(),它使用很多可选输入参数,其中三个密切相关的参数是:

• 加载弹出窗口的 URL。

• 弹出窗口的字符串名称。

• 弹出窗口的特性,例如高度和宽度、窗口是否可以调整大小等等。


window.open() 函数的完整讨论已经超出了本文的范围;要学习更多内容,请参阅技术文档。

与其他显示警告框的方法相似,ClientSidePage 类包含用于显示弹出窗口的两个方法 - 一个呈现显示窗口的自包含 script> 块,另一个仅返回 JavaScript 脚本本身。除了打开弹出窗口的方法外,还有一组关闭当前窗口的方法。(可能会出现这样的情况,您希望以编程方式基于某些客户端或服务器端事件来关闭弹出窗口。)

Public Sub DisplayPopup(ByVal url As String, ByVal options As String)
RegisterStartupScript(Guid.NewGuid().ToString(), _
"script language=""JavaScript"">" & _
GetPopupScript(url, options) & _
"/script>")
End Sub

Public Function GetPopupScript(ByVal url As String, _
ByVal options As String) As String
Return "var w = window.open(""" & _
url & """, null, """ & options & """);"
End Function

Public Sub CloseWindow(Optional ByVal refreshParent As Boolean = False)
RegisterClientScriptBlock("CSP-close-popup", _
"script language=""JavaScript"">" & _
GetCloseWindowScript(refreshParent) & "/script>")
End Sub

Public Function GetCloseWindowScript(Optional _
ByVal refreshParent As Boolean = False) As String
Dim script As String
If refreshParent Then
script &= "window.opener.location.reload();"
End If

Return "self.close();"
End Function

该代码的执行示例可以在本文的代码下载中找到。同时,您还将发现一个示例网页,它具有一个在与 ASP.NET 网页相同的目录中列出文件的 DataGrid。这个 DataGrid 具有两列:显示超级链接的 TemplateColumn,当单击时打开显示所选文件内容的弹出窗口;以及该文件的名称(如图 3 所示)。



图 3. 带有弹出窗口的 DataGrid

DataGrid 的标记利用 GetPopupScript 方法,如下所示:






View File



HeaderText="Filename">



ASP.NET 网页 ViewFile.aspx 打开其名称在 querystring 中指定的文件并显示其内容(如图 4 所示)。



图 4. 在弹出窗口中显示 Web.config 的内容

注 弹出窗口最适用于只有 Intranet 应用程序的情况,因为很多 Internet 用户利用某种弹出阻止软件,例如 Google 工具栏。实际上,利用 Microsoft Windows XP Service Pack 2,Microsoft Internet Explorer 将会在默认情况下配置为阻止弹出窗口。但是,当用户访问受信任站点或本地 Intranet 区域中的站点时,弹出窗口将仍然会出现。有关在 Windows XP Service Pack 2 中阻止 Internet Explorer 弹出窗口功能的详细信息,请务必阅读 Changes to Functionality in Microsoft Windows XP Service Pack 2。

在回发前确认
在本文的前面部分,我们研究了如何显示客户端警告框,这是带有“OK”按钮的模式对话框。JavaScript 提供被称为确认对话框的更具有交互风格的警告框。使用 confirm(message) 函数显示确认对话框并通过 message 输入参数与“OK”和“Cancel”按钮指定显示带有文本的对话框的效果。如果用户单击“OK”,confirm(message) 函数会返回 true;如果他们单击“Cancel”,则返回 false。

通常情况下,确认对话框用于确保用户在提交窗体之前希望继续。当单击 HTML 元素(例如提交按钮)提交窗体时,如果 HTML 元素触发返回 false 的客户端事件处理程序,窗体提交就被取消。通常,确认对话框用于网页中,如下所示:

form ...>
onclick="return confirm('Are you sure you want
to submit this form?');" />
/form>

当用户单击“Click Me to Submit the Form”按钮时,他们将会看到确认对话框,询问他们是否确实希望提交该窗体(如图 5 所示)。如果用户单击“OK”,confirm() 将返回 true,该窗体将被提交。但是,如果他们单击“Cancel”按钮,confirm() 将返回 false,该窗体提交将被取消。



图 5. 确认 JavaScript 的结果

设想您具有一个 DataGrid带有标签为“Delete”的一列按钮。在单击该按钮时,该窗体将会回发,并且选定的记录将被删除。在此例中,您可能希望复查用户是否确实希望删除该记录。此时,要使用客户端确认对话框将会非常理想。您可以用对话框提示用户,声明如下所示的内容:“This will permanently delete the record.Are you sure you want to continue?”如果用户单击“OK”,该窗体将会回发,并且记录将被删除;如果他们单击“Cancel”,该窗体将不会回发,而且记录也不会被删除。

要添加在单击按钮后显示确认对话框所必需的客户端 JavaScript,请简单地使用 Attributes 集合来添加客户端 onclick 事件处理程序。具体说来,要将 onclick 事件处理程序代码设置为:return confirm(message);。为了提供 DataGrid 的 ButtonColumn 的这种功能,您需要在 DataGrid 的 ItemCreated 或 ItemDataBound 事件处理程序中以编程方式引用 Button 或 LinkButton 控件,并且在那里设置 onclick 属性。有关详细信息,请参阅 http://aspnet.4guysfromrolla.com/articles/090402-1.aspx。

确认 AutoPostBack DropDownLists
尽管通常在单击按钮时使用确认对话框,但是还可以在更改下拉列表时使用它们。例如,您可能具有一个当特定的 DropDownList Web 控件发生更改时会自动回发的网页。(DropDownList Web 控件具有一个 AutoPostBack 属性,如果设置为 True,只要 DropDownList 的选定项目发生更改就会导致窗体回发。)

直观地讲,您可能认为对 DropDownList 添加确认对话框与对 Button Web 控件添加这种对话框相同。也就是说,简单地将 DropDownList 的客户端 onchange 属性更改为如下内容:return confirm(...);。使用:

DropDownListID.Attributes("onchange") = "return confirm(...);"

遗憾的是,这并不会按期望工作,因为 AutoPostBackDropDownList 的 onchange 属性将设置为会导致回发的 JavaScript,即对客户端 __doPostBack 函数的调用。当您自己借助编程方式设置 onchange 属性时,最后的结果是呈现的客户端 onchange 事件处理程序同时具有您的代码和对 __doPostBack 的调用:



记住,我们确实希望发生的情况是,如果确认返回 true,就调用 __doPostBack 函数,因为之后页面将会被回发。通过利用 Attributes 集合将 onchange 事件设置为:if (confirm(...)),我们可以完成这一操作,而该代码会生成以下标记,该标记正是我们所希望的:



乍看起来,这似乎会具有所期望的效果。如果用户从下拉列表中选择不同的项目,将会出现一个确认框。如果用户单击“OK”,该窗体将回发;如果用户单击“Cancel”,该窗体回发会暂停。尽管问题在于下拉列表维持用户选定的项目以启动下拉列表的 onchange 事件。例如,设想下拉列表加载正在进行选择的项目 x,然后用户选择项目 y。这将会触发下拉列表客户端 onchange 事件,它将会显示确认对话框。现在,设想用户点击“Cancel”- 下拉列表将仍然选择项目 y。我们希望的是将选择转回到项目 x。

要实现此目的,我们需要做两件事情:

1.
编写一个“记住”选定下拉列表项目的 JavaScript 函数。

2.
在下拉列表的客户端 onchange 事件中,如果用户单击“Cancel”,您需要将下拉列表转换回“已记住的”值。


步骤 1 必须为下拉列表和函数(当页面加载时运行,并且记录下拉列表的值)创建全局脚本变量。步骤 2 要求为下拉列表的客户端 onchange 属性更改为如下所示内容:

if (!confirm(...)) resetDDLIndex(); else __doPostBack();

其中 resetDDLIndex 是 JavaScript 函数,它将下拉列表选定的值返回到“已记住的”值。用于此目的的客户端脚本应该如下所示:



script language="JavaScript">
var savedDDLID = document.getElementById("ddlID").value;

function resetDDLIndex() {
document.getElementById("ddlID").value = savedDDLID;
}
/script>

通过在 ClientSidePage 类中创建 helper 方法,这个必要的脚本可以轻松地生成。

Public Sub ConfirmOnChange(ByVal ddl As DropDownList, ByVal message As String)
'Register the script block
If Not IsStartupScriptRegistered("CSP-ddl-onchange") Then
RegisterStartupScript("CSP-ddl-onchange", _
"script language=""JavaScript"">" & _
"var CSP_savedDDLID = " & _
document.getElementById('" & _
ddl.ClientID & "').value;" & vbCrLf & _
"function resetDDLIndex() {" & vbCrLf & _
" document.getElementById('" & " & _
" ddl.ClientID & "').value = CSP_savedDDLID;" &
vbCrLf & _
"}" & vbCrLf & _
"/script>")
End If

ddl.Attributes("onchange") = _
"if (!confirm('" & message.Replace("'", "\'") & _
"')) resetDDLIndex(); else "
End Sub

要使用这段代码,简单地调用网页上每个 AutoPostBackDropDownList 的该方法,当网页上的选定项目发生更改时要在该网页上显示对话框。

未保存而退出时进行确认
在我所创建的大多数每个数据驱动的 Web 应用程序中,始终会有用户可以编辑数据库特定信息的特定页面。非常简单的一个示例是带有一系列 TextBox 和 DropDownList Web 控件的页面,而数据库数据填充在这些控件中。用户可以进行任何适当的修改,然后单击“Save”按钮将他们所做的更改保存到数据库。

当我创建这些页面时,通常会以两个 Button Web 控件来结束页面:“Save”按钮和“Cancel”按钮。“Save”按钮将任意更改保存回数据库,而“Cancel”按钮不保存任何更改退出页面。尽管两个按钮看起来可能是一个完美的设计,但有时用户会在他们想要单击“Save”按钮时意外地单击“Cancel”按钮,这样就会丢失了他们对数据所做的所有更改。为防止这种情况发生,可以在“Cancel”按钮上使用确认框,它只有在网页上的任意文本框或下拉列表发生更改时才会出现。也就是说,如果用户对数据进行了任意更改,然后单击“Cancel”,确认框将会提示他们是否确实要在不保存的情况下退出。(如果用户只是单击“Cancel”,而没有更改任何数据,将不会显示这样的确认框。)

这种用户体验可以通过少量客户端 JavaScript 来实现。基本上可以说,它需要一个 JavaScript 全局变量 isDirty,在初始时为 false,但只要触发窗体字段的 onchange 事件,它就会设置为 true。如果 isDirty 为 true,则还有一个显示确认对话框的 JavaScript 函数。“Cancel”按钮的 onclick 客户端事件处理程序限定为从该 JavaScript 函数返回结果。以下 HTML 说明了这个概念:

script language="JavaScript">
var isDirty= false;
function checkForChange(msg) {
if (isDirty) return confirm(msg); else return true;
}
/script>

Name:


id="btnCancel"
onclick="return checkForChange('You have made changes to the data
since last saving. If you continue, you will lose these
changes.');" />

可以通过将该脚本生成移动到 ClientSidePage 类,简单地生成这个脚本。具体说来,我们可以创建下列三个方法:

Protected Sub RegisterOnchangeScript()
If Not IsClientScriptBlockRegistered("CSP-onchange-function") Then
RegisterClientScriptBlock("CSP-onchange-function", _
"script language=""JavaScript"">" & _
"var isDirty= false;" & vbCrLf & _
"function CSP_checkForChange(msg) {" & vbCrLf & _
" if (isDirty) return confirm(msg); " & _
"else return true;" & vbCrLf & _
"}" & vbCrLf & _
"/script>")
End If
End Sub

Public Sub MonitorChanges(ByVal c As WebControl)
RegisterOnchangeScript()
If TypeOf c Is CheckBox Or TypeOf c Is CheckBoxList _
Or TypeOf c Is RadioButtonList Then
c.Attributes("onclick") = "isDirty = true;"
Else
c.Attributes("onchange") = "isDirty = true;"
End If
End Sub

Public Sub ConfirmOnExit(ByVal c As WebControl, ByVal message As String)
RegisterOnchangeScript()
c.Attributes("onclick") = _
"return CSP_checkForChange('" & message.Replace("'", "\'") &
"');"
End Sub

要创建表现这个行为的网页,我们只需要将其服务器端代码隐藏类派生自 ClientSidePage,并且在 Page_Load 事件处理程序中,为需要客户端 onchange 事件的每个 Web 控件对 MonitorChanges 进行调用,并且为在单击时应该显示警告用户是否进行更改并退出页面的每个 Button、LinkButton 和 ImageButton,对 ConfirmOnExit 进行调用。

注MonitorChanges 方法使用 onclick 客户端事件,而不是用于 CheckBox、CheckBoxList 和 RadioButtonList Web 控件的 onchange。这是因为这些控件将 标记或 table> 限制在复选框或很多复选框或单选按钮附近。在我利用 Internet Explorer 进行测试时,我发现在应用到 或 table> 时,选中复选框或单击单选按钮并没有选择 onchange 事件,但是却触发了 onclick 事件。

图 6 显示了带有两个 TextBox Web 控件、一个 DropDownList Web 控件和一个 CheckBox Web 控件的 ASP.NET 网页示例。如下面的 Page_Load 事件处理程序所示,所有这些 Web 控件都会被监视所做的更改。配置“Cancel”按钮 btnCancel,这样如果在进行更改后单击它,将显示一个确认对话框。



图 6. 带有确认的对话框

Public Class ConfirmOnExit
Inherits ClientSidePage

Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
'Specify what controls to check for changes
MonitorChanges(name)
MonitorChanges(age)
MonitorChanges(favColor)
MonitorChanges(chkSilly)

ConfirmOnExit(btnCancel, _
"You have made changes to the data since last saving." & _
" If you continue, you will lose these changes.")
End Sub

...
End Class

注客户端 onchange 事件不能用于 Netscape 的旧版本中。同样,Internet Explorer 5.0 的 onchange 事件也具有一些已报告的问题(在 Internet Explorer 5.01 SP 1 中已经修复)。

而且,这种方法将不会像利用 DropDownList Web 控件那样将 AutoPostBack 设置为 True,而是回发将重置 isDirty 的值。这个问题有很多解决方案,例如使用隐藏窗体字段,指示出回发窗体数据是否以 dirty 开始。我将实现这个操作的过程作为练习留给读者。

创建客户端 MessageBox 控件
由于确认对话框是一种防止意外点击的非常好的方法,可以潜在地降低到 Web 服务器的回发数量,有几种特定方案您可能希望显示确认对话框,并且能够在服务器端确定用户是否单击了“OK”或“Cancel”。(记住,对于确认对话框,如果用户单击“Cancel”,该窗体则不会回发。)而且,JavaScript 中的警告和确认框在外观上非常受限。幸运的是,客户端 VBScript 通过其 MsgBox 函数提供了更丰富的消息框体验。

在过去的项目中,我需要客户端模式消息框,无论单击什么按钮,它都可以引起回发。作为响应,我构建了自定义编译的 ASP.NET 服务器控件来满足这些要求。此外,客户端消息框还使用 VBScript 的 MsgBox 函数来提供更丰富的消息框体验。

注 在 Microsoft Internet Explorer 浏览器中,VBScript 只作为客户端脚本编辑语言。要考虑到这一点,如果访问浏览器是 Internet Explorer,那么我的服务器控件只能使用 VBScript。如果是非 Internet Explorer 浏览器,则服务器端控件使用 JavaScript。

对这种自定义服务器控件深入的讨论可以完全保证整个文章的正确性,因此无需将重点放在控件的内部工作原理上,让我们分析如何在 ASP.NET 网页中使用 MessageBox 控件。(控件的完整资源以及使用该控件的示例 ASP.NET 网页都可以从本文的下载中获得。)

要在 ASP.NET 网页中使用 MessageBox 控件,首先要将 MessageBox 控件添加到 Visual Studio .NET 工具箱。通过右键单击工具箱、从工具箱中选择“Add/Remove Items”、然后浏览到 MessageBox 程序集可以实现上述任务。要将客户端消息框添加到网页,只要将其从工具箱拖动到该设计器即可。图 7 显示了 Visual Studio .NET 设计器中的 MessageBox 控件。



图 7. 显示模式消息框

MessageBox 类具有很多可以进行配置的属性,以调整消息框的外观:

• Buttons. 指定要显示的按钮。ButtonOptions 枚举中定义的选项可以为:OkOnly、OkCancel、AbortRetryIgnore、YesNoCancel、YesNo 或 RetryCancel。

• DisplayWhenButtonClicked. 点击后将显示客户端消息框的按钮 Web 控件的 ID。如果希望由于点击某个特定按钮而显示消息框,请使用这个属性。

• Icon. 显示在消息框中的图标;选项定义于 IconOptions 枚举中。有效的值为:Critical、Question、Exclamation 和 Information。

• Prompt. 显示在消息框中的文本。

• Title. 消息框的标题。


一旦将 MessageBox 控件添加到 ASP.NET 网页,下一个挑战就是使其根据特定客户端操作进行显示。DisplayWhenButtonClicked 属性允许您指定页面上按钮 Web 控件的 ID点击后将会显示消息框。另外,您还可以通过调用客户端函数 mb_show(id) 来显示消息框,其中 ID 是 MessageBox 控件的 ID。

不管您选择在消息框中显示什么按钮配置,当单击任意按钮时,随后就会发生回发并且触发 MessageBox 控件的 Click 事件。通过在设计器中简单地双击 MessageBox,可以为此事件创建一个事件处理程序。事件处理程序的第二个输入参数是类型 MessageBoxClickedEventArgs,它包含一个返回用户单击消息框按钮的信息的 ButtonClicked 属性。

MessageBox 控件在如下情况下非常有用,当您希望快速为用户提供模式对话框,而不管用户的选择以及回发中的结果。要查看操作中的 MessageBox 控件,请签出源代码下载中的 MsgBoxDemo.aspx 页面。

返回页首
小结
本文一开始就研究了网页中客户端脚本的常见使用,然后转向分析将客户端脚本插入到 ASP.NET 网页中的方法和技术。正如我们看到的那样,Page 类包含很多旨在借助编程方式从服务器端代码隐藏类插入客户端脚本块的方法。这些方法也常用于自定义编译的服务器控件,请参阅我以前文章中的讨论: Injecting Client-Side Script from an ASP.NET Server Control.

除了添加脚本块外,客户端功能通常必须与由某些 HTML 元素引发的客户端事件结合在一起。要借助编程方式通过服务器端代码隐藏类指定 Web 控件的客户端事件处理程序,请使用 Attributes 集合,它可用作所有 Web 控件的属性。

本文的后半部分应用了前半部分中涉及的内容,显示了如何在 ASP.NET 网页中实现常用客户端功能。我们看到了如何扩展 Page 类,这样我们可以利用代码隐藏类轻松地显示警告框、将页面加载上的焦点设置到特定 Web 控件、如何显示弹出窗口以及如何显示确认对话框。我们还研究了创建自定义服务器控件,它使用 VBScript 来提供更丰富的客户端消息框用户体验,而且无需考虑单击按钮就可以导致回发。

祝大家编程愉快!

2.
How to use clientscript in custom controls
By Andy Smith
From http://weblogs.asp.net/asmith/articles/25465.aspx

So you want to build a custom asp.net control. And you want to have some whiz bang, complex script that goes with it. Well, there are a few methods on System.Web.UI.Page that help you emit script into the right places, but the docs on these methods don't really tell you the whole picture at once. A beginning control developer ends up hacking his way thru things, and asking tons of questions on this stuff in the lists. It's taken me a while, but now I've got what I consider to be the Right Way© to emit script for your controls, and I will now impart this knowledge onto the masses ( err, the 7 people who read this blog ). Not to say that I follow this advice to the letter for every control of mine, but hey, it was a learning experience.

So you want to build a custom asp.net control. And you want to have some whiz bang, complex script that goes with it. Well, there are a few methods on System.Web.UI.Page that help you emit script into the right places, but the docs on these methods don't really tell you the whole picture at once. A beginning control developer ends up hacking his way thru things, and asking tons of questions on this stuff in the lists. It's taken me a while, but now I've got what I consider to be the Right Way© to emit script for your controls, and I will now impart this knowledge onto the masses ( err, the 7 people who read this blog ). Not to say that I follow this advice to the letter for every control of mine, but hey, it was a learning experience.

First off, here are the relevant methods on Page for script stuff:

GetPostBackEventReference
RegisterArrayDeclaration
RegisterClientScriptBlock
RegisterStartupScript
There are more, for more esoteric needs, but this is what i'm covering right now
GetPostBackEventReference
Like the docs hint at but don't quite say, this method returns the script that calls __doPostBack. However, never type the function name "__doPostBack". Neither function name nor the specifics of the args it takes, are guarenteed to be the same for future versions of asp.net. Just use the returned value of this function instead. Always.

The Three Registers
Here's the deal on how these three should work together. RegisterClientScriptBlock says what to do, RegisterArrayDeclaration says who to do it to, and RegisterStartupScript just says Go.

If that was confusing, here's a lot more detail...

RegisterClientScriptBlock is where you put generic, non-control-instance-referencing code. That's not to say it can't have knowledge of the control design, but it shouldn't have knowledge of any specific controls on the page, by ID or any other way. Any of your control's IDs should be variables that this library uses. This lets you have 1 big library of code that can just be plopped onto the page with no worries about multiple controls or page structure or anything. It should have an entry point function ( I recommend something like MetaBuilders_WebControls_FooControl_Init ) where everything gets set up, properties are set, event handlers are added, etc. I'll go into good practices for this script later, for now i'm going to stay at the overview level.

RegisterArrayDeclaration is where you put the control id's that you are avoiding in the code library. Simply pick a name for your array ( I recommend the namespace qualified plural name of your control, with _'s, MetaBuilders_WebControls_FooControls ) and use the ClientID or UniqueID as the value, depending on needs. ( needs which I'll go over later ) This array is then accessed in your library's Init function in order to get references to each control instance on the page. Sometimes you'll find a need to include more than just the ID in the array value, when that happens, you can use a special property-value syntax i'll get into later.

RegisterStartupScript should generally have one line of code. Just call the Init function and kick things off.

How To Write The Library
Ok, so far I've only said that the lib should have an Init, and use the ClientIDs in the registered array to do stuff. Here's how I would write the lib... First, a typical Init skeleton looks something like this:

function MetaBuilders_FooControl_Init() {
// Make sure the browser supports the DOM calls or JScript version being used.
if ( !MetaBuilders_FooControl_BrowserCapable() ) { return; }

// Loop thru the array of control ClientIDs and get a reference to the element
for ( var i = 0; i < MetaBuilders_FooControls.length; i++ ) {
var myFooControl = document.getElementById( MetaBuilders_FooControls[i] );

//TODO Do stuff with myFooControl

}
}

function MetaBuilders_FooControl_BrowserCapable() {
if ( typeof( document.getElementById ) == "undefined" ) {
return false;
}
//TODO Add any more checks you need to
return true;
}

If you need to support browsers that don't have getElementById, then you'll need to do a few emit the UniqueID into the array, and use a form/input searching function to find the correct control. That would look something like this:

function MetaBuilders_FooControl_FindControl(uniqueID) {
for( var i = 0; i < document.forms.length; i++ ) {
var theForm = document.forms[i];
var theControl = theForm[uniqueID];
if ( theControl != null ) {
return theControl;
}
}
return null;
}

But in short, the Init method loops thru the array of IDs, and does fun stuff with the control. But what if you have composite control, and you want to do fun stuff with the child controls. Then your array declaration and your Init function will look a bit different. You'll want to give RegisterArrayDeclaration the ClientIDs of all the child controls you want to interact with instead of just the one parent. So lets say you have a composite control with two textboxes inside. your array bit might look like this:

Page.RegisterArrayDeclaration("{ ID:'" + this.ClientID + "', firstTextBoxID:'" + this.firstTextBox.ClientID + "', secondTextBoxID:'" + this.secondTextBox.ClientID + "' }");

then your init function might change to this:

function MetaBuilders_FooControl_Init() {
// Make sure the browser supports the DOM calls or JScript version being used.
if ( !MetaBuilders_FooControl_BrowserCapable() ) { return; }

// Loop thru the array of control ClientIDs and get a reference to the element
for ( var i = 0; i < MetaBuilders_FooControls.length; i++ ) {
var fooControlProperties = MetaBuilders_FooControls[i];
var myFooControl = document.getElementById( fooControlProperties.ID );
var myFirstTextBox = document.getElementById( fooControlProperties.firstTextBoxID );
var mySecondTextBox = document.getElementById( fooControlProperties.secondTextBoxID );

//TODO Do stuff with myFooControl and the child controls

}
}

The syntax I used in the array declaration ends up looking like this:

var MetaBuilders_FooControls = new Array( { ID:'fooControl1', firstTextBoxID:'fooControl1_firstTextBox', secondTextBoxID:'fooControl1_secondTextBox } );

It declares each item in the array as an object with the properties you set with the propertyName:value syntax. This style can be used with any properties, not just child control ids.

This brings us to custom server property values that the script needs to use. If you have a property on your control that simply needs to be sent to the script for use there, then the easiest way is to add another item to the RegisterArrayDeclaration call and change Init to grab it from the array. If the property needs to be a two-way variable, get/set on both server and client, then I suggest you add a HtmlInputHidden control and use its .Value. The serverside property will simply wrap its value, and you can get-set it on the clientside easily once you emit its ClientID in the array.

Ok... now... clientside events. I suggest that you attach event handlers in the Init function instead of setting the onFoo attributes on the control. The reason for this is that it keeps all your script in one place. The serverside c# or VB code doesn't need to know the specifics of your javascript, it only needs to give the script the info it needs for the current instance via the array declaration. This help speed up dev time, as you can stay in one file for changing event handlers and such.

Ok, so what's the code look like then? Well, you basically have 2 choices, you can either set the .onfoo property, or use the addEventListener/attachEvent methods. Using the onfoo property method allows you to easily access the calling control of the event via the "this" keyword, which comes in quite handy. However, it has the problem that it completely takes over the event. If there's also a tag attribute for it, this will replace that one. The other method, of course, has the exact opposite characteristics. "this" can't refer to the control, but it doesn't interfere with anybody else.

So how do you decide which is best? My recommendation is to use the .onfoo property if you are attaching events to your own child controls, and use the addEventListener/attachEvent methods if you are attaching events to controls outside of your jurisdiction. So lets look at some code...

This example will show the .onfoo property way. this code goes inside the Init function:

myFirstTextBox.OtherTextBox = mySecondTextBox;
myFirstTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
myFirstTextBox.onchange = myFirstTextBox.UpdateOtherTextBox;

mySecondTextBox.OtherTextBox = myFirstTextBox;
mySecondTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
mySecondTextBox.onchange = mySecondTextBox.UpdateOtherTextBox;

....

function MetaBuilders_FooControl_UpdateOtherTextBox() {
// note that by using the method, "this" refers to the textbox for the event.
this.OtherTextBox.value = this.value;
}

Now, whenver one textbox changes, the other will be updated. Take a closer look at the event hookup. First I create a new method on each textbox, UpdateOtherTextBox, by setting this new property to a function pointer. then I set the onchange event to the textbox's own method. This two-line technique is what allows me to use "this" in the handler.

However, when you are attaching to random controls, you don't want to be just overtaking their events like this, so you have to do some gymnastics. Here's an example from my DefaultButtons control where I attach to the onfocus and onblur events of page-level textboxes:

if ( typeof( inputControl.addEventListener ) != "undefined" ) {
inputControl.addEventListener("focus",DefaultButton_RegisterDefault,false);
inputControl.addEventListener("blur",DefaultButton_UnRegisterDefault,false);
} else if ( typeof ( inputControl.attachEvent ) != "undefined" ) {
inputControl.attachEvent("onfocus",DefaultButton_RegisterDefault);
inputControl.attachEvent("onblur",DefaultButton_UnRegisterDefault);
} else {
inputControl.onfocus = DefaultButton_RegisterDefault;
inputControl.onblur = DefaultButton_UnRegisterDefault;
}


The reason that this code is so big is because browsers are very different. addEventListener is the official W3C way of attaching events. attachEvent is the IE way, and I default to the .onfoo way if neither is supported. The problem is that this complicates my handler code, as "this" no longer refers to the control raising the event. To fix this, you need to get a handler on the data for the event. Here's how you get the element raised by an event, again from my DefaultButtons code:

function DefaultButton_RegisterDefault(e) {
// src here is the control which raised the event.
var src = DefaultButton_GetSrcElement(e);

//Usefull stuff removed for clarity
}

function DefaultButton_GetSrcElement(e) {
if ( typeof( window.event ) != "undefined" ) {
return window.event.srcElement;
}
if ( e != null && typeof( e.target ) != "undefined" ) {
return e.target;
}
return null;
}

Ok, so once you have your custom properties and your event handlers set up, now you just implement whatever cool stuff you want to make your control do its thing.

Oh, one more thing with the code. If you take another look at this code:

myFirstTextBox.OtherTextBox = mySecondTextBox;
myFirstTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
myFirstTextBox.onchange = myFirstTextBox.UpdateOtherTextBox;

mySecondTextBox.OtherTextBox = myFirstTextBox;
mySecondTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
mySecondTextBox.onchange = mySecondTextBox.UpdateOtherTextBox;

You'll see that I actually make properties on each child control that reference the other child controls. This is a very useful thing to do, as it makes referencing the related child controls from the event handlers much easier.

and now we move on to...

Packaging
UPDATE: This part has changed, thanks to some great comments on the article.
So now that you know who to write the code, how to you incorporate it into your projects? I use vstudio for all my control development, so I'll tell you how I do it with that tool. What I've found to be the best is to simply put all the script code into a .js file as an embedded resource. Simply right-click on the project, and choose Add New Item, choose script file, and set it as an embedded resource in the properties window.

Now to emit your code to the browser you use code like this:

if ( !Page.IsClientScriptBlockRegistered(scriptKey) ) {
using (System.IO.StreamReader reader = new System.IO.StreamReader(typeof(FooControl).Assembly.GetManifestResourceStream(typeof(FooControl), "FooControl.js"))) {
String script = "script language='javascript' type='text/javascript' >\r\n\r\n /script>";
this.Page.RegisterClientScriptBlock(scriptKey, script);
}
}


Ok, that code is nice, but where do you put it?
This is generally how I handle the actual task of script registration:

protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
this.RegisterScript();
}

protected virtual void RegisterScript() {
// All the code that calls Page.RegisterFoo methods goes here.
}

PreRender is generally a good place to put script registrations because it is the last event where you can call these methods and they still take effect. You want to do it as late as possible because you never know when a page developer is going to change properties on your control that might change the script you produce.

Another tip for the serverside code... You generally don't want to Register your script if your .Visible property is false. And, depending on your script and functionality, you might not want to Register your script if your .Enabled property is false.

So anyway, I hope you got some useful information here. If you have any questions, feel free to send me a message or leave a comment.

P.S. almost none of the code here has actually been tested. I just wrote this stuff directly into the blog. I don't think there are problems with the code itself, but hey, I may have missed something, and this was intended as conceptual code anyway. Let me know if you find something obviously wrong.



<< Home

This page is powered by Blogger. Isn't yours?