A Crash Course on ASP.NET Control Development: Managing Postbacks from Within Custom Controls

 

Dino Esposito
Solid Quality Learning

April 2006

Applies to:
   ASP.NET

Summary: This article explains how to create an ASP.NET custom control that captures data on the client, and then posts that data to the server across postbacks. (17 printed pages)

Click here to download the code sample for this article.

Contents

Introduction
Capturing Data on the Client
Rendering the TrackBar Control
Making the TrackBar an Interactive Control
TrackBar in Action
Detecting Relevant Posted Data
The IPostBackDataHandler Interface
Conclusion

Introduction

Postbacks are an essential part of the ASP.NET programming style. Pages are designed to post to themselves and retrieve their last-known good state. Next, pages update their state with any data coming from the client that was posted with the new request. When this is done, the page renders out the markup for the browser. While this brief abstract is probably good enough to get you to the big picture of the ASP.NET mechanics, it still skips over a number of key details.

The essential takeaway is that pages control the overall process, but they delegate actual tasks to all child controls. In other words, pages do not retrieve the last-known good state (from the viewstate), but instead ask individual controls to do that. Likewise, ASP.NET pages activate child controls when it comes to processing posted data. In the end, all tasks required in order to properly handle a postback event are accomplished by controls. If you're going to write a custom control that fires postback events, you must be aware of this, and be ready to apply proper techniques.

In this article, I'll review the steps required to create a custom control that captures data on the client, and posts that data to the server across postbacks.

Capturing Data on the Client

All ASP.NET custom controls fall into one of the following categories:

  • Non-interactive controls
  • Action-based controls
  • Postback controls

Non-interactive controls serve the purpose of displaying data in a way that is easy to navigate and understand. The typical example is a custom list-bound control.

Action-based controls incorporate some clickable elements (for example, hyperlinks and/or link buttons) in their user interface. When the user clicks any of these elements, the host page posts back. On the server, the control executes any required action and regenerates the markup for the browser. Basically, these controls execute actions over successive requests. They expose links, but handle clicking events internally. The GridView, and most its variations, fall into this category.

Postback controls are ASP.NET controls made of one or more HTML input fields. From a server-side perspective, these controls are either composite controls or variations of existing controls. Their user interface is typically made of a combination of stock controls such as TextBox and DropDownList. Mapped to HTML <input> fields, these controls represent the most common way to capture data on the client so that it can be sent to the server for further processing.

With the notable exception of TextBox, DropDownList, ListBox, CheckBox, and RadioButton, no ASP.NET controls have the capability to capture data on the client. What if you need a control that captures data out of user actions?

First, you need to create a user interface that provides guidance to the user. Next, you need to pack any captured information into a segment of the HTTP request, and unpack it on the server. Sounds a bit confusing? Let me clarify it with a couple of examples.

Consider the TextBox control.

The TextBox control captures and sends to the server any data the user types inside of it. This ability, though, is built in the HTML <input type=text> tag the TextBox control emits. In this particular case, the browser's understanding of the HTML markup language provides for an apparently magical behavior: what the user types is sent over the wire with the next request. Imagine you have the following in the .aspx page.

<asp:textbox runat="server" id="TextBox1" text="" />

On the client, this translates to the following:

<input type="text" id="TextBox1"value="" />

When the user causes a postback to the server, the contents of any input fields are serialized to a HTTP packet, as follows:

TextBox1=...&TextBox2=...

The ellipsis indicates the value in the input buffer when the page was posted.

The bottom line is that what the user types in a text box is automatically packed into a HTTP request and sent to the server. What happens on the server? I'll return to this shortly. First, though, let's expand on the concept of client-side data retrieval be looking at a second example.

Consider a completely custom control such as a TrackBar. Like in Windows Forms, an ASP.NET TrackBar control serves the purpose of letting users select a value through a gauge. In Windows Forms, the TrackBar control takes advantage of drag-and-drop to let users make their selection. As a user, you drag the bar in the track between a minimum and a maximum value; the control calculates the related value and makes it available to other parts of the application. Let's try to apply this pattern to ASP.NET.

