LANGUAGES: C#
ASP.NET VERSIONS: 1.x
Inherit and Extend
Create Powerful Custom Web Controls
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.
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.
using
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.
Figure 4 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!
using
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);
}
}
}
Figure 4: This is the full source
code for an ImageRollover control (top) and ImageRolloverButton control (bottom).
The sample code in this article is available for
download.
This article was originally published in
ASP.NET Pro Magazine.