Showing posts with label Dynamic Controls. Show all posts
Showing posts with label Dynamic Controls. Show all posts

Saturday, July 28, 2007

Implementing ITemplate as Anonymous Method

ITemplate interface is used by multiple ASP.NET controls to setup repeatable or dynamically instantiated parts of a control. 

The ITemplate interface consists of a single method InstantiateIn which takes single parameter container.  Implementation is expected to instantiate template controls in the given container when InstantiateIn method is invoked.

public interface ITemplate {
void InstantiateIn(Control container);
}

 


ASP.NET runtime supports ITemplate by providing built-in facilities for creating instances of ITemplate from markup.  ASP.NET runtime assigns UpdatePanel.ContentTemplate property a reference to ITemplate implementation which instantiates a Button.

<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:Button ID="Button1" runat="server" Text="Button" />
</ContentTemplate>
</
asp:UpdatePanel>

Most often ITemplate interface implemented by providing ASP.NET markup for server control properties as in the previous example or as UserControls. 


However, if you often need to implement ITemplate interface in code it becomes quite boring to create a new class for each case. Such classes typically look like:

using System.Web.UI;
using System.Web.UI.WebControls;

public class TemplateImplementation : ITemplate {
private string m_someUsefulData;

public TemplateImplementation(string someUsefulParameter) {
m_someUsefulData = someUsefulParameter;
}

public void InstantiateIn(Control container) {
Button b = new Button();
b.ID = "B";
b.Text = m_someUsefulData;
container.Controls.Add(b);
}
}

 


and then used like:

emailField.ItemTemplate = new TemplateImplementation("Some Text");

This class constructor takes parameters and store them in private fields for use them in the InstantiateIn method.  You need to pass everything you need to instantiate controls into the constructor.


Fortunately, compiler can do all this routine for us allowing to write straightforward code to instantiate required controls in context of the place where ITemplate property is assigned.  This can be accomplished by creating generic ITemplate implementation taking delegate to a method creating controls as constructor parameter.

public delegate void InstantiateTemplateDelegate(Control container);
public class GenericTemplateImplementation : ITemplate {
private InstantiateTemplateDelegate m_instantiateTemplate;

public void InstantiateIn(Control container) {
m_instantiateTemplate(container);
}

public GenericTemplateImplementation(InstantiateTemplateDelegate instantiateTemplate) {
m_instantiateTemplate = instantiateTemplate;
}
}

This simple ITemplate implementation can be used to assign ItemTemplate property of a TemplateField column in the GridView.  Your code will be similar to:

string buttonText = "Get e-mail";
TemplateField emailField = new TemplateField();
emailField.HeaderText = "Dynamically Added E-Mail Field";
emailField.ItemTemplate = new GenericTemplateImplementation(
delegate(Control container)
{
Button b = new Button();
b.ID = "b";
b.Text = buttonText;
container.Controls.Add(b);
});
GridView1.Columns.Add(emailField);

 


The code creating button in the previous sample can access buttonText variable which is defined in the context where ItemTemplate property is assigned.  You, therefore, avoid creating new class and passing the entire required context to its constructor. 

Wednesday, July 4, 2007

Dynamically Created Controls: Dynamic Rows Sample

Sample code: DynamicRowsSample.zip

I described most important problems you may face when dealing with dynamically created controls with in my post (Dynamically Created Controls in ASP.NET).  The sample code in that post demonstrated most of the described problems and showed how to created pages with dynamic layout.

However, the previous sample does not make it obvious how to implement another common scenario with dynamic controls. So, I am going to explain ho to build a web page with dynamically added and removed rows like:

Web Page with Dynamic Rows

One Row

To separate implementation of one row and a web page handling dynamic rows, I placed implementation of a row into a user control in WebUserControl.ascx.  The WebUserControl class exposes events and properties required to communicate with the web page.

    public event EventHandler InsertAbove;
public event EventHandler InsertBellow;
public event EventHandler Remove;

public string Text {
get {
return c_textBox.Text;
}
}

