Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

A Virtual Form Web Custom Control

0.00/5 (No votes)
23 Jan 2009CPOL6 min read 20.6K   166  
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).

Listing 1
XML
<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
XML
<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:

HTML
<script src="/WebResource.axd?d=j246orv_38DeKtGbza6y6A2&amp;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
C#
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.

Listing 4
JavaScript
    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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)