You can do a lot of home improvement with a hammer, screwdriver, and tape measurer; however, certain jobs require more specialized tools. Programming is no different. You can do almost anythign with the base toolset provided by Microsoft, but sometimes, to make life easier and code cleaner, developing your own Custom Controls are the way to go.
I’ll discuss how to develop ASP.NET custom controls by inheriting from an existing control and providing a bit of additional functionality. This is probably the easiest way to begin writing custom controls and can be very useful in your projects.
MyButton: A Better .NET Button
Let’s take a fairly simple example that can be used in nearly every web application you develop, the noble button. The button already comes in several flavors through .NET, such as a LinkButton, ImageButton, and the standard Button. However, it does have a few drawbacks that we’re going to aim to correct with our own custom button called MyButton.
The first drawback is that, by default, a button can be clicked several times causing a postback each time you click the button. For most pages, this is a non-issue because they’re fast enough the user does not have time to “double-post”. This does occur, however, when a long process runs in codebehind wired against the button being clicked. We’ll attempt to provide functionality so once the button is clicked, it will be disabled from the user to prevent multiple triggers of the button.
Next, we’ll want to fix a common problem when you request client-side confirmation of the button (javascript confirm() function), and even upon cancelling (negating the confirmation), the button posts regardless.
Finally, we’ll provide the ability to use a button as a simple redirecting url without causing a postback.
View the demo and see what we’re getting into ahead of time.
The MyButton Class
We’ll want to begin by create a new C# Class Library Project called CustomControls. Once you’ve created this, you’ll want to add a reference to System.Web, since, by default, class libraries do not reference web-specific controls. We can later add this class library reference to any of our projects which would like to consume the MyButton (and all other custom controls you may create).
Below is the MyButton Class in it’s entirety. I’ve added a number of properties to enhance the standard button (which as you can see, it is inherited from), such as:
- CSSClass
Specifies the CSS class of the button in a normal/default state. Overridden to provide a default CSSClass to match built in CSS. By default, this is MyButton_button.
- DisabledCSSClass
Specifies the CSS class of the button when it is in a disabled state. This is only visible if DisableButtonOnClick is true (default). By default, this is MyButton_buttonDisabled.
- DisableButtonOnClick
Disables the button after it is clicked. By default, this is true.
- DisableBuiltInCSS
Disables the built in CSS reference to be included (both in the control and on the page). By default, this is false.
- DisableBuiltInClientSideCode
Disables the built in clientside (javascript) code to be included (both in the control and on the page). By default, this is false.
- ControlPath
Specifies the path to the control external files from the current file location. By default, this is ~/Controls/MyButton/.
- IsSimpleRedirect
Specifies whether this button acts like a simple redirect (by calling window.location).
- AutoPostBack
Specifies whether the control will cause a postback. By default, this is true. If IsSimpleRedirect is true, AutoPostBack will be overridden as false.
- RequiresConfirmation
Specifies whether the button will show a confirmation dialog box and require the user to confirm before submitting. By default, this is false.
- ConfirmationText
If RequiresConfirmation is true, this specifies the text that will be shown to confirm the button click action. By default, this reads “Are you sure you want to proceed?”
The button will implement some javascript and have default CSS packaged with it to assist in the increased functionality and look a bit nicer. These files could have been made as embedded resources, but to make them easy to customize for each project, and to utilize resource caching, they’ve been maintained as external files and placed in a ControlPath directory, which is configurable.
using System; using System.Text; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.ComponentModel; using System.Web.UI.HtmlControls; namespace CustomControls { [AspNetHostingPermission(System.Security.Permissions.SecurityAction.Demand, Level=AspNetHostingPermissionLevel.Minimal), AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level=AspNetHostingPermissionLevel.Minimal), DefaultProperty("Text"), ToolboxData("<{0}:MyButton runat=\"server\"></{0}:MyButton>")] public class MyButton : Button { /// <summary> /// The default css filename for the control. /// </summary> const string baseCssFileName = "MyButton.css"; /// <summary> /// The default javascript filename for the control. /// </summary> const string baseJSFileName = "MyButton.js"; /// <summary> /// Flag to determine if the css tag has already been added or not. /// (so the css reference isn't added every time a control exists on the page.) /// </summary> private bool cssTagIsAdded = false; /// <summary> /// Flag to determine if the js script tag has already been added or not. /// (so the script tag isn't added every time a control exists on the page.) /// </summary> private bool jsTagIsAdded = false; /// <summary> /// Assign a CSS class to the control. /// </summary> [Category("Custom"), Description("The css class to apply to this button.")] public override string CssClass { get { if(string.IsNullOrEmpty(_cssClass)) { _cssClass = "MyButton_button"; } return _cssClass; } set { _cssClass = value; } } private string _cssClass; /// <summary> /// Assign a CSS class to the control when the control is in a disabled state. /// </summary> [Category("Custom"), Description("The css class to apply after the button is clicked.")] public string DisabledCssClass { get { if(string.IsNullOrEmpty(_disabledCssClass)) { _disabledCssClass = "MyButton_buttonDisabled"; } return _disabledCssClass; } set { _disabledCssClass = value; } } private string _disabledCssClass; /// <summary> /// Confirmation text to be displayed if the button requires confirmation. /// </summary> [Category("Custom"), Description("The confirmation question to be asked when the button is clicked and requires confirmation.")] public string ConfirmationText { get { if(string.IsNullOrEmpty(_confirmationText)) { _confirmationText = "Are you sure you want to proceed?"; } return _confirmationText; } set { _confirmationText = value; } } private string _confirmationText; /// <summary> /// The path to the external javascript, css, and image resources the defaults for this control uses. /// </summary> [Category("Custom"), Description("The path to the external javascript, css, and image resources the defaults for this control uses.")] public string ControlPath { get { if(string.IsNullOrEmpty(_controlPath)) { _controlPath = "~/Controls/MyButton/"; } if (!_controlPath.EndsWith("/")) { _controlPath += "/"; } return _controlPath; } set { _controlPath = value; } } private string _controlPath; /// <summary> /// Determines whether to use the built in css file and page reference for the control. /// By default, this is false. /// </summary> [Category("Custom"), Description("Determines whether to register a css class at the top of the html.")] public bool DisableBuiltInCSS { get { if (_disableBuiltInCSS == null) { _disableBuiltInCSS = false; } return (bool)_disableBuiltInCSS; } set { _disableBuiltInCSS = value; } } private bool? _disableBuiltInCSS; /// <summary> /// Determines whether to use the built in javascript file and page reference for the control. /// By default, this is false. /// </summary> [Category("Custom"), Description("Determines whether to register an external clientside file to the html.")] public bool DisableBuiltInClientSideCode { get { if (_disableBuiltInClientsideCode == null) { _disableBuiltInClientsideCode = false; } return (bool)_disableBuiltInClientsideCode; } set { _disableBuiltInClientsideCode = value; } } private bool? _disableBuiltInClientsideCode; /// <summary> /// Determines whether to run built in javascript code to disable the button after being clicked to /// prevent multiple postbacks by users. /// By default, this is true. /// </summary> [Category("Custom"), Description("Determines whether to disable the button after click or not.")] public bool DisableButtonOnClick { get { if (_disableButtonOnClick == null) { _disableButtonOnClick = true; } return (bool)_disableButtonOnClick; } set { _disableButtonOnClick = value; } } private bool? _disableButtonOnClick; /// <summary> /// Determines whether this button simply acts as a clientside redirect. /// </summary> [Category("Custom"), Description("Determines whether this button simply acts as a clientside redirect.")] public bool IsSimpleRedirect { get { if (_isSimpleRedirect == null) { _isSimpleRedirect = false; } return (bool)_isSimpleRedirect; } set { _isSimpleRedirect = value; } } private bool? _isSimpleRedirect; /// <summary> /// Redirect URL for simple clientside redirect. /// </summary> [Category("Custom"), Description("Redirect URL for simple clientside redirect.")] public string RedirectURL { get; set; } /// <summary> /// Determines whether clicking the button requires a confirmation. /// </summary> [Category("Custom"), Description("Determines whether clicking the button requires a confirmation.")] public bool RequiresConfirmation { get; set; } /// <summary> /// Determines whether this control should cause a postback. /// </summary> [Category("Custom"), Description("Should this control cause a postback?")] public bool AutoPostBack { get { if (_autoPostBack == null) { _autoPostBack = true; } return (bool)_autoPostBack; } set { _autoPostBack = value; } } private bool? _autoPostBack; protected override void OnPreRender(EventArgs e) { //Embed javascript tag if (this.DisableBuiltInClientSideCode == false && jsTagIsAdded == false) { string jsPath = this.ControlPath.Replace("~", Context.Request.ApplicationPath.TrimEnd('/')) + baseJSFileName; HtmlGenericControl js = new HtmlGenericControl("script"); js.Attributes["type"] = "text/javascript"; js.Attributes["src"] = jsPath; this.Page.Header.Controls.Add(js); jsTagIsAdded = true; } //Embed css tag if (this.DisableBuiltInCSS == false || (string.IsNullOrEmpty(this.CssClass) && this.CssClass != baseCssFileName) && cssTagIsAdded == false) { string cssPath = this.ControlPath.Replace("~", Context.Request.ApplicationPath.TrimEnd('/')) + baseCssFileName; HtmlLink cssLink = new HtmlLink(); cssLink.Href = cssPath; cssLink.Attributes.Add("rel", "stylesheet"); cssLink.Attributes.Add("type", "text/css"); this.Page.Header.Controls.Add(cssLink); cssTagIsAdded = true; } base.OnPreRender(e); } protected override void Render(HtmlTextWriter writer) { StringBuilder sb = new StringBuilder(); //If simple redirect and redirect url is provided add clientside redirect without postback if (this.IsSimpleRedirect && !string.IsNullOrEmpty(this.RedirectURL)) { //Add javascript to redirect page sb.Append("window.location='"); sb.Append(this.RedirectURL); sb.Append("';"); this.UseSubmitBehavior = false; this.AutoPostBack = false; } //If not a confirmation button, disables button to prevent double postbacks if (this.DisableButtonOnClick && this.RequiresConfirmation == false) { sb.Append("doDisable('"); sb.Append(base.ClientID); sb.Append("', '"); sb.Append(this.DisabledCssClass); sb.Append("', "); sb.Append(this.AutoPostBack.ToString().ToLower()); sb.Append(");"); } if (!string.IsNullOrEmpty(this.OnClientClick)) { sb.Append(this.OnClientClick); } //If requires confirmation, else if clientside button only and no postback should occur if (this.RequiresConfirmation) { this.UseSubmitBehavior = false; this.AutoPostBack = false; sb.Append(string.Format("var val = confirm('{0}'); if(val) __doPostBack('{1}','');", this.ConfirmationText, this.ClientID)); } else if ((this.IsSimpleRedirect && !string.IsNullOrEmpty(this.RedirectURL)) || this.AutoPostBack == false) { //Add javascript to disable the postback that occurs on ASP.NET buttons sb.Append("return false;"); } writer.AddAttribute("class", this.CssClass); writer.AddAttribute("onclick", sb.ToString()); base.Render(writer); } } }
The Render method is where most of the logic occurs based on the properties provided to the control. The Render method is where I’m injecting the Javascript to help with much of the new functionality.
Miscellaneous MyButton Files
As mentioned, I’m using external files for the javascript and CSS. Here are the files as I currently have them. They are completely open to be configured to add additional features or customize based on project.
Client-side code (javascript) – MyButton.js
/* Set's timer to call doDisableButton() after it is clicked, the reason this fires instead of directly calling doDisableButton() is because a disabled control will not postback, so there needs to be a slight delay. param id = the id of the button being disabled. param css = the css class being used for the disabled state (visual appearance). param submit = bool flag whether to submit form or not */ function doDisable(id, css, submit) { if(submit) { //document.forms[0].submit(); //removed to allow AJAX calls document.getElementById(id).click(); } window.setTimeout("doDisableButton('" + id + "', '" + css + "')", 0); } /* Disables the button and sets the button's css class to a disabled state. param id = the id of the button being disabled. param css = the css class being used for the disabled state (visual appearance). */ function doDisableButton(id, css) { var obj = document.getElementById(id); obj.disabled = true; obj.className = css; }
CSS – MyButton.css
/*The button control - normal state*/ .MyButton_button { background-color: #000033; background-image: url("bg_nav.gif"); background-repeat: repeat; font-size: 10px; font-weight: bold; color: #FFFFFF; border: 1px solid; padding-top: 2px; padding-bottom: 2px; cursor: pointer; } /*The button control - hover state*/ .MyButton_button:hover { /*background-image: url("bg_nav_red.gif"); -- Uncomment this when we have a hover state to add by default*/ color: #FFFFCC; } /*The button control - after clicked if disable button is turned on*/ .MyButton_buttonDisabled { background-color: #CCC; background-image: none; font-size: 10px; font-weight: bold; color: #FFFFFF; border: 1px solid; padding-top: 2px; padding-bottom: 2px; cursor: pointer; }
You’ll notice in the CSS there is also a hover state. I added this to demonstrate that even though there is not an explicit CSS class name property in the control for hoverstate, additional flexibility can be included through the CSS.
These files will need to reside in the application which is consuming your custom control, so you will need to create a Controls\MyButton directory (or similar if pointing to another directory through the ControlPath property). I find it also useful to create this in the C# class library project to maintain the original files even thought they are not technically used from there.
You will want to also include any graphics that you are calling from your CSS file in this directory as well. In this case, we have two, one for each state of the button.
Let’s Take it for a Spin
Let’s begin by setting up a quick demo page using our new custom button control. The following is a couple of scenarios that highlight the use of the custom button as opposed to the regular button you would normally use.
Feel free to modify the code as much as you like to provide a working template of various button uses in your own applications.
<asp:Label ID="TimeLabel" runat="server"></asp:Label><br /> <p>The above label helps to demonstrate whether a postback is ocurring or not in the following examples.</p> <hr /> <cc1:MyButton ID="MyButton1" runat="server" Text="Plain Button" /><br /> <p>This is the default button which causes a server-side post back.</p> <hr /> <cc1:MyButton ID="MyButton2" runat="server" Text="Confirmation Button" RequiresConfirmation="true" ConfirmationText="Are you sure you like waffles?" /><br /> <p>This button causes a server-side post back if the confirmation question is answered positively. If not confirmed, no postback will occur fixing the ASP.NET button issue for posting upon not confirmed response.</p> <hr /> <cc1:MyButton ID="MyButton3" runat="server" Text="Redirect to Google" IsSimpleRedirect="true" RedirectURL="http://www.google.com" /><br /> <p>This button acts to redirect to a url. It does not cause a postback to occur.</p> <hr /> <cc1:MyButton ID="MyButton4" runat="server" Text="Clientside Hello World - No Postback" AutoPostBack="false" OnClientClick="alert('Hello World');" DisableButtonOnClick="false" /><br /> <p>This button does not cause a postback and instead fires any clientside code. In this example, a hello world alertbox.</p> <hr /> <cc1:MyButton ID="MyButton5" runat="server" Text="No Double Postback" /><br /> <p>This button demonstrates the default behavior of disabling the button after clicking it, to eliminate a double-post back scenario on long server-side processing. A 2 second sleep is called when clicking this button.</p>
Don’t forget to register your new control on your page like the following: (Note: This may be in a different namespace depending on where you’ve created the custom controls project.)
<%@ Register Assembly="CustomControls" Namespace="CustomControls" TagPrefix="cc1" %>
We’ll also need just a slight bit of code behind (written in VB) to help highlight the features we’re demonstrating. The code here is not necessary for using the buttons.
First, we are going to assign the current timestamp to the TimeLabel label everytime the page loads so we can easily track when a postback is ocurring and when it is not. Second, for our example #5, “No Double Postback”, we’re going to add a time delay so we can show the button being disabled and disallowing users to click causing multiple postbacks during lenthy server-side processes.
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load TimeLabel.Text = Now() End Sub Protected Sub MyButton5_Click(ByVal sender As Object, ByVal e As EventArgs) Handles MyButton5.Click Threading.Thread.Sleep(2000) End Sub
At this point, you should have a completely functioning collection of demo’s for using the MyButton custom control and a pretty good idea on how to begin developing your own custom controls.
Keep in mind, in this scenario, we’ve built custom code onto a pre-existing control (the ASP.NET button control); however, if we need even greater customization, we can create custom controls from scratch, a topic I’ll attempt to cover down the road.
Good luck in your next custom coding control project, and please leave any suggestions or questions in the comments below!