For example, the TrackBar custom control will render out an HTML table with some JavaScript to let users drag a cell border right or left in order to increase or decrease the returned value. In the end, an ASP.NET TrackBar control is made of an HTML table (definitely not an interactive control) and some script code. As you can see, there's no classic input field involved that the browser knows how to handle. How can you capture client data and make sure it is handed on to the server?

In this case, when building the markup for the TrackBar control, you also add a hidden input field. Later, the JavaScript code injected in the page will take care of writing to this field any information to be passed to the server. A hidden field is like a input text field, except that browsers don't display it. A hidden field, therefore, works like a perfect cargo field for any data captured on the client that is to be processed on the server.

As the next step, I'll show how to build an ASP.NET TrackBar control that emits a resizable HTML table, proper script code, and a hidden field. The hidden field will be used to indicate the value the user selected through drag-and-drop. Next, I'll discuss how to retrieve and process the contents of the hidden field on the server, and how to update the state of the control properly.

Rendering the TrackBar Control

The TrackBar control is created as a composite control and given a few numeric properties: MinValue, MaxValue, and Value.

public class TrackBar : CompositeControl
{ ... }

As you can guess, MinValue and MaxValue define the range of values the user will choose from. Both are integers, and both can take negative values as well. The Value property indicates the currently selected value in the specified range.

public int MinValue
{
    get
    {
        object o = ViewState["MinValue"];
        if (o == null)
            return 0;
        return (int)o;
    }
    set 
    {
        ViewState["MinValue"] = value;
        if (Value < value)
            Value = value;
    }
}
public int MaxValue
{
    get
    {
        object o = ViewState["MaxValue"];
        if (o == null)
            return 100;
        return (int)o;
    }
    set 
    {
        ViewState["MaxValue"] = value;
        if (Value > value)
            Value = value;
    }
}
public int Value
{
    get
    {
        object o = ViewState["Value"];
        if (o == null)
            return 0;
        return (int)o;
    }
    set 
    {
        if (value < MinValue)
            value = 0;
        if (value > MaxValue)
            value = MaxValue;
        ViewState["Value"] = value; 
    }
}

The default range goes from 0 to 100. Note that the set modifiers for the properties check the value being assigned for consistency. For example, a value smaller than the minimum is not accepted, and it is automatically set equal to the current minimum.

The structure of the TrackBar is quite simple. It consists of a single-row table with three cells. The leftmost cell indicates the quantity of the current value with respect to the range. The central cell has a fixed width—normally a few pixels—and behaves like the bar you can move to select a new value. Finally, the rightmost cell indicates what's left to reach the maximum value.

Aa479050.ccc-postback01(en-us,MSDN.10).gif

Figure 1. The TrackBar control in action in Visual Studio 2005

The black cell indicates the moving bar of the control.

TrackBar is a composite control, and as such, its markup is created by overriding the base CreateChildControls method. The following code snippet shows the typical implementation of the method.

protected override void CreateChildControls()
{
     base.CreateChildControls();
     Controls.Clear();
     CreateControlHierarchy();
     ClearChildViewState();
}

As you can see, the most interesting things take place in the body of CreateControlHierarchy—an arbitrary function that most composite controls use, and that they make protected and overridable for further extensibility.

Internally, CreateControlHierarchy builds the aforementioned table structure and adds the root Table object to the Controls collection of the TrackBar control.

protected virtual void CreateControlHierarchy()
{
    Table outer = new Table();
    TableRow row = new TableRow();
    outer.Rows.Add(row);

    // Value-cell
    TableCell valueCell = new TableCell();
    row.Cells.Add(valueCell);

    // Pointer-cell
    TableCell pointerCell = new TableCell();
    pointerCell.BackColor = Color.Black;
    row.Cells.Add(pointerCell);

    // Left-cell
    TableCell leftCell = new TableCell();
    row.Cells.Add(leftCell);

    // Complete the control
    BuildTrackBar(valueCell, leftCell);

    // Save the control tree
    Controls.Add(outer);

    // Register a helper hidden field
    Page.ClientScript.RegisterHiddenField(ID, "");
}