User control fires its events in response to buttons clicks. For example, "Insert Above" button click is handled by:

    protected void InserAboveClick(object sender, EventArgs e) {
if (InsertAbove != null) {
InsertAbove(this, e);
}
}

Web Page Structure


The sample web page consists of 3 areas:



  • A placeholder for dynamically created rows
  • A label to display sample result
  • A button to do a postback (just to play with)

These areas are described in ASP.NET markup as:

<div>
<asp:PlaceHolder runat="server" ID="c_placeHolder">
</asp:PlaceHolder>
<br />
<asp:Label ID="c_resultLabel" runat="server"
Text="Label">
</asp:Label>
<br />
<asp:Button ID="c_postBackButton" runat="server" Text="Do PostBack!" />
</div>

View State and Control Ids


Dynamically created controls must be re-created on each postback and these re-created controls must have the same IDs as they had when were rendered previous time.  You need to know all these details when creating controls on postback.  The best place to keep this information between postbacks is a page view state.


Each dynamically created control must get its unique ID and this ID shall not be changed between postbacks.  Otherwise ASP.NET will not be able to restore a view state of the control and its children.  The sample application allows inserting and removing rows, so it is not good idea to assign IDs each line incrementally.  If you do so, and user clicks "remove" button, the next line will get ID of the removed line and view state will be restored incorrectly. 


I, therefore, allocate each row ID only once and do not re-use the same ID for newly created rows.  The LastControlId is a ViewState backed property which remembers ID of the last row created by a user.

    public int LastControlId {
get {
object v = ViewState["LastControlId"];
return v != null ? (int)v : 0;
}
set {
ViewState["LastControlId"] = value;
}
}

IDs of the currently existing rows are stored in another ViewState backed property: ControlIdList.  This ArrayList holds an ordered list of IDs.  It is used on each postback to create rows with correct IDs.

    public ArrayList ControlIdList {
get {
object v = ViewState["ControlIdList"];
return v != null ? (ArrayList)v : null;
}
set {
ViewState["ControlIdList"] = value;
}
}

Post Back


ASP.NET does not remember dynamically created controls neither in ViewState nor anywhere else.  It is a developer responsibility to reconstruct any dynamically created controls on each postback.  So, you need to create your rows on each postback.  Information stored in ViewState backed properties is enough to create controls with correct IDs.  ASP.NET will do the remaining job on restoring controls state.


In my earlier post I explained that the best place to re-create dynamic controls is LoadViewState method.  Following this recommendation, the sample application creates rows in LoadViewState method by calling CreateControls method.

    protected override void LoadViewState(object savedState) {
base.LoadViewState(savedState);
CreateControls();
}

Creating Controls


CreateControls method loops over IDs stored in the ControlIdList property, creates rows and adds them to placeholder exactly in the same order as they are stored in the ControlIdList.  (IDs in the list may be unordered because users are allowed to insert rows).

    private Hashtable m_controls = new Hashtable();

private void CreateControls() {
ArrayList ids = ControlIdList;
if (ids != null) {
foreach (int id in ids) {
WebUserControl control = CreateControl(id);
c_placeHolder.Controls.Add(control);
}
}
}

private WebUserControl CreateControl(int id) {
WebUserControl result =
(WebUserControl)LoadControl("WebUserControl.ascx");
result.ID = "c_" + id;
result.InsertAbove += InsertRowAbove;
result.InsertBellow += InsertRowBellow;
result.Remove += RemoveRow;
m_controls[id] = result;
return result;
}


References to created controls are saved for future use in the m_controls hash-table.  Saving references to dynamically created controls allows to avoid using FindControl method later.


Initialization


When a user first loads the page it has no ViewState and LoadViewState is not invoked.  However, the page should show the first row to user.  The first row can be created by the same CreateControls method, but you need to initialize ViewState backed properties before calling this method.  I did in the OnLoad method.  (You can do it in the OnInit method as well, but you cannot move exactly the same implementation to OnInit).

    protected override void OnLoad(EventArgs e) {
base.OnLoad(e);
if (!IsPostBack) {
LastControlId = 1;
ArrayList idList = new ArrayList();
idList.Add(0);
ControlIdList = idList;
CreateControls();
}
DisplayResult();
}

