A Crash Course on ASP.NET Control Development: Building New Controls from the Ground Up

 

Dino Esposito
Solid Quality Learning

October 2005

Applies to:
   Microsoft ASP.NET

Summary: Sometimes you just need to build an ASP.NET from the ground up, without relying on the existing controls. In this article, Dino shows you how to plan, design, and develop these controls. (20 printed pages)

Download the sample code in Visual Basic, CCCNewControlVB.msi.

Download the sample code in C#, CCCNewControlCS.msi.

Contents

Introduction
ASP.NET Controls from the Ground Up
Control Versus WebControl
Control Rendering
Control State
Events
Naming Containment
Themes
Styling the Control
Applying Styles
Conclusion

Introduction

There are two main situations in which ASP.NET developers feel the need to create custom controls. It happens when developers need a control that simply doesn't exist in the ASP.NET built-in toolbox. It also happens when developers need a control that is similar to one of the native controls but not close enough to justify its use. In the latter case, developers typically derive a new control from an existing one and add/override members as appropriate. I covered this approach in A Crash Course on ASP.NET Control Development: Deriving New Controls from Existing Classes; in this work, I'll discuss techniques and tricks to design and code completely new ASP.NET controls that address functionalities that ASP.NET doesn't provide out of the box.

ASP.NET Controls from the Ground Up

What's the difference between ASP.NET derived controls and brand new ASP.NET controls? Aren't we talking about the same kind of animal?

The answer to the latter question is obvious. We are, of course, still talking about the same thing—ASP.NET controls. To be precise, what I label here as "new controls" are, in the end, derived controls. So again, what's the difference between "derived" and "brand new" controls?

All ASP.NET controls derive, directly or indirectly, from a common base class—System.Web.UI.Control. "Derived" controls typically inherit from specialized classes that automatically endow derived controls with a well-known set of capabilities. If you derive a custom control from, say, DropDownList you get a new control that behaves like a drop-down list except for the new members and capabilities that your implementation would add. The inherited capabilities include the specific behavior of a drop-down list, but also rendering, postback, design-time features, naming containment, client-side scripting, data binding, and perhaps styles and templates support. If required, all these capabilities have to be manually coded in a "brand new" control. A "brand new" ASP.NET control typically inherits directly from Control or from a slightly more specialized, but still abstract, class like WebControl.

You choose to create a brand new control when you realize you need functionality that none of the predefined controls is able to offer. You derive the new control from either Control or WebControl and code manually any of the features you need. At the very minimum, you need to put in rendering and styling capabilities, plus any specific function you want the control to provide. In addition, you can optionally drive the design-time appearance and behavior through descriptive strings, smart-tag verbs, and even a custom rendering engine.

As I pointed out in Deriving New Controls from Existing Classes, ASP.NET 2.0 supplies a fairly long list of feasible base classes, each providing a well-defined set of built-in features—DataBoundControl, ListControl, and CompositeDataBoundControl, to name a few. If the control you need is not the direct emanation of an existing, working control (a specialized form of drop-down list), choosing the right base class is critical. The base class determines the gap between what you get free from ASP.NET and the desired result. What's not in the base class you must provide through code.

In this article, I'll take it the hard way and discuss how to build a sample control from Control. In subsequent articles, I'll cover data-bound and composite controls as well.

Control Versus WebControl

ASP.NET never requires you to build your custom control completely from scratch. Even the most basic classes in the hierarchy—Control and WebControl—implement a good deal of basic functionality and let you focus mostly on the specific features you need to implement.

The System.Web.UI.Control class defines the properties, methods, and events common to all ASP.NET server controls. The list includes the collection of child controls (the Controls collection), identification properties such as ID and UniqueID, view state management, skins, and connections to the rest of the page's hierarchy (for example, Parent and Page).

Derived from Control, the WebControl class adds extra properties and methods, mostly regarding control styles that affect rendering. These properties include ForeColor, BackColor, Font, ToolTip, CssClass, Height, and Width. Like Control, WebControl is defined in the System.Web.UI.WebControls namespace and represents the official base class for the family of Web server controls in ASP.NET.

Which one is the best option? In general, a new ASP.NET control that renders a customizable user interface (UI) derives from WebControl. Authors of controls that don't provide specific user-interface features are better off starting from Control.

