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>

6 comments:

Anonymous said...

This is definately an insight, but perhaps a little drawn out in places, making it difficult to grasp.

Nice work though and thanks.
I am still non the wiser sadly.

Anonymous said...

Thank you for this very helpful explanation!

I am currently working on dynamically loading a set of custom controls, and each of those controls dynamically loads checkbox inside a table.

I was able to use scenario 1 (loading controls on init) since I know beforehand how many controls I will need (it's dynamic so that it can vary without having to change the code).

I then used scenario 3 for the more complex matter of reconstructing the entire table, and checkboxes inside. It took a little while to make sure I was properly setting the IDs and recreating the objects, but it worked!

Yuriy Solodkyy said...

I posted one more sample on dynamic controls, see later posts.

Anonymous said...

I think I could love you! :D

Anonymous said...

Thanks, superb!

Anonymous said...

Thanks so much.

I used your method of recreating the controls in LoadViewState(), but I had to recreate them during the postback as well to rebind my xml data.

That was the final touch in creating a editable treeview control. I'm goin' on break.