Note that initialization is only required when a user first opens the page.


Handling User Actions (Postback Events)


Server event fires when user clicks on any of the "Insert Above", "Insert Bellow" or "Remove".  It fires after the LoadViewState method is invoked, so dynamic controls are already reconstructed.  These controls have already loaded their previous ViewState.  TextBoxes and similar controls have already processed post-data and their Text properties reflect last changes made by the user.


A task of event handlers is to prepare the page for rendering reflecting changes requested by the user and to save these changes allowing correct reconstruction of the page after a postback.

    private void InsertRowBellow(object sender, EventArgs e) {
WebUserControl control = (WebUserControl)sender;
int index = control.Parent.Controls.IndexOf(control) + 1;
CreateControlAt(index);
DisplayResult();
}

private void CreateControlAt(int index) {
int id = GetNewId();
WebUserControl newControl = CreateControl(id);
ControlIdList.Insert(index, id);
c_placeHolder.Controls.AddAt(index, newControl);
}

private int GetNewId() {
int id = LastControlId++;
return id;
}

The CreateControlAt method does the most important job.  It generates new ID for the row which is unique across postbacks, creates new row controls and inserts it at the right position.  The CreateControlAt method also updates the ControlIdList property to allow correct reconstruction of the page on the next post-back. 


Using Dynamic Controls


The sample application displays concatenated text from all the text boxes below the rows.  The label text is updated in the OnLoad method. If the page layout is changed later in the event handler the label text is updated once again to reflect recent changes.

    private void DisplayResult() {
StringBuilder sb = new StringBuilder();
bool first = true;
foreach (int id in ControlIdList) {
if (!first) {
sb.Append(", ");
}
else {
first = false;
}
WebUserControl control = (WebUserControl)m_controls[id];
sb.Append(control.Text);
}
c_resultLabel.Text = sb.ToString();
}

Once Again



  1. Save enough information in view state to be able to reconstruct dynamically created controls.
  2. Override LoadViewState and reconstruct dynamically created controls using the the information retrieved from the view state.
  3. Initialize your page in the OnLoad method by setting initial values in the ViewState and then call your method to reconstruct controls.
  4. Create, remove or update your controls in response to user actions in event handlers.  Do not forget to update view state data used to reconstruct controls on the next postback.

Tuesday, June 26, 2007

Configuring Dynamically Created User Controls Containing UpdatePanel

How Is UpdatePanel Different?

UpdatePanel is a rare ASP.NET (actually ASP.NET AJAX Extensions) control which benefits from the TemplateInstanceAttribute and especially TemplateInstance.Single case.  Its ContentTemplate property is decorated with [TemplateInstance(TemplateInstance.Single)] attribute.  This setting allows accessing declaratively created controls in the ContentTemplate via page fields generated by ASP.NET

The PreInit event is not intended for setting up any controls, but it is nice place to see a difference between Panel and UpdatePanel

If you have the following controls on your page:

        <asp:ScriptManager runat="server" ID="c_scriptManager">
</asp:ScriptManager>
<div>
<asp:Label runat="server" ID="c_label"></asp:Label>
<asp:UpdatePanel runat="server" ID="c_updatePanel">
<ContentTemplate>
<asp:Button runat="server" ID="c_buttonForTest"
Text="Just a Button" />
</ContentTemplate>
</asp:UpdatePanel>
<asp:Panel runat="server" ID="c_panel">
<asp:Button runat="server" ID="c_buttonForTest2"
Text="Just a Test" />
</asp:Panel>
</div>

and the following code behind:

    protected override void OnInit(EventArgs e) {
base.OnInit(e);
if (c_buttonForTest != null) {
c_label.Text += "c_buttonForTest ";
}
if (c_buttonForTest2 != null) {
c_label.Text += "c_buttonForTest2 ";
}
}

you will notice that label output is just c_buttonForTest2.  What does it mean?   It means that UpdatePanel does not instantiate its ContentTemplate until after the OnInit phase. 


So, be careful.  Public page fields holding references to controls might be yet un-initialized before the OnInit phase and it is not a good idea to access controls in PreInit phase.  You may get "Object reference not set to an instance of an object" exception.


Where Is It Important?


As I said earlier it is recommended to set up dynamically created controls before they are added to the parent control Controls collection.  This is necessary to avoid storing these settings in the ViewState.  ASP.NET starts tracking ViewState changes immediately after the OnInit event has fired.


This code snippet is used to add a button to the placeholder control:

               TextBox textBox = new TextBox();
textBox.ID = "c_textBox";
textBox.ForeColor = Color.Green;
c_placeholder.Controls.Add(textBox);

It follows recommendation and sets control properties before it is added to the placeholder.  The same is possible for user controls loaded from the .ascx file:

                WebUserControl3 userControl = 
(WebUserControl3)LoadControl(@"~/Scenario 3/WebUserControl3.ascx");
userControl.ID = "c_rightUserControl";
userControl.ImportantColor = Color.BlueViolet;
c_bodyPlaceHolder.Controls.Add(userControl);

This sample also works for WebSuserControl3.ImportantColor implemented as:

    public Color ImportantColor {
get {
return c_importantLabel.ForeColor;
}
set {
c_importantLabel.ForeColor = value;

}
}

and c_importantLabel being simple static control inside the user control like:

<%@ Control Language="C#" ClassName="WebUserControl3" %>
<h2>
WebUserControl3</h2>
<p>
<asp:Label ID="c_importantLabel" runat="server" Text="Important Label"></asp:Label></p>

However, if you anytime decide to move c_importantLabel inside the UpdatePanel, you will get "Object reference not set to an instance of an object" exception.


User control OnInit fires inside the call to c_bodyPlaceHolder.Controls.Add(userControl) and c_importantLabel gets its value their. 


What Is the Correct Way to Create Controls Dynamically?


I posted a long story about the problems you can face while creating controls dynamically and how to avoid these problems.  One of the important conclusions was to set control properties before adding them to the parent control Controls collection.  As it is shown here, it is not good for user controls containing UpatePanel. 


MSDN says that Init event is raised after all controls have been initialized. 



Init: Raised after all controls have been initialized and any skin settings have been applied. Use this event to read or initialize control properties.


The same applies to user controls.

So, approach demonstrated in my previous post is a little incorrect.  It works fine almost everywhere, but there are cases when it does not.  It is necessary to move initialization of user control properties to its Init event handler.  C# 2,0 anonymous methods are very helpful in this case.  The problem is fixed by wrapping user control initialization in:

                WebUserControl3 userControl = 
(WebUserControl3)LoadControl(@"~/Scenario 3/WebUserControl3.ascx");
userControl.ID = "c_rightUserControl";
userControl.Init += delegate {
userControl.ImportantColor = Color.BlueViolet;
};
c_bodyPlaceHolder.Controls.Add(userControl);

This code works regardless of whether c_importantLabel is inside the UpdatePanel or not.  The c_importantLabel field is already initialized when Init event fires.


Since the old approach works almost in all cases, I do not think it is necessary to wrap initialization of any control in an anonymous method.  However,  I would do it for initialization of any user control from the outside.


I have updated the sample application from the post about Dynamically Created Controls in ASP.NET to use UpdatePanels everywhere.  The only other change is using anonymous method to initialize WebUserControl1.


Attachment: DynamicallyCreatedControlsAjax.zip


Alternative Point of View: Bug in User Control


An alternative point of view on the same problem can be: "User control shall be ready to handle assignment to its properties before its children are completely initialized". 


The converted sample application demonstrates that WebUserControl2 which follows this principle works fine without any changes and allows initialization of its properties before the Init event.


Anyway, you may have methods in your user control returning internal controls and they will return null before Init event.


What about Other Controls?


TabPanel and AccordionPane are similar to UpdatePanel in their behavior. 

Sunday, June 17, 2007

Dynamically Created Controls in ASP.NET

I spent some time before I find a pattern to work with dynamically created controls in ASP.NET that satisfies my requirements. I tried multiple approaches and faced multiple problems. If you like to avoid getting into the same problems see them here.

(Sample code: DynamicallyCreatedControls.zip)

Why Dynamically Created Controls?

For example you need to make your application flexible enough to support custom fields.

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);
// textBox.ForeColor = Color.Green;
}
}

See: Scenario 1/Default_02.aspx

(All the code snippets here can be incrementally applied to sample ASPX file provided at the end)


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 also important to configure your dynamically created control before you add it to the parent control. Once control is added to the parent control 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 http://weblogs.asp.net/infinitiesloop/archive/2006/08/03/Truly-Understanding-Viewstate.aspx 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 always not a good idea in ASP.NET to retrieve values posted by controls from the Request.Form collection. At least, you will have to guess the correct control ID. It is better to go the regular ASP.NET way.

        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. 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 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 custom input form depending on selected item in the drop-down list. The traditional way is to use MultiView control. MultiView approach is again simple, good and reliable unless you have to create hundreds of views inside or 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 cannot do it 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.


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. Why does it happen? There is nobody to fire Click event on postback. You have not 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 until they should 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 it means that Scenario 1 approach is not applicable here. You need to find a place to re-create controls where controls 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 existing code a little and move code which creates buttons into 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 it is 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. I you set a breakpoint in CreateButtons method 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 happens?


Assigning ID 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 click 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 Click event, because button ID has changed between the post-backs. The button you have clicked and the button you see after are different buttons for the ASP.NET.


As I said on the beginning, assigning ID to dynamically created controls is a good practice, but it can also lead to problems if misused. A sample code with ID assigned to dynamically created buttons goes here:

        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 first 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 steps you have to follow to get dynamically created controls working, before showing more problems:



  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 inc 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 somewhere. (optionally)

Step 6 can be often omitted, but as you can change control values in the event handlers, it is better to re-create your controls once again.


More Hidden Problems


So, why is it necessary to cerate controls twice when 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 make step 6 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. 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 it:

        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 Button with a view state from 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 recommend going another way and keep everything more compliant with ASP.NET page lifecycle.


Creating Controls in CreateChildControls Method


To solve the problem with exceptions after change of the drop-down selected item, you need to create controls for 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 first time for the control. By default the CreateChildControls is called on PreRender phase if page is not in the postback mode and may be called before processing post-back data if 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 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 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 PreRender event is fired in 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 may be 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 existing) the LoadViewState method. Unfortunately 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


On of typical scenarios when you need to load user controls dynamically is to respond to change of selected node in the TreeView control or list box.


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


Application is composed from four ASP.NET files: Default.aspx, MasterWebUserControl.ascx, WebUserControl1.ascx and WebUserCorntrol2.ascx. (See full source code at the end).


Default.aspx is a simple sample showing how to load user controls dynamically. It always loads the same controls without any attempt to configure it.

        protected override void CreateChildControls() {
base.CreateChildControls();
c_placeHolder.Controls.Clear();
UserControl userControl = (UserControl)LoadControl(@"~/Scenario 3/MasterWebUserControl.ascx");
userControl.ID = "c_masterUserControl";
c_placeHolder.Controls.Add(userControl);
}

WebUserControl1.ascx and WebUserControl2.ascx show that demonstrated approach works not only for web pages, but for user controls as well and even if user controls are instantiated dynamically. Internally these two user controls are very different. The first one is very simple and its behavior is controlled from the outside world. The Initialize method creates TextBoxes dynamically inside the user control. The second user control (WebUserControl2.ascx) is organized differently. It behaves like a standalone control and its content is controlled via NumberOfControls public property.


