虚拟表单 Web 自定义控件
您是否曾想过,“要是有一个控件——就像面板控件一样——您只需用它来包装一些输入控件,设置一个简单的属性(指向当按下 Enter 键时应该被“点击”的控件的 ID),这就足够了。” 那么,现在有一个这样的控件了。
引言
我最近遇到一个问题,我开发的一个网站的页面顶部有表单字段(用于登录或搜索),并且当我将光标放在页面下方的一个表单字段中时,如果我按下 Enter 键,触发的是页面上第一个按钮的点击事件(顶部栏中的按钮,而不是我希望它使用的按钮)。这种设置很常见:母版页包含一个带有 `runat="server"` 属性的 `form` 标签,并且这个 `form` 包裹了页面的全部内容。这一切似乎都运行正常,直到我遇到注册页面,当我想点击“立即注册”按钮时,我只是按了 Enter。结果没有注册我,而是出现了一个消息,解释说用户名和密码是必需的。由于这些字段也存在于注册表单上,我感到很困惑,更何况当我用鼠标点击“立即注册”按钮时,一切都正常。通过代码调试,我很快就意识到发生了什么,并翻找了我几年前写的一些 JavaScript 代码,这些代码可以监听 Enter 键,取消默认行为,并调用另一个方法。我将这段代码添加到页面,一切都好了,直到我发现另一个页面有同样的问题。就在那时我想,“要是有一个控件——就像面板控件一样——您只需用它来包装一些输入控件,设置一个简单的属性(指向当按下 Enter 键时应该被“点击”的控件的 ID),这就足够了。”
现在,有一个这样的控件了。
编辑:实际上,正如 Onskee1 在下面的评论中指出的那样,标准的 ASP `Panel` 控件有一个“`DefaultButton`”属性,它实现了类似的功能;但是,它只允许您使用 ASP `Button` 控件作为指定的按钮。如果您想使用 ASP `LinkButton` 控件或任何其他类型的控件作为默认按钮,它对您没有任何作用。所以,如果您只使用 ASP `Button` 控件,我建议您使用该属性。否则,请继续阅读……
使用代码
您可以根据需要逐页注册控件,或者将其添加到 `web.config` 文件的 `<Pages>` 部分,使其可供站点上的任何页面使用(列表 1)。
<pages enableeventvalidation="true"
enableviewstatemac="true" enablesessionstate="true">
<controls>
<add assembly="WilliaBlog.Net.Examples"
namespace="WilliaBlog.Net.Examples" tagprefix="WBN">
</add>
</controls>
</pages>
然后,以将控件包装您的输入的方式将其添加到页面中
<WBN:VirtualForm id="vf1" runat="server" SubmitButtonId="Button1" UseDebug="false">
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<asp:Button ID="Button1" runat="server" Text="Button1" OnClick="Button1_Click" />
</WBN:VirtualForm>
从列表 2 可以看出,您应该使用按钮的服务器端 ID(虚拟表单控件会自动将其转换为客户端 ID)。在线测试。
工作原理
实际的服务器端虚拟表单 Web 自定义控件非常简单。它继承自 `System.Web.UI.WebControls.Panel`,并包含一个属性,允许您设置按下 Enter 键时要推送的 `Button` 或 `LinkButton` 的 ID。为了方便移植,我选择将 JavaScript 文件嵌入到 DLL 中。(最初,我将其放在一个外部 `.JS` 文件中,并添加到母版页的头部,但是如果我——或者其他人——想在另一个站点上使用这个控件,这将增加额外的设置复杂性——他们必须记住加载那个 JavaScript 文件,并且虽然我们可以将它托管在一个中心位置供所有站点共享,但将文件嵌入 DLL 似乎是明智的选择。)这种技术的缺点是,每次请求页面时,`js` 文件都必须通过网络传输,因此无法在客户端缓存,这会对 Web 应用程序的低带宽用户产生负面影响。因此,我使用了 YUI Compressor 来删除所有注释和额外的空格,并在 DLL 中包含了两个版本的脚本。当您将 `UseDebug` 属性设置为 true 时,它将使用冗长的版本,这在 Firebug 中更容易调试,但是当一切正常工作时,通过从控件声明中省略此属性或将其设置为 false 来使用压缩版本。
要使文件可从您的服务器控件的程序集中访问,只需将文件添加到项目中,转到“属性”窗格,然后将“生成操作”设置为“嵌入的资源”。要将文件暴露给 Web 请求,您需要添加列表 3 中第 7 行和第 8 行这样的代码。这些条目将嵌入的资源暴露出来,以便 `ClientScriptManager` 既可以访问文件,又能知道它是什么类型的文件。您也可以通过这种方式嵌入 CSS、图像和其他类型的文件。 **注意**:项目默认命名空间(在项目属性页的“应用程序”选项卡中定义)需要作为文件名的前缀添加到嵌入式资源中。
因此,除了暴露这些属性之外,控件真正做的就是覆盖 `onPreRender` 事件并将一些 JavaScript 注入到页面中。列表 3 中第 75 到 79 行注入了对嵌入式 JavaScript 文件的链接,它在页面源代码中显示为类似这样:
<script src="/WebResource.axd?d=j246orv_38DeKtGbza6y6A2&t=633439907968639849"
type="text/javascript"></script>
接下来,它动态生成一个脚本来创建一个虚拟表单对象的实例,传递 `VirtualForm` 服务器控件的客户端 ID 和我们希望它使用的按钮的客户端 ID,并将此注册为页面上的启动脚本。
1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Text;
5 using System.Web;
6 using System.Web.UI;
7 using System.Web.UI.WebControls;
8
9 // Script Resources
10 [assembly: WebResource("WilliaBlog.Net.Examples.VirtualForm_Debug.js",
"text/javascript")]
11 [assembly: WebResource("WilliaBlog.Net.Examples.VirtualForm_min.js",
"text/javascript")]
12
13 namespace WilliaBlog.Net.Examples
14 {
15
16 [ToolboxData("<{0}:VirtualForm runat=server></{0}:VirtualForm>")]
17 public class VirtualForm : System.Web.UI.WebControls.Panel
18 {
19 [Bindable(true), DefaultValue("")]
20 public string SubmitButtonId
21 {
22 get
23 {
24 string s = (string)ViewState["SubmitButtonId"];
25 if (s == null)
26 {
27 return string.Empty;
28 }
29 else
30 {
31 return s;
32 }
33 }
34 set { ViewState["SubmitButtonId"] = value; }
35 }
36
37 [DefaultValue(false)]
38 public bool UseDebug
39 {
40 get
41 {
42 string s = (string)ViewState["UseDebug"];
43 if (string.IsNullOrEmpty(s))
44 {
45 return false;
46 }
47 else
48 {
49 return s.ToLower() == "true";
50 }
51 }
52 set { ViewState["UseDebug"] = value; }
53 }
54
55 public VirtualForm() : base() { }
56
57 protected override void OnPreRender(System.EventArgs e)
58 {
59 if (!string.IsNullOrEmpty(this.SubmitButtonId))
60 {
61 Control theButton = this.FindControl(this.SubmitButtonId);
62 if ((theButton != null))
63 {
64 string resourceName;
65 if (this.UseDebug)
66 {
67 resourceName =
"WilliaBlog.Net.Examples.VirtualForm_Debug.js";
68 }
69 else
70 {
71 resourceName = "WilliaBlog.Net.Examples.VirtualForm_min.js";
72 }
73
74 ClientScriptManager cs = this.Page.ClientScript;
75
76 string scriptLocation =
cs.GetWebResourceUrl(this.GetType(), resourceName);
77 if (!cs.IsClientScriptIncludeRegistered("VirtualFormScript"))
78 {
79 cs.RegisterClientScriptInclude("VirtualFormScript",
scriptLocation);
80 }
81
82 // New script checks for "Sys" Object, if found
// events will be rewired after updatepanel refresh.
83 StringBuilder sbScript = new StringBuilder(333);
84 sbScript.AppendFormat("<script type=\"text/javascript\">{0}",
Environment.NewLine);
85 sbScript.AppendFormat(" // Ensure postback works after " +
"update panel returns{0}",
Environment.NewLine);
86 sbScript.AppendFormat(" function " +
"ResetEventsForMoreInfoForm() {{{0}",
Environment.NewLine);
87 sbScript.AppendFormat(" var vf_{0} = new WilliaBlog.Net." +
"Examples.VirtualForm(" +
"document.getElementById" +
"('{0}'),'{1}');{2}", this.ClientID,
theButton.ClientID, Environment.NewLine);
88 sbScript.AppendFormat(" }}{0}", Environment.NewLine);
89 sbScript.AppendFormat(" if (typeof(Sys) !== \"undefined\"){{{0}",
Environment.NewLine);
90 sbScript.AppendFormat(" Sys.WebForms.PageRequestManager." +
"getInstance().add_endRequest(" +
"ResetEventsForMoreInfoForm);{0}",
Environment.NewLine);
91 sbScript.AppendFormat(" }}{0}", Environment.NewLine);
92 sbScript.AppendFormat(" var vf_{0} = new WilliaBlog.Net." +
"Examples.VirtualForm(document.getElementById('{0}'),'{1}');{2}",
this.ClientID, theButton.ClientID, Environment.NewLine);
93 sbScript.AppendFormat("</script>");
94
95 string scriptKey = string.Format("initVirtualForm_" +
this.ClientID);
96
97 if (!cs.IsStartupScriptRegistered(scriptKey))
98 {
99 cs.RegisterStartupScript(this.GetType(), scriptKey,
sbScript.ToString(), false);
100 }
101 }
102 }
103 base.OnPreRender(e);
104 }
105 }
106 }
JavaScript
当然,大部分代码是 JavaScript。第 13 到 62 行只是创建了 `WilliaBlog` 命名空间,我从 Yahoo! 用户界面库 (YUI) 中“借用”了它。`WilliaBlog.Net.Examples.VirtualForm` 对象从第 65 行开始。本质上,它遍历父 `Div`(此 `div` 的 ID 作为参数传递给构造函数)中的每个输入控件(第 159-164 行),并为它们中的每一个分配一个 `onkeypress` 事件(`handleEnterKey`)。除了 Enter 键之外的所有按键都会透明地通过代码,但一旦检测到 Enter 键,就会取消默认行为,而是调用 `submitVirtual` 函数。该函数只是检查您提供的按钮是输入(图像或提交按钮)还是链接(超链接或链接按钮),并通过调用前者或后者的 `click()` 方法或导航到后者的 `href` 属性来模拟点击。`removeEvent` 和 `stopEvent` 方法实际上从未被调用,但我为了以防万一包含了它们。
1 /*********************************************************************
2 *
3 * File : VirtualForm_Debug.js
4 * Created : April 08
5 * Author : Rob Williams
6 * Purpose : This is the fully annotated, easy to understand
and modify version of the file,
however I would recommend you use
7 * something like the YUI Compressor
(http://developer.yahoo.com/yui/compressor/) to minimize load time.
8 * This file has its Build Action Property set to "Embedded Resource"
which embeds the file inside the dll, so we never have to
9 * worry about correctly mapping a path to it.
10 *
11 **********************************************************************/
12
13 if (typeof WilliaBlog == "undefined" || !WilliaBlog) {
14 /**
15 * The WilliaBlog global namespace object.
* If WilliaBlog is already defined, the
16 * existing WilliaBlog object will not be overwritten so that defined
17 * namespaces are preserved.
18 * @class WilliaBlog
19 * @static
20 */
21 var WilliaBlog = {};
22 }
23
24 /**
25 * Returns the namespace specified and creates it if it doesn't exist
26 * <pre>
27 * WilliaBlog.namespace("property.package");
28 * WilliaBlog.namespace("WilliaBlog.property.package");
29 * </pre>
30 * Either of the above would create WilliaBlog.property, then
31 * WilliaBlog.property.package
32 *
33 * Be careful when naming packages. Reserved words may work in some browsers
34 * and not others. For instance, the following will fail in Safari:
35 * <pre>
36 * WilliaBlog.namespace("really.long.nested.namespace");
37 * </pre>
38 * This fails because "long" is a future reserved word in ECMAScript
39 *
40 * @method namespace
41 * @static
42 * @param {String*} arguments 1-n namespaces to create
43 * @return {Object} A reference to the last namespace object created
44 */
45 WilliaBlog.RegisterNamespace = function() {
46 var a=arguments, o=null, i, j, d;
47 for (i=0; i<a.length; i=i+1) {
48 d=a[i].split(".");
49 o=WilliaBlog;
50
51 // WilliaBlog is implied, so it is ignored if it is included
52 for (j=(d[0] == "WilliaBlog") ? 1 : 0; j<d.length; j=j+1) {
53 o[d[j]]=o[d[j]] || {};
54 o=o[d[j]];
55 }
56 }
57
58 return o;
59 };
60
61 //declare the 'WilliaBlog.Net.Examples' Namespace
62 WilliaBlog.RegisterNamespace("WilliaBlog.Net.Examples");
63
64 //declare Virtual Form Object
65 WilliaBlog.Net.Examples.VirtualForm = function(formDiv,submitBtnId)
66 {
67 this.formDiv = formDiv; //The id of the div that represents our Virtual Form
68 this.submitBtnId = submitBtnId;
//The id of the button or Linkbutton that should be clicked when pushing Enter
69
70 // When using these functions as event delegates the
// this keyword no longer points to this object as it is out of context
71 // so instead, create an alias and call that instead.
72 var me = this;
73
74 this.submitVirtual = function()
75 {
76 var target = document.getElementById(me.submitBtnId);
77 //check the type of the target: If a button then call the click method.
78 if(target.tagName.toLowerCase() === 'input')
79 {
80 document.getElementById(me.submitBtnId).click();
81 }
82 //If a link button then simulate a click.
83 if(target.tagName === 'A')
84 {
85 window.location.href = target.href;
86 }
87 };
88
89 this.handleEnterKey = function(event){
90 var moz = window.Event ? true : false;
91 if (moz) {
92 return me.MozillaEventHandler_KeyDown(event);
93 } else {
94 return me.MicrosoftEventHandler_KeyDown();
95 }
96 };
97
98 //Mozilla handler (also Handles Safari)
99 this.MozillaEventHandler_KeyDown = function(e)
100 {
101 if (e.which == 13) {
102 e.returnValue = false;
103 e.cancel = true;
104 e.preventDefault();
// call the delegate function that
// simulates the correct button click
105 me.submitVirtual();
106 return false;
107 }
108 return true;
109 };
110
111 //IE Handler
112 this.MicrosoftEventHandler_KeyDown = function()
113 {
114 if (event.keyCode == 13) {
115 event.returnValue = false;
116 event.cancel = true;
// call the delegate function that simulates
// the correct button click
117 me.submitVirtual();
118 return false;
119 }
120 return true;
121 };
122
123 this.addEvent = function(ctl, eventType, eventFunction)
124 {
125 if (ctl.attachEvent){
126 ctl.attachEvent("on" + eventType, eventFunction);
127 }else if (ctl.addEventListener){
128 ctl.addEventListener(eventType, eventFunction, false);
129 }else{
130 ctl["on" + eventType] = eventFunction;
131 }
132 };
133
134 this.removeEvent = function(ctl, eventType, eventFunction)
135 {
136 if (ctl.detachEvent){
137 ctl.detachEvent("on" + eventType, eventFunction);
138 }else if (ctl.removeEventListener){
139 ctl.removeEventListener(eventType, eventFunction, false);
140 }else{
141 ctl["on" + eventType] = function(){};
142 }
143 };
144
145 this.stopEvent = function(e)
146 {
147 if (e.stopPropagation){
148 // for DOM-friendly browsers
149 e.stopPropagation();
150 e.preventDefault();
151 }else{
152 // For IE
153 e.returnValue = false;
154 e.cancelBubble = true;
155 }
156 };
157
158 //Grab all input elements within virtual form (contents of a div with divID)
159 this.inputs = this.formDiv.getElementsByTagName("input");
160
161 //loop through them and add the keypress event
//to each to listen for the enter key
162 for (var i = 0; i < this.inputs.length; i++){
163 this.addEvent(this.inputs[i],"keypress",this.handleEnterKey);
164 }
165 }
历史
- v1.0。