Chapter 8. Custom Controls

As you learned in Chapter 6, the .NET Framework class library (FCL) features a rich collection of server controls that provide ready-made building blocks for Web forms. FCL control types range from the simple (Label) to the sublime (Calendar, DataGrid, Repeater, and others), and they enable developers to build sophisticated Web applications without getting lost in a maze of HTML and client-side scripting.

Diverse as they are, the FCL’s built-in server controls can’t possibly accommodate the needs of every developer. The time will come when you need a control that’s not part of the FCL. Then you’ll have two choices: purchase the control from a third party or write it yourself. This chapter is about the latter: how to write custom controls that look and feel like FCL controls but encapsulate functionality that FCL controls don’t. Among other things, you’ll learn how controls render themselves to Web pages, how controls process postback data and generate postbacks of their own, how controls use view state to persist data across requests, how to write controls that do some of their processing on the client side by emitting JavaScript, and how to build controls that escape the bonds of HTML by deriving their appearance from dynamically generated images. You’ll see many working examples, including one that paints a realistic-looking odometer onto a Web page and another that creates numbers-only text input fields.

ASP.NET’s server control architecture is such that writing a simple control is relatively easy and writing a complex control is—well—complex. Still, the ability to write custom controls is an essential skill that every ASP.NET developer should possess. Even if you never intend to write a custom control, you’ll find the knowledge in this chapter edifying because it speaks volumes about how ASP.NET works.

Custom Control Basics

The starting point for a custom control is System.Web.UI.Control, which defines the fundamental characteristics of a server control. You build a custom control by deriving from Control, overriding a few virtual methods here and there, and adding the methods, properties, and events that make your control unique. It’s also possible to write custom controls by deriving from FCL control classes such as DataGrid, but I’m assuming here and throughout the remainder of this chapter that your intent is to build controls from scratch rather than modify existing ones.

Control’s most important virtual method, and the method that’s overridden in almost every custom control, is Render. Render is called each time a control is executed—that is, when the page that hosts the control is requested. It affords a control the opportunity to render itself by emitting HTML to the client. Render receives through its parameter list an HtmlTextWriter reference whose Write method writes HTML to the output stream.

Your First Custom Control

The AutoCounter Control contains the source code for a simple control that writes “Hello, world” to a Web page. The using statements at the top of the file identify the namespaces containing the types that the control uses. The namespace statement encloses the class definition in a custom namespace named Wintellect. Namespaces aren’t optional when you write custom controls; control classes must be scoped to a namespace. The Hello class represents the control itself. It derives from System.Web.UI.Control, and it overrides the Render method that it inherits from its base class. Render does nothing more than write “Hello, world” to the output stream by calling HtmlTextWriter.Write.

Despite its simplicity, this example demonstrates three important principles of custom control programming:

  • Classes representing custom controls derive from System.Web. UI.Control

  • Custom controls render themselves to the client by overriding Control.Render

  • Custom control classes must be enclosed in namespaces

There’s more (of course!), but these principles permeate the fabric of custom control programming and are embodied in each and every sample presented in this chapter.

Before you can test the Hello control, you need to compile it. Here’s the command:

csc /t:library /out:HelloControl.dll hello1.cs

The output is a DLL named HelloControl.dll that contains the control’s implementation. HelloControl.dll should be placed in the bin subdirectory of the application that uses the control. As you’re well aware by now, the application root’s bin subdirectory is a magic place that ASP.NET looks to resolve references to types not found in the assemblies ASP.NET links to by default. “Application root” is any directory that is an IIS virtual directory.

Example 8-1. A simple custom control.

Hello1.cs

using System;
using System.Web.UI;

namespace Wintellect
{
    public class Hello : Control
    {
        protected override void Render (HtmlTextWriter writer)
        {
            writer.Write ("Hello, world");
        }
    }
}

Testing the Hello Control

The Web form in Example 8-20 uses the Hello control to write “Hello, world” to a Web page. The statement

<win:Hello RunAt="server" />

declares a control instance. The statement

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="HelloControl" %>

enables ASP.NET to make sense of the declaration. TagPrefix defines the prefix used in tags that declare instances of the control. Namespace identifies the namespace that the control belongs to. Assembly identifies the assembly in which the control is implemented. Note the absence of a TagName attribute. TagName isn’t needed for custom controls, as it is for user controls, because the control’s class name doubles as its tag name.

Example 8-2. Using the Hello control.

Hello1.aspx

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="HelloControl" %>

<html>
  <body>
    <form runat="server">
      <win:Hello RunAt="server" />
    </form>
  </body>
</html>

Here’s a three-step procedure for testing the Hello control:

  1. Copy Hello1.aspx to your PC’s wwwroot directory.

  2. Copy HelloControl.dll to wwwrootin.

  3. Start your browser and type http://localhost/hello1.aspx into the address bar.

In response, a bare-bones Web page containing the text “Hello, world” should appear, as shown in Example 8-31.

Output from the Hello control.
Figure 8-3. Output from the Hello control.

Improving the Hello Control: Adding Properties

Hello demonstrates the basics of custom control authoring, but it’s merely a start. When you design a custom control, you should strive to make it as programmable as possible by including public properties that developers can use to tweak its appearance and behavior. Public properties double as attributes in tags that declare control instances and can also be utilized by server-side scripts. Adding a property is easy: you simply declare the property in the control class and implement its get and set methods as you would for any other managed type.

Example 8-4 lists an improved version of the Hello control that implements a public property named Name. Rather than write “Hello, world” to the host page, the revised control writes “Hello” followed by the value encapsulated in the Name property.

Example 8-4. Improved Hello control.

Hello2.cs

using System;
using System.Web.UI;

namespace Wintellect
{
    public class Hello : Control
    {
        string MyName = "";

        public string Name
        {
            get { return MyName; }
            set { MyName = value; }
        }

        protected override void Render (HtmlTextWriter writer)
        {
            writer.Write ("<h1>Hello, " + Name + "</h1>");
        }
    }
}

The Web form in Example 8-5 uses the new and improved Hello control. The Name attribute in the control tag initializes the control’s Name property with the string “Jeff”, changing the text that appears in the Web page to “Hello, Jeff”. Because the output is enclosed in an <h1> element, the text appears in a headline font (Figure 8-6).

Example 8-5. Using the improved Hello control.

Hello2.aspx

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="HelloControl" %>

<html>
  <body>
    <form runat="server">
      <win:Hello Name="Jeff" RunAt="server" />
    </form>
  </body>
</html>
Output from the improved Hello control.
Figure 8-6. Output from the improved Hello control.

More About HtmlTextWriter

Write is one of many HtmlTextWriter methods that you can use in a control’s Render method to output HTML. WriteFullBeginTag, WriteEndTag, and other HtmlTextWriter methods simplify rendering code by letting the developer think in terms of HTML tags and attributes rather than raw text. For example, rather than write this:

writer.Write ("<h1>Hello, " + Name + "</h1>");

You can write this:

writer.WriteFullBeginTag ("h1");
writer.Write ("Hello, " + Name);
writer.WriteEndTag ("h1");

WriteFullBeginTag writes out the specified tag in angle brackets, while WriteEndTag outputs the specified tag surrounded by angle brackets and prefixed with a / symbol, as in </h1>.

This simple example belies the dramatic improvements that WriteEndTag and other HtmlTextWriter methods can lend to your code. Here’s a code snippet that uses Write to output an HTML <input> tag containing a number of attributes:

writer.Write ("<input type="text"");

