SingingEels : Development Community & Resource

Login

Articles

  • ADO.NET (2)
  • ASP.NET (36)
  • LINQ (5)
  • Security (2)
  • Silverlight (3)
  • SQL (7)
  • Standards (5)
  • WCF (2)

Syndication

  • Articles RSS
  • Blogs RSS

Contribute

  • Our Authors List
  • Member Sign-Up
  • Suggestions Box

Dynamically Created Controls in ASP.NET

(Jun 19 2007 - 08:04:26 AM by Yuriy Solodkyy) - [print article]

I spent some time before I found a pattern to work with dynamically created controls in ASP.NET that satisfied my requirements. I tried multiple approaches and faced multiple problems. If you'd like to avoid getting into the same problems I got, learn about them here first.

Why Dynamically Created Controls?

While its much easier to create web pages with static controls in ASP.NET, you may need to make your application flexible enough to support custom fields. For example, you may need to generate different form layouts for different report templates.

Scenario 1: Creating Controls on Page_Init Event

It is the best practice to create controls dynamically in the Page_Init handler. If you do so, dynamically created controls have the same lifecycle as controls declared in the .aspx file.

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

   for (int i = 0; i < 5; i++) {
       TextBox textBox = new TextBox();
       textBox.ID = "c_textBox" + i.ToString();
       textBox.ForeColor = Color.Green;
       c_placeholder.Controls.Add(textBox);
   }
}

(See: Scenario 1/Default_02.aspx)

(All the code snippets here can be incrementally applied to sample ASPX file provided at the end. However, you may refer to attachement to get prebuilt samples. Attachment: DynamicallyCreatedControls.zip)

It is not important to assign ID in this scenario, but I consider it as a good habit to do so. I will explain later why. It is important, however, to configure your dynamically created control before you add it to the parent control. Once control is added to the parent control, the ViewState tracking mechanism is in place and every change you make to the control properties is recorded in the page ViewState. Moreover, you will fail to configure controls differently, if you attempt to set up the control differently on post-back. See "Truly Understanding Viewstate" for more details.

Using Dynamically Created Controls

Once you have added code that dynamically creates controls on your page you will likely need to get values posted by these controls. It is not a good idea in ASP.NET to retrieve values posted by controls from the Request.Form collection. For one thing, you will have to guess the correct control ID. It's better to go the regular ASP.NET way (retrieving the value from the control itself such as the "Text" property of a TextBox).

private TextBox[] m_dynamicTextBoxes;

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

   c_button.Click += new EventHandler(ButtonClick);
   m_dynamicTextBoxes = new TextBox[5];

   for (int i = 0; i < 5; i++) {
       TextBox textBox = new TextBox();
       textBox.ID = "c_textBox" + i.ToString();
       c_placeholder.Controls.Add(textBox);
       m_dynamicTextBoxes[i] = textBox;
   }
}

protected void ButtonClick(object sender, EventArgs e) {
   c_label.Text = "";

   foreach (TextBox textBox in m_dynamicTextBoxes) {
       c_label.Text += textBox.Text + "; ";        
   }
}

(See: Scenario 1/Default_03.aspx)

Quite often you don't need to store references to your dynamically created controls to get the values out. The alternative way is to add event handlers to your controls and get values from the sender in the event handler. The choice should depend on your needs.

Scenario 2: Changing Page Layout in Response to User Actions

Scenario 1 is a simple and reliable way to create pages with a configurable layout. You can add plug-in modules, custom fields or change parts of the page depending on currently logged in user this way.

However, creating controls in the Init event handler is not always possible. You may need to load particular UserControl or generate a custom input form depending on selected item in the drop-down list. The traditional way is to use MultiView control. The MultiView approach is again simple, good and reliable unless you have to create hundreds of views inside as it takes quite long time instantiate each view.

