Introduction
I recently ran into an issue where a site I was developing had form fields in the header area of the page (for logging in or searching) and that if I had my cursor in a form field further down the page and I hit the Enter key, it was the click event of the first button on the page (the button in the header, rather than the button I wanted it to use) that fired. The setup was a common one: the master page contains a form
tag with a runat="server"
attribute and that form wraps the entire content of the page. It all seemed to work fine, until I found myself on the registration page and instead of clicking on the Register button, I just hit Enter. Instead of registering me, a message appeared to explain that a username and password were required. Since those fields also exist on the registration form, I was puzzled, more so when I used the mouse to click on the 'Register Now' button and it all worked. Stepping through the code, I quickly realized what was happening, and rummaged around my code archives until I found some JavaScript I wrote a couple of years ago that would listen for the Enter key, cancel the default behavior, and call another method. I added this snippet to the page, and all was well, until I found another page with the same issue. It was at that point I thought "wouldn't it be nice if there was a control - like a panel control - that you could simply be used to wrap some input controls, set a single property (to the ID of the control that should be 'clicked' when the Enter key is pushed), and that was all you needed to do?".
Well now, there is such a control.
Edit: Actually, as Onskee1 points out in the comments below, the standard ASP Panel
control has a "DefaultButton
" property which implements similar functionality; however, it only allows you to use ASP Button
controls as your designated button. If you want to use an ASP LinkButton
control or some other type of control as your default button, it does nothing for you. So, if you are using ASP Button
controls exclusively, I recommend you use that property. If not, then read on...
Using the code
You can register the control on a page by page basis as needed, or you can add it to the <Pages>
section of the web.config file to make it available to any page on the site (Listing 1).
Listing 1
<pages enableeventvalidation="true"
enableviewstatemac="true" enablesessionstate="true">
<controls>
<add assembly="WilliaBlog.Net.Examples"
namespace="WilliaBlog.Net.Examples" tagprefix="WBN">
</add>
</controls>
</pages>
Then, add the control to the page in such a way that it wraps your inputs:
Listing 2
<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>
As you can see from Listing 2, you should use the server side ID of the button (this will be automatically converted to the client-side ID by the virtual form control). Test it live.
How it works
The actual server side Virtual Form web custom control is really very simple. It inherits System.Web.UI.WebControls.Panel
and contains a property to allow you to set the ID of the Button
or LinkButton
you want to push when hitting Enter. I chose to embed the JavaScript file into the DLL for the sake of portability. (Originally, I had it in an external .JS file that was added to the header in the master page, but if I - or somebody else - wanted to use this control on another site, that adds an additional layer of complexity in the setup - they have to remember to load that JavaScript file, and while we could host it in a central location for all sites to share, embedding the file in the DLL seemed the wisest choice.) The downside to this technique is that the js file has to travel over the wire every time the page is requested, and cannot therefore be cached on the client, which will negatively impact any low-bandwidth users of the web application. For that reason, I have used the YUI Compressor to strip out all the comments and additional whitespace, and included two versions of the script in the DLL. When you set the property UseDebug
to true, it will use the long verbose version which makes it much easier to debug in firebug, but when it is all working, use the compressed version by omitting this property from the control declaration or by setting it to false.
To make files accessible from your server control’s assembly, simply add the file to your project, go to the Properties pane, and set the Build Action to Embedded Resource. To expose the file to a web request, you need to add code like lines 7 and 8 in Listing 3 below. These entries expose the embedded resource so that the ClientScriptManager
can both get to the file and know what kind of file it is. You could also embed CSS, images, and other types of files in this way. Note: The project’s default namespace (as defined in the Application tab of the project's Properties page) needs to be added as a prefix to the filename of the embedded resources.
So, besides exposing these properties, all the control really does is override the onPreRender
event and inject some JavaScript into the page. Lines 75 to 79 in Listing 3 inject the link to the embedded JavaScript file, which appears in the page source as something like this:
<script src="/WebResource.axd?d=j246orv_38DeKtGbza6y6A2&t=633439907968639849"
type="text/javascript"></script>
Next, it dynamically generates a script to create a new instance of the virtual form object, passing in the clientid of the VirtualForm
server control and the clientid of the button we want it to use, and register this as a startup script on the page.
Listing 3
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
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
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 }
The JavaScript
Most of the code is, of course, JavaScript. Lines 13 to 62 simply create the WilliaBlog
namespace and I 'borrowed' it from the The Yahoo! User Interface Library (YUI). The WilliaBlog.Net.Examples.VirtualForm
object begins on line 65. Essentially, it loops through every input control (lines 159-164) within the parent Div
(the ID of this div
is passed as an argument to the constructor) and assigns an onkeypress
event (handleEnterKey
) to each of them. All keystrokes other than the Enter key pass through the code transparently, but as soon as Enter is detected, the default behavior is cancelled and the submitVirtual
function is called instead. That function simply checks to see if the button you supplied is an input (image or submit button) or an anchor (hyperlink or link button), and simulates a click on it, either by calling the click()
method of the former or by navigating to the href
property of the latter. The removeEvent
and stopEvent
methods are never actually called, but I included them for good measure.
Listing 4
1
12
13 if (typeof WilliaBlog == "undefined" || !WilliaBlog) {
14
21 var WilliaBlog = {};
22 }
23
24
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
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
62 WilliaBlog.RegisterNamespace("WilliaBlog.Net.Examples");
63
64
65 WilliaBlog.Net.Examples.VirtualForm = function(formDiv,submitBtnId)
66 {
67 this.formDiv = formDiv;
68 this.submitBtnId = submitBtnId;
69
70
71
72 var me = this;
73
74 this.submitVirtual = function()
75 {
76 var target = document.getElementById(me.submitBtnId);
77
78 if(target.tagName.toLowerCase() === 'input')
79 {
80 document.getElementById(me.submitBtnId).click();
81 }
82
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
99 this.MozillaEventHandler_KeyDown = function(e)
100 {
101 if (e.which == 13) {
102 e.returnValue = false;
103 e.cancel = true;
104 e.preventDefault();
105 me.submitVirtual();
106 return false;
107 }
108 return true;
109 };
110
111
112 this.MicrosoftEventHandler_KeyDown = function()
113 {
114 if (event.keyCode == 13) {
115 event.returnValue = false;
116 event.cancel = true;
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
149 e.stopPropagation();
150 e.preventDefault();
151 }else{
152
153 e.returnValue = false;
154 e.cancelBubble = true;
155 }
156 };
157
158
159 this.inputs = this.formDiv.getElementsByTagName("input");
160
161
162 for (var i = 0; i < this.inputs.length; i++){
163 this.addEvent(this.inputs[i],"keypress",this.handleEnterKey);
164 }
165 }
History