ControlFreak
LANGUAGES: C#
ASP.NET VERSIONS: 1.0 | 1.1
Inheritand Extend
CreatePowerful Custom Web Controls
By Steve C.Orr
If acontrol didn't work the way you wanted it to in the old ActiveX days, you wereout of luck. Happily, the situation has improved. But before I go any further...
Welcome to Control Freak!I'm Steve Orr andI'll be your host. In the coming months this column will be dedicated toproviding you with all the information you need to use and create Web controls of all kinds to stimulate your development andspur you on to success. Over time, I intend toalternate between VB.NET and C# in a vain attempt to try to keep everyone satisfied. Let me know what you like -and what you don't - soyou'll get what you want! Now, as I was saying...
The Webcontrols offered with ASP.NET provide impressive functionality, but they mightnot always support the features you desire. You can solve this problem bycreating your own Web controls that encapsulate the functionality you need.That, however, can be a lot of work. This article explains how to minimize yourefforts and maximize the results by extending pre-existing controls. It alsoexplains how to emit client-side JavaScript functions to create robust,user-friendly controls.
Refiningthe Textbox
TheASP.NET TextBox control works well for general text entry, but what ifyou want to get more specific, such as limiting entry to just numbers? To dothis you need a function to filter out non-numeric characters, and you needthis function to be called every time the user enters a character into thetextbox. It would be convenient to do this with server-side code, but thatwould require a round trip to the server every time the user presses a key.This would be slow and inefficient. Client-side JavaScript is needed, insteadof server-side code, to make such a scenario work gracefully. For instance,this snippet of HTML and JavaScript works great:
<asp:textbox
runat="server"OnKeyPress="ValidateNumeric()"/>
<scriptlanguage="Javascript">
function ValidateNumeric()
{
var keyCode = window.event.keyCode;
if (keyCode > 57 || keyCode < 48)
window.event.returnValue = false;
}
</script>
Becausethe ASCII codes 48 through 57 correspond to the numeric keys 0 through 9, thiscode filters out any other keys by returning false if they're found. Ifyou want to allow decimal points, you should also allow keyCode 46, which represents the decimal point.
To makethis code more reusable and maintainable it should be turned into a custom Webcontrol. This new control should encapsulate all the functionality of theexisting textbox, and it should emit the above mix of HTML and JavaScript. Theserver-side C# code shown in Figure 1 does just that.
using System;
usingSystem.Web.UI;
usingSystem.Web.UI.WebControls;
usingSystem.ComponentModel;
namespaceASPNETPRO
{
public class NumericTextbox:
System.Web.UI.WebControls.TextBox
{
protected override voidOnPreRender(EventArgs e)
{
if(!this.Page.IsClientScriptBlockRegistered(
"ValidateNumericScript"))
this.Page.RegisterClientScriptBlock(
"ValidateNumericScript",
"<scriptlanguage=javascript>" +
"functionValidateNumeric(){" +
"var keyCode =window.event.keyCode;" +
"if (keyCode > 57 || keyCode < 48)" +
"window.event.returnValue =false;}</script>");
Attributes.Add("onKeyPress","ValidateNumeric()");
base.OnPreRender(e);
}
public override string Text
{
get {return(base.Text);}
set {try{
base.Text=Convert.ToInt32(value).ToString();}
catch{};}
}
}
}
Figure1: This NumericTextboxextends the basic TextBox control by adding both client-side andserver-side validation to ensure only numbers are entered.
Thisexample starts by inheriting from the standard TextBox control. Thisautomatically gives you all the standard TextBox functionality. The OnPreRenderevent is overridden, so the necessary client-side JavaScript can be emitted.The ValidateNumeric JavaScript function is output via the RegisterClientScriptBlockmethod of the page (unless it has been output by another instance of thiscontrol) and it's linked to the client-side OnKeyPress event that ispresent in the object model of Internet Explorer 4.0 and above. (By the way,when overriding a method, you'll almost always want to call the equivalentmethod of the base class so it will continue to implement any functionalitythat may be within that base class method. Forgetting this step can cause bugsthat are difficult to track down.)
It'sgood practice to perform validation on the client and server whenever possible,because client-side script support varies in different browsers. Therefore, theserver-side Text property is also overridden to ensure non-numeric datadoes not enter the control through any means, and that down-level browsers willstill be supported at least through server-side validation. In this case,invalid entries are ignored, although you might choose to raise an error undersuch circumstances. The VB.NET IsNumeric function would work great hereto avoid raising inefficient errors. Unfortunately, C# has no equivalentfunction. As an optimization, you might consider writing a comparable function,or referencing the VB.NET library to use its IsNumeric function.
ImproveYour Image
The Imagecontrol displays images on your Web page well enough; however, it provides nofunctionality for rollover effects. You're on your own if you want to changethe image as the user moves the mouse over it. But don't worry; I'm here foryou. The code sample in Figure 2 shows how to inherit from the standard Imagecontrol and add some basic rollover support.
using System;
usingSystem.Web;
usingSystem.Web.UI;
usingSystem.ComponentModel;
namespace ASPNETPRO
{
[DefaultProperty("ImageURL"),ToolboxData(
"<{0}:ImageRolloverrunat=server></{0}:ImageRollover>")]
public class ImageRollover:
System.Web.UI.WebControls.Image
{
private string s;
[Bindable(true),Category("Appearance"),
DefaultValue(""), Editor(typeof(
System.Web.UI.Design.ImageUrlEditor),
typeof(System.Drawing.Design.UITypeEditor))]
public virtual string RolloverImageUrl
{
get
{
string s =(string)ViewState["RolloverImageUrl"];
return((s == null) ? String.Empty :s);
}
set
{
s=value;
ViewState["RolloverImageUrl"] = s;
Attributes.Add("onMouseOver", "this.src='"+s+"'");
}
}
public override string ImageUrl
{
get {return(base.ImageUrl);}
set {base.ImageUrl=value;
Attributes.Add("onMouseOut","this.src='"
+ base.ImageUrl + "'");
}
}
}
Figure2: By inheritingfrom the standard Image Web control, you can create a basic ImageRollovercontrol with very little code.
Theexample in Figure 2 starts by inheriting from the standard Imagecontrol. This gives you a lot of functionality for free. Then a private stringvariable (named ImageUrl) is declared to hold the value for the new propertyyou're adding. At design time you want this property to behave much like thestandard ImageUrl property from the Image Web control. To thisend, a few standard attributes are present, such as the "Appearance" categoryto make sure our new property is grouped together with the existing ImageUrlproperty in the property window. The "Editor" is also specified to ensure theellipsis button is displayed in the property window for the control at designtime, just as the ImageUrl property is. When this button is clicked, itopens a useful standard dialog box for choosing images.
In theget/set blocks of the RolloverImageURL property, you'll notice the valueis stored in ViewState so it willpersist between postbacks. Additionally, whenever this property is modified,the appropriate client-side JavaScript is outputted to make the image changewhen the mouse moves over the image. This is done in the client-side onMouseOverevent that is present in the object model of Internet Explorer version 4.0 andabove.
Ofcourse, the image needs to change back to the original image again, once themouse is no longer hovering over the image. To do this, the ImageUrlproperty is overridden to add similar JavaScript to the HTML output of thecontrol. The base control's ImageUrl property is used to manage ViewState for this property.
It'sworth noting that you can easily change the ImageRollover control to an ImageButtonRollovercontrol by simply changing the word "Image" to "ImageButton" throughout thecode in Figure 2.
At thispoint you should be able to create a new Web Control Library project, add thecode in Figure 2, and compile it into a DLL. You can then add the control toyour toolbox, drop it onto any Web form, and live happily ever after.
Cache Flow
Becausethe control appears to work well, I suppose this article could end right here.Ah, but not so fast! If you try using the control from a remote client for thefirst time, you'll notice a slight delay when you move your mouse over theimage before it changes. This delay is awkward and unprofessional. Just toconfuse things, the second time you try this on the same remote client thedelay won't be there.
Why isthis happening? The first time the mouse moves over the image, the browserrequests the mouse-over image from the remote server, thus causing the delay.The second time you move your mouse over the image, the browser simply grabsthe image out of the local cache, so there is no visible delay. For debuggingpurposes you can repeat this delay by clearing your browser's cache betweenpage visits.
So howdo you prevent this delay from happening when a user first visits your page?The only feasible way is to pre-fetch the image and cache it yourself, so it'sready and waiting the first time the user moves their mouse over the initialimage. This is generally done with client-side JavaScript. A handy way tooutput such JavaScript is with the RegisterStartupScript method of the Pageobject. Unlike the RegisterClientScriptBlock method, code that is outputwith RegisterStartupScript is intended to be executed as soon as it'sloaded into the browser. Consider this code snippet that will be added to therollover control:
protectedoverride void OnPreRender(EventArgs e)
{
this.Page.RegisterStartupScript("MyImageKey",
"<scriptlanguage=javascript>MyImage='"+s+"'</script>");
base.OnPreRender(e);
}
Byoverriding the OnPreRender event of the underlying Image control,the required client-side script can be emitted. This script immediately loadsthe image and holds it in a client-side variable named MyImage. Then theclient-side onMouseOver event can be modified to change the image to thevalue of this variable. Here's the modified server-side code that emits thatclient-side script:
Attributes.Add("onMouseOver","this.src=MyImage");
Compilethe code, clear your cache, and you'll see the delay is now gone. However, anew problem is created that you'll only notice if you have more than oneinstance of the control on your page containing different images. In thissituation, all the controls will end up using the same rollover image, whichprobably does not meet your requirements.
ConflictingGoals
Theproblem is that all your controls are referencing the same client sidevariable, MyImage. Obviously this variable can only contain one image,so the controls are conflicting with each other. The solution is to use uniquevariable names for each instance of the control. One of the easiest ways toaccomplish this is to use the unique client-side ID that ASP.NET automatically generatesfor every control. In Figure 3, that name is concatenated with the MyImagevariable to keep it unique. Additionally, all the JavaScript generation codehas also been moved to the OnPreRender event to keep things easy tomaintain.
protectedoverride void OnPreRender(EventArgs e)
{
Attributes.Add("onMouseOver","this.src=MyImage" +
this.ClientID);
Attributes.Add("onMouseOut","this.src='" +
base.ImageUrl + "'");
this.Page.RegisterStartupScript("MyImageKey" +
this.ClientID, "<scriptlanguage=javascript>MyImage" +
this.ClientID + "='" + s +"'</script>");
base.OnPreRender(e);
}
Figure3: This version ofthe OnPreRender event combines all the JavaScript generation code intoone place for improved maintainability.
If younow run your project and view the HTML that is output to the browser, you'llnotice the variable is named MyImageImageRollover1. If you have a secondinstance of the control on your page, you'll also notice a variable named MyImageImageRollover2.
Listing Onecontains the source code for this improved and complete version of the ImageRollovercontrol. It also includes the source code for a nearly identical ImageButtonRollovercontrol.
You nowhave the source code for three useful Web controls that can be used across manyprojects. Let me know if you decide to enhance these controls further; I'd loveto hear about your new features. Hopefully you now also have a fundamentalunderstanding of inheritance, custom Web controls, and interaction withclient-side JavaScript. Good luck and happy coding!
Thesample code in this article is available for download.
Steve C.Orr is a MicrosoftMVP in ASP.NET and an MCSD. He's been programming professionally in the Seattlearea for more than 10 years. He's worked on numerous projects with Microsoftand currently works with such companies as Able Consulting, LUMEDX, and TheCadmus Group, Inc. Find him at http://Steve.Orr.net or e-mail him at mailto:Steve@Orr.net.
Begin Listing One - ImageRollover and ImageButtonRollover
using System;
using System.Web;
using System.Web.UI;
using System.ComponentModel;
namespace ASPNETPRO
{
///<summary>
/// ImageRolloverControl
///</summary>
[DefaultProperty("ImageURL"),
ToolboxData(
"<{0}:ImageRolloverrunat=server></{0}:ImageRollover>")]
public class ImageRollover :
System.Web.UI.WebControls.Image
{
private string s;
///<devdoc>
/// <para>Gets or sets
/// the URL reference to the image to display
/// when the mouse is moved over the image.</para>
///</devdoc>
[Bindable(true),Category("Appearance"),
DefaultValue(""), Editor(typeof(
System.Web.UI.Design.ImageUrlEditor),
typeof(System.Drawing.Design.UITypeEditor))]
public virtual stringRolloverImageUrl
{
get
{
strings = (string)ViewState["RolloverImageUrl"];
return((s == null) ? String.Empty : s);
}
set
{
s = value;
ViewState["RolloverImageUrl"] = s;
}
}
public override stringImageUrl
{
get {return(base.ImageUrl);}
set {base.ImageUrl = value;}
}
protected override voidOnPreRender(EventArgs e)
{
Attributes.Add("onMouseOver","this.src=MyImage" +
this.ClientID);
Attributes.Add("onMouseOut","this.src='" +
base.ImageUrl+ "'");
this.Page.RegisterStartupScript("MyImageKey"+
this.ClientID,
"<scriptlanguage=javascript>MyImage" +
this.ClientID+ "='" + s + "'</script>");
base.OnPreRender(e);
}
}
///<summary>
/// ImageRolloverButtonControl
///</summary>
[DefaultProperty("ImageURL"),ToolboxData(
"<{0}:ImageRolloverButton " +
"runat=server></{0}:ImageRolloverButton>")]
public class ImageRolloverButton :
System.Web.UI.WebControls.ImageButton
{
private string s;
///<devdoc>
/// <para>Gets or sets the URL reference
/// to the image to display when the mouse
/// is moved over the image.</para>
///</devdoc>
[Bindable(true),Category("Appearance"),
DefaultValue(""), Editor(typeof(
System.Web.UI.Design.ImageUrlEditor), typeof(
System.Drawing.Design.UITypeEditor))]
public virtual stringRolloverImageUrl
{
get
{
strings = (string)ViewState["RolloverImageUrl"];
return((s== null) ? String.Empty : s);
}
set
{
s=value;
ViewState["RolloverImageUrl"] = s;
}
}
public override stringImageUrl
{
get {return(base.ImageUrl);}
set {base.ImageUrl=value;}
}
protected override voidOnPreRender(EventArgs e)
{
Attributes.Add("onMouseOver","this.src=MyImage" +
this.ClientID);
Attributes.Add("onMouseOut","this.src='" +
base.ImageUrl+ "'");
this.Page.RegisterStartupScript("MyImageKey"+
this.ClientID,"<scriptlanguage=javascript>MyImage"
+ this.ClientID+ "='" + s + "'</script>");
base.OnPreRender(e);
}
}
}
End ListingOne