Few ASP.NET topics are as widely discussed or misunderstood as "Dynamic Controls". I thought I was aware of all the issues, until a recent project had me questioning my sanity. The project needed CONDITIONAL dynamic controls, which is the focus of this article. Hopefully, I can save you some of the frustrations I went through.
Visual Studio.NET is a direct descendant of the original, ground-breaking Visual Basic. In that IDE, you coded by dragging controls onto a form, setting some properties, and then coding for events. It was a major breakthrough in programming productivity.
For Web development, however, I find it's too much of a good thing. Web applications are highly dynamic, and highly responsive to user actions. The idea of developing an application by dragging all the controls you might ever need onto a form at design time, doesn't always work well in practice.
So, you create your controls in code. Everything appears fine, the first time. Then your dynamic (or "run time") controls disappear off the page. Or they appear, but you can't retrieve their properties. Or they have old post data, or even data from another control!
Why? The web is stateless, even in ASP.NET. If your code created a control the first time the code runs, it has to create the control the NEXT time the code runs. In between, the controls simply don't exist. The ASP.NET Web Server controls, static or dynamic, must be recreated every single time the page is accessed.
To illustrate the problem, consider the following code snippet:
public class WebForm1 : System.Web.UI.Page
{
private void Page_Load(object sender, System.EventArgs e)
{
if (IsPostBack)
{
}
else
{
LinkButton lnkMyLinkButton = new LinkButton();
Page.FindControl("Form1").Controls.Add(lnkMyLinkButton);
lnkMyLinkButton.ID = "lnkMyLinkButton";
lnkMyLinkButton.Text = "Click Me!";
lnkMyLinkButton.Click += new EventHandler(lnkMyLinkButton_Click);
}
}
private void lnkMyLinkButton_Click(object sender, EventArgs e)
{
Response.Write("You clicked me!");
}
}
This will compile and run just fine. The first time the page is accessed, the LinkButton is created and renders a hyperlink onto the Page. The user clicks it and… nothing happens. Not only does the "You clicked me!" text not appear on the page, the hyperlink itself disappears.
Why? Because the code path on PostBack doesn't create any controls. It doesn't matter what happened in the past. Yes, you created a control, but that's history. That control doesn't exist now, in this page cycle. No control, no event. No event, no "Response.Write()". When the code finishes, it renders an empty page, because you created no controls. If you comment out the IsPostBack if/else conditional, leaving just the LinkButton block of code in the Page_Load() method, everything will work as expected. Every code path will create the LinkButton control.
What, though, if you NEED a conditional? What if certain controls should be created, for example, in response to some event? I call those "conditional dynamic controls", and this article will present several strategies to handle them.
That's right. The first strategy for conditional dynamic controls is "don't make them".
Consider the scenario of a website with some navigational links, and content for each of those links. One link might be a "new user registration" form. We don't need those controls every time the page runs, we just need them when [1] they click the link and [2] when they submit the form. Another link might present some content from a database, along with links for more detail. Similarly, we need these controls only when they click the link, and also if they click one of the "detail" links.
Our first thought might be to create the controls at run time, and then find some way of restoring them conditionally. That's a dark and twisted road, my friends.
Consider, instead, putting the various control sets into Panel controls. Panels are rendered as HTML "div" elements. You can apply CSS styles to make them visible or invisible. When the user clicks a link, instead of dynamically creating controls, make them visible by toggling the CSS "Visibility" property. With this technique, you can create your controls at design time, and avoid the conditional dynamic control morass. In fact, you can potentially avoid server roundtrips, as CSS can be manipulated via client-side JavaScript.
You've analyzed your project, and simply must create conditional dynamic controls. OK. Do you need to recreate them, though? For example, consider our "new user registration" scenario. You decide to create those controls in response to the click event of one of the navigational links. The user submits the form, and now you have to recreate those controls. Or, do you? Does the user really need to see the same form again? Or, would a "thank you, form submitted" response be more appropriate?
What you need is the post data from those controls, not necessarily the controls themselves. In this case, you can use the Request.Form object to gather your data. Gather the data, process it (write it to a database, etc.), and then write out your thank you message, perhaps into a PlaceHolder:
PlaceHolder1.Controls.Clear();
PlaceHolder1.Controls.Add(new LiteralControl("Thank you.");
One challenge is to know, upon running PostBack code, that there is any form data to be processed. You need to preserve the "state" of the previous cycle, in which you created the data entry form. I suggest using the ViewState mechanism.
When you create the form controls, set a ViewState variable: ViewState.Add("mode","user_registration"); then, on PostBack:
if (ViewState["mode"].ToString() == "user_registration")
{
// iterate through Request.Form
}
You'll need to initialize this ViewState variable prior to testing it, of course. I suggest doing so the first time the page loads. The following program demonstrates this technique.
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
namespace ct
{
///
/// Summary description for WebForm1.
///
public class WebForm1 : System.Web.UI.Page
{
protected System.Web.UI.WebControls.LinkButton LinkButton1;
private void Page_Load(object sender, System.EventArgs e)
{
if (IsPostBack)
{
if (ViewState["mode"].ToString() == "user_registration")
{
// iterate through Request.Form
Response.Write(Request.Form["txt"].ToString());
}
}
else
{
ViewState.Add("mode","initial_value");
}
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.LinkButton1.Click +=
new System.EventHandler(this.LinkButton1_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
private void LinkButton1_Click(object sender, System.EventArgs e)
{
TextBox txt = new TextBox();
Page.FindControl("Form1").Controls.Add(txt);
txt.ID = "txt";
Button btn = new Button();
Page.FindControl("Form1").Controls.Add(btn);
btn.Text = "Submit";
btn.ID = "btn";
ViewState.Add("mode","user_registration");
}
}
}
In some cases, you must recreate the controls. Consider a system where the user may need to make several entries to a list. Each time they submit the form, you collect the entry, add it to the list, and then return with the same form so that the user can make additional entries.
Let's try it. Let's create a new project to simulate this system. We'll create three static controls. Put the WebForm into "flow layout", and drag a LinkButton, a PlaceHolder, and a Button onto the form.
When the user clicks the hyperlink, we'll create a dynamic TextBox and put it into the PlaceHolder. When the user fills out the value, and clicks "Submit", we want to create an "alert" dialog to display the textbox value. We want the TextBox to remain on the page so the user can enter another value.
We'll create a new method, "CreateTextBox", to create our TextBox control. This is so we can create it, conditionally, any time we need.
The first time the code runs, we initialize our ViewState variable. Next, the user clicks the link, and we run our code again. This time, the LinkButton1_Click method runs, and we [1] set the ViewState variable and [2] create our control by calling the CreateTextBox() method.
The user fills out the textbox and clicks "submit". This cycle, we fall into the "IsPostBack" code, check the ViewState, and recreate our TextBox control.
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
namespace dyn_ctrls
{
public class WebForm2 : System.Web.UI.Page
{
protected System.Web.UI.WebControls.PlaceHolder PlaceHolder1;
protected System.Web.UI.WebControls.Button Button1;
protected System.Web.UI.WebControls.LinkButton LinkButton1;
TextBox txtBox;
private void Page_Load(object sender, System.EventArgs e)
{
// Put user code to initialize the page here
if (IsPostBack)
{
if (ViewState["mode"].ToString() == "1")
{
CreateTextBox();
Page.RegisterStartupScript("","<script>" +
"alert('" + txtBox.Text + "');</script>");
}
}
else
{
ViewState.Add("mode","0");
}
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.LinkButton1.Click +=
new System.EventHandler(this.LinkButton1_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
private void LinkButton1_Click(object sender, System.EventArgs e)
{
CreateTextBox();
ViewState.Add("mode","1");
}
private void CreateTextBox()
{
txtBox = new TextBox();
PlaceHolder1.Controls.Add(txtBox);
txtBox.ID = "txt";
}
}
}
Notice what happens when we try to retrieve the value of the TextBox? It's empty! We've done everything correctly. Each code path that needs the dynamic control, creates the control. Yet, the recreated control doesn't synchronize to the Form's Post data.
Something is drastically wrong with this picture. We've fallen afoul of the "ASP.NET Page Life Cycle". You've heard about it. But you're not going to solve this problem until you understand it.
Every single time an ASP.NET page is accessed, the following stages occur:
First, the "Initialization" stage creates the Page class itself, and then recreates the control hierarchy. The what? The static, design time controls. The ones you dragged onto the form in Visual Studio.NET. It does not create the dynamic controls, because it doesn't know they exist. (You never see this stage. The code for creating the controls is stored in the .cs file, which is ran by the ASP.NET Engine.)
Second, the "LoadViewState" stage. It's at this point that the ASP.NET Engine rebuilds the StateBag and instantiates the ViewState object. The various ViewState properties are tied to their controls (that is, the controls created in Step 1).
Third, the LoadPostBackData stage. This is where the HTTP Post Headers are processed for form data, and tied to their controls.
Fourth, the Load event. This is the one we all know about. We code the Page_Load method, after all. When a control's load event occurs, it will now have its state restored from the previous page cycle, courtesy of the previous stages.
Fifth, events. At this stage, the ASP.NET Engine raises control events, based on ViewState and PostBack Data. If the user clicked a button, that's passed via ViewState, and the engine knows to raise that button's "Click" event. That's why when you step through code, you only get to the event methods AFTER you run through the Page_Load method.
Six, the new ViewState is saved. You may have changed the properties of some controls in this cycle. So, everything is saved in ViewState, which renders to the page as a hidden form variable.
Seventh and last, the HTML for the browser is rendered, and the program ends.
Hopefully, by now you understand the problem with our previous code sample. We don't recreate the dynamic controls until the Load stage. By that time, LoadViewState and LoadPostBackData have already occurred. When they ran, our controls didn't exist. By the time our controls exist, the data has passed us by. Therefore our newly created dynamic TextBox doesn't contain the Post Data we're trying to find. Can you say "Chicken and Egg Paradox"?
We need to create our controls earlier. We need to create them during the Initialization stage. Does this look familiar?
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.LinkButton1.Click +=
new System.EventHandler(this.LinkButton1_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
It's the "keep your hands off" code created by Visual Studio.NET. We see that it's handling the Initialization event. It runs when the Page is Initialized. If you put a breakpoint in there, and debug, you'll see that this code runs BEFORE Page_Load. In fact, it's in this code block that the Page Class' load event is delegated to the Page_Load method.
Look close. It's an override of the base.OnInit() method. The only additional code in the override, is a call to the "InitializeComponent()" method that we're not supposed to modify. We're going to modify it. Specifically, we're going to recreate our dynamic controls inside of it.
Consider this attempt:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
namespace dyn_ctrls
{
public class WebForm3 : System.Web.UI.Page
{
protected System.Web.UI.WebControls.PlaceHolder PlaceHolder1;
protected System.Web.UI.WebControls.Button Button1;
protected System.Web.UI.WebControls.LinkButton LinkButton1;
TextBox txtBox;
private void Page_Load(object sender, System.EventArgs e)
{
// Put user code to initialize the page here
if (IsPostBack)
{
if (ViewState["mode"].ToString() == "1")
{
Page.RegisterStartupScript("","<script>" +
"alert('" + txtBox.Text + "');</script>");
}
}
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.LinkButton1.Click +=
new System.EventHandler(this.LinkButton1_Click);
this.Load += new System.EventHandler(this.Page_Load);
if (IsPostBack)
{
if (ViewState["mode"].ToString() == "1")
{
CreateTextBox();
}
}
else
{
ViewState.Add("mode","0");
}
}
#endregion
private void LinkButton1_Click(object sender, System.EventArgs e)
{
CreateTextBox();
ViewState.Add("mode","1");
}
private void CreateTextBox()
{
txtBox = new TextBox();
PlaceHolder1.Controls.Add(txtBox);
txtBox.ID = "txt";
}
}
}
What we've done in this version of the code, is to move our ViewState initialization and control recreation code, into the InitializeComponent method. The first time the code runs, we initialize the ViewState variable.
On subsequent PostBacks, we test to see if ViewState mode is "1". If it is, we create our controls. We're creating our controls in the Initialization stage, so by the time we get to Page_Load, the ViewState and PostBack data is synchronized with our control.
Do you see the problem? In the Initialization stage, the LoadViewState stage hasn't occured. So the statement if (ViewState["mode"].ToString() == "1") will generate the Object reference not set to an instance of an object error. We can't test a ViewState variable at this stage because the ViewState object doesn't exist at this stage.
Bummer. I wouldn't have led you down this path, though, if there wasn't a workaround. In this case, a simple Find/Replace will solve the problem. Find "ViewState", replace with "Session".
The ASP.NET Session mechanism exists outside of the ASP.NET Page Life Cycle. Instead of using ViewState to persist our state information, we can use Session variables, which are available throughout the entire ASP.NET Page Life Cycle.
Using the Session state is a viable solution. However, for large applications or high traffic sites, loading up the Session with a lot of data can degrade performance. We need to find a way to use ViewState. Look again at the ASP.NET Page Life Cycle:
We need to recreate our controls before stage 3. We can't test ViewState in Stage 1. What about doing it at Stage 2?
We saw that Visual Studio.NET overrides the base.OnInit() method. Nothing prevents us from using the same trick. We'll override the base.LoadViewState() method.
protected override void LoadViewState(object savedState)
{
base.LoadViewState(savedState);
if( ViewState["mode"].ToString() == "1")
{
CreateTextBox();
}
}
If we add this method to our code, we can go back to using ViewState variables instead of Session variables. We'll take our code out of InitializeComponent(), and go back to initializing the ViewState variable in Page_Load.
Stage 1, Initialization, will run without any interference from us. The first time the page runs, LoadViewState does NOT run, so we don't have to initialize our ViewState flag until Page_Load.
When the user clicks the "Create TextBox" link, we create the TextBox, and set our ViewState variable.
Next Page Cycle, Initialization runs, then our LoadViewState override. We start by running the base.LoadViewState(), which instantiates the ViewState object. Now we can check the value of our ViewState flag, and conditionally create our TextBox.
Next, LoadPostBackData runs, which ties the Form data to our control.
When we hit Page_Load, our TextBox has been recreated, and has the Text property set to the value entered by the user.
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
namespace dyn_ctrls
{
public class WebForm1 : System.Web.UI.Page
{
protected System.Web.UI.WebControls.PlaceHolder PlaceHolder1;
protected System.Web.UI.WebControls.Button Button1;
protected System.Web.UI.WebControls.LinkButton LinkButton1;
TextBox txtBox;
private void Page_Load(object sender, System.EventArgs e)
{
// Put user code to initialize the page here
if (IsPostBack)
{
if (ViewState["mode"].ToString() == "1")
{
Page.RegisterStartupScript("","<script>"+
"alert('" + txtBox.Text + "');</script>");
}
}
else
{
ViewState.Add("mode","0");
}
}
protected override void LoadViewState(object savedState)
{
base.LoadViewState(savedState);
if( ViewState["mode"].ToString() == "1")
{
CreateTextBox();
}
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.LinkButton1.Click +=
new System.EventHandler(this.LinkButton1_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
private void LinkButton1_Click(object sender, System.EventArgs e)
{
CreateTextBox();
ViewState.Add("mode","1");
}
private void CreateTextBox()
{
txtBox = new TextBox();
PlaceHolder1.Controls.Add(txtBox);
txtBox.ID = "txt";
}
}
}
Powerful, scalable, and dynamic ASP.NET applications will require creating conditional dynamic controls. By being aware of the ASP.NET Page Life Cycle and overriding the base.LoadViewState() method, you can use such controls with confidence.
Thomas D. Greer has years of experience in the printing business. He held the position of Director of Development for Consolidated Graphics, where he wrote the COIN eCommerce platform. Prior to that he was Vice-President of Technology of a large printing company acquired by Consolidated Graphics, where he was responsible for the development of a completely custom-written plant management system still in use.
Today Thomas provides consulting, development, implementation, and training services to commercial printers. He can be reached on the web at www.tgreer.com.
Perhaps you'd like to read some other technical articles I've written?
If you'd like to discuss this article, or make suggestions for future articles, join my free discussion forum.
My logo was designed by Rus Anderson, a skilled graphic artist with a wealth of experience in user interface and web design. I told Rus I wanted something simple and clean, that conveyed my expertise in document automation technologies. I also wanted an association with PostScript and PDF. I'm very pleased with the result. The "document icon" has become ubiquitous. It's obvious, when viewing the logo, that I'm involved in document production and automation. The red color is associated with Adobe, PostScript, and PDF. Overall, the effect is clean and memorable. Thanks, Rus!