Internally the second control uses the same approach to dynamically created controls with CreateChildControls as described in scenario 2. You can set NumberOfControls property anytime and it will recreate controls if CreateChildControls has been already called.

    public int NumberOfControls {
get {
int result = 0;
object v = ViewState["NumberOfControls"];
if (v != null) {
result = (int)v;
}
return result;
}
set {
ViewState["NumberOfControls"] = value;
if (ChildControlsCreated) {
CreateDynamicControls();
}
}
}

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

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

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

private void CreateDynamicControls() {
int count = NumberOfControls;
c_placeHolder.Controls.Clear();
for (int i = 0; i < count; i++) {
Button button = new Button();
button.ID = "c_button_" + i;
button.Text = i.ToString();
button.Click += new EventHandler(Button_Click);
c_placeHolder.Controls.Add(button);
}
}

The most interesting is MasterWebUserControl.ascx. It manages the whole appearance of the application. The dynamic layout of this user control is controlled by two parameters: list display mode and selected item in the list. Both these parameters are stored in the ViewState backed properties: Mode and SelectedItemValue correspondingly. The LoadViewState method is forced by saving wrapper Pair object into the ViewState.


CreateRightControl method loads one of the WebUserControl1.ascx or WebUserControl2.ascx depending on the item selected on the left. WebuserControl2.ascx knows how to recreate its dynamically created controls, so MasterWebUserControl.ascx does not re-create it in the case when only NumberOfControls needs to be changed.


The code snippet is too long to place it here, so I refer you to appendix or attachment.


Brief Summary


While its much easy to create web pages with static controls in ASP.NET, nothing prevents you from creating control dynamically and using the consistent approach for this purpose.


The consistent approach to creating controls dynamically may be following:



  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 dynamic part of the page int the properties backed in 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 are processed twice: before OnLoad and after OnLoad

Sample ASPX


Scenario 1

<%@ Page Language="C#" %>

<%@
Import Namespace="System.Drawing"
%>
<!DOCTYPE
html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<html
xmlns="http://www.w3.org/1999/xhtml"
>
<head
runat="server"
>
<title>Untitled Page
</title>

<script runat="server"
>
// CODE PLACEHOLDER
</script>

</head>
<body>
<form id="form1" runat="server"
>
<div>
<div>
<asp:Label runat="server" ID="c_label"
></asp:Label>
</div>
<div>
<asp:PlaceHolder runat="server" ID="c_placeholder"
></asp:PlaceHolder>
</div>
<div>
<asp:Button runat="server" ID="c_button" Text="ClickMe!"
/>
</div>
</div>
</form>
</body>
</html>

Scenario 2

<%@ Page Language="C#" %>

<%@
Import Namespace="System.Drawing"
%>
<!DOCTYPE
html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<html
xmlns="http://www.w3.org/1999/xhtml"
>
<head
id="Head1" runat="server"
>
<title>Untitled Page
</title>

<script runat="server"
>
private TextBox[] m_dynamicTextBoxes;

protected override void OnInit(EventArgs e) {
base.OnInit(e);
//CODE PLACEHOLDER
}

protected void SampleDropDownList_SelectedIndexChanged(object sender, EventArgs e) {
//CODE PLACEHOLDER
}

</script>

</head>
<body>
<form id="form1" runat="server"
>
<div>
<div>
<asp:Label runat="server" ID="c_label"
></asp:Label>
<br
/>
<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
/>
</div>
<div>
<asp:PlaceHolder runat="server" ID="c_placeholder"
></asp:PlaceHolder>
</div>
<div>
<asp:Button runat="server" ID="c_button" Text="ClickMe!"
/>
</div>
</div>
</form>
</body>
</html>

Scenario 3


Default.aspx

<%@ Page Language="C#" %>

<!DOCTYPE
html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<html
xmlns="http://www.w3.org/1999/xhtml"
>
<head
runat="server"
>
<title>Untitled Page
</title>