However, take these two rules with a grain of salt. There might be situations in which you would reasonably do otherwise. For example, you can derive from Control also if you want to provide a smaller set of UI features than WebControl defines. Likewise, Control is the best option if you have a control with distinct blocks of user interface, each of which requires individual settings for colors, fonts, and measures. When too many graphical properties are required, you might also want to consider defining styles. Styles can be added both to Control and WebControl.

Let's exemplify with some code. I'll build a Headline control, made of a title and a body composed in a two-row table. Here's an example of the expected markup:

<msdn:Headline runat="server" id="headline1" 
      Title="This is the title" Text="This is the news" /> 

The Headline control inherits from WebControl, as below:

public class Headline : System.Web.UI.WebControls.WebControl
{
    :
}

Table 1 lists the properties defined on the control and gives an idea of the overall behavior.

Table 1. Properties defined on the Headline control

Property Description
ExpandButtonText Text for the caption of the expand button.
IconUrl URL to the image rendered in the top-left corner of the control.
ShowText Boolean value, indicates whether the text of the news is initially displayed. By default, only the title is displayed.
Text Indicates the text of the news story.
Title Indicates the title of the news story.

As mentioned, the control is rendered through a table with two rows. The first row contains up to three cells—icon, title, and button to expand or collapse the text of the news. The icon cell is optional and displayed only if a valid URL for the image is specified. The caption of the button to expand/collapse is customizable through the ExpandButtonText property. A Boolean property—ShowText—determines whether or not the text is initially displayed. After the first display, clicking the expand button toggles on and off the visibility of text.

Control Rendering

An ASP.NET control renders out through the Render method. To take total control of the control's rendering you should override the Render method.

protected override void Render(HtmlTextWriter output)

The HTML text writer object is a sort of buffer where you can accumulate all the text to be output. You can compose markup using the methods of the HtmlTextWriter object or using plain strings. This is indeed the fastest way for controls to generate their markup, but unfortunately it doesn't result in as much readable code. If you take this route for a reasonably complex control, your final code will look like an intricate mess of nested if-then-else statements. Your code will be hard to read and maintain. For this reason, another approach is often preferred—building the control tree programmatically. Here's a possible implementation for Render.

protected override void Render(HtmlTextWriter output)
{
     // Ensure it renders at design time without custom designers
     EnsureChildControls();
            
     // Style controls before rendering
     PrepareControlForRendering();

     // Renders out
     RenderContents(output);
}

The EnsureChildControls method checks whether the control tree has been successfully built. If not, the method forces the creation of the tree. Next, the control is styled as appropriate to ensure that design-time settings are correctly applied. Finally, the control is rendered out to HTML. This brief explanation leaves (at least) three questions unanswered:

  • Who does create the control tree?
  • Who and how does style controls?
  • What's RenderContents?

The Control class provides an overridable method named CreateChildControls.

protected override void CreateChildControls()

{

// Clears child controls

Controls.Clear();

// Build the control tree

CreateControlHierarchy();

ClearChildViewState();

}

The method is invoked during the loading phase of the control, immediately after the page request. You typically clear the existing collection of child controls (and any related viewstate) and rebuild the new hierarchy of controls. In this particular case, the method CreateControlHierarchy is a helper routine. Several built-in controls, though, have a method with a similar behavior and prototype defined as protected and virtual. In other words, calling a tree builder method from within CreateChildControls can be considered a best practice.

protected virtual void CreateControlHierarchy()
{
   // Create the containing table and two child rows
   Table t = new Table();
   TableRow row1 = new TableRow();
   TableRow row2 = new TableRow();
   t.Rows.Add(row1);
   t.Rows.Add(row2);

   // Populate rows with cells as appropriate
   PopulateRow1(row1);
   if (_textDisplayed)
       PopulateRow2(row2);

   // Connect to the parent
   Controls.Add(t);
}

In CreateControlHierarchy you define the structure of your control. The preceding code sets up a table with two rows and defines cells as appropriate. At the end, the root control—in this case, the Table—is appended to the Controls collection of the current control. This step is essential for the new control to render out.

As mentioned, the second row of the Headline control is optional. Its visibility depends on how many times the user clicked the expand button. The ShowText property indicates the initial state of the news text. After the first display, any click on the button toggles the visibility of the text. The private Boolean member _textDisplayed tracks the current situation: true means that the text is displayed, false means that only the title of the news is shown. (Figure 1 offers a preview of the control in action on a sample page. I'll return on this later.)

Aa479309.ccc3_fig01(en-us,MSDN.10).gif

Figure 1. The Headline control in action

The last statement probably caused a cold shiver to run down the spines of developers who ever wrote at least one ASP.NET server control. Why is that? The text claims that a private member of the control class is used to maintains some state—the visibility flag of the news text. In ASP.NET controls, any piece of information that is needed across postbacks should go in the view state, if not in the cache or session state. A private member is not an option—at least in ASP.NET 1.x, I'd say.

Imagine for a moment that you place the value stored by _textDisplayed in a private or protected property saved in the view state. What if another developer makes use of the Headline control in a page with the view state disabled? Quite simply, the information carried by _textDisplayed would be lost and the control left in an inconsistent state. That's why in ASP.NET 2.0 you have the control state.

Control State

The control state is a special data container introduced to create a sort of protected area inside the classic view state. Anything you store in the control state remains there until it is explicitly removed. Any private or protected properties that are both persistent and critical to the control's health should be stored in the ASP.NET 2.0 control state.

The implementation of the control state is left to the programmer—meaning that there's no table equivalent to the ViewState object to fill with values. Developers have to manually implement serialization and deserialization for any data that form the control state. The control state is processed along with the view state information and undergoes the same treatment as serialization and Base64 encoding. The control state is also persisted within the same view state's hidden field. What's needed to store _textDisplayed in the control state?

First, you override the OnInit method to inform the host page that the control requires control state management.

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
    Page.RegisterRequiresControlState(this);
}

At this point, the host page will place calls to a couple of methods the control must expose—LoadControlState and SaveControlState.

private bool _textDisplayed;
protected override object SaveControlState()
{
   return _textDisplayed;
}
protected override void LoadControlState(object savedState)
{
    if (savedState == null)
      return;

    _textDisplayed = (bool) savedState;
}

Although fairly simple, the preceding gives the gist of control state. SaveControlState returns an object that contains any state you want to safely persist across postbacks. In this simple case, you just return the value of the _textDisplayed member. In more complex cases, you group all the values in a container object of your choice—Pair, Triplet, arrays, collections.

LoadControlState does the reverse and sets all the various members with the values received as an argument. In this way, the information about the visibility of the text of the news happily survives postbacks and can be accessed using a private Boolean member.

Events

The button in Figure 1 is a canonical postback button. When clicked, the page posts back and the Headline control updates its state by changing the visibility of the text. Here's how the button is created:

Button btn = new Button();
btn.Click += new EventHandler(OnClick);
btn.Text = this.ExpandButtonText;
cell.Controls.Add(btn);

The Click event handler is shown below:

private void OnClick(object sender, EventArgs e)
{
   // Update _textDisplayed to reflect the click
   _textDisplayed = !_textDisplayed;

   // Track that the hierarchy must be recreated 
   ChildControlsCreated = false;
}

Two key things happen here. First, the _textDisplayed member is toggled. Second, the internal ChildControlsCreated property is reset. This property is defined on the Control class and indicates that all child controls have been created. This property is set to true after CreateChildControls is called by the framework—that is, after the control is loaded and fully initialized.

As you know, though, the initialization of the control is completed before the postback event is handled. This means that the control hierarchy doesn't reflect the changes induced by the new value of _textDisplayed. By setting CreateChildControls to false, you force the framework to regenerate the hierarchy before rendering.

How about notifying the page code of this click event? You can, for example, fire a custom event and let the page know that the Headline control is hiding or showing the text of the news. If needed, you can also offer page code a chance to cancel the event. Here's how to set up a custom event.

public event EventHandler<HeadlineExpandedEventArgs> HeadlineExpanded;

The new EventHandler<T> generic type greatly simplifies the definition of custom events in ASP.NET controls. All you need to do is create a class for carrying your data. The class can be as simple as shown below:

public class HeadlineExpandedEventArgs : EventArgs
{
    public bool BeingClosed; 
}

To fire the event, you add the following code to OnClick.

HeadlineExpandedEventArgs args = new HeadlineExpandedEventArgs();
args.BeingClosed = _textDisplayed;
OnHeadlineExpanded(args);

According to another common practice, you define a protected virtual method to trigger the event. This method has the form of OnXXX where XXX is the event name and the following implementation:

protected virtual void OnHeadlineExpanded(HeadlineExpandedEventArgs args)
{
     if (HeadlineExpanded != null)
         HeadlineExpanded(this, args); 
}

At this point, you may think you're just ready to intercept the HeadlineExpanded event from within a host page. You add the following code to a sample page, and request it.

protected void Headline1_HeadlineExpanded(object sender, 
       SampleControls.HeadlineExpandedEventArgs e)
{
    if (e.BeingClosed)
        Response.Write("Being closed");
    else
        Response.Write("Being opened");
}

You click the button, the page posts back, but the event is not fired. The preceding code is never reached. What's up?

Naming Containment

When a page posts back, the ASP.NET machinery restores the view state and orders all child controls to initialize themselves. Next, it attempts to figure out which control is going to handle the postback event. The control in charge of this is the server control whose ID matches the postback information in the request. There are two ways in which a postback can be requested from the client—through a submit button or a piece of script. In the former case, the ID of the clicked button is stored in the query string. In the latter case, an additional hidden field—aptly named _EVENTTARGET—contains the ID of the control that caused the postback. Whatever happens on the client, the ASP.NET server machinery can track it down and proceed.

Consider our scenario, instead. The control the page user clicks on is a push button contained inside Headline1. By default, though, the posted ID is ctl02—the automatically generated ID of the button. (I didn't set an explicit button ID in the code; if you do, that ID will be returned.) See Figure 2.

Aa479309.ccc3_fig02(en-us,MSDN.10).gif

Figure 2. A postback event involving the Headline control

Note   The tool used in the screenshot to snoop into the details of each HTTP request is IEWatch 2.0. You can get it at http://www.iewatch.com.

In its search for a control matching the ctl02 ID, ASP.NET looks only for controls that are direct children of the root form. Unfortunately, the ctl02 control is a child of Headline1, which is, in turn, a child of the form. As a result, no matching control is found and the postback passes unhandled. What can you do in a custom control to force ASP.NET to detect a postback occurring inside a top-level control? You have to implement the INamingContainer interface in the control.

public class Headline : WebControl, INamingContainer
{
  :
}

INamingContainer is a marker interface and doesn't require the implementation of any member. It simply changes the convention used to name internal controls. By applying the interface, the ID of the button control becomes Headline1$ctl02, as in Figure 3.

Aa479309.ccc3_fig03(en-us,MSDN.10).gif

Figure 3. A postback event involving the Headline control now acting as a naming container

The ASP.NET page framework knows how to handle control IDs that contain a $ symbol. In this case, it tokenizes the ID string and looks for a postback control named ctl02 in the tree rooted in Headline1. The control is found and the postback event gets correctly handled.

Themes

Server controls in ASP.NET 2.0 support themes. Custom controls also support themes. Once you've created a skin file, you simply register any custom control you want and skin it as usual. Here's an example:

<%@ Register Assembly="SampleControls" 
             Namespace="SampleControls" TagPrefix="msdn" %>
:
<msdn:Headline runat="server" 
      BackColor="lightyellow"
      IconUrl="images/star.png"
/>

Not all public properties of a server control are themeable. The Microsoft guidelines for ASP.NET themes state that only properties that affect the control's appearance should be themed. As far as the Headline control is concerned, only the ShowText and IconUrl properties should be themed; Text and Title should not. To enforce this rule, you can flag properties with the Themeable attribute. The attribute takes a Boolean parameter indicating whether or not themeability is enabled. For this reason, you only need apply the attribute to unthemeable properties. Here's an example:

[Themeable(false)]
public string Text {
...
}

If you then add the Text property to a skin file, the project won't compile.

Styling the Control

Related to themes and visual settings are control styles. When a control counts too many graphical properties, you might want to consider creating styles. A style allows you to group multiple settings in a single object. In addition, you get full support from the Visual Studio 2005 designer and let users work with your control more easily.

A style object is a class that inherits from Style. However, the vast majority of style objects you find sprinkled around the ASP.NET Framework 2.0 are classes derived from TableItemStyle, which in turn derives from Style.

The Headline control can have two styles—for the title and text. For completeness, you can also think of a third possible style for the expand button. Let's focus on the title style. You can create additional styles by simply adapting the code presented here. The title style property, TitleStyle, is defined as follows. It is an instance of the custom type TitleItemStyle.

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public TitleItemStyle TitleStyle
{
   get
   {
      if (_titleStyle == null)
          _titleStyle = new TitleItemStyle();
      if (IsTrackingViewState)
         ((IStateManager)_titleStyle).TrackViewState();
          return _titleStyle;
   }
}

The property is read-only and wraps a private member named _titleStyle.

private TitleItemStyle _titleStyle;

Any style properties in any ASP.NET controls are persistent across postbacks in the view state. Styles are more complex objects than integers or strings; for this reason, they are stored in the view state through a custom manager object. This explains why there's no use of the ViewState table in the preceding code snippet. The Style class implements the IStateManager interface and exposes methods to save visual settings in the view state in a performance-savvy way. Classes derived from Style (or TableItemStyle) override the methods of IStateManager to serialize and deserialize their own custom settings. The TitleItemStyle class is no exception.

The TitleItemStyle class extends TableItemStyle by adding a few attributes: BackColorClosed, ForeColorClosed, CellPadding, and CellSpacing. The latter two adjust cell spacing in the outermost table; the former two color properties change the foreground and background of the title row when the text of the news is not displayed. (See Figure 1.)

The TitleItemStyle class is defined as follows:

public class TitleItemStyle : TableItemStyle, IStateManager
{
    private Color _backColorClosed, _foreColorClosed;
    private int _cellPadding, _cellSpacing;

    public TitleItemStyle() {}

    // Public Members
    public Color BackColorClosed
    {
       get { return _backColorClosed; }
       set { _backColorClosed = value; }
    }
    public Color ForeColorClosed
    {
       get { return _foreColorClosed; }
       set { _foreColorClosed = value; }
    }
    public int CellPadding
    {
       get { return _cellPadding; }
       set { _cellPadding = value; }
    }
    public int CellSpacing
    {
       get { return _cellSpacing; }
       set { _cellSpacing = value; }
    }
    :
}

The listing still lacks the most interesting part, which is the implementation of the IStateManager members. The interface counts three methods and one property. Table 2 provides a brief description.

Table 2. The IStateManager interface

Member Description
IsTrackingViewState A Boolean property that indicates whether the view state is currently being tracked for changes.
LoadViewState The method deserializes what was previously serialized by the SaveViewState method.
SaveViewState The method serializes additional properties specific to this class.
TrackViewState The method activates tracking on the view state for changes.

The implementation of the members relies on the base class as much as possible.

bool IStateManager.IsTrackingViewState
{
    get { return base.IsTrackingViewState; }
}
void IStateManager.TrackViewState()
{
   base.TrackViewState();
}
object IStateManager.SaveViewState()
{
   object[] state = new object[2];
   state[0] = base.SaveViewState();
   object[] extraData = new object[4];
   extraData[0] = _backColorClosed;
   extraData[1] = _foreColorClosed;
   extraData[2] = _cellPadding;
   extraData[3] = _cellSpacing;
   state[1] = (object) extraData;
   return state;
}
void IStateManager.LoadViewState(object state)
{
   if (state == null)
       return;
   object[] myState = (object[]) state;
   base.LoadViewState(myState[0]);
   object[] extraData = (object[])myState[1];
   _backColorClosed = (Color)extraData[0];
   _foreColorClosed = (Color)extraData[1];
   _cellPadding = (int)extraData[2];
   _cellSpacing = (int)extraData[3];
}

The key part of this code is the body of SaveViewState. This method packs style-specific properties into an appropriate data structure. You typically use a Pair object if you're serializing a pair of values, or a Triplet in case three values are being processed. For more than three values, you either use a combination of Pair and Triplets or opt for a simpler and properly sized array of object.

SaveViewState allocates an array of two elements—one is for the object composed by the base class, and one is for the array of objects specific to the style. Next, it allocates an array of four elements—one for each custom style attribute—and copies there the current values of corresponding private members. LoadViewState just does the reverse.

Whenever you define a custom style object, you must override the methods of IStateManager to persist your custom attributes. The code below shows the typical way of doing it that is broadly used throughout the ASP.NET framework.

The advantage of styles versus individual properties is patent. Multiple attributes can be grouped in a single object, viewed and edited at design time through an expandable object (see Figure 4). Also, naming is more natural. Once you have styles for, say, title and text rows, you can set the respective background color using the BackColor property on the corresponding style object. With individual properties, you have to use different names such as TitleBackColor and TextBackColor to avoid collisions.

Aa479309.ccc3_fig04(en-us,MSDN.10).gif

Figure 4. The TitleStyle property in the Visual Studio 2005 designer

Whether you use styles or individual properties, you still need to make sense of the BackColor property (and a few more related properties) defined on the base WebControl class. In this particular case, BackColor can still be used to style the outermost table, whereas styles are used with individual rows. More reasonably, instead, you might want to get rid of BackColor and other unnecessary visual properties. To do so, you derive Headline from Control instead of WebControl.

Applying Styles

Visual Studio 2005 recognizes style properties and generates proper markup for them.

<msdn:Headline ID="Headline1" runat="server" 
      Text="Text of the news" Title="Title of the news"   
      ShowText="True" IconUrl="images/star.png">
  <TitleStyle HorizontalAlign="Center" BackColor="Lime" 
      BackColorClosed="224, 224, 224" CellPadding="5"
      Font-Bold="True" Font-Names="Arial" Font-Size="Larger" />
  <TextStyle BackColor="khaki" Font-Italic="true" Font-Size="small" />
</msdn:Headline>

The Render method needs to call into a method that applies styles to the constituent elements of the control's hierarchy. Here's how to do it:

protected virtual void PrepareControlForRendering()
{
   // Make sure there are controls to work with
   if (Controls.Count != 1)
       return;

   // Apply the table style
   Table t = (Table) Controls[0];
   t.CopyBaseAttributes(this);
   if (ControlStyleCreated)
      t.ApplyStyle(ControlStyle);

   // Cell spacing/padding set on title work for the whole table
   t.CellPadding = TitleStyle.CellPadding;
   t.CellSpacing = TitleStyle.CellSpacing;

   // Style the title row
   TableItemStyle s = new TableItemStyle();
   s.MergeWith(TitleStyle);
   if (!_textDisplayed)
   {
       s.BackColor = TitleStyle.BackColorClosed;
       s.ForeColor = TitleStyle.ForeColorClosed;
    }
    TableRow row1 = t.Rows[0];
    row1.ApplyStyle(s);
    int index = 0;
    if (!String.IsNullOrEmpty(IconUrl))
        index ++;
    row1.Cells[index].HorizontalAlign = TitleStyle.HorizontalAlign;

    // Style the text row
    TableRow row2 = t.Rows[1];
    row2.ApplyStyle(TextStyle);
}

The method walks through the hierarchy of constituent controls and styles them individually. For example, the CopyBaseAttributes imports in the root table properties such as AccessKey, Enabled, ToolTip, TabIndex, and Attributes as set by the page author on Headline. As a control developer, you can override CreateControlStyle to fix the default style of your control. If this method is overridden, and the control has a predefined style, this is imported in the root table through the ApplyStyle method.

Note that cell attributes must be applied to the table as a whole even though I defined them as properties of the title row.

In the title row, you need to modify the background color based on run-time conditions—the value of _textDisplayed. For this reason, you create an intermediate style object, load the TitleStyle property into it, and then override the BackColor and ForeColor properties as appropriate. The HorizontalAlign property on TableItemStyle indicates the alignment of the text in the row. For the title row, we only need to apply this setting to the cell that contains the title.

Finally, styling the text row is as easy as calling ApplyStyle on the corresponding table row.

Conclusion

In this article, we walked through the key steps involved with the creation of a custom ASP.NET 2.0 control from the ground up; that is, starting from an abstract base class and building all key capabilities—rendering, themes, control state postback, and styles. The code shown in this article requires ASP.NET 2.0. However, most of the concepts and techniques can be applied to ASP.NET 1.x as well. The exceptions are control state and themes.

It is worth noticing that the Headline control developed in this article works nicely at design time in the Visual Studio 2005 ASP.NET designer. It is even more important to note that no significant effort was made to make it work at design time. Put another way, there are many more design-time capabilities that can be added to a control. But that will make good fodder for another article in this crash course on ASP.NET control development. Stay tuned.

 

About the author

Dino Esposito is a Solid Quality Learning mentor and the author of Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005). Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch at cutting@microsoft.com or join the blog at https://weblogs.asp.net/despos.

© Microsoft Corporation. All rights reserved.