For example, if you have multiple reports on your server and you need to create input boxes for report parameters when user select a report, it is not a good idea to create views with input for each report on page Init. Apparently, it is better to create only input boxes for active report template, but you can not do this on page Init. It is not possible, because you don't know what user choice is at this moment. Every control on the page still has its default (original) value.

Handling Events

A natural way to respond to user actions is to handle the appropriate events. I have added a drop-down list to the sample page to choose the page layout.

<asp:DropDownList ID="c_sampleDropDownList" runat="server" AutoPostBack="True"
   OnSelectedIndexChanged="SampleDropDownList_SelectedIndexChanged">
   <asp:ListItem Value="1">Sample 1</asp:ListItem>
   <asp:ListItem Value="2">Sample 2</asp:ListItem>
   <asp:ListItem Value="3">Sample 3</asp:ListItem>
</asp:DropDownList><br />

(See: Scenario 2/Default_04.aspx)

If you try to handle OnSelectedIndexChanged and dynamically create controls there as it is shown below you will get an interesting behavior.

protected void SampleDropDownList_SelectedIndexChanged(object sender, EventArgs e) {
   string selectedValue = c_sampleDropDownList.SelectedValue;

   if (!string.IsNullOrEmpty(selectedValue)) {
       int aSample = Convert.ToInt32(selectedValue);

       for (int i = 0; i < aSample; i++) {
           Button button = new Button();
           button.Text = i.ToString();
           c_placeholder.Controls.Add(button);
       }
   }
}

(See: Scenario 2/Default_05.aspx)

This code should create one, two or three buttons on the page depending on the selected sample. If you replace stub event handler SampleDropDownList_SelectedIndexChanged in the sample ASPX with this code snippet, you can see that it indeed creates buttons as described. So, what is wrong? If you try clicking these buttons, they just disappear. Even worse, if you try to handle Click event for these buttons, the event is not fired.

To see for yourself, add this method to sample code:

void Button_Click(object sender, EventArgs e) {
   throw new Exception("The method or operation is not implemented.");
}

And add this handler to buttons before adding them to c_placeholder.Controls collection:

button.Click += new EventHandler(Button_Click);

(See: Scenario 2/Default_06.aspx)

If you run the website, choose any sample in the drop-down and then click one of the 0, 1 or 2 buttons you will not get an exception. This happens because there is nobody to fire the Click event on postback. You have not re-created your buttons.

Re-Creating Controls on Post-Back

It is now obvious that controls created in response to user action must be re-created on each postback so they do not disappear because of user action. The first intention is to create them on page Init, but if you try following this way, you will find that c_sampleDropDownList control still has its default selected value. Unfortunately this means that the approach in Scenario 1 is not applicable here. You need to find a place to re-create controls where the controls will have their values loaded.

The first candidate for such a place is page Load event. (It is good enough, but not the best. It will be clear why later.) If you refactor the existing code a little and move the code which the creates buttons into a separate method you should come with something like:

protected void SampleDropDownList_SelectedIndexChanged(object sender, EventArgs e) {
   CreateButtons();
}

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

   CreateButtons();
}

private void CreateButtons() {
   string selectedValue = c_sampleDropDownList.SelectedValue;

   if (!string.IsNullOrEmpty(selectedValue)) {
       int aSample = Convert.ToInt32(selectedValue);
       for (int i = 0; i < aSample; i++) {
           Button button = new Button();
           button.Text = i.ToString();
           button.Click += new EventHandler(Button_Click);
           c_placeholder.Controls.Add(button);
       }
   }
}

(See: Scenario 2/Default_07.aspx)

If you run this code, you will see that it works almost as expected. First of all, page shows button "0" immediately as "Sample 1" is the default value in the drop-down. Moreover you get error page on attempt to click on this button (rare case when its actually good to see the error page). But if you try to change the value in the drop-down you will be disappointed. You get more controls than you expected. Set a breakpoint in CreateButtons method and you will see that execution hits this point twice after you change value in drop-down. A mere attempt to fix a problem by clearing c_placeholder.Controls in the CreateButtons shows that the problem is a little deeper.