<script runat="server"
>
protected override void CreateChildControls() {
c_placeHolder.Controls.Clear();
UserControl userControl = (UserControl)LoadControl(@"~/Scenario 3/MasterWebUserControl.ascx");
userControl.ID = "c_masterUserControl";
c_placeHolder.Controls.Add(userControl);
base.CreateChildControls();

}

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

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


</script>

</head>
<body>
<form id="form1" runat="server"
>
<div>
<asp:PlaceHolder ID="c_placeHolder" runat="server"
></asp:PlaceHolder>
</div>
<asp:Button runat=server ID="c_button" Text="Do Postback"
/>
</form>
</body>
</html>


MasterWebUserControl.ascx

<%@ Control Language="C#" ClassName="MasterWebUserControl" %>

<script
runat="server"
>
private TreeView c_itemsTreeView;
private ListBox c_itemsListBox;
private WebUserControl1 c_rightControl1;
private WebUserControl2 c_rightControl2;

public string Mode {
get {
string result = c_viewDropDownList.SelectedValue;
object v = ViewState["Mode"];
if (v != null) {
result = (string)v;
}
return result;
}
set {
ViewState["Mode"] = value;
}
}

public string SelectedItemValue {
get {
string result = "";
object v = ViewState["SelectedItemValue"];
if (v != null) {
result = (string)v;
}
return result;
}
set {
ViewState["SelectedItemValue"] = value;
}
}

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

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

protected override void CreateChildControls() {
base.CreateChildControls();
CreateItemsList(false);
CreateRightControl();
}

private void CreateItemsList(bool setValue) {
c_itemsTreeView = null;
c_itemsListBox = null;
c_itemListPlaceHolder.Controls.Clear();
string view = Mode;
switch (view) {
case "T":
c_itemsTreeView = CreateTreeView();
c_itemListPlaceHolder.Controls.Add(c_itemsTreeView);
break;
case "L":
c_itemsListBox = CreateListBox();
c_itemListPlaceHolder.Controls.Add(c_itemsListBox);
if (setValue) {
c_itemsListBox.SelectedValue = SelectedItemValue;
}
break;
}
}

private void CreateRightControl() {

string key = SelectedItemValue;
string kind = "";
int count = 0;
if (!string.IsNullOrEmpty(key)) {
string[] parts = key.Split('-');
kind = parts[0];
if (parts[1] == "*") {
kind = "";
count = 0;
}
else {
count = Convert.ToInt32(parts[1]) + 1;
}
}

if (kind == "2" && c_rightControl2 != null) {
c_rightControl2.NumberOfControls = count;
}
else {
c_bodyPlaceHolder.Controls.Clear();
c_rightControl1 = null;
c_rightControl2 = null;

if (!string.IsNullOrEmpty(kind)) {
UserControl userControl = (UserControl)LoadControl(@"~/Scenario 3/WebUserControl" + kind + ".ascx");
userControl.ID = "c_rightUserControl";
switch (kind) {
case "1":
c_rightControl1 = userControl as WebUserControl1;
break;
case "2":
c_rightControl2 = userControl as WebUserControl2;
break;
}
if (c_rightControl1 != null) {
c_rightControl1.Initialize(count);
}
if (c_rightControl2 != null) {
c_rightControl2.NumberOfControls = count;
}
c_bodyPlaceHolder.Controls.Add(userControl);
}
}
}

private ListBox CreateListBox() {
ListBox result = new ListBox();
result.ID = "c_itemsList";
result.AutoPostBack = true;
result.Style.Add("height", "100%");
result.SelectedIndexChanged += new EventHandler(SelectedIndexChanged);
PopulateListBox(result);
return result;
}

private void PopulateListBox(ListBox listBox) {
for (int i = 0; i < 5; i++) {
ListItem item = new ListItem(string.Format("1: {0}", i), string.Format("1-{0}", i));
listBox.Items.Add(item);
}
for (int i = 0; i < 5; i++) {
ListItem item = new ListItem(string.Format("2: {0}", i), string.Format("2-{0}", i));
listBox.Items.Add(item);
}
}