Constituent table cells are configured in the BuildTrackBar method, where the value of the MinValue, MaxValue, and Value properties is processed. Note also that a new hidden field is added in order to contain client-generated information for the server-side part of the control. It is essential that this hidden field be flagged with the same ID as the current control.

The BuildTrackBar helper method performs a sort of data binding on the just-created table structure.

protected void BuildTrackBar(TableCell valueCell, TableCell leftCell)
{
    int percDone = GetPercentageValue(Value);
    int percLeft = 100 - percDone;

    valueCell.BackColor = Color.Orange;
    valueCell.Attributes["Width"] = String.Format("{0}%", percDone);
    leftCell.Attributes["Width"] = String.Format("{0}%", percLeft);
}

The BuildTrackBar method relates the content of the Value property to MinValue and MaxValue, and translates that into a percentage. The percentage is used to properly size the cells of the table. For example, in an 0–1000 interval, a value of 400 is rendered through the following HTML.

<table><tr>
<td width="40%" />
<td width="5px" />
<td width="60%" />
</tr></table>

So, in the end, the client user viewing the page is served HTML markup that renders out as a table. An HTML table, as you should know, is not an input field. How can the user interact with the table to specify a new value? And how can this new value be forwarded to the server in order to update the Value property across postbacks?

Making the TrackBar an Interactive Control

A table element is not interactive in the HTML sense of the word. However, by adding a bit of JavaScript code, you can let the user play with it and generate an input value. How? By using client-side drag-and-drop. Let's take a look at the following HTML page snippet.

<table width="300px" border="1"
     onmousemove="OnMouseMove()"
     onmouseup="OnMouseUp()">
<tr>
   <td id="_Done" width="4%" style="background-color:cyan;" />
   <td width="5px" style="cursor:e-resize;" onmousedown="Resize()" /> 
   <td id="_ToDo" width="96%" /> 
</tr>
</table>

As the user hovers over the central cell, the mouse cursor changes to the horizontal arrow to indicate that resizing is possible. By clicking the left mouse button, the user triggers a custom JavaScript function named Resize. This function sets an internal Boolean flag to track that a drag operation is occurring. In addition, it also caches some information about the table that might be useful later. As the user moves over the parent table, other two JavaScript functions are invoked: OnMouseMove and OnMouseUp. As their names suggest, they are invoked when the mouse moves, and when the mouse button is released, respectively.

When the mouse button is released, the current mouse position is detected and put in relationship with the overall width of the table. This calculation determines the new value of the trackbar. The value is reflected by the page's user interface—a JavaScript-driven update of the table's cells size—and also written to the companion hidden field.

To be able to infer a value from the mouse movements, and to properly resize the table cells dynamically, you need to obtain a reference to the table object as represented in the browser's Document Object Model (DOM). The Resize function does this as follows.

var bDragMode;
var Done_Width; 
var TotalWidth;
var done, todo;
var doneID, todoID;
var hiddenFieldName;
var hiddenField;

function Resize()
{
   bDragMode = true;
   
   // Find the parent table
   var elem = window.event.srcElement;
   while (elem.tagName != "TABLE") 
      elem = elem.parentElement;
   if (elem != null)
   {
      // Find the companion hidden field
      TotalWidth = elem.clientWidth;
      hiddenFieldName = elem.id;
      var index = hiddenFieldName.lastIndexOf("_");
      hiddenFieldName = hiddenFieldName.substring(0,index);
      hiddenField = document.getElementById(hiddenFieldName);
   }

   // Find the "value" cell
   doneID = elem.id + "_Done";
   done = document.getElementById(doneID);
   Done_Width = done.clientWidth;
   
   // Find the "remaining" cell
   todoID = elem.id + "_ToDo"
   todo = document.getElementById(todoID)
}

