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 24, 2007

GridView: The Very Simple Grouping

While there are many many third party grid components on the market, it is often necessary to support and enhance existing applications and they often employ GridView to display and edit data.  One of the features that standard GridView lacks is a very simple grouping. 

I know several ways to display grouped data in a grid.  The following is quite flexible when you need to display data with multiple nested groupings. 

 GridViewGrouping

Moreover, this grouping style is very easy to accomplish by hooking two GridView events:  RowDataBound and DataBound.

The desired view is built by setting RowSpan property on the first row in the group and hiding first cell in each subsequent row in the group.

    protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e) {
DataRowView currentDataRowView = (DataRowView)e.Row.DataItem;
if (currentDataRowView != null) {
DataRow currentDataRow = currentDataRowView.Row;
if (m_firstGroupDataRow != null) {
object lastValue = m_firstGroupDataRow["ProductSubcategoryID"];
object currentValue = currentDataRow["ProductSubcategoryID"];
if (currentValue.Equals(lastValue)) {
m_rowSpan++;
e.Row.Cells[0].Visible = false;
return;
}
if (m_rowSpan > 1) {
m_firstGroupGridRow.Cells[0].RowSpan = m_rowSpan;
}
}
m_firstGroupGridRow = e.Row;
m_firstGroupDataRow = currentDataRow;
m_rowSpan = 1;
}
}

protected void GridView1_DataBound(object sender, EventArgs e) {
if (m_rowSpan > 1) {
m_firstGroupGridRow.Cells[0].RowSpan = m_rowSpan;
}
}

The code snipped is only to illustrate the approach.  It is usually better to move this code into the standalone class.  Instances of this class can be configured to extend the GridView control behavior.


Sample implementation of GridViewGroup class based on these code snippets are available in the attachment: GridViewGroup.zip.


This code snippet demonstrates one of three steps necessary to get GridView display data with grouping:

    protected void GridView1_DataBinding(object sender, EventArgs e) {
GridViewGroup first = new GridViewGroup(GridView1, null, "ProductSubcategoryID");
GridViewGroup second = new GridViewGroup(GridView1, first, "Color");
}

 The other steps are:



  1. Order your data source by grouping columns in the first place

  2. Add GridView1_DataBinding handler to your GridView DataBinding event

The result is:


GridView2

Friday, June 22, 2007

GridView with Multiple Rows Selected, Controls in the Template Column and Edit Mode

It is common to implement selection of multiple rows in a  GridView with a help of checkbox control embedded in the TemplateColumn.  You will have to write something like this in your ASPX.

        <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" 
DataSourceID="SqlDataSource" >
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:CheckBox ID="CheckBox1" runat="server" />
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="Name" HeaderText="Name" />
<asp:BoundField DataField="ModifiedDate" HeaderText="ModifiedDate" />
<asp:CommandField ShowEditButton="True" />
</Columns>
</asp:GridView>



However, if you also need to to support data editing in the GridView and you add CommandField column with ShowEditButton="True" you will find that your solution does not work as desired. 




If you click on "edit" link when you have several rows selected in your GridView, you lose your selection.  This behavior does not depend on EnableViewState value.




This strange behavior is explained by the following:




CheckBox control restores its Checked property from the post-data submitted by the browser on post-back.  Post-data processing triggers data binding of the GridView control and post-data are processed before the OnLoad phase in the page lifecycle.  However, when GridView handles Click event on [Edit] LinkButton in the command column, GridView needs to redo data binding again to create edit row.  This second data binding in the page lifecycle create new CheckBox controls which never process post-data and their Checked property value remains false.  It can be easily seen if you enable page Trace and handle DataBinding event in the GridView by the following handler:

    protected void GridView1_DataBinding(object sender, EventArgs e) {
Trace.Write("DATABINDING");
}



Now, when you click on [Edit] link in any row, your trace will be similar to: 




ASPTrace




Note two DATABINDING twice in the trace log.




Solution




The simplest solution that works for me is to store references to selected rows in the ViewState and restore state of checkboxes on data binding.  This can easily accomplished by:

    protected void CheckBox1_CheckedChanged(object sender, EventArgs e) {
CheckBox checkbox = (CheckBox)sender;
if (checkbox.Checked) {
ViewState[checkbox.UniqueID] = true;
}
else {
ViewState.Remove(checkbox.UniqueID);
}
}

protected void CheckBox1_DataBinding(object sender, EventArgs e) {
CheckBox checkbox = (CheckBox)sender;
checkbox.Checked = ViewState[checkbox.UniqueID] != null;
}



CheckBox must be configured as:

    <asp:CheckBox ID="CheckBox1" runat="server" 
OnCheckedChanged="CheckBox1_CheckedChanged" OnDataBinding="CheckBox1_DataBinding"/>



It requires page ViewState to be enabled, of course.




UPDATE:




Sure this solution will not work with inserts or deletes being made to data source, unless you replace checkbox.UniqueID with the value of key field in the data source.

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>