private TreeView CreateTreeView() {
TreeView result = new TreeView();
result.ID = "c_itemsList";
result.Style.Add("height", "100%");
result.SelectedNodeChanged += new EventHandler(SelectedNodeChanged);
PopulateTreeViewBox(result);
return result;
}

private void PopulateTreeViewBox(TreeView treeView) {
CreateTree(treeView, 1);
CreateTree(treeView, 2);
}

private void CreateTree(TreeView treeView, int kind) {
TreeNode root = new TreeNode(string.Format("--- {0} ---", kind), string.Format("{0}-*", kind));
for (int i = 0; i < 5; i++) {
TreeNode item = new TreeNode(string.Format("{0}: {1}", kind, i), string.Format("{0}-{1}", kind, i));
root.ChildNodes.Add(item);
}
treeView.Nodes.Add(root);
}

void SelectedIndexChanged(object sender, EventArgs e) {
SelectedItemValue = c_itemsListBox.SelectedValue;
CreateRightControl();
}

void SelectedNodeChanged(object sender, EventArgs e) {
SelectedItemValue = c_itemsTreeView.SelectedValue;
CreateRightControl();
}

protected void c_viewDropDownList_SelectedIndexChanged(object sender, EventArgs e) {
Mode = c_viewDropDownList.SelectedValue;
CreateItemsList(true);
}
</script>

<table
border="1" cellpadding="0" cellspacing="0" style="width: 100%; height: 100%"
>
<tr>
<td colspan="2" style="height: 48px"
>
<asp:Label ID="c_viewLabel" runat="server" Text="View"
></asp:Label>
<asp:DropDownList ID="c_viewDropDownList" runat="server" OnSelectedIndexChanged="c_viewDropDownList_SelectedIndexChanged"
AutoPostBack="True"
>
<asp:ListItem Selected="True" Value="T">TreeView
</asp:ListItem>
<asp:ListItem Value="L">List View
</asp:ListItem>
</asp:DropDownList></td>
</tr>
<tr>
<td style="width: 200px; height: 300px;"
>
<asp:PlaceHolder ID="c_itemListPlaceHolder" runat="server"
></asp:PlaceHolder>
</td>
<td>
<asp:PlaceHolder ID="c_bodyPlaceHolder" runat="server"
></asp:PlaceHolder>
</td>
</tr>
</table>


WebUserControl1.ascx

<%@ Control Language="C#" ClassName="WebUserControl1" %>
<h2>
WebUserControl1
</h2>
<script
runat="server"
>

public void Initialize(int count) {
c_placeHolder.Controls.Clear();
for (int i = 0; i < count; i++) {
TextBox tb = new TextBox();
tb.ID = "c_textbox_" + i;
c_placeHolder.Controls.Add(tb);
}
}
</script>
<asp:PlaceHolder
runat=server ID=c_placeHolder
>
</asp:PlaceHolder>


WebuserControl2.ascx

<%@ Import Namespace="System.Drawing" %>
<%@
Control Language="C#" ClassName="WebUserControl2"
%>
<h2>
WebUserControl2
</h2>

<script
runat="server"
>
protected override void OnInit(EventArgs e) {
base.OnInit(e);
}

public int NumberOfControls {
get {
int result = 0;
object v = ViewState["NumberOfControls"];
if (v != null) {
result = (int)v;
}
return result;
}
set {
ViewState["NumberOfControls"] = value;
if (ChildControlsCreated) {
CreateDynamicControls();
}
}
}

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

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

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

private void CreateDynamicControls() {
int count = NumberOfControls;
c_placeHolder.Controls.Clear();
for (int i = 0; i < count; i++) {
Button button = new Button();
button.ID = "c_button_" + i;
button.Text = i.ToString();
button.Click += new EventHandler(Button_Click);
c_placeHolder.Controls.Add(button);
}
}

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

</script>

<asp:PlaceHolder
runat="server" ID="c_placeHolder"
></asp:PlaceHolder>