Manage Dynamically Created Controls

Take advantage of automatic state maintenance for dynamic controls.

CoreCoder

LANGUAGES: C#

TECHNOLOGIES: Controls | Dynamic Creation | View State | Postback Events

 

Manage Dynamically Created Controls

Take advantage of automatic state maintenance for dynamic controls.

 

By Dino Esposito

 

One ASP.NET feature that most captures the attention of programmers is the full correspondence between markup elements and instances of controls. Any tags in the body of an .aspx page that has the runat attribute equal to "server" are translated into instances of a .NET Framework class. This apparently simple fact has an important repercussion: It allows you to create and configure controls dynamically. For example, you can read profile information out of a data source and build the page to serve to the user dynamically; you can arrange grids of data with optional columns that users toggle on and off at their convenience; and you can assemble composite controls and forms on the fly.

 

In ASP.NET applications, you can introduce dynamic contents in your page layout in several ways - not simply create controls when needed. In this article, I'll provide a brief overview of these techniques, then I'll focus on the dynamic creation of control instances and the issues it poses for state maintenance and postback data management.

 

Be Dynamic

Generally speaking, you have three ways to make your pages host a varied set of controls each time they display to the browser. You can include all controls you ever need in the page layout and turn them on and off using the Visible attribute; you can load small subforms from user-control files (resources saved with an .ascx extension); or, as this article describes in detail, you can use the "new" operator to create instances of controls dynamically. Naturally, each approach has its pros and cons, and there are scenarios in which each works better than the others.

 

First, consider a case in which you include in the source of the page all possible controls the Web form might need. When the page is loaded initially, the ASP.NET runtime parses the .aspx source and creates a dynamic class. This class derives, directly or indirectly, from Page, and its primary task is building the tree of controls that belong to the main form. At parse time, all tags with the runat attribute equal to the server are located and transformed in code. For example, the tag originates this private method on the dynamically created class that represents the page in action:

 

private Control BuildControlButton1()

{

   Button ctrl = new Button();

   this.Button1 = ctrl;

   ctrl.ID = "Button1";

   ctrl.Text = "Button";

   return ctrl;

}

 

Similar methods are generated for all server-side controls. As a result, when the ASP.NET runtime begins processing the request, it instantiates the Page class, thus initializing all contained server controls irrespective of how many and which controls actually will be rendered. When the page enters rendering mode, only the controls with the Visible attribute set to True will generate HTML. You can add or exclude controls from view simply by toggling on and off the Visible attribute, but an instance of all declared controls always is created. As you'll see in a moment, this is an important aspect to consider.

 

Another way to create contents dynamically is through the Page class's LoadControl method:

 

// Create an instance of the user control

Control c = LoadControl(urlOfAscx);

 

// Link it to a particular position in the layout

thePlaceHolder.Controls.Add(c);

 

The source code of the control must be saved in a separate file with the .ascx extension. You load it from a virtual path and bind it to the Controls collection of a server control. This technique is useful when portions of the user interface can be rendered according to different skins. To ensure the dynamically created control is placed in a particular position within the parent form, you can use a placeholder and link the control to the placeholder's Controls collection.

 

Create Controls Dynamically

As I mentioned, using the "new" operator is the principal way to create dynamic controls in ASP.NET. This code shows a page that adds a new textbox control when you click on a button:

 

  

  

onclick="OnClick" />

 

The OnClick event handler looks as it does in this code snippet:

 

public void OnClick(object sender, EventArgs e) {

   AddTextBox();

}

public void AddTextBox() {

   TextBox ctl = new TextBox();

   ctl.Text = "Hello";

   placeHolder.Controls.Add(ctl);

}

 

The newly created control is added to the placeholder, giving you full control over its position, even in a flow layout. Is there something wrong with this code? Not until you add another control to the page that can initiate a postback. Add another button and bind it to some server-side method:

 

onclick="OnPostBack" />

 

If you click on this new button after creating the dynamic control, you might be unpleasantly surprised: The dynamic control disappears when the page refreshes in the browser (see Figure 1).

 


Figure 1. The dynamically created control disappears when the page posts back. There's no code outside the event handler of the Create button to create the control upon postbacks caused by other controls.

 

Because the textbox is created only by the server event bound to the Create button, nothing guarantees it will be created again when another element causes the round trip (for example, the PostBack button). In particular, when the page posts back because someone clicked on the PostBack button, there's no path in the code that actually re-creates the textbox.

 

To ensure dynamic controls are created upon each postback, you should create them in the Page_Load event. Depending on the requirements of the application, though, this is not always possible. So how can you ensure that a control created by a particular postback event is always re-created? Furthermore, how can you ensure that the view state is restored correctly?

 

Remember the Controls You Created

To track the controls created dynamically, you must use a persistent data store, such as the view state. A simple trick you can employ consists of adding some information to the page's view state whenever a dynamic control is created. The syntax and semantics of what you add to the view state are completely up to you.

 

The code in Figure 2 shows you how to extend the previous code to write a note in the page's view state when a particular control is added. Whenever the page posts back, the code in the Page_Load handler checks the page's view state and re-creates the control if needed. For this pattern to work, you must insulate the control factory code in a method that can be invoked from various places such as AddTextBox.

 

public void Page_Load() {

   if (IsPostBack) {

      if (ViewState["AddedControl"] != null) {

         // Re-create the control but do not

         // restore settings configured outside

         // the proc (i.e., MaxLength and BackColor)

         TextBox t = AddTextBox();  

      }  

   }

}

