A Virtual Form Web Custom Control
Ever think "wouldn't it be nice if there was a control - like a panel control - that you could simply use 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
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).
<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:
<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.
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 }
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.
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 }
History
- v1.0.