If you add to the beginning of CreateButtons:

c_placeholder.Controls.Clear();

(See: Scenario 2/Default_08.aspx)

You will see that number of buttons on the page is correct, but they stopped working again. And it is interesting that they have stopped working for one click only. So, what happened?

Assigning IDs to Dynamically Created Controls

If you view the source HTML before you click the not-working button and after you have clicked it, you will notice a small difference. The buttons have different HTML IDs before and after the post-back. I got ctl04 and ctl05 before the post-back and ctl02 and ctl03 after the post-back.

ASP.NET button recognizes events by checking for a value for its ID in the Request.Form collection. (In truth it happens differently and controls do not check Request.Form collection by themselves. Page passes post data to controls by their IDs and to controls that are registered to be notified about post data). ASP.NET does not fire the Click event, because the button's ID has changed between the post-backs. The button you have clicked and the button you see after are different buttons for ASP.NET.

As I said in the beginning, assigning IDs to dynamically created controls is a good practice, but it can also lead to problems if misused. Here is a code sample with IDs assigned to dynamically created buttons:

private void CreateButtons() {
   c_placeholder.Controls.Clear();
   string selectedValue = c_sampleDropDownList.SelectedValue;

   if (!string.IsNullOrEmpty(selectedValue)) {
       int aSample = Convert.ToInt32(selectedValue);

       for (int i = 0; i < aSample; i++) {
           Button button = new Button();
           button.ID = "c_button_" + i.ToString();
           button.Text = i.ToString();
           button.Click += new EventHandler(Button_Click);
           c_placeholder.Controls.Add(button);
       }
   }
}

(See: Scenario 2/Default_09.aspx)

This time you get error on the click even after you change the value in the drop-down. The described approach is good enough for many cases, but it has its limitations as well. I will recap the steps you have to follow to get dynamically created controls working, before showing more problems and more reliable, but complex approach:

  1. Create a method (CreateDynamicControls) which creates your controls (you may inspect values of other controls to decide which controls you should create).
  2. Check that you assign all properties to the dynamic controls before adding controls to the placeholder.
  3. Check that you assign unique ID to each dynamically created control.
  4. Do not forget to clear the placeholder before adding any controls.
  5. Call your CreateDynamicControls method in the page Load event handler (or override OnLoad method as in this sample).
  6. Call your CreateDynamicControls method in all event handlers that must change the layout of dynamic part of the page.
  7. Store references to your controls in private fields if you need to access their values later somewhere (optionally).

More Hidden Problems

So, why is it necessary to create controls twice when the page layout changes? Is it not enough to create controls for new page layout only and omit step 6 in the suggest scenario? To see what difference step 6 makes I suggest replacing throwing an exception in Button_Click handler with the following code snippet:

void Button_Click(object sender, EventArgs e) {
   Button b = sender as Button;
   b.ForeColor = Color.Blue;
   b.Font.Bold = true;
}

(See: Scenario 2/Default_10.aspx)

If you click any of dynamically created buttons it becomes in bold and blue. The bold and blue style is reset by changing the value in the drop-down.

Now, if you try removing the call to CreateButtons in the SampleDropDownList_SelectedIndexChanged, you can see that dynamically created controls still work. The only noticeable difference is that bold and blue style is no longer reset by changing value in the drop-down. This behavior is explained by the fact that persisted ControlsViewState is removed from the collection once control has consumed it. When you create controls twice, controls consume ControlsViewState on the first time. Then they are created without ViewState second time. This behavior leads to the following question - "What if dynamically created controls after the post-back are different than original?" I have made minor changes to the last sample to illustrate this:

private void CreateButtons() {
   c_placeholder.Controls.Clear();
   string selectedValue = c_sampleDropDownList.SelectedValue;

   if (!string.IsNullOrEmpty(selectedValue)) {
       int aSample = Convert.ToInt32(selectedValue);
       if (aSample == 3) {
           for (int i = 0; i < aSample; i++) {
               ImageButton button = new ImageButton();
               button.ID = "c_button_" + i.ToString();
               button.AlternateText = i.ToString();
               c_placeholder.Controls.Add(button);
           }
       }
       else {
           for (int i = 0; i < aSample; i++) {
               Button button = new Button();
               button.ID = "c_button_" + i.ToString();
               button.Text = i.ToString();
               button.Click += new EventHandler(Button_Click);
               c_placeholder.Controls.Add(button);
           }
       }
   }
}

(See: Scenario 2/Default_11.aspx)

Note that for the "Sample 3" ImageButtons are created instead of Buttons. If you try changing value in the drop-down and clicking buttons you will occasionally receive the exception:

An error has occurred because a control with id 'c_button_0' could not be located or a different control is assigned to the same ID after postback. If the ID is not assigned, explicitly set the ID property of controls that raise postback events to avoid this error.

This is because you tried to feed a Button with view state information from an ImageButton. And that is why I said that re-creating control in page Load event is not the best place for doing this.

The simplest solution to the encountered problem is to use different IDs for ImageButtons and Buttons. If you try this you will find that it works. Usually it is not hard to follow this requirement, but if dynamically created controls come from different parts of the system and from different developers, it is easy to get unexpected behavior.

I would, however, recommend going another way and keep everything more compliant with ASP.NET page lifecycle.

Creating Controls in CreateChildControls Method

To solve the problem of exceptions after changing he drop-down selected item, you need to create controls for the old value of the drop-down first and then for the new value in the even handler. This first time could be on Init or LoadViewState phases of the page lifecycle.

Every ASP.NET control has a CreateChildControls method. It is intended to be used by control authors to create composite controls. The Page itself is a some kind of composite control. The CreateChildControls method is invoked when the EnsureChildControls method is invoked for the first time for the control. By default the CreateChildControls is called on PreRender phase if the page is not in the postback mode and may be called before processing post-back data if the page is in the postback mode.

If you try creating your controls in the CreateChildControls you have to get the old value somehow. In some cases, controls like a TextBox or DropDownList hold their old values after view state has been restored, but this behavior is due to implementation details. You should not therefore rely on this behavior. Some other controls (TreeView, CheckBoxList) never store their value in the view state and therefore you rely on controls to provide old value. The typical solution is to store this value in a property backed in the ViewState.

public string Sample {
   get {
       string result = "1";
       object v = ViewState["Sample"];

       if (v != null) {
           result = (string)v;
       }

       return result;
   }

   set {
       ViewState["Sample"] = value;
   }
}

The property then shall be set in the change event and used to determine which controls should be created dynamically. Its value is already available in CreateChildControls on postback as it is called after the ViewState is loaded.

protected void SampleDropDownList_SelectedIndexChanged(object sender, EventArgs e) {
   Sample = c_sampleDropDownList.SelectedValue;
   CreateButtons();
}

protected override void OnLoad(EventArgs e) {
   base.OnLoad(e);
}

protected override void CreateChildControls() {
   base.CreateChildControls();
   CreateButtons();
}

private void CreateButtons() {
   c_placeholder.Controls.Clear();
   string selectedValue = Sample;

   if (!string.IsNullOrEmpty(selectedValue)) {
       int aSample = Convert.ToInt32(selectedValue);

       if (aSample == 3) {
           for (int i = 0; i < aSample; i++) {
               ImageButton button = new ImageButton();
               button.ID = "c_button_" + i.ToString();
               button.AlternateText = i.ToString();
               c_placeholder.Controls.Add(button);
           }
       }
       else {
           for (int i = 0; i < aSample; i++) {
               Button button = new Button();
               button.ID = "c_button_" + i.ToString();
               button.Text = i.ToString();
               button.Click += new EventHandler(Button_Click);
               c_placeholder.Controls.Add(button);
           }
       }
   }
}