public void OnClick(object sender, EventArgs e) {

   TextBox t = AddTextBox();

 

   // Modify properties outside the proc to

   // simulate generic changed to the

   // control's view state

   t.MaxLength = 5;

   t.BackColor = Color.Yellow;

}

public TextBox AddTextBox() {

   TextBox ctl = new TextBox();

   ctl.Text = "Hello";

   placeHolder.Controls.Add(ctl);

 

   // Records that a dynamic control has been added

   ViewState["AddedControl"] = "true";

   return ctl;

}

Figure 2. The OnClick event creates a textbox when invoked and stores a value in the view state to track it. Whenever the page posts back, the code in Page_Load re-creates the control if needed.

 

When I first tested this code, I was quite surprised that it worked. I was sure the control would be re-created correctly, but I was doubtful about the correct restoration of the control's view state and postback data.

 

To ensure no path in the code could reiterate settings such as MaxLength and BackColor programmatically (both are stored in the TextBox view state), I placed them outside the AddTextBox method deliberately. As a matter of fact, in Figure 2, the BackColor and MaxLength properties are set only if you choose to run the OnClick method. So, what was happening to magically restore the view state and the postback data of a dynamically created textbox?

 

Give It a Second Try

A common misconception among ASP.NET programmers is that the page restores the state completely and processes postback data when the Page_Load event fires. Well, the code in Figure 2 is proof that, for some controls, something else happens after Page_Load fires to restore the view state and postback data. I also tried moving the code of Page_Load into Page_Init, only to get a runtime error. As expected, the view state is null at that time, so the code can't work. While testing the sample page, I had the Trace option turned on. I can't say how many times I had a trace output before me, but I always failed to make sense of the pair of begin/end messages described as the "ProcessPostData Second Try."

 

It turns out that the ASP.NET runtime processes posted values in two steps. The first step occurs immediately after restoring the view state, as the documentation clearly states. Then, the Load event fires to the page and recursively to all child controls. By now, you might think the page is pretty much done and only needs to raise server changes and postback events. Nevertheless, before raising events, the runtime gives a second try to controls to process postback data.

 

You can consider this feature to be specific ASP.NET support for dynamically created controls. Basically, during the first step, the runtime processes the values in the query string or the Request.Form collection, depending on the HTTP method used to post the form. If no match is found between one of the posted IDs and controls in the page, the ID/value pair is copied into a temporary collection that will be processed again during the second try - guess what happens now?

 

During the first step, the Request.Form collection contains a reference to the dynamically created textbox, but no control with that ID exists at the time. For this reason, the ID of the control is set aside for later. Between the first and second steps, Page_Load fires, and in the example, I re-create the dynamic control just in time for the second try to catch it and process postback data correctly. This is really, really cool!

 

But my enthusiasm for this discovery was short-lived: It's OK for the posted values, but what about the view state? Who is restoring the view state of a dynamically created control, and when?

 

The view state cannot be restored in the traced "LoadViewState" timeframe because, at that time, the dynamic control hasn't been re-created yet. Loading the view state also has nothing to do with restoring postback data. Nevertheless, the view state is managed correctly. As my mother used to say, "If you don't know something, just ask." So, I asked the textbox to trace when the view state actually was restored (see Figure 3).

 

namespace AspNetPro

{

  public class TextBox : System.Web.UI.WebControls.TextBox

  {

     public override int MaxLength

     {

        get {

              Page.Trace.Warn("get MaxLength");

              return base.MaxLength;

        }

        set {

               base.MaxLength = value;

              Page.Trace.Warn("set maxLength");

        }

      }

 

     protected override void LoadViewState(object state)

     {

        base.LoadViewState(state);

        Page.Trace.Warn("Loading state for " + UniqueID);

      }

  }

}

Figure 3. Here is a custom TextBox class that traces the view-state restoration.

 

I derived a class from TextBox exclusively to override LoadViewState and one of the properties known to be stored in the view state (such as MaxLength). The overrides are minimal and limited to tracing the fact that a call was made. This is a debugging technique I sometimes use to step into the internals of system classes.

 

The new AspNetPro.TextBox class is used in Figure 2 in lieu of the original TextBox class. Figure 4 shows the results and reveals the mystery.

 


Figure 4. A derived class can help you determine internal behaviors of system classes.

 

The view state of a dynamically created control is restored within Page_Load automatically, particularly when the control is added to the parent's Controls collection. In fact, the Controls.Add method always makes a call to load the view state recursively for controls that are added dynamically.

 

In ASP.NET, there's only one way to create controls: Use the "new" operator. The runat=server attribute is simply its declarative counterpart. In light of this, dynamically created controls are no exception. I was pleased to discover, however, that the ASP.NET framework also has effective support for restoring state and postback data for dynamically created controls. In the end, you have only one trick to take care of: Re-creating the controls in the Page_Load event.

 

The sample code in this article is available for download.

 

Dino Esposito is a trainer and consultant who specializes in ASP.NET, ADO.NET, and XML. The author of Building Web Solutions with ASP.NET and ADO.NET and the upcoming Programming ASP.NET (both from Microsoft Press), Dino also is a co-founder of http://www.VB2TheMax.com. E-mail him at mailto:[email protected].

 

Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.

 

 

 

Hide comments

Comments

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

Plain text

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