writer.Write (" name="");
writer.Write (UniqueID);
writer.Write (""");

writer.Write (" id="");
writer.Write (ClientID);
writer.Write (""");

writer.Write (" value="");
writer.Write (Text);
writer.Write (""");

writer.Write (">");

Here’s the equivalent code recast to use WriteBeginTag, WriteEndTag, and WriteAttribute. The WriteBeginTag method emits an HTML tag without a closing angle bracket so that attributes can be added. The WriteAttribute method adds an attribute to a tag started with WriteBeginTag:

writer.WriteBeginTag ("input");
writer.WriteAttribute ("type", "text");
writer.WriteAttribute ("name", UniqueID);
writer.WriteAttribute ("id", ClientID);
writer.WriteAttribute ("value", Text);
writer.Write (HtmlTextWriter.TagRightChar);

The rewritten code not only reads better, it executes faster, too. You’ll see numerous examples like this one in the chapter’s remaining sample controls.

Postbacks and Postback Data

Suppose you wanted to build a custom control similar to the FCL’s TextBox control. On the surface, this sounds easy enough. You’d start by deriving from Control and overriding its Render method with one that outputs an <input type="text"> tag. Most likely, you’d also implement a Text property to expose the control’s text. Here’s what the derived class might look like:

using System;
using System.Web.UI;

namespace Wintellect
{
    public class MyTextBox : Control
    {
        string MyText = "";

        public string Text
        {
            get { return MyText; }
            set { MyText = value; }
        }

        protected override void Render (HtmlTextWriter writer)
        {
            writer.WriteBeginTag ("input");
            writer.WriteAttribute ("type", "text");
            writer.WriteAttribute ("name", UniqueID);

            if (ID != null)
                writer.WriteAttribute ("id", ClientID);

            if (Text.Length > 0)
                writer.WriteAttribute ("value", Text);

            writer.Write (HtmlTextWriter.TagRightChar);
        }
    }
}

This code sample illustrates a pair of subtle but important points that control developers should take to heart:

  • If a tag output by a control includes a Name attribute, the value of that attribute should be taken from the UniqueID property that the control inherits from Control.

  • If a tag output by a control includes an Id attribute, the value of that attribute should be taken from the ClientID property that the control inherits from Control.

UniqueID and ClientID are important because, unlike the ID property, they’re never null. Even if the tag that declares a custom control instance lacks an Id attribute, UniqueID and ClientID assume values defined by the system that give the control a unique identity. And unlike ID, UniqueID and ClientID give each control instance a unique identity even when a replicator-type control (such as a Repeater) is used to create multiple control instances.

Here’s a tag that declares a MyTextBox instance in a Web form:

<win:MyTextBox ID="UserName" Text="Bill" RunAt="server" />

And here’s the output:

<input type="text" name="UserName" id="UserName" value="Bill">

So far, there’s nothing here that you haven’t seen already. But now comes the hard part. When a postback occurs, MyTextBox should update its Text property from the postback data so that a server-side script can read the text typed into the control. In other words, let’s say the user types “Gates” into the text box and submits the form back to the server, generating an HTTP POST with the following message body:

UserName=Gates

The MyTextBox control created on the server must read the message body and update its Text property accordingly. Which brings up a question: how?

The IPostBackDataHandler Interface

The answer is an interface named IPostBackDataHandler, which belongs to the FCL’s System.Web.UI namespace. Implementing IPostBackDataHandler enables a control to access data accompanying a postback and update its properties accordingly. The interface defines two methods, both of which must be implemented in a class that derives from it:

  • LoadPostData, which the .NET Framework calls to pass postback data to the control

  • RaisePostDataChangedEvent, which is called after LoadPostData to give the control the opportunity to fire events stemming from changes in its internal state following a postback

Forget about RaisePostDataChangedEvent for a moment; we’ll talk about it later. LoadPostData is the method that interests us for now because it’s the one we can use to grab the text typed into a text box created by MyTextBox. LoadPostData is prototyped this way:

bool LoadPostData (string postDataKey, NameValueCollection postCollection)

When LoadPostData is called, postCollection holds all the data that accompanied the postback—not just for the control whose LoadPostData method was called, but for all controls. The individual data items in postCollection are indexed, and postDataKey holds the index of the data item that corresponds to the control whose LoadPostData method was called. (The index is actually the control ID, but that’s an implementation detail that has no bearing on the code you write.) If the control emits an <input type="text"> tag and the user types “Gates” into it, postCollection[postDataKey] equals “Gates” when LoadPostData is called.

Example 8-7 contains the source code for a MyTextBox control whose Text property is updated on each and every postback. MyTextBox derives not only from Control but from IPostBackDataHandler. Yes, it’s true that a managed type can have only one base class, but it’s perfectly legal to derive from one base class and one or more interfaces. All an interface does is define abstract methods that must be overridden in a derived class. The fact that MyTextBox derives from IPostBackDataHandler indicates that it implements the IPostBackDataHandler interface. And because it derives from IPostBackDataHandler, it’s obliged to override the LoadPostData and RaisePostDataChangedEvent methods. MyTextBox’s LoadPostData method retrieves the postback data generated from the text that the user typed into the input field. Then it writes the value to its own Text property.

Example 8-7.  MyTextBox control.

MyTextBox1.cs

using System;
using System.Web.UI;
using System.Collections.Specialized;

namespace Wintellect
{
    public class MyTextBox : Control, IPostBackDataHandler
    {
        string MyText = "";

        public string Text
        {
            get { return MyText; }
            set { MyText = value; }
        }

        public bool LoadPostData (string postDataKey,
            NameValueCollection postCollection)
        {
            Text = postCollection[postDataKey];
            return false;
        } 

        public void RaisePostDataChangedEvent ()
        {
        }

        protected override void Render (HtmlTextWriter writer)
        {
            writer.WriteBeginTag ("input");
            writer.WriteAttribute ("type", "text");
            writer.WriteAttribute ("name", UniqueID);

            if (ID != null)
                writer.WriteAttribute ("id", ClientID);

            if (Text.Length > 0)
                writer.WriteAttribute ("value", Text);

            writer.Write (HtmlTextWriter.TagRightChar);
        }
    }
}

You can try out MyTextBox with the Web form in Example 8-8. First compile MyTextBox1.cs into an assembly named MyTextBoxControl.dll and place it in the application root’s bin directory. Then bring up the page in your browser, type something into the text box, and click the Test button. The text that you typed should appear below the text box, proof that the control updated its Text property from the postback data.

Example 8-8.  MyTextBox test page.

MyTextBoxPage1.aspx

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="MyTextBoxControl" %>

<html>
  <body>
    <form runat="server">
      <win:MyTextBox ID="Input" Text="Type something here"
        RunAt="server" />
      <asp:Button Text="Test" OnClick="OnTest" RunAt="server" /><br>
      <asp:Label ID="Output" RunAt="server" />
    </form>
  </body>
</html>

<script language="C#" runat="server">
  void OnTest (Object sender, EventArgs e)
  {
      Output.Text = Input.Text;
  }
</script>

Are you surprised by the complexity of MyTextBox? Who would have thought that a control as simple as TextBox—the FCL class that MyTextBox is patterned after—would have to do so much just to do so little? TextBox is an exemplary class because despite its outward simplicity, its implementation is moderately complex. It also demonstrates some of the key facets of the server control programming model. In the next several sections, we’ll enhance MyTextBox until it’s practically a plug-in replacement for TextBox. The goal isn’t to build a better TextBox: it’s to understand the intricacies of control programming using a familiar control type as a baseline.

View State

Most controls fire events of one type or another. For example, Button controls fire Click events, and ListBoxes fire SelectedIndexChanged events. TextBoxes fire TextChanged events when the text submitted via a postback doesn’t match the text that the TextBox returned to the browser.

Sounds simple, right? But there’s a huge gotcha waiting to ensnare the unwary developer. A server control’s lifetime matches that of a single HTTP request. If MyTextBoxPage.aspx is requested 100 times, ASP.NET creates 100 different MyTextBox objects to fulfill those requests. Because MyTextBox is reinstantiated each and every time a request arrives, it can’t very well use data members to store state from one request to the next. In other words, MyTextBox can’t possibly determine whether its text has changed by doing this:

public bool LoadPostData (string postDataKey,
    NameValueCollection postCollection)
{
    string NewText = postCollection[postDataKey];
    if (NewText != MyText) {
        // The control’s text has changed
    }
    return false;
} 

Why? Because MyText won’t necessarily equal what you set it to in the last request. Instead, it will hold the value it was initialized with when the control was instantiated.

One of the most difficult aspects of building stateful programs on top of stateless protocols is figuring out how to hold state between requests. That’s why ASP.NET offers a mechanism called view state. View state is a place where controls can store state in such a way that it remains valid from one request to the next. It’s particularly useful for controls that fire change events and that therefore require a mechanism for retaining state across requests. ASP.NET does the hard part by storing the state. Your job is to tell it what to store.

ASP.NET exposes its view state mechanism through a Control property named ViewState. The property’s type is StateBag, which is a dictionary-like class that stores key/value pairs. The following statement adds an integer to view state and keys it with the string “Count”:

ViewState["Count"] = 1;

The next statement reads the integer back from view state. The cast is necessary because view state is typed to store generic Objects:

int count = (int) ViewState["Count"];

The magic here is that “Count” can be written to view state in one request and read back in the next one. Therefore, view state is an exceedingly easy-to-use means for persisting a control’s internal state from one page invocation to the next.

Change Events

Now that you know about view state, you’re prepared for the next logical step in MyTextBox’s evolution: adding TextChanged events. Declaring an event is no big deal; events were discussed briefly in Chapter 2 and again in Chapter 7. The following statement declares an event named TextChanged whose type is EventHandler. Recall that EventHandler is one of the standard delegates defined in the System namespace:

public event EventHandler TextChanged;

With the event thusly declared, firing a TextChanged event is as simple as this:

if (TextChanged != null)
    TextChanged (this, new EventArgs ());

The question that remains is when to fire the event. That’s where IPostBackDataHandler’s other method, RaisePostDataChangedEvent, comes in.

RaisePostDataChangedEvent exists for the sole purpose of enabling controls that update their properties from postback data to fire change events. RaisePostDataChangedEvent is called right after LoadPostData, but it’s called only if LoadPostData returns true. In Example 8-7, RaisePostDataChangedEvent contains no code because it’s never called (note LoadPostData’s return value). Here’s the proper way to implement LoadPostData and RaisePostDataChangedEvent in controls that support change events:

  • Persist property values that serve as the basis for change events in view state.

  • In LoadPostData, extract the new property values from the postback data and the old property values from view state and then compare the two. If a property changed, return true so that RaisePostDataChangedEvent will be called. Otherwise, return false.

  • In RaisePostDataChangedEvent, fire your change events.

Example 8-9 contains a modified version of MyTextBox that fires TextChanged events. The data member MyText is gone; it’s no longer needed now that Text is stored in view state. LoadPostData has been modified to compare the old text to the new and return true if they’re unequal. And RaisePostDataChangedEvent fires a TextChanged event to let interested parties know that a change occurred.

Example 8-9.  MyTextBox control with TextChanged events.

MyTextBox2.cs

using System;
using System.Web.UI;
using System.Collections.Specialized;

namespace Wintellect
{
    public class MyTextBox : Control, IPostBackDataHandler
    {
        public event EventHandler TextChanged;

        public string Text
        {
            get
            {
                string text = (string) ViewState["MyText"];
                return (text == null) ? "" : text;
            }
            set { ViewState["MyText"] = value; }
        }

        public bool LoadPostData (string postDataKey,
            NameValueCollection postCollection)
        {
            string temp = Text;
            Text = postCollection[postDataKey];
            return (temp != Text);
        } 

        public void RaisePostDataChangedEvent ()
        {
            if (TextChanged != null)
                TextChanged (this, new EventArgs ()); // Fire event
        }

        protected override void Render (HtmlTextWriter writer)
        {
            writer.WriteBeginTag ("input");
            writer.WriteAttribute ("type", "text");
            writer.WriteAttribute ("name", UniqueID);

            if (ID != null)
                writer.WriteAttribute ("id", ClientID);

            if (Text.Length > 0)
                writer.WriteAttribute ("value", Text);

            writer.Write (HtmlTextWriter.TagRightChar);
        }
    }
}

The Web form in Example 8-10 responds to TextChanged events by displaying a message underneath the control. To see for yourself, open MyTextBoxPage2.aspx in your browser (don’t forget to regenerate MyTextBoxControl.dll by compiling MyTextBox2.cs first) and click the Test button to force a postback. Nothing visible should happen because the input text didn’t change. Now edit “Type something here” and click Test again. This time, “Text changed” should appear under the control, demonstrating that it fired a TextChanged event.

Example 8-10. Revised test page for MyTextBox.

MyTextBoxPage2.aspx

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="MyTextBoxControl" %>

<html>
  <body>
    <form runat="server">
      <win:MyTextBox ID="Input" Text="Type something here"
        OnTextChanged="OnTextChanged" RunAt="server" />
      <asp:Button Text="Test" RunAt="server" /><br>
      <asp:Label ID="Output" RunAt="server" />
    </form>
  </body>
</html>

<script language="C#" runat="server">
  void Page_Load (Object sender, EventArgs e)
  {
      Output.Text = ""; // Reset the Label control
  }

  void OnTextChanged (Object sender, EventArgs e)
  {
      Output.Text = "Text changed";
  }
</script>

How View State Works

Are you curious to know how ASP.NET saves the data that you write to view state? Bring up MyTextBoxPage2.aspx in your browser, click the Test button, and check out the HTML that comes back. Here’s what you’ll see:

<html>
  <body>
    <form name="_ctl0" method="post" action="mytextboxpage2.aspx" id="_ctl0">
      <input type="hidden" name="__VIEWSTATE" 
        value="dDwtNzIwNTMyODUzO3Q8O2w8aTwxPjs+O2w8dDw7bDxpPDE+O2k8NT47PjtsPH
        Q8cDxsPE15VGV4dDs+O2w8VHlwZSBzb21ldGhpbmcgaGVyZTs+Pjs7Pjt0PHA8cDxsPFR
        leHQ7PjtsPFxlOz4+Oz47Oz47Pj47Pj47Pg==" />

      <input type="text" name="Input" value="Type something here">
      <input type="submit" name="_ctl1" value="Test" /><br>
      <span id="Output"></span>
    </form>
  </body>
</html>

The key is the hidden input control named __VIEWSTATE. It doesn’t show up in the Web page because it’s marked type=“hidden.” It has no UI because it doesn’t need one; it’s there for the sole purpose of round-tripping view state to the client and back. View state isn’t stored on the Web server. It’s transmitted to the client in a hidden control and then transmitted back to the server as part of the form’s postback data. The value of __VIEWSTATE is a base-64 encoded version of all the data written to view state by all the page’s controls, plus any view state saved by the page itself, plus a hash value generated from the page’s contents that enables ASP.NET to detect changes to the page.

This answers one of the most common questions that newcomers ask about ASP.NET: “What’s all that __VIEWSTATE stuff I see when I do a View/Source?” Now you know. That’s how ASP.NET components persist state across round trips. View state is typically used to detect changes in control state, but it has other uses, too. I once used it to help a sortable DataGrid remember which column it was last sorted on. I didn’t have to modify DataGrid. In the page that hosted the DataGrid, I simply wrote a sort expression to view state when the DataGrid was sorted and retrieved it from view state whenever I needed it. Because Page derives indirectly from Control, and because pages are instances of classes derived from Page, pages can access view state using the same ViewState property that controls use.

View State Security

If you submit a Web form over an unencrypted channel, it’s entirely conceivable that someone could intercept the view state accompanying the request and modify it, possibly for malicious purposes. To guard against such occurrences without resorting to HTTPS, include the following statement at the top of your ASPX files:

<%@ Page EnableViewStateMac="true" %>

The “Mac” in EnableViewStateMac stands for message authentication code. Setting EnableViewStateMac to true appends a hash of view state combined with a validation key to every __VIEWSTATE value returned from this page. Following a postback, ASP.NET verifies that view state wasn’t tampered with by rehashing it and comparing the new hash value to the one round-tripped to the client. A snooper can’t alter __VIEWSTATE and escape detection without updating the hash too. But updating the hash is next to impossible because the validation key is known only to the server.

EnableViewStateMac ensures that alterations don’t go unnoticed, but it doesn’t protect view state from prying eyes or physically prevent it from being altered. For an extra measure of security over unencrypted connections, add the following entry to your Web server’s Machine.config file:

<machineKey validation="3DES" />

Henceforth, ASP.NET will encrypt view state using symmetric Triple DES encryption. Encrypted view state can’t be read by mere mortals unless they manage to compromise your Web server and steal the encryption key. That key is randomly generated by ASP.NET (unless you specify otherwise using additional entries in Machine.config) and stored by the Web server’s Local Security Authority (LSA).

Generating Postbacks

MyTextBox is now a reasonable facsimile of TextBox, but it still lacks an important ingredient: an AutoPostBack property. Setting AutoPostBack to true programs a TextBox to fire a TextChanged event the moment it loses the input focus following a text change. Without AutoPostBack, TextChanged events don’t fire until the page posts back to the server for some other reason, such as a button click. AutoPostBack forces the postback to occur immediately. How it forces postbacks isn’t obvious to the casual observer. Rather than explain how AutoPostBack works and follow up with a code sample, I’ll show you the code first and then explain how it works.

Example 8-11 contains the third and final version of MyTextBox. Unlike the previous versions, this one implements a public property named AutoPostBack whose value is stored in a private field (MyAutoPostBack). The latest version also implements Render differently: it adds an OnChange attribute to the <input type="text"> tag. Here’s the relevant code:

if (AutoPostBack)
    writer.WriteAttribute ("onchange", "javascript:" +
        Page.GetPostBackEventReference (this));

Execute a View/Source command after fetching MyTextBoxPage3.aspx (Example 8-12) and you’ll see this:

<input type="text" name="Input" id="Input" value="Type something here"
  onchange="javascript:__doPostBack(‘Input’,’’)">
    .
    .
    .    
<input type="hidden" name="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" value="" />
<script language="javascript">
<!--
	function __doPostBack(eventTarget, eventArgument) {
		var theform = document._ctl0;
		theform.__EVENTTARGET.value = eventTarget;
		theform.__EVENTARGUMENT.value = eventArgument;
		theform.submit();
	}
// -->
</script>

See how it works? The OnChange attribute designates a handler for DHTML OnChange events. A text input field fires an OnChange event when it loses the input focus following a text change. Page.GetPostBackEventReference returns code that calls a JavaScript function named __doPostBack. It also writes __doPostBack to a script block returned to the client. When the __doPostBack function is called in response to an OnChange event, it programmatically submits the form to the server by calling submit on the DHTML object representing the form (theform). In other words, AutoPostBack works its magic with some clever client-side script. And the script is simple enough that it works with just about any browser that supports JavaScript.

Example 8-11.  MyTextBox control with AutoPostBack.

MyTextBox3.cs

using System;
using System.Web.UI;
using System.Collections.Specialized;

namespace Wintellect
{
    public class MyTextBox : Control, IPostBackDataHandler
    {
        bool MyAutoPostBack = false;
        public event EventHandler TextChanged;

        public string Text
        {
            get
            {
                string text = (string) ViewState["MyText"];
                return (text == null) ? "" : text;
            }
            set { ViewState["MyText"] = value; }
        }

        public bool AutoPostBack
        {
            get { return MyAutoPostBack; }
            set { MyAutoPostBack = value; }
        }

        public bool LoadPostData (string postDataKey,
            NameValueCollection postCollection)
        {
            string temp = Text;
            Text = postCollection[postDataKey];
            return (temp != Text);
        } 

        public void RaisePostDataChangedEvent ()
        {
            if (TextChanged != null)
                TextChanged (this, new EventArgs ()); // Fire event
        }

        protected override void Render (HtmlTextWriter writer)
        {
            writer.WriteBeginTag ("input");
            writer.WriteAttribute ("type", "text");
            writer.WriteAttribute ("name", UniqueID);

            if (ID != null)
                writer.WriteAttribute ("id", ClientID);

            if (Text.Length > 0)
                writer.WriteAttribute ("value", Text);

            if (AutoPostBack)
                writer.WriteAttribute ("onchange", "javascript:" +
                    Page.GetPostBackEventReference (this));

            writer.Write (HtmlTextWriter.TagRightChar);
        }
    }
}

You can try out the AutoPostBack property with the Web page in Example 8-12. MyTextBoxPage3.aspx is identical to MyTextBoxPage2.aspx save for the AutoPostBack attribute in the <win:MyTextBox> tag. Press the Tab key a few times to move the input focus around on the page. Then tab to the MyTextBox control, edit its text, and press Tab again. This time, “Text changed” should appear underneath the control indicating that a TextChanged event fired—this despite the fact that you didn’t click the Test button to submit the form to the server.

Example 8-12. Final test page for MyTextBox.

MyTextBoxPage3.aspx

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="MyTextBoxControl" %>

<html>
  <body>
    <form runat="server">
      <win:MyTextBox ID="Input" Text="Type something here"
        OnTextChanged="OnTextChanged" AutoPostBack="true"
        RunAt="server" />
      <asp:Button Text="Test" RunAt="server" /><br>
      <asp:Label ID="Output" RunAt="server" />
    </form>
  </body>
</html>

<script language="C#" runat="server">
  void Page_Load (Object sender, EventArgs e)
  {
      Output.Text = ""; // Reset the Label control
  }

  void OnTextChanged (Object sender, EventArgs e)
  {
      Output.Text = "Text changed";
  }
</script>

The IPostBackEventHandler Interface

IPostBackDataHandler is for controls that update their properties from postback data. Because FCL controls such as TextBox, CheckBox, RadioButton, and ListBox all wrap HTML elements that transmit postback data, all implement the IPostBackDataHandler interface, too.

The .NET Framework defines a second postback-related interface named IPostBackEventHandler. Not to be confused with IPostBackDataHandler, IPostBackEventHandler enables controls that generate postbacks to be notified when they cause postbacks to occur. LinkButton is one example of an FCL control that implements IPostBackEventHandler. Its server-side processing regimen includes firing Click and Command events, but only if it was the LinkButton that caused the postback to occur in the first place.

IPostBackEventHandler’s sole method, RaisePostBackEvent, is called by ASP.NET when a control that implements IPostBackEventHandler posts a page back to the server. RaisePostBackEvent is prototyped this way:

void RaisePostBackEvent (string eventArgument)

The one and only parameter passed to RaisePostBackEvent is the second parameter passed to __doPostBack to generate the postback. Here’s the line again in MyTextBox that adds an OnChange attribute to the <input> tag when AutoPostBack is true:

writer.WriteAttribute ("onchange", "javascript:" +
    Page.GetPostBackEventReference (this));

And here’s the resulting output:

onchange="javascript:__doPostBack(‘Input’,’’)"

In this example, RaisePostBackEvent’s eventArgument parameter is an empty string because __doPostBack’s second parameter is an empty string. But suppose you called GetPostBackEventReference this way:

writer.WriteAttribute ("onchange", "javascript:" +
    Page.GetPostBackEventReference (this, "Hello"));

Now the OnChange attribute looks like this:

onchange="javascript:__doPostBack(‘Input’,’Hello’)"

And RaisePostBackEvent can read the string from its parameter list:

void RaisePostBackEvent (string eventArgument)
{
    string arg = eventArgument; // "Hello"
}

The ability to pass application-specific data to controls that generate postbacks comes in handy when the action taken by the control in RaisePostBackEvent depends on the action that generated the postback on the client side. You’ll see what I mean in the next section. But first let’s see a real-world example of IPostBackEventHandler in action.

The source code in Example 8-13 is that of a control named MyLinkButton. Like its FCL namesake, LinkButton, it creates a hyperlink that posts back to the server. When the postback occurs, the control fires a Click event. Here are two important elements of its design:

  • MyLinkButton’s Render method emits a text string surrounded by an HTML <a> element. The element’s Href attribute points to __doPostBack, which submits the form to the server.

  • MyLinkButton implements IPostBackEventHandler. When the postback occurs, ASP.NET calls the control’s RaisePostBackEvent method, which in turn fires a Click event. If the page contains 100 MyLinkButtons, only one fires a Click event because only one has its RaisePostBackEvent method called.

Example 8-14 contains a very simple Web form that you can use to test MyLinkButton. Because the @ Register directive refers to an assembly named MyLinkButtonControl, you need to compile MyLinkButton.cs into a DLL named MyLinkButtonControl.dll before executing the page.

Example 8-13.  MyLinkButton control.

MyLinkButton.cs

using System;
using System.Web.UI;

namespace Wintellect
{
    public class MyLinkButton : Control, IPostBackEventHandler
    {
        string MyText = "";
        public event EventHandler Click;

        public string Text
        {
            get { return MyText; }
            set { MyText = value; }
        }

        public void RaisePostBackEvent (string eventArgument)
        {
            if (Click != null)
                Click (this, new EventArgs ());
        }

        protected override void Render (HtmlTextWriter writer)
        {
            // Output an <a> tag
            writer.WriteBeginTag ("a");
            if (ID != null)
                writer.WriteAttribute ("id", ClientID);
            writer.WriteAttribute ("href", "javascript:" +
                Page.GetPostBackEventReference (this));
            writer.Write (HtmlTextWriter.TagRightChar);

            // Output the text bracketed by <a> and </a> tags
            if (Text.Length > 0)
                writer.Write (Text);

            // Output a </a> tag
            writer.WriteEndTag ("a");
        }
    }
}
Example 8-14.  MyLinkButton test page.

MyLinkButtonPage.aspx

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="MyLinkButtonControl" %>

<html>
  <body>
    <form runat="server">
      <win:MyLinkButton Text="Click Me" OnClick="OnClick"
        RunAt="server" /><br>  
      <asp:Label ID="Output" RunAt="server" />
    </form>
  </body>
</html>

<script language="C#" runat="server">
  void OnClick (Object sender, EventArgs e)
  {
      Output.Text = "Click!";
  }
</script>

The AutoCounter Control

Let’s sum up what you’ve learned thus far with a custom control that handles postback data and postback events. The control is named AutoCounter. AutoCounter renders a text box sandwiched between < and > buttons (Figure 8-15). When clicked, < and > increment or decrement the value in the text box and fire an Increment or a Decrement event. Manually typing a number into the text box and submitting the form to the server fires a CountChanged event. AutoCounter exposes one property—Count—that you can use to read and write the value displayed. Typing a non-numeric value into the control resets the count to 0.

AutoCounter’s source code, shown in Example 8-16, reveals the innermost secrets of its design and operation. There’s nothing here you haven’t seen before; the difference is that this time, it’s all under one roof. AutoCounter implements IPostBackDataHandler in order to update its Count property (and fire a CountChanged event) when the user types a value into the text box and posts back to the server. It also implements IPostBackEventHandler so that it can fire Increment and Decrement events when the < and > buttons are clicked. To generate postbacks, AutoCounter’s Render method encloses the < and > in HTML anchor elements (<a>) whose Href attributes point to the __doPostBack function returned by GetPostBackEventReference.

Pay particular attention to how GetPostBackEventReference is called. For the < button, it’s called this way:

writer.WriteAttribute ("href", "javascript:" +
    Page.GetPostBackEventReference (this, "dec"));

And for the > button, it’s called like this:

writer.WriteAttribute ("href", "javascript:" +
    Page.GetPostBackEventReference (this, "inc"));

RaisePostBackEvent uses the second parameter passed to GetPostBackEventReference to determine whether to fire an Increment event or a Decrement event:

if (eventArgument == "dec") {
      ...
    if (Decrement != null)
        Decrement (this, new EventArgs ());
}
else if (eventArgument == "inc") {
      ...
    if (Increment != null)
        Increment (this, new EventArgs ());
}

This is a great example of how GetPostBackEventReference’s optional second parameter can be used on the server to determine what generated the postback when a control emits multiple postback elements.

AutoCounter control in action.
Figure 8-15.  AutoCounter control in action.
Example 8-16.  AutoCounter control.

AutoCounter.cs

using System;
using System.Web.UI;
using System.Collections.Specialized;

namespace Wintellect
{
    public class AutoCounter : Control, IPostBackDataHandler,
        IPostBackEventHandler
    {
        public event EventHandler Decrement;
        public event EventHandler Increment;
        public event EventHandler CountChanged;

        public int Count
        {
            get
            {
                int count = 0;
                if (ViewState["Count"] != null)
                    count = (int) ViewState["Count"];
                return count;
            }
            set { ViewState["Count"] = value; }
        }

        public bool LoadPostData (string postDataKey,
            NameValueCollection postCollection)
        {
            int temp = Count;
            try {
                Count = Convert.ToInt32 (postCollection[postDataKey]);
            }
            catch (FormatException) {
                Count = 0;
            }
            return (temp != Count);
        } 

        public void RaisePostDataChangedEvent ()
        {
            if (CountChanged != null)
                CountChanged (this, new EventArgs ());
        }

        public void RaisePostBackEvent (string eventArgument)
        {
            if (eventArgument == "dec") {
                Count--;
                if (Decrement != null)
                    Decrement (this, new EventArgs ());
            }
            else if (eventArgument == "inc") {
                Count++;
                if (Increment != null)
                    Increment (this, new EventArgs ());
            }
        }

        protected override void Render (HtmlTextWriter writer)
        {
            // Output an <a> tag
            writer.WriteBeginTag ("a");
            writer.WriteAttribute ("href", "javascript:" +
                Page.GetPostBackEventReference (this, "dec"));
            writer.Write (HtmlTextWriter.TagRightChar);

            // Output a less-than sign
            writer.Write ("&lt;");

            // Output a </a> tag
            writer.WriteEndTag ("a");

            // Output an <input> tag
            writer.Write (" ");
            writer.WriteBeginTag ("input");
            writer.WriteAttribute ("type", "text");
            writer.WriteAttribute ("name", UniqueID);
            if (ID != null)
                writer.WriteAttribute ("id", ClientID);
            writer.WriteAttribute ("value", Count.ToString ());
            writer.WriteAttribute ("size", "3");
            writer.Write (HtmlTextWriter.TagRightChar);
            writer.Write (" ");

            // Output another <a> tag
            writer.WriteBeginTag ("a");
            writer.WriteAttribute ("href", "javascript:" +
                Page.GetPostBackEventReference (this, "inc"));
            writer.Write (HtmlTextWriter.TagRightChar);

            // Output a greater-than sign
            writer.Write ("&gt;");

            // Output a </a> tag
            writer.WriteEndTag ("a");
        }
    }
}

As usual, you need to compile AutoCounter.cs and place the resulting DLL in the bin directory before you run it. Then use AutoCounterPage.aspx (Example 8-17) to take it for a test drive. AutoCounterPage.aspx responds to Increment, Decrement, and CountChanged events by displaying descriptive text at the bottom of the page. Click the < and > buttons a time or two to see what I mean. Then type a number into the control’s text box and click Submit. The value that you entered should be echoed to the page.

Example 8-17.  AutoCounter test page.

AutoCounterPage.aspx

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="AutoCounterControl" %>								 

<html>
  <body>
    <h1>AutoCounter Demo</h1>
    <hr>
    <form runat="server">
      <win:AutoCounter ID="MyCounter" Count="5"
        OnDecrement="OnDecrement" OnIncrement="OnIncrement"
        OnCountChanged="OnCountChanged" RunAt="server" />
        <br><br>
      <asp:Button Text="Submit" RunAt="server" />
    </form>
    <hr>
    <asp:Label ID="Output" RunAt="server" />
  </body>
</html>

<script language="c#" runat="server">
  void Page_Load (Object sender, EventArgs e)
  {
      Output.Text = "";
  }

  void OnDecrement (Object sender, EventArgs e)
  {
      Output.Text = "Count decremented to " + MyCounter.Count;
  }

  void OnIncrement (Object sender, EventArgs e)
  {
      Output.Text = "Count incremented to " + MyCounter.Count;
  }

  void OnCountChanged (Object sender, EventArgs e)
  {
      Output.Text = "Count changed to " + MyCounter.Count;
  }
</script>

Composite Controls

Occasionally, building custom controls by combining other controls is useful. A control that serves as a container for other controls is called a composite control, and it, too, is an important element of ASP.NET’s server control architecture.

All Control-derived classes have the innate ability to act as containers for other controls. Contained controls, or child controls, are exposed through the parent control’s Controls property, which is inherited from Control. Controls’ type is ControlCollection, which provides methods and properties for adding controls, removing controls, enumerating controls, and more. System.Web.UI.Page counts Control among its base types. Its Controls collection defines all the controls on the page.

Composite controls come in two basic varieties: declarative and programmatic. A declarative custom control contains other controls declared in a Web form. The FCL’s Panel control is one example of a declarative composite. It acts as a container for other controls and allows them to be manipulated programmatically, but it doesn’t create the controls that it contains: you create these controls by declaring them between <asp:Panel> and </asp:Panel> tags. By contrast, a programmatic composite creates the controls that it hosts programmatically. Both types of composites are discussed in the sections that follow.

Declarative Composites

Here’s the simplest composite control you can build:

using System.Web.UI;

namespace Wintellect
{
    public class CompositeControl : Control
    {
    }
}

On the surface, it doesn’t seem as if this control could do anything useful. But check this out:

<%@ Register TagPrefix="win" Namespace="Wintellect" 
    Assembly="CompositeControl" %>

<html>
  <body>
    <form runat="server">
      <win:CompositeControl ID="MyComposite" RunAt="server">
        <asp:Label Text="Hello!" RunAt="server" /><br>
        <asp:Label Text="Goodbye!" Runat="server" />
      </win:CompositeControl>
    </form>
  </body>
</html>

In this example, CompositeControl serves as a container for a pair of Label controls. ASP.NET automatically adds the Label controls to the CompositeControl’s Controls collection. Furthermore, CompositeControl’s inherited Render method calls the child controls’ Render methods. If a server-side script prevents CompositeControl’s Render method from being called by setting the control’s Visible property (which it inherits from Control) to false, like this:

MyComposite.Visible = false;

the children’s Render methods won’t be called either. Consequently, the children will disappear from the Web page—all because you turned off the control that contains them.

The GroupBox Control

Windows developers are familiar with the group box control, which draws a stylish border around other controls and visually groups them together. HTML 4 and later versions support a <fieldset> element that looks very much like a group box. The following HTML displays three radio buttons surrounded by a group box:

<html>
  <body>
    <form>
      <fieldset>
        <legend>Colors</legend>
        <input type="radio" name="Color" value="red">Red<br>
        <input type="radio" name="Color" value="green">Green<br>
        <input type="radio" name="Color" value="blue">Blue
      </fieldset>
    </form>
  </body>
</html>

The <fieldset> element is an ideal candidate to be encapsulated in a custom control, and a composite control fits the bill perfectly. Example 8-19 contains the source code for an ASP.NET GroupBox control. It renders its children by calling the base class’s Render method, but it surrounds the child controls’ output with <fieldset> and </fieldset> tags. Example 8-20 contains a GroupBox test page, and Figure 8-18 shows the output. Clicking the check box at the top of the page toggles the GroupBox control (and by extension, its children) on and off by alternately setting the GroupBox’s Visible property to true and false.

RadioButtonList is a composite control, too, so this example nests one composite control inside another. The ListItems are children of the RadioButtonList, and the RadioButtonList is a child of the GroupBox. That means—you guessed it—the GroupBox has grandchildren!

Controls grouped with a GroupBox control.
Figure 8-18. Controls grouped with a GroupBox control.
Example 8-19.  GroupBox control.

GroupBox.cs

using System;
using System.Web.UI;

namespace Wintellect
{
    public class GroupBox : Control
    {
        string MyText = "";

        public string Text
        {
            get { return MyText; }
            set { MyText = value; }
        }

        protected override void Render (HtmlTextWriter writer)
        {
            // Output a <fieldset> tag
            writer.WriteBeginTag ("fieldset");
            if (ID != null)
                writer.WriteAttribute ("id", ClientID);
            writer.Write (HtmlTextWriter.TagRightChar);

            // Output a <legend> element
            if (Text.Length > 0) {
                writer.WriteBeginTag ("legend");
                writer.Write (Text);
                writer.WriteEndTag ("legend");
            }

            // Output the content between <fieldset> and </fieldset> tags
            base.Render (writer);

            // Output a </fieldset> tag
            writer.WriteEndTag ("fieldset");
        }
    }
}
Example 8-20.  GroupBox test page.

GroupBoxPage.aspx

<%@ Register TagPrefix="win" Namespace="Wintellect" 
    Assembly="GroupBoxControl" %>

<html>
  <body>
    <form runat="server">
      <asp:CheckBox ID="Toggle" Text="Show colors" 
        OnCheckedChanged="OnToggle" AutoPostBack="true"
        Checked="true" RunAt="server" /><br>
      <win:GroupBox ID="MyGroupBox" Text="Colors" RunAt="server">
        <asp:RadioButtonList RunAt="server">
          <asp:ListItem Text="Red" Selected="true" RunAt="server" />
          <asp:ListItem Text="Green" RunAt="server" />
          <asp:ListItem Text="Blue" RunAt="server" />
        </asp:RadioButtonList>
      </win:GroupBox>
    </form>
  </body>
</html>

<script language="C#" runat="server">
  void OnToggle (Object sender, EventArgs e)
  {
      MyGroupBox.Visible = Toggle.Checked;
  }
</script>

Programmatic Composites

Programmatic composite controls create child controls programmatically. All controls inherit a virtual CreateChildControls method from Control that can be overridden in a derived class. The .NET Framework calls CreateChildControls very early in the control’s lifetime, so it’s the perfect place to instantiate child controls. The following control is the programmatic equivalent of the declarative control presented a few moments ago—the one containing Label controls with the greetings “Hello!” and “Goodbye!”

using System.Web.UI;
using System.Web.UI.WebControls;

namespace Wintellect
{
    public class CompositeControl : Control
    {
        protected override void CreateChildControls () 
        {
            Label label1 = new Label ();
            label1.Text = "Hello!";
            Controls.Add (label1);
            Controls.Add (new LiteralControl ("<br>"));
            Label label2 = new Label ();
            label2.Text = "Goodbye!";
            Controls.Add (label2);
        }
    }
}

In its override of CreateChildControls, CompositeControl instantiates the Label controls with new and adds them to its Controls collection with ControlCollection.Add. It also uses a Literal control to insert a <br> element between the Labels.

The LoginControl

The Web page in Figure 8-21 is built around a composite control named LoginControl. LoginControl is functionally similar to the login control presented in Chapter 7. When instantiated, it creates two TextBox controls and a Button control and adopts them as child controls. If the user enters a user name and password and clicks the button, the control fires a Login event. A server-side script can process the event and retrieve the user name and password by reading the control’s UserName and Password properties. LoginControl’s source code appears in Example 8-22. You’ll find a page to test it with in Example 8-23.

One nuance of composite controls that you should be aware of regards the INamingContainer interface. All but the most trivial composite controls should derive from INamingContainer. If they don’t, they’re liable to suffer strange maladies, including events that don’t fire properly. INamingContainer’s purpose is to allow ASP.NET to assign child controls names that are unique with respect to other controls on the page. Fortunately, INamingContainer requires no implementation. It’s a signal interface, meaning it has no methods. Simply including INamingContainer in a control’s list of base types is sufficient to “implement” the interface.

Custom login control.
Figure 8-21. Custom login control.
Example 8-22.  LoginControl source code.

LoginControl.cs

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Wintellect
{
    public class LoginControl : Control, INamingContainer
    {
        TextBox MyUserName = new TextBox ();
        TextBox MyPassword = new TextBox ();
        public event EventHandler Login;

        public string UserName
        {
            get { return MyUserName.Text; }
            set { MyUserName.Text = value ; }
        }

        public string Password
        {
            get { return MyPassword.Text; }
            set { MyPassword.Text = value ; }
        }

        protected override void CreateChildControls () 
        {
            Controls.Add (MyUserName);
            Controls.Add (new LiteralControl ("<br>"));
            Controls.Add (new LiteralControl ("<br>"));

            MyPassword.TextMode = TextBoxMode.Password;
            Controls.Add (MyPassword);
            Controls.Add (new LiteralControl ("<br>"));
            Controls.Add (new LiteralControl ("<br>"));

            Button button = new Button ();
            button.Text = "Log In";
            Controls.Add (button);

            button.Click += new EventHandler (OnLogin);
        }

        protected void OnLogin (Object sender, EventArgs e)
        {
            if (Login != null && UserName.Length > 0 &&
                Password.Length > 0)
                Login (this, new EventArgs ());
        }
    }
}
Example 8-23.  LoginControl test page.

LoginControlPage.aspx

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="LoginControl" %>							 

<html>
  <body>
    <h1>Login Control Demo</h1>
    <hr>
    <form runat="server">
      <win:LoginControl ID="Login" OnLogin="OnLogin" RunAt="server" />
    </form>
    <hr>
    <asp:Label ID="Output" RunAt="server" />
</html>
<script language="C#" runat="server">
  void OnLogin (Object sender, EventArgs e)
  {
      Output.Text = "Hello, " + Login.UserName;
  }
</script>

Server Controls and Client-Side Scripting

You can do a lot with custom server controls by returning ordinary HTML to the client. But some of the most exotic (and potentially useful) server controls in the world return client-side script as well. The ASP.NET validation controls are a great example. They couldn’t validate user input on the client side without some help from the browser, and one way to get the browser involved in the execution of a control is to return script that the browser understands.

The chief benefit of writing controls that return client-side script is performance. The more you can do on the client, the better a page will perform. Why? Because executing code in a browser is far faster than sending HTTP requests to a server. Years ago I took part in an online discussion forum that diagrammed message threads with a browser-based tree control. Using the forum was a nightmare because each time you clicked a plus sign to expand a branch of the tree, a request went back to the server, which generated a new GIF depicting the tree with the branch expanded and streamed the GIF back to the client. With a dial-up connection, a branch took a minute or more to expand! That was a classic example of how not to design Web pages. Now that most browsers support client-side scripting, the same tree control can be implemented in script and branches can be expanded and collapsed locally, without incurring round trips to the server.

High-performance tree controls are just one example of the wonders you can accomplish with client-side script. Server controls and scripting are a match made in heaven because a control can hide the script that it relies on under the hood where it belongs. Few veteran Windows developers are experts in client-side scripting too, but they don’t have to be to use controls that generate script. That, after all, is what encapsulation is all about: hiding difficult implementation details behind the veil of reusable components so that anyone can write sophisticated applications, regardless of their background or experience level.

Most controls that emit client-side script return JavaScript, also known as JScript and ECMAScript, rather than VBScript. VBScript works fine in Internet Explorer, but it’s unsupported in other browsers. Returning JavaScript provides a measure of browser independence. In reality, that independence is more measured than we would like because it’s difficult to write scripts that work equally well in Internet Explorer, Netscape Navigator, and other browsers. Still, I’ll use JavaScript for all the sample controls in this chapter that return client-side script and, when necessary, customize the script on the fly to match the browser type, enabling it to work in Internet Explorer and Navigator.

Returning JavaScript to the Client

The simplest way to return JavaScript from a custom control is to output it from the control’s Render method. The control in Example 8-24 does just that. It’s a simple control called MessageButton that renders out an <input type="submit"> tag. Inside the tag is an OnClick attribute that calls JavaScript’s alert function when the button is clicked. If you declare a control instance like this:

<win:MessageButton Text="Click Me" Message="Hello, world" RunAt="server" />

the control renders itself this way:

<input type="submit" name="_ctl1" value="Click Me"
  onclick="javascript:alert (‘Hello, world’)">

As a result, clicking the button pops up “Hello, world!” in a message box.

Example 8-24.  MessageButton control, version 1.

MessageButton1.cs

using System;
using System.Web.UI;

namespace Wintellect
{
    public class MessageButton : Control
    {
        string MyText = "";
        string MyMessage = "";

        public string Text
        {
            get { return MyText; }
            set { MyText = value; }
        }

        public string Message
        {
            get { return MyMessage; }
            set { MyMessage = value; }
        }
        protected override void Render (HtmlTextWriter writer)
        {
            writer.WriteBeginTag ("input");
            writer.WriteAttribute ("type", "submit");
            writer.WriteAttribute ("name", UniqueID);

            if (ID != null)
                writer.WriteAttribute ("id", ClientID);

            if (Text.Length > 0)
                writer.WriteAttribute ("value", Text);

            if (Message.Length > 0)
                writer.WriteAttribute ("onclick",
                    "javascript:alert (’" + Message + "’)");

            writer.Write (HtmlTextWriter.TagRightChar);
        }
    }
}

The RegisterClientScriptBlock Method

Returning client-side script from a control’s Render method is fine when the script is simple enough to be embedded in control tags, but what about more complex scripts? Many controls return client-side script blocks containing functions that are called from control tags. Here’s how the OnClick attribute in the previous section would look if instead of calling alert directly, it called a local function named doAlert:

<script language="javascript">
<!--
function doAlert (message)
{
    alert (message);
}
-->
</script>
  .
  .
  .
<input type="submit" name="_ctl1" value="Click Me"
  onclick="javascript:doAlert (‘Hello, world’)">

Rather than return blocks of client-side script by manually writing them out with HtmlTextWriter, controls should return them with Page.RegisterClientScriptBlock. RegisterClientScriptBlock returns a block of client-side script and ensures that it’s returned only once, no matter how many controls on the page use it. RegisterClientScriptBlock accepts two string parameters: one that assigns the script a name, and another that contains the script itself. If a control outputs a block of script using HtmlTextWriter, a page containing 10 instances of the control will see the same script block replicated 10 times. If the control uses RegisterClientScriptBlock, however, the block is returned only one time because each control instance registers the script block using the same name.

The version of MessageButton in Example 8-25 displays message boxes by calling a local function in a client-side script block. The block is returned by RegisterClientScriptBlock. Note where RegisterClientScriptBlock is called: in the OnPreRender override. ASP.NET calls OnPreRender on every control on a page before calling any of the controls’ Render methods. Calling RegisterClientScriptBlock from OnPreRender ensures that the script block is registered early enough in the page rendering process to allow ASP.NET to control the position of the script block in the output. As you’ve probably already surmised, OnPreRender is another of the virtual methods that a control inherits from Control.

Example 8-25.  MessageButton control, version 2.

MessageButton2.cs

using System;
using System.Web.UI;

namespace Wintellect
{
    public class MessageButton : Control
    {
        string MyText = "";
        string MyMessage = "";

        public string Text
        {
            get { return MyText; }
            set { MyText = value; }
        }

        public string Message
        {
            get { return MyMessage; }
            set { MyMessage = value; }
        }

        protected override void OnPreRender (EventArgs e)
        {
            Page.RegisterClientScriptBlock (
                "MessageButtonScript",
                "<script language="javascript">
" +
                "<!--
"                             +
                "function doAlert (message)
"       +
                "{
"                                +
                "    alert (message);
"             +
                "}
"                                +
                "-->
"                              +
                "</script>"
            );
        }

        protected override void Render (HtmlTextWriter writer)
        {
            writer.WriteBeginTag ("input");
            writer.WriteAttribute ("type", "submit");
            writer.WriteAttribute ("name", UniqueID);

            if (ID != null)
                writer.WriteAttribute ("id", ClientID);

            if (Text.Length > 0)
                writer.WriteAttribute ("value", Text);

            if (Message.Length > 0)
                writer.WriteAttribute ("onclick",
                    "javascript:doAlert (’" + Message + "’)");

            writer.Write (HtmlTextWriter.TagRightChar);
        }
    }
}

RegisterClientScriptBlock prevents a function from being downloaded to a page multiple times. ASP.NET supports a similar method named RegisterStartupScript whose purpose isn’t to return client-side JavaScript functions but to return ordinary script—code that’s not contained in functions—that executes when the page loads. The difference between RegisterClientScriptBlock and RegisterStartupScript is the location at which they position the scripts registered with them. RegisterClientScriptBlock puts the scripts near the top of the document, shortly after the <form> tag. RegisterStartupScript puts them just before the </form> tag. The placement of the startup script is important because if the script interacts with other elements on the page, those elements must be loaded before the script executes. Placing startup script near the end of the document ensures that other elements on the page are present and accounted for when the script runs.

Keeping Your Code Off the Client

The <script> tag supports a Src attribute that permits scripts to be referenced remotely. The following code creates a message button that calls a function named doAlert from a JS file on the server:

<script language="javascript" src="/jscript/messagebutton.js">
</script>
  .
  .
  .
<input type="submit" value="Click Me"
  onclick="javascript:doAlert (‘Hello, world’)">

Messagebutton.js contains doAlert’s implementation. Keeping the code on the server serves two purposes:

  • It hides your client-side script so that it can’t be seen with a View/Source command. (It does not, however, prevent technically savvy users from retrieving scripts from the browser cache.)

  • If multiple pages on your site use a common set of JavaScript functions, packaging the functions in a JS file and referencing them on the server means you can update every page that uses them by modifying one source-code file.

This is precisely how ASP.NET’s validation controls work. They emit a modest amount of client-side script to the browser, but most of their work is done by functions in a script file named WebUIValidation.js that’s stored on the server.

Example 8-26 contains the final version of MessageButton—one that emits a <script> tag with a Src attribute pointing to a JavaScript file on the server. Functionally, this version is identical to the previous two. But view the source code returned to the browser, and you’ll see that the doAlert function is no longer visible.

Example 8-26.  MessageButton control, version 3, and the associated JavaScript file.

MessageButton3.cs

using System;
using System.Web.UI;

namespace Wintellect
{
    public class MessageButton : Control
    {
        string MyText = "";
        string MyMessage = "";

        public string Text
        {
            get { return MyText; }
            set { MyText = value; }
        }

        public string Message
        {
            get { return MyMessage; }
            set { MyMessage = value; }
        }

        protected override void OnPreRender (EventArgs e)
        {
            Page.RegisterClientScriptBlock (
                "MessageButtonRemoteScript",
                "<script language="javascript" " +
                    "src="/JScript/MessageButton.js"></script>");
        }

        protected override void Render (HtmlTextWriter writer)
        {
            writer.WriteBeginTag ("input");
            writer.WriteAttribute ("type", "submit");
            writer.WriteAttribute ("name", UniqueID);

            if (ID != null)
                writer.WriteAttribute ("id", ClientID);

            if (Text.Length > 0)
                writer.WriteAttribute ("value", Text);

            if (Message.Length > 0)
                writer.WriteAttribute ("onclick",
                    "javascript:doAlert (’" + Message + "’)");

            writer.Write (HtmlTextWriter.TagRightChar);
        }
    }
}

MessageButton.js

function doAlert (message)
{
    alert (message);
}

Now that you’ve seen the mechanics of how controls return client-side script, let’s put this newfound knowledge to work building some intelligent controls that do their work without incurring round trips to the server.

One of the most common scripted elements found on Web sites is the “rollover image.” When the cursor goes over the top of it, the image changes; when the cursor moves away, the image reverts to its original form. Usually, the image is combined with a hyperlink so that clicking it jumps to another URL. The rollover effect adds visual flair that draws the user’s attention and says “click here.”

Rollover images are relatively simple to implement with JavaScript and DHTML. The following HTML file demonstrates how image rollovers work. An <img> tag is enclosed in an anchor element. The anchor element includes OnMouseOver and OnMouseOut attributes that dynamically change the image’s Src attribute when the mouse enters and leaves the image:

<html>
  <body>
    <a href="next.html"
      onmouseover="javascript:document.myimage.src=‘logo2.jpg’"
      onmouseout="javascript:document.myimage.src=‘logo1.jpg’">
      <img name="myimage" src="logo1.jpg">
    </a>
  </body>
</html>

The RolloverImageLink control in Example 8-27 encapsulates this behavior in a custom control. Assuming RolloverImageLink is compiled into a DLL named RolloverImageLinkControl.dll, creating a rollover image hyperlink is as simple as this:

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="RolloverImageLinkControl" %>
    .
    .
    .
<win:RolloverImageLink NavigateUrl="next.html" RunAt="server"
  OnImageUrl="image1.jpg" OffImageUrl="image2.jpg" />

The control has three public properties: NavigateUrl, OnImageUrl, and OffImageUrl. NavigateUrl identifies the target of the hyperlink. OnImageUrl and OffImageUrl identify the images shown when the cursor is over the image and when it’s not. The source code should be easy to understand given that the control’s output looks very much like the HTML shown previously.

Example 8-27.  RolloverImageLink control.

RolloverImageLink.cs

using System;
using System.Web.UI;

namespace Wintellect
{
    public class RolloverImageLink : Control
    {
        string MyNavigateUrl = "";
        string MyOnImageUrl = "";
        string MyOffImageUrl = "";

        public string NavigateUrl
        {
            get { return MyNavigateUrl; }
            set { MyNavigateUrl = value; }
        }

        public string OnImageUrl
        {
            get { return MyOnImageUrl; }
            set { MyOnImageUrl = value; }
        }

        public string OffImageUrl
        {
            get { return MyOffImageUrl; }
            set { MyOffImageUrl = value; }
        }

        protected override void Render (HtmlTextWriter writer)
        {
            // Output an <a> tag
            writer.WriteBeginTag ("a");

            if (NavigateUrl.Length > 0)
                writer.WriteAttribute ("href", NavigateUrl);

            if (OnImageUrl.Length > 0 && OffImageUrl.Length > 0) {
                writer.WriteAttribute ("onmouseover",
                    "javascript:document." + ClientID + ".src=’" +
                     OnImageUrl + "’");
                writer.WriteAttribute ("onmouseout",
                    "javascript:document." + ClientID + ".src=’" +
                     OffImageUrl + "’");
            }
            writer.Write (HtmlTextWriter.TagRightChar);

            // Output an <img> tag
            writer.WriteBeginTag ("img");
            writer.WriteAttribute ("name", ClientID);
            if (OffImageUrl.Length > 0)
                writer.WriteAttribute ("src", OffImageUrl);
            writer.Write (HtmlTextWriter.TagRightChar);

            // Output a </a> tag
            writer.WriteEndTag ("a");
        }
    }
}

The NumTextBox Control

For years, Windows developers have customized edit controls by processing the EN_CHANGE notifications that fire as individual characters are typed and filtering out unwanted characters. A common application for this technique is to build numeric edit controls that accept numbers but reject letters and other symbols.

You can achieve the same effect in Web applications with JavaScript and DHTML. Modern browsers fire OnKeyDown events as characters are typed into a text box. A Web page can register a handler for OnKeyDown events and filter out unwanted characters by returning false from the event handler. The following Web page demonstrates how it works. The OnKeyDown attribute in the <input type ="text"> tag activates the JavaScript isKeyValid function on each keystroke. The isKeyValid function examines the key code and returns true if the key represents a numeric character or any of a number of auxiliary keys, such as Tab, Backspace, and Delete. Try it out and you’ll find that the input field won’t accept any characters other than the numbers 0 through 9:

<html>
  <head>
    <script language="javascript">
    <!--
      var keys = new Array (8, 9, 13, 33, 34, 35, 36, 37, 39, 45, 46);

      function isKeyValid (keyCode)
      {
          return ((keyCode >= 48 && keyCode <= 57) ||
              isAuxKey (keyCode));
      }

      function isAuxKey (keyCode)
      {
          for (i=0; i<keys.length; i++)
              if (keyCode == keys[i])
                  return true;
          return false;
      }
    -->
    </script>
  </head>
  <body>
    <form>
      <input type="text" name="quantity"
      onkeydown="javascript:return isKeyValid (window.event.keyCode)">
    </form>
  </body>
</html>

This example works in Internet Explorer 4 and later. To work in Netscape Navigator 4 and later, the script must be modified:

<html>
  <head>
    <script language="javascript">
    <!--
      function isKeyValid (keyCode)
      {
          return ((keyCode >= 48 && keyCode <= 57) ||
              keyCode == 8 || keyCode== 13);
      }
    -->
    </script>
  </head>
  <body>
    <form>
      <input type="text" name="quantity"
        onkeydown="javascript:return isKeyValid (event.which)">
    </form>
  </body>
</html>

Once more, this behavior is begging to be encapsulated in a custom control to shield the developer from the nuances of DHTML and client-side script. It also presents the perfect opportunity to demonstrate adaptive rendering—varying the script that a control outputs based on the browser it renders to.

Example 8-28 contains the source code for a NumTextBox control that trivializes the task of creating numbers-only text boxes:

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="NumTextBoxControl" %>
    .
    .
    .
<win:NumTextBox ID="Quantity" RunAt="server" />

NumTextBox exposes its content through a Text property that throws an exception if a noninteger value is assigned. It uses RegisterClientScriptBlock to register the <script> block containing isKeyValid and its helpers. And it works with both Internet Explorer and Netscape Navigator, thanks to adaptive rendering logic that returns one set of client-side script to Internet Explorer and another to Navigator. If the requestor is neither Internet Explorer nor Navigator (or is a down-level version of either), the control returns no client-side script because chances are it won’t work anyway.

How does NumTextBox adapt its output to the browser type? By using the Request object’s Browser property, which contains a variety of information about the browser that made the request, including its make and version number. As described in Chapter 7, ASP.NET reads the User-Agent headers accompanying HTTP requests and populates the Browser property with information inferred from those headers. NumTextBox checks the browser’s type and version number by reading Browser’s Type and MajorVersion properties:

string browser = Context.Request.Browser.Type.ToUpper ();
int version = Context.Request.Browser.MajorVersion;
  .
  .
  .
if (browser.IndexOf ("IE") > -1 && version >= 4) {
    // Internet Explorer 4 or later
}
else if (browser.IndexOf ("NETSCAPE") > -1 && version >= 4) {
    // Netscape Navigator 4 or later
}

For Internet Explorer, Browser.Type returns a string of the form “IE4,” while for Navigator it returns a string such as “Netscape4.” Using String.IndexOf to check for the substrings “IE” and “Netscape” detects requests emanating from these browsers.

Example 8-28.  NumTextBox control.

NumTextBox.cs

using System;
using System.Web.UI;

namespace Wintellect
{
    public class NumTextBox : Control
    {
        string MyText = "";

        string IEClientScriptBlock =
            "<script language="javascript">
"               +
            "<!--
"                                           +
            "var keys = new Array (8, 9, 13, 33, 34, 35, "     +
                "36, 37, 39, 45, 46);
"                       +
            "function isKeyValid (keyCode)
"                  +
            "{
"                                              +
            "    return ((keyCode >= 48 && keyCode <= 57) || " +
                "isAuxKey (keyCode));
"                       +
            "}
"                                              +
            "function isAuxKey (keyCode)
"                    +
            "{
"                                              +
            "    for (i=0; i<keys.length; i++)
"              +
            "        if (keyCode == keys[i])
"                +
            "            return true;
"                       +
            "    return false;
"                              +
            "}
"                                              +
            "-->
"                                            +
            "</script>";

       string NetscapeClientScriptBlock =
            "<script language="javascript">
"               +
            "<!--
"                                           +
            "function isKeyValid (keyCode)
"                  +
            "{
"                                              +
            "    return ((keyCode >= 48 && keyCode <= 57) || " +
                "keyCode == 8 || keyCode == 13);
"            +
            "}
"                                              +
            "-->
"                                            +
            "</script>";
        public string Text
        {
            get { return MyText; }
            set
            {
                // Make sure value is numeric before storing it
                Convert.ToInt64 (value);
                MyText = value;
            }
        }

        protected override void OnPreRender (EventArgs e)
        {
            string browser = Context.Request.Browser.Type.ToUpper ();
            int version = Context.Request.Browser.MajorVersion;

            if (browser.IndexOf ("IE") > -1 && version >= 4)
                Page.RegisterClientScriptBlock ("NumTextBoxScript",
                    IEClientScriptBlock);
            else if (browser.IndexOf ("NETSCAPE") > -1 && version >= 4)
                Page.RegisterClientScriptBlock ("NumTextBoxScript",
                    NetscapeClientScriptBlock);
        }

        protected override void Render (HtmlTextWriter writer)
        {
            string browser = Context.Request.Browser.Type.ToUpper ();
            int version = Context.Request.Browser.MajorVersion;

            writer.WriteBeginTag ("input");
            writer.WriteAttribute ("type", "text");
            writer.WriteAttribute ("name", UniqueID);

            if (ID != null)
                writer.WriteAttribute ("id", ClientID);

            if (Text.Length > 0)
                writer.WriteAttribute ("value", Text);

            if (browser.IndexOf ("IE") > -1 && version >= 4)
                writer.WriteAttribute ("onkeydown",
                    "javascript:return isKeyValid (window.event.keyCode)");
            else if (browser.IndexOf ("NETSCAPE") > -1 && version >= 4)
                writer.WriteAttribute ("onkeydown",
                    "javascript:return isKeyValid (event.which)");

            writer.Write (HtmlTextWriter.TagRightChar);
        }
    }

Graphical Controls

By writing to the output stream with HtmlTextWriter, a custom control can render anything that can be expressed in HTML. Controls that require more latitude than HTML provides can return graphical images. The images can be stored statically (for example, in JPEG files) on the server, or they can be generated at run time and even customized for each request. Returning dynamically generated images frees controls from the limitations of HTML and opens the door to a world of possibilities, from controls that render graphs and pie charts to controls that render maps, formatted reports, and virtually anything else you can dream up.

The secret to authoring a graphical control is to code its Render method to return an <img> tag. If the image is static—that is, if it’s stored in a file on the server—the tag’s Src attribute identifies the image file:

<img src="staticimage.jpg">

If the image is dynamically generated, however, the Src attribute must point to a URL that creates the image on the fly. The following <img> tag references a URL that dynamically generates an image based on input passed in the query string:

<img src="imagegen.ashx?shape=circle&color=red">

What is ImageGen.ashx? It’s not a file; it’s an HTTP handler. Specifically, it’s an HTTP handler you write that parses a query string and returns a dynamically generated image. We haven’t talked about HTTP handlers yet. Let’s remedy that shortcoming right now.

HTTP Handlers

HTTP handlers are one of the fundamental building blocks of ASP.NET. An HTTP handler is a class that handles HTTP requests for a specific endpoint (URL) or set of endpoints on a Web server. HTTP handlers built into ASP.NET handle requests for ASPX files, ASCX files, and other ASP.NET file types. In addition, you can extend ASP.NET with HTTP handlers of your own. Entries in Web.config map URLs to HTTP handlers. (We haven’t talked about Web.config and the role that it plays in configuring ASP.NET applications yet, but don’t worry: you’ll learn all about it in the next chapter.) The following statements in a Web.config file map requests for ImageGen.ashx targeted at this directory (the directory that hosts Web.config) and its subdirectories to a class named ImageGen in an assembly named DynaImageLib.dll:

<httpHandlers>
  <add verb="*" path="ImageGen.ashx" type="ImageGen, DynaImageLib" />
</httpHandlers>

When an HTTP request arrives for ImageGen.ashx, ASP.NET instantiates ImageGen and passes it the request. Assuming ImageGen is an image generator, it responds by creating an image and returning it in the HTTP response. Here’s a generic template for an HTTP handler that creates an image in memory and returns it to the requestor as a JPEG. The hard part—building the image and writing it out to the HTTP response as a JPEG-formatted bit stream—is vastly simplified by the FCL’s Bitmap and Graphics classes:

public class ImageGen : IHttpHandler
{
    public void ProcessRequest (HttpContext context)
    {
        // Create a bitmap that measures 128 pixels square
        Bitmap bitmap = new Bitmap (128, 128,
            PixelFormat.Format32bppArgb);

        // Create a Graphics object for drawing to the bitmap
        Graphics g = Graphics.FromImage (bitmap);

        // TODO: Use Graphics methods to draw the image
          .
          .
          .
        // Set the response’s content type to image/jpeg
        context.Response.ContentType = "image/jpeg";

        // Write the image to the HTTP response
        bitmap.Save (context.Response.OutputStream, ImageFormat.Jpeg);

        // Clean up before returning
        bitmap.Dispose ();
        g.Dispose ();
    }

    public bool IsReusable
    {
        // Returning true enables instances of this class to be
        // pooled and reused. Return false if ImageGen instances
        // should NOT be reused.
        get { return true; }
    }
} 

ImageGen can be deployed in its own assembly or in the same assembly as a control. Once deployed, it’s invoked by ASP.NET whenever an HTTP request arrives for ImageGen.ashx. “Invoke” means ASP.NET instantiates ImageGen and calls its ProcessRequest method. ProcessRequest receives an HttpContext object whose Request property provides access to input parameters encoded in the query string. ProcessRequest writes to the HTTP response using the HttpContext object’s Response property. To return an image, ProcessRequest saves the bits making up the image to the stream represented by HttpContext.Response.OutputStream.

Incidentally, it doesn’t matter that there is no file named ImageGen.ashx; what’s important is that ImageGen.ashx is mapped to ImageGen via Web.config. You don’t have to use the file name extension ASHX for HTTP handlers, but ASHX is a widely used convention. Using ASHX as the handler’s file name extension also prevents you from having to register a special file name extension in the IIS metabase. (If you picked an arbitrary file name extension—say, IGEN—for your HTTP handler, you’d also have to map *.igen to Aspnet_isapi.dll in the IIS metabase. Otherwise, IIS wouldn’t forward requests for IGEN files to ASP.NET.) ASHX files are already mapped to ASP.NET in the IIS metabase, so requests for files with ASHX file name extensions are automatically handed off to ASP.NET. You can also write ASPX files that generate and return images, but using a dedicated HTTP handler is cleaner because it requires no additional files on the server.

With this background in mind, let’s close the chapter with a control that returns a dynamically generated image depicting an odometer. One application for such a control is to implement the ubiquitous hit counters found on sites all over the Web.

The Odometer Control

The Odometer control shown in Figure 8-29 renders itself using dynamically generated images. The control’s programmatic interface consists of the following public properties:

Property

Description

Count

Gets and sets the value displayed by the control

Digits

Gets and sets the number of digits displayed

Width

Gets and sets the control’s width

Height

Gets and sets the control’s height

ForeColor

Gets and sets the color of the control’s numbers

BackColor1

Gets and sets the first of two background colors behind the numbers

BackColor2

Gets and sets the second of two background colors behind the numbers

BorderColor

Gets and sets the color of the control’s border

The two BackColor properties merit further explanation. The Odometer control uses a pair of LinearGradientBrushes (discussed in Chapter 4) to paint the background behind the numbers. It exposes the colors of these brushes through BackColor1 and BackColor2. By default, BackColor1 is black and BackColor2 is light gray, which produces a background that fades from black to light gray and then back to black again, yielding the realistic look depicted in Figure 8-29. If you prefer a flat background, set BackColor1 and BackColor2 to the same color. Setting both to red produces red cells behind the numbers.

The Odometer control in action.
Figure 8-29. The Odometer control in action.

It’s simple to add an Odometer control to a Web page and configure it to display the number 1,000:

<%@ Register TagPrefix="win" Namespace="Wintellect"
  Assembly="OdometerControl" %>
    .
    .
    .
<win:Odometer Count="1000" RunAt="server" />

The following statement configures the control to display five digits (“01000”) instead of the four that would normally be displayed for 1,000:

<win:Odometer Count="1000" Digits="5" RunAt="server" />

The next statement does the same thing, but it also configures the control to display numbers against a flat black background:

<win:Odometer Count="1000" Digits="5" RunAt="server"
  BackColor1="black" BackColor2="black" />

If you’d like, you can set Count’s value at run time by initializing it from a Page_Load handler:

<win:Odometer ID="MyOdometer" RunAt="server" />
    .
    .
    .
<script language="C#" runat="server">
  void Page_Load (Object sender, EventArgs e)
  {
      MyOdometer.Count = 1000;
  }
</script>

In all likelihood, you’d retrieve the count from a database or other data source rather than hardcode into the ASPX file.

Before using the control in a Web page, you must do the following:

  • Compile Odometer.cs into a DLL and place the DLL in the application root’s bin directory. The @ Register directive in the preceding example assumes the DLL’s name is OdometerControl.dll.

  • Copy the Web.config file in Example 8-30 to the application root (or to any subdirectory containing an ASPX file that uses an Odometer control). If the directory already contains a Web.config file, simply add the <httpHandlers> section to the existing file.

Odometer outputs <img> tags whose Src attributes reference OdometerImageGen.ashx. Web.config maps requests for OdometerImageGen.ashx to the HTTP handler OdometerImageGen, which lives in the same DLL as the control.

Example 8-30. Web.config file for the Odometer control.

Web.config

<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*" path="OdometerImageGen.ashx"
        type="Wintellect.OdometerImageGen, OdometerControl" />
    </httpHandlers>
  </system.web>
</configuration>

How the Odometer Control Works

Odometer.cs (Example 8-31) is a fine example of how to write custom controls that output dynamically generated images. The Odometer class represents the control itself. Its Render method outputs an <img> tag that points to OdometerImageGen.ashx as the image source. The URL includes a query string that provides the handler with all the information it needs regarding the odometer’s appearance:

<img src="OdometerImageGen.ashx?Count=1000&Digits=5...">

When the browser fetches OdometerImageGen.ashx from the server, ASP.NET activates OdometerImageGen thanks to the following statement in Web.config:

<add verb="*" path="OdometerImageGen.ashx"
  type="Wintellect.OdometerImageGen, OdometerControl" />

OdometerImageGen is implemented in Odometer.cs alongside Odometer. Its ProcessRequest method generates an image based on the inputs contained in the query string. ProcessRequest offloads the actual work of creating the image to a local method named GenerateImage. It then transmits the image back in the HTTP response by calling Bitmap.Save on the Bitmap returned by GenerateImage and directing the output to the Response object’s OutputStream.

Example 8-31.  Odometer control.

Odometer.cs

using System;
using System.Web;
using System.Web.UI;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Text;

namespace Wintellect
{
    public class Odometer : Control
    {
        int MyCount = 0;
        int MyDigits = 0;
        int MyWidth = 128;
        int MyHeight = 48;
        Color MyForeColor = Color.White;
        Color MyBackColor1 = Color.Black;
        Color MyBackColor2 = Color.LightGray;
        Color MyBorderColor = Color.Gray;

        public int Count
        {
            get { return MyCount; }
            set
            {
                if (value >= 0)
                    MyCount = value;
                else
                    throw new ArgumentOutOfRangeException ();
            }
        }

        public int Digits
        {
            get { return MyDigits; }
            set
            {
                if (value >= 0)
                    MyDigits = value;
                else
                    throw new ArgumentOutOfRangeException ();
            }
        }

        public int Width
        {
            get { return MyWidth; }
            set
            {
                if (value >= 0)
                    MyWidth = value;
                else
                    throw new ArgumentOutOfRangeException ();
            }
        }

        public int Height
        {
            get { return MyHeight; }
            set
            {
                if (value >= 0)
                    MyHeight = value;
                else
                    throw new ArgumentOutOfRangeException ();
            }
        }

        public Color ForeColor
        {
            get { return MyForeColor; }
            set { MyForeColor = value; }
        }

        public Color BackColor1
        {
            get { return MyBackColor1; }
            set { MyBackColor1 = value; }
        }

        public Color BackColor2
        {
            get { return MyBackColor2; }
            set { MyBackColor2 = value; }
        }

        public Color BorderColor
        {
            get { return MyBorderColor; }
            set { MyBorderColor = value; }
        }

        protected override void Render (HtmlTextWriter writer)
        {
            StringBuilder builder = new StringBuilder ();

            builder.Append ("OdometerImageGen.ashx?");
            builder.Append ("Count=");
            builder.Append (Count);
            builder.Append ("&Digits=");
            builder.Append (Digits);
            builder.Append ("&Width=");
            builder.Append (Width);
            builder.Append ("&Height=");
            builder.Append (Height);
            builder.Append ("&ForeColor=");
            builder.Append (ForeColor.ToArgb ().ToString ());
            builder.Append ("&BackColor1=");
            builder.Append (BackColor1.ToArgb ().ToString ());
            builder.Append ("&BackColor2=");
            builder.Append (BackColor2.ToArgb ().ToString ());
            builder.Append ("&BorderColor=");
            builder.Append (BorderColor.ToArgb ().ToString ());

            writer.WriteBeginTag ("img");
            writer.WriteAttribute ("src", builder.ToString ());
            if (ID != null)
                writer.WriteAttribute ("id", ClientID);
            writer.Write (HtmlTextWriter.TagRightChar);
        }
    }

    public class OdometerImageGen : IHttpHandler
    {
        public void ProcessRequest (HttpContext context)
        {
            // Extract input values from the query string
            int Count = Convert.ToInt32 (context.Request["Count"]);
            int Digits = Convert.ToInt32 (context.Request["Digits"]);
            int Width = Convert.ToInt32 (context.Request["Width"]);
            int Height = Convert.ToInt32 (context.Request["Height"]);

            Color ForeColor = Color.FromArgb
                (Convert.ToInt32 (context.Request["ForeColor"]));
            Color BackColor1 = Color.FromArgb
                (Convert.ToInt32 (context.Request["BackColor1"]));
            Color BackColor2 = Color.FromArgb
                (Convert.ToInt32 (context.Request["BackColor2"]));
            Color BorderColor = Color.FromArgb
                (Convert.ToInt32 (context.Request["BorderColor"]));

            // Generate an image to return to the client
            Bitmap bitmap = GenerateImage (Count, Digits,
                Width, Height, ForeColor, BackColor1, BackColor2,
                BorderColor);

            // Set the content type to image/jpeg
            context.Response.ContentType = "image/jpeg";

            // Write the image to the HTTP response
            bitmap.Save (context.Response.OutputStream,
                ImageFormat.Jpeg);

            // Clean up
            bitmap.Dispose ();
        }

        public bool IsReusable
        {
            get { return true; }
        }

        Bitmap GenerateImage (int Count, int Digits,
            int Width, int Height, Color ForeColor, Color BackColor1,
            Color BackColor2, Color BorderColor)
        {
            const int BorderWidth = 4;
            const int MinCellWidth = 16;
            const int MinCellHeight = 24;

            // Make sure Digits is sufficient for Count to be displayed
            int digits = Digits;
            int places = Places (Count);
            if (digits < places)
                digits = places;

            // Compute the width of a single character cell and
            // the width and height of the entire image
            int CellWidth = System.Math.Max (Width / digits,
                MinCellWidth);
            Width = (CellWidth * digits) + BorderWidth;
            Height = System.Math.Max (Height, MinCellHeight);

            // Create an in-memory bitmap
            Bitmap bitmap = new Bitmap (Width, Height,
                PixelFormat.Format32bppArgb);

            // Create the fonts and brushes that will be used to
            // generate the image
            Font font = new Font ("Arial", Height / 2);
            Brush brushForeColor = new SolidBrush (ForeColor);
            Brush brushBorderColor = new SolidBrush (BorderColor);

            // Create a Graphics object that can be used to draw to
            // the bitmap
            Graphics g = Graphics.FromImage (bitmap);

            // Fill the bitmap with the border color
            g.FillRectangle (brushBorderColor, 0, 0, Width, Height);

            // Create a StringFormat object for displaying text
            // that is centered horizontally and vertically
            StringFormat format = new StringFormat ();
            format.Alignment = StringAlignment.Center;
            format.LineAlignment = StringAlignment.Center;

            // Initialize the values used to extract individual
            // digits from Count
            int div1 = (int) System.Math.Pow (10, digits);
            int div2 = div1 / 10;

            // Draw the digits and their backgrounds
            for (int i=0; i<digits; i++) {
                Rectangle rect =
                    new Rectangle (i * CellWidth + BorderWidth,
                    BorderWidth, CellWidth - BorderWidth,
                    Height - (2 * BorderWidth));

                Rectangle top = rect;
                top.Height = (rect.Height / 2) + 1;
                Rectangle bottom = rect;
                bottom.Y += rect.Height / 2;
                bottom.Height = rect.Height / 2;

                Brush brushBackColor1 =
                    new LinearGradientBrush (top, BackColor1,
                    BackColor2, LinearGradientMode.Vertical);

                Brush brushBackColor2 =
                    new LinearGradientBrush (bottom, BackColor2,
                    BackColor1, LinearGradientMode.Vertical);

                g.FillRectangle (brushBackColor2, bottom);
                g.FillRectangle (brushBackColor1, top);

                string num = ((Count % div1) / div2).ToString ();
                g.DrawString (num, font, brushForeColor, rect, format);

                div1 /= 10;
                div2 /= 10;

                brushBackColor1.Dispose ();
                brushBackColor2.Dispose ();
            }

            // Clean up and return
            font.Dispose ();
            brushForeColor.Dispose ();
            brushBorderColor.Dispose ();
            g.Dispose ();

            return bitmap;
        }

        // Compute the number of places (digits) in an input value
        int Places (int val)
        {
            int count = 1;
            while (val / 10 > 0) {
                val /= 10;
                count++;
            }
            return count;
        }
    }
}

Summary

Here’s a summary of the important concepts presented in this chapter:

  • Custom controls are authored by deriving from System.Web.UI.Control.

  • A custom control overrides the Render method it inherits from Control to render itself to a Web page.

  • Implementing IPostBackDataHandler enables a control to update property values using data transmitted in postbacks and also to fire change events.

  • Controls that fire change events persist property values in view state in order to detect changes in property values.

  • Controls can force postbacks by rendering HTML elements that use client-side scripts to submit forms to the server. Page.GetPostBackEventReference outputs the postback script and returns code that calls the postback function.

  • Implementing IPostBackEventHandler enables a control to fire events when an element it rendered to a page causes a postback.

  • Composite controls serve as containers for other controls. A composite can be populated with child controls declaratively or programmatically.

  • Controls have the option of returning client-side script when they render themselves to a Web page. Page.RegisterClientScriptBlock ensures that functions returned in a script block are returned just once per request.

  • Controls can use the Browser property of the Request object to determine which type of browser submitted the request and adapt their output accordingly.

  • Graphical controls can be authored by returning <img> tags whose Src attributes reference image files stored on the server or HTTP handlers that generate images and return them to the client.

  • The @ Register directive enables custom controls to be used in Web forms.

Controls are the atoms from which Web forms are composed. Encapsulating complex rendering and behavioral logic in custom controls is a great way to share your knowledge with other developers and shield them from implementation details at the same time. As you design and implement controls of your own, keep in mind the principles discussed in this chapter. And feel free to use the samples contained herein as the basis for controls of your own.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.227.4