Inherit and Extend

Create Powerful Custom Web Controls

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:[email protected].

 

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

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish