ControlFreak
LANGUAGES: C#
ASP.NET VERSIONS: 1.0 | 1.1
Inherit
and Extend
Create
Powerful Custom Web Controls
By Steve C.
Orr
If a
control didn't work the way you wanted it to in the old ActiveX days, you were
out of luck. Happily, the situation has improved. But before I go any further...
Welcome to Control Freak!
I'm Steve Orr and
I'll be your host. In the coming months this column will be dedicated to
providing you with all the information you need to use and create Web controls of all kinds to stimulate your development and
spur you on to success. Over time, I intend to
alternate 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 - so
you'll get what you want! Now, as I was saying...
The Web
controls offered with ASP.NET provide impressive functionality, but they might
not always support the features you desire. You can solve this problem by
creating your own Web controls that encapsulate the functionality you need.
That, however, can be a lot of work. This article explains how to minimize your
efforts and maximize the results by extending pre-existing controls. It also
explains how to emit client-side JavaScript functions to create robust,
user-friendly controls.
Refining
the Textbox
The
ASP.NET TextBox control works well for general text entry, but what if
you want to get more specific, such as limiting entry to just numbers? To do
this you need a function to filter out non-numeric characters, and you need
this function to be called every time the user enters a character into the
textbox. It would be convenient to do this with server-side code, but that
would 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, instead
of 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()"/>
<script
language="Javascript">
function ValidateNumeric()
{
var keyCode = window.event.keyCode;
if (keyCode > 57 || keyCode < 48)
window.event.returnValue = false;
}
</script>
Because
the ASCII codes 48 through 57 correspond to the numeric keys 0 through 9, this
code filters out any other keys by returning false if they're found. If
you want to allow decimal points, you should also allow keyCode 46, which represents the decimal point.
To make
this code more reusable and maintainable it should be turned into a custom Web
control. This new control should encapsulate all the functionality of the
existing textbox, and it should emit the above mix of HTML and JavaScript. The
server-side C# code shown in Figure 1 does just that.
using System;
using
System.Web.UI;
using
System.Web.UI.WebControls;
using
System.ComponentModel;
namespace
ASPNETPRO
{
public class NumericTextbox:
System.Web.UI.WebControls.TextBox
{
protected override void
OnPreRender(EventArgs e)
{
if
(!this.Page.IsClientScriptBlockRegistered(
"ValidateNumericScript"))
this.Page.RegisterClientScriptBlock(
"ValidateNumericScript",
"<script
language=javascript>" +
"function
ValidateNumeric(){" +
"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{};}
}
}
}
Figure
1: This NumericTextbox
extends the basic TextBox control by adding both client-side and
server-side validation to ensure only numbers are entered.
This
example starts by inheriting from the standard TextBox control. This
automatically gives you all the standard TextBox functionality. The OnPreRender
event is overridden, so the necessary client-side JavaScript can be emitted.
The ValidateNumeric JavaScript function is output via the RegisterClientScriptBlock
method of the page (unless it has been output by another instance of this
control) and it's linked to the client-side OnKeyPress event that is
present 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 equivalent
method of the base class so it will continue to implement any functionality
that may be within that base class method. Forgetting this step can cause bugs
that are difficult to track down.)
It's
good practice to perform validation on the client and server whenever possible,
because client-side script support varies in different browsers. Therefore, the
server-side Text property is also overridden to ensure non-numeric data
does not enter the control through any means, and that down-level browsers will
still be supported at least through server-side validation. In this case,
invalid entries are ignored, although you might choose to raise an error under
such circumstances. The VB.NET IsNumeric function would work great here
to avoid raising inefficient errors. Unfortunately, C# has no equivalent
function. As an optimization, you might consider writing a comparable function,
or referencing the VB.NET library to use its IsNumeric function.
Improve
Your Image
The Image
control displays images on your Web page well enough; however, it provides no
functionality for rollover effects. You're on your own if you want to change
the image as the user moves the mouse over it. But don't worry; I'm here for
you. The code sample in Figure 2 shows how to inherit from the standard Image
control and add some basic rollover support.
using System;
using
System.Web;
using
System.Web.UI;
using
System.ComponentModel;
namespace ASPNETPRO
{
[DefaultProperty("ImageURL"),
ToolboxData(
"<{0}:ImageRollover
runat=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 + "'");
}
}
}
Figure
2: By inheriting
from the standard Image Web control, you can create a basic ImageRollover
control with very little code.
The
example in Figure 2 starts by inheriting from the standard Image
control. This gives you a lot of functionality for free. Then a private string
variable (named ImageUrl) is declared to hold the value for the new property
you're adding. At design time you want this property to behave much like the
standard ImageUrl property from the Image Web control. To this
end, a few standard attributes are present, such as the "Appearance" category
to make sure our new property is grouped together with the existing ImageUrl
property in the property window. The "Editor" is also specified to ensure the
ellipsis button is displayed in the property window for the control at design
time, just as the ImageUrl property is. When this button is clicked, it
opens a useful standard dialog box for choosing images.
In the
get/set blocks of the RolloverImageURL property, you'll notice the value
is stored in ViewState so it will
persist between postbacks. Additionally, whenever this property is modified,
the appropriate client-side JavaScript is outputted to make the image change
when the mouse moves over the image. This is done in the client-side onMouseOver
event that is present in the object model of Internet Explorer version 4.0 and
above.
Of
course, the image needs to change back to the original image again, once the
mouse is no longer hovering over the image. To do this, the ImageUrl
property is overridden to add similar JavaScript to the HTML output of the
control. The base control's ImageUrl property is used to manage ViewState for this property.
It's
worth noting that you can easily change the ImageRollover control to an ImageButtonRollover
control by simply changing the word "Image" to "ImageButton" throughout the
code in Figure 2.
At this
point you should be able to create a new Web Control Library project, add the
code in Figure 2, and compile it into a DLL. You can then add the control to
your toolbox, drop it onto any Web form, and live happily ever after.
Cache Flow
Because
the 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 the
first time, you'll notice a slight delay when you move your mouse over the
image before it changes. This delay is awkward and unprofessional. Just to
confuse things, the second time you try this on the same remote client the
delay won't be there.
Why is
this happening? The first time the mouse moves over the image, the browser
requests 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 grabs
the image out of the local cache, so there is no visible delay. For debugging
purposes you can repeat this delay by clearing your browser's cache between
page visits.
So how
do 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's
ready and waiting the first time the user moves their mouse over the initial
image. This is generally done with client-side JavaScript. A handy way to
output such JavaScript is with the RegisterStartupScript method of the Page
object. Unlike the RegisterClientScriptBlock method, code that is output
with RegisterStartupScript is intended to be executed as soon as it's
loaded into the browser. Consider this code snippet that will be added to the
rollover control:
protected
override void OnPreRender(EventArgs e)
{
this.Page.RegisterStartupScript("MyImageKey",
"<script
language=javascript>MyImage='"+s+"'</script>");
base.OnPreRender(e);
}
By
overriding the OnPreRender event of the underlying Image control,
the required client-side script can be emitted. This script immediately loads
the image and holds it in a client-side variable named MyImage. Then the
client-side onMouseOver event can be modified to change the image to the
value of this variable. Here's the modified server-side code that emits that
client-side script:
Attributes.Add("onMouseOver",
"this.src=MyImage");
Compile
the code, clear your cache, and you'll see the delay is now gone. However, a
new problem is created that you'll only notice if you have more than one
instance of the control on your page containing different images. In this
situation, all the controls will end up using the same rollover image, which
probably does not meet your requirements.
Conflicting
Goals
The
problem is that all your controls are referencing the same client side
variable, MyImage. Obviously this variable can only contain one image,
so the controls are conflicting with each other. The solution is to use unique
variable names for each instance of the control. One of the easiest ways to
accomplish this is to use the unique client-side ID that ASP.NET automatically generates
for every control. In Figure 3, that name is concatenated with the MyImage
variable to keep it unique. Additionally, all the JavaScript generation code
has also been moved to the OnPreRender event to keep things easy to
maintain.
protected
override 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, "<script
language=javascript>MyImage" +
this.ClientID + "='" + s +
"'</script>");
base.OnPreRender(e);
}
Figure
3: This version of
the OnPreRender event combines all the JavaScript generation code into
one place for improved maintainability.
If you
now run your project and view the HTML that is output to the browser, you'll
notice the variable is named MyImageImageRollover1. If you have a second
instance of the control on your page, you'll also notice a variable named MyImageImageRollover2.
Listing One
contains the source code for this improved and complete version of the ImageRollover
control. It also includes the source code for a nearly identical ImageButtonRollover
control.
You now
have the source code for three useful Web controls that can be used across many
projects. Let me know if you decide to enhance these controls further; I'd love
to hear about your new features. Hopefully you now also have a fundamental
understanding of inheritance, custom Web controls, and interaction with
client-side JavaScript. Good luck and happy coding!
The
sample code in this article is available for download.
Steve C.
Orr is a Microsoft
MVP in ASP.NET and an MCSD. He's been programming professionally in the Seattle
area for more than 10 years. He's worked on numerous projects with Microsoft
and currently works with such companies as Able Consulting, LUMEDX, and The
Cadmus 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>
/// ImageRollover
Control
///
</summary>
[DefaultProperty("ImageURL"),
ToolboxData(
"<{0}:ImageRollover
runat=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 string
RolloverImageUrl
{
get
{
string
s = (string)ViewState["RolloverImageUrl"];
return((s == null) ? String.Empty : s);
}
set
{
s = value;
ViewState["RolloverImageUrl"] = s;
}
}
public override string
ImageUrl
{
get {return(base.ImageUrl);}
set {base.ImageUrl = value;}
}
protected override 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,
"<script
language=javascript>MyImage" +
this.ClientID
+ "='" + s + "'</script>");
base.OnPreRender(e);
}
}
///
<summary>
/// ImageRolloverButton
Control
///
</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 string
RolloverImageUrl
{
get
{
string
s = (string)ViewState["RolloverImageUrl"];
return((s
== null) ? String.Empty : s);
}
set
{
s=value;
ViewState["RolloverImageUrl"] = s;
}
}
public override string
ImageUrl
{
get {return(base.ImageUrl);}
set {base.ImageUrl=value;}
}
protected override 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,"<script
language=javascript>MyImage"
+ this.ClientID
+ "='" + s + "'</script>");
base.OnPreRender(e);
}
}
}
End Listing
One