(See: Scenario 2/Default_12.aspx)

There are several pitfalls with this approach. CreateChildControls is first called before the PreRender event is fired in the non-post-back case. Therefore, anytime you need to access your controls you have to call EnsureChildControls. Another problem arises if you call EnsureChildControls too early. You will not be able to create your controls until ViewState is restored.

If you attempt to implement the same inside the user control you may be lucky or you may not. It is possible that you will find that CreateChildControls is not called until PreRender phase. The solution is to call EnsureChildControls after (inside before exiting) the LoadViewState method. Unfortunately the LoadViewState method is only called if you saved anything on SaveViewState. To get it working I wrapped the result of the SaveViewState into the Pair object with second value null.

protected override object SaveViewState() {
   return new Pair(base.SaveViewState(), null);
}

protected override void LoadViewState(object savedState) {
   base.LoadViewState(((Pair)savedState).First);

   EnsureChildControls();
}

Scenario 3 demonstrates how this approach works.

Scenario 3: Loading User Controls Dynamically with Dynamically Created Controls

One of typical scenarios when you need to load user controls dynamically is to respond to the change of a selected node in the TreeView control or a ListBox.

The sample application for scenario 3 demonstrates how to use the described technique to nest dynamically created controls and dynamically loaded user controls. The UI consists of three parts: navigation style selection drop-down, left navigation panel and right content panel. The navigation style selection drop-down changes the appearance of the navigation panel. The navigation panel allows you to choose what content should be displayed on the right. Again, different nodes create a different number of TextBoxes or Buttons on the right.

The application is composed from four ASP.NET files: Default.aspx, MasterWebUserControl.ascx, WebUserControl1.ascx and WebUserCorntrol2.ascx (See full source code in the attachment above).

Follow this consistent approach to creating controls dynamically:

  1. Create controls in the CreateChildControls method.
  2. Call EnsureChildControls in the LoadViewState.
  3. Wrap and unwrap view state in the Pair object to force calling LoadViewState.
  4. Save layout of the dynamic part of the page in properties backed in the ViewState.
  5. Recreate dynamic controls in response to user actions in event handlers.

Other Important Notes:

  1. Configure your controls before adding them to controls collection of the parent controls.
  2. Assign unique IDs to dynamically created controls.
  3. Keep references to dynamically created controls in local fields.
  4. Remember that post data is processed twice: before OnLoad and after OnLoad.
  • Jul 18 2007 - 02:22:52 PM yhzz

    Hi,

    It is a great post and it helped me a lot. Thanks.

    Can you explain how to do the same when i want to add a user control with a button in it to a specific line in a gridview after the user clicked on the row.

    I already manage to add the user control on click but i can't fire the event (button click) in the user event.

    I think the problem is that the container of the user control is changing (every time another row).

    Any ideas?

  • Jul 18 2007 - 07:46:02 PM Timothy Khouri

    Hmmm... I'd recommend that you post your question (with some sample code) to the ASP.NET forums (http://forums.asp.net). That way many people can look into the problem and you can supply code examples, etc.

  • Jun 30 2009 - 05:09:00 AM zaikin

    Thanks a lot! Your article is very helpful

You must be logged in to add comments. If you have not already done so, you can create an account here. If you already are a member, you first need to login before you can comment.

People to Follow

Experts in the categories related to this article.

  • Jonathan Carter

Related Blogs

These are the most recent blog posts related to this article.

  • Follow up to Self Sorting GridView
  • A Change to the MVC "ActionSelectionAttribute"?
  • How to Handle "Side Content" in ASP.NET MVC
  • LINQ to SQL - Am I Hitting The Database?
  • ASP.NET MVC - Issue with CSS Class Name on ActionLinks

Related Ads

SingingEels.com as of Jul 03 2009 - 04:59:35 PM - (0.0312544)