function OnMouseMove()
{
    if (!bDragMode) return;

   done.style.display = "";
   todo.style.display = "";
    
    window.event.srcElement.style.cursor = "e-resize";
}

function OnMouseUp()
{
   if(!bDragMode)   
       return;
   bDragMode = false;
   Update();
   todo.style.cursor = "default";
   done.style.cursor = "default";
}

In order to properly identify involved cells, a bit of naming convention is required. The JavaScript code, therefore, assumes that the leftmost cell (which represents the value) is named XXX_Done, where XXX is the name of the parent control—for example, TrackBar1. Likewise, XXX_ToDo is the name of the rightmost cell, which represents the remaining part of the interval. Finally, the parent table is named XXX_Table, and the hidden field has the same ID as the control.

All of these conventions are arbitrary, but they are essential. In other words, you can use other conventions if you want, but you need to use some conventions in order to be able to retrieve due elements easily. Using any conventions doesn't affect the generality of the solution, because you can assume that conventions are hard-coded in the server-side code of the control.

In light of this, some portions of the BuildTrackBar helper method need to be tweaked. The following code snippet fixes the creation of the outermost table.

Table outer = new Table();
outer.ID = "Table";
outer.Attributes["onmousemove"] = "OnMouseMove()";
outer.Attributes["onmouseup"] = "OnMouseUp()";
TableRow row = new TableRow();
outer.Rows.Add(row);

The next code snippet demonstrates how to tweak cells accordingly:

// Value-cell
TableCell valueCell = new TableCell();
valueCell.ID = outer.ID + "_Done";
row.Cells.Add(valueCell);

// Pointer-cell
TableCell pointerCell = new TableCell();
pointerCell.BackColor = Color.Black;
pointerCell.Attributes["onmousedown"] = "Resize()";
pointerCell.Attributes["width"] = "5px";
pointerCell.Style["cursor"] = "e-resize";
row.Cells.Add(pointerCell);

// Left-cell
TableCell leftCell = new TableCell();
leftCell.ID = outer.ID + "_ToDo"; 
row.Cells.Add(leftCell);

Finally, you need to inject proper JavaScript code into the markup that is served to the client. Script injection requires that you register your script with the ClientScript object on the Page class. The most common place to do this is the OnPreRender override.

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    // Add some script code
    if (!Page.ClientScript.IsClientScriptIncludeRegistered("Trackbar.js"))
    {
        Page.ClientScript.RegisterClientScriptInclude(
            "trackbar.js", 
            Page.Request.ApplicationPath + "/trackbar.js");
    }
}

There are various ways to inject script code into a page. If the code doesn't require dynamic changes, you can insert it through a <script> include tag. Alternatively, you can incorporate the script into the assembly, and register it through the RegisterClientScriptResource method. Incorporating the script file into the assembly is beneficial from a deployment perspective, because everything that you need is in the assembly. If you bring script in through a separate file, you need to deploy the JavaScript file in addition to the control's assembly.

TrackBar in Action

Figure 2 shows the TrackBar ASP.NET control in action in a sample page. You move the cursor to the black cell—the trackbar pointer—and then click and drag. As you move the mouse, the cursor remains in the form of an horizontal arrow, meaning that the resize operation is occurring.

Aa479050.ccc-postback02(en-us,MSDN.10).gif

Figure 2. Trackbar in action

When you are done, you release the mouse button; the JavaScript code in the page computes the new value in the trackbar and updates the client-side table. The following fragment of code shows how it does this.

perc = CalculatePerc();
done.width = perc + "%";
todo.width = (100-perc) + "%";

In this code, done and todo are references to the table cells that represent the orange and white blocks in Figure 2. Once the new value is known as a percentage, the width of both cells is updated through the browser's DOM.

So far so good.

With the current version of the control, you can display an HTML table to the user, and have him or her resize cells by using drag-and-drop. As a result, the HTML table reflects the new size, as Figure 2 clearly demonstrates.

So far so good.

What do you think will happen if the user clicks any button in the page and causes a postback? The changed value is lost. The Value property of the TrackBar control is never updated with the client-generated value, and the trackbar displays the old value upon postback.

This misbehavior has two main causes. First, the value generated on the client—the user input—is not transmitted to the server. Second, if the value is posted in some way, the TrackBar control has no built-in logic to properly detect and handle this value.

The first cause is easy to fix. Once the JavaScript code has determined the new value of the trackbar, you must save the value in a location that preserves the value and, more importantly, transmits the contents to the server with the next request. As explained earlier, the TrackBar control emits a hidden field with the same ID as the server control. The JavaScript code needs to write the new value to the hidden field.

hiddenField.value = perc;

The hiddenField JavaScript variable contains a reference to the hidden field. As you've seen earlier in the article, the reference is set when you start the drag operation.

Detecting Relevant Posted Data

Any hidden fields in an ASP.NET page are automatically posted with the request. This means that when the user clicks a button in a page that contains the TrackBar control, the contents of the companion hidden fields are automatically embedded with the HTTP request and reach the server. Having data posted to the server, though, doesn't guarantee that the data will be properly recognized and handled.

In ASP.NET, highly interactive controls that need to process posted data in order to update their own state need to fulfill an additional requirement—the implementation of the IPostBackDataHandler interface.

When a new request comes in, the ASP.NET runtime instantiates a Page-derived class to serve it. This class first initializes all controls, based on the attributes statically set in the .aspx source file; next, it matches element names in the request with existing control instances.

If a match is found, the page class verifies that the control implements the IPostBackDataHandler interface. If yes, it calls a method on the interface in order to give the control a chance to update its own state with client-generated values. Let's briefly review what happens with a TextBox control.

Imagine that a text box named TextBox1 is instantiated. This control generates a text input field with the same name. The user writes some text in the input field and posts back. The HTTP packet will contain the following.

TextBox1=...

Here, the ellipsis stands for the client contents of the text box.

On the server, the page class compares TextBox1 with the ID of all controls in the page. If no match is found, the posted value is ignored. Otherwise, if a match is found—that is, if the page contains a control named TextBox1—the page checks whether the control implements the IPostBackDataHandler interface. The TextBox control does implement the interface.

The page class invokes the LoadPostData method on the interface, and gives the control a chance to update its own state. The TextBox implementation of the LoadPostData method checks the posted value against the current value of the Text property. If the two differ, the posted value wins, and its contents are used to override the Text property.

A similar mechanism must be implemented for updating the Value property of the TrackBar control.

The IPostBackDataHandler Interface

The IPostBackDataHandler interface counts two methods: LoadPostData and RaisePostDataChangedEvent. Their signature is as follows.

bool LoadPostData(string postDataKey, NameValueCollection postValues)
void RaisePostDataChangedEvent()

LoadPostData is called during the page lifecycle, in between the Init and Load events—that is, in the phase when the page is building an up-to-date state for each of its controls. Before the Init event is fired, each control is instantiated as hard-coded in the source .aspx file. Next, the attached viewstate is processed to restore the last-known good state for each control. Finally, each control whose ID matches an entry in the request form is called to some action. The ultimate goal of the LoadPostData method is to update the current state of the control with any posted data of interest.

It is important to note that LoadPostData is invoked if, and only if, there's an entry in the request form with the same ID as the control. Returning to trackbar example, let's consider the following script.

<x:TrackBar id="TrackBar1" runat="server" />

As mentioned, the TrackBar control emits an HTML table, plus a hidden field with the same TrackBar1 ID. This guarantees that when the client page posts back, the contents of the hidden field are posted, thus triggering the IPostBackDataHandler interface on a control flagged with the TrackBar1 ID.

bool LoadPostData(string postDataKey, NameValueCollection postValues)
{
    double dbl = -1;
    int postedValue = -1;
    bool bOK = Double.TryParse(postValues[postDataKey], out dbl);
    if (bOK)
    {
        postedValue = GetAbsoluteValue((int) dbl);
        if (postedValue != Value)
        {
            Value = postedValue;
            return true;
        }
    }

    return false;
}

LoadPostData receives a string parameter set to the ID of the control—postDataKey—and the full collection of posted values. This means that each control could, in theory, check values on the whole request form, and that it is in no way limited to reading the sole entry with the matching ID.

LoadPostData extracts the value(s) of interest from the collection of posted values, and compares it to any of its properties that are subject to client-side actions. In this particular case, by design, the hidden field carries the percentage value the user set through drag-and-drop. This value is not good for setting the Value property directly, because the Value property represents an absolute value.

Once converted to an absolute value in a min–max range of acceptable values, the posted value is compared to the Value property. If the two match, nothing happened on the client that needs be tracked on the server. In this case, LoadPostData just exits and returns false. Otherwise, the Value property is updated with the posted value, and LoadPostData returns true.

The return value from LoadPostData plays a critical role in the economy of the control. If true, the page will later invoke the second method on the IPostDataDataHandler interface: RaisePostDataChangedEvent.

The goal of the RaisePostDataChangedEvent method is to fire a server-side event in order to let the page know that the state of the control has been updated across the latest postback. The typical implementation of the RaisePostDataChangedEvent method entails firing a control-defined event. For the TrackBar control, I have defined a ValueChanged event, as follows.

public event EventHandler ValueChanged;
protected virtual void OnValueChanged(EventArgs e)
{
    if (ValueChanged != null)
        ValueChanged(this, e);
}

The implementation of the interface method proceeds as follows:

void RaisePostDataChangedEvent()
{
    EventArgs e = new EventArgs();
    OnValueChanged(e);
}

Needless to say, you can either have a void implementation for RaisePostDataChangedEvent, or have it fire a richer event that deliver more information to handler. In this case, you might want to derive a class from EventArgs, add as many members as needed, and fill them up before firing the event.

In light of this, host pages can then catch changes on the trackbar by using the following code.

protected void TrackBar1_ValueChanged(object sender, EventArgs e)
{
    Response.Write(TrackBar1.Value.ToString());
}

The DefaultEvent attribute can be used to have Visual Studio 2005 generate a stub for the event as users double-click the control.

[DefaultEvent("ValueChanged")]
public class TrackBar : CompositeControl, IPostBackDataHandler
{
  :
}

Once LoadPostData has incorporated client input in the TrackBar control's state, the new value becomes part of the control. The next time the page asks the control to render out its markup, the displayed value will reflect the value determined on the client—exactly what happens with text box and drop-down list controls, which retain user input across postbacks.

Conclusion

There are a number of useful interfaces that custom controls can implement in order to make the programming interface richer. IPostBackDataHandler is perhaps the most powerful, because it makes it possible to transform virtually any sensible combination of script and HTML into a stateful ASP.NET control.

The use of hidden fields is key in similar scenarios, because hidden fields are the only available means for grabbing custom information on the client and passing it on the server to a particular control. The association between ASP.NET server controls and their HTML counterpart elements occurs through matching IDs. For this reason, it is essential that you name a control's hidden field with the same ID as the control. This ensures that any data carried back by the hidden field will be correctly associated with the control. Data must be written to the hidden field by using a script.

The sample TrackBar control doesn't have the capability of autonomously posting back. It has to wait for other controls—actually, the HTML elements of other ASP.NET controls—to post. Typically, these controls are buttons, link buttons, and controls that support the AutoPostBack property. To add postback capabilities to your controls, you have to resort to another powerful interface: IPostBackEventHandler. But that's another story, and it would require another article in order to be entirely told. Stay tuned.

 

About the author

Dino Esposito is a mentor at Solid Quality Learning, and the author of Programming Microsoft ASP.NET 2.0Core Reference and Programming Microsoft ASP.NET 2.0 ApplicationsAdvanced Topics, both from Microsoft Press. Late-breaking news is available at https://weblogs.asp.net/despos.

© Microsoft Corporation. All rights reserved.