How to create deeply nested Asp.net Dynamic Controls ?

Posted on : 28-09-2010 | By : manitra | In : C#

25

Creating controls at runtime (dynamic controls) in Asp.net is both tricky and unintuitive. This article will explain a pattern to make it easier.

The main advantages of this strategy are :
- ability to create deeply nested controls with unlimited depth
- each dynamicaly created controls have normal states (Viewstate is not broken)
- you can create those controls whenever you want (including OnClick events, PreRender and Render phases)
- no hacks with postback arguments are required

[UPDATE (2011/08/01)] : “M” found that the PersistentPanel doesn’t work well when it is instantiated in a markup file (aspx/ascx/master …) so I would advice you to instanciate it via code in the CreateChildControl method. The source code in the bottom of the page have been updated to reflect that.

The online demo

To help you understand what am I talking about here is an online example of deeply nested and dynamically created controls using Asp.Net.

You can create as much nested controls as you want and test that each controls persists its state upon postbacks.

The implementation

The PersistentPanel

The PersistentPanel is just a Panel wich persists its child controls collection using the viewstate automatically. This is a key control because it recreates the dynamically created controls on each post back during the right life-cycle phase : OnLoadViewstate. Thanks to this early recreation, those controls can persist their state in the ViewState like any controls declared in the markup page during the design time.

This kind of component is quite common now a days but the particularity here is that I do not try to persits all the nested controls but only the direct children. Indeed, if you try to persist and recreate the whole hierarchy, you’ll encounter problems and will have to handle a lot of special cases. More over since event handlers are not persisted, the restored components wont work.

The implementation process of the PersitentPanel :

  • during the save process of the viewstate we save the control hierarchy (type+Id only) using a serializable entity that store the control type, its Id and and a list of children
  • during the restore process of the viewstate we refill the Controls collection using the previously saved control hierarchy
  1.     public class PersistentPanel : Panel
  2.     {
  3.         private static string ControlsViewStateKey = "Controls";
  4.         public int MaxDepth
  5.         {
  6.             get
  7.             {
  8.                 var value = ViewState["MaxDepth"];
  9.                 if (value != null) return (int)value;
  10.                 return 1;
  11.             }
  12.             set
  13.             {
  14.                 ViewState["MaxDepth"] = value;
  15.             }
  16.         }
  17.  
  18.         protected override void LoadViewState(object savedState)
  19.         {
  20.             base.LoadViewState(savedState);
  21.             var persisted = (ControlEntity)ViewState[ControlsViewStateKey];
  22.             if (persisted != null)
  23.                 foreach (var child in persisted.Children)
  24.                     Controls.Add(child.ToControl());
  25.         }
  26.  
  27.         protected override object SaveViewState()
  28.         {
  29.             ViewState[ControlsViewStateKey] = new ControlEntity(this, MaxDepth);
  30.             var result = base.SaveViewState();
  31.             return result;
  32.         }
  33.     }

The parent of the dynamic controls

The component wich will dynamicaly create the controls will first embed a PersistentPanel. And each time it will want to add a control it will add that control in the PersistentPanel’s controls collection. Here is an example :

  1. public class CustomerView : Page {
  2.     private PersistentPanel ctlPanel;
  3.     // —— //
  4.     protected void ctlAdd_click(object sender, EventArgs e){
  5.         ctlPanel.Controls.Add(new Textbox{Text=Datetime.Now.ToString();});
  6.     }
  7.  
  8. }

Combining both to build a hierarchical data editor

Now we have a persistant panel and know that dynamically created controls are persisted, we’ll create a control that would create other complexes controls wich will have the same type as their creator. This would give us a powerfull control that would be able to display or edit hierarchical data wich, in our case, is a filter expression. We’ll have
- a control to edit scalar filter
- a control to edit composite filter
The scalar filter will just contain 3 simple controls for the field name, the operator and the value. The composite filter editor will be the interesting one. Indeed, it’s gonna contains a variable number of scalar editor and other composite filter editor. So it will use a persistant panel to host those nested controls. And that’s it !

  1. public class compositeView : WebControl, INamingContainer

Conclusion

The important things to remember are that :
- a control can be created at anytime, but it must be recreated on each postback during/before the LoadViewState of its container
- the ID of the dynamic control must be the same
- event handlers are not persisted, you have to rewire them up on each postback, the PersistantPanel has the ControlRestored event wich is the best place to do so.

Download the source code

The online demo application is available here :

Have fun !

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Netvibes
  • Technorati
  • TwitThis

Comments (25)

This a great explanation and sample!
At first my search on this subject led me to articles and blogs, which were plain wrong, buggy or ‘hack’-ish.
This is the right way to do it. It helped me a lot!

the simple.aspx only works for textboxes. if you add a label or textbox, it will remain on the page after postback, but properties, like Text are lost. how come?

Hello M,

This is probably because you set the Text property before adding the control to the controls hierarchy.

The solution is to add the control the the panel’s control collection first, then set the Text property :

This is not ok:
var label = new Label();
label.Text = “my custom text”;
ctlPersistentPanel.Controls.Add(label);

This is OK :
var label = new Label();
ctlPersistentPanel.Controls.Add(label);
label.Text = “my custom text”;

The reason of this is that the Viewstate starts to track changes only when you add a control in the controls hierarchy. So if you set a custom value to a property (persisted in the Viewstate like Label.Text) before adding it in the controls hierarchy, the Viewstate won’t be able to detect that you changed it so it won’t save the value at all.

Manitra

no, sorry that’s not it.
it just only works for textboxes, all other controls loose their properties after a few clicks or they end up in the wrong control!

Hello M,

Could you post your code so that I can understand what’s going on.

Thanks.

this works:
ctlContent.Controls.Add(new TextBox { Text = “Created on ” + DateTime.Now.ToString(“h:m:s”) });

this does not work (control is added, but text on the button is lost after adding more then one button):
ctlContent.Controls.Add(new Button { Text = “Created on ” + DateTime.Now.ToString(“h:m:s”) });

this does not work either:

var label = new Label();
ctlContent.Controls.Add(label);
label.Text = “…”;

Hello Manitra

I have a blank user control (child) and i create some Labell and a LinkButton. I also add an event to the LinkButton.

I then load that user control dynamically in another usercontrol (parent). Both controls are rendered correctly but when i click on the LinkButton, the event isnt fired.

My Code in the childcontrol :

namespace moduleSiteFinityMDC
{
[RequireScriptManager]
public class controleMDC : SimpleView
{

private string s_etoil;

public event EventHandler OnLinkButtonClick;

protected LinkButton labLibelleProduit
{
get
{
return this.Container.GetControl(“labLibProduit”, true);
}
}

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (this.Page.IsPostBack)
_initialisationDesControles();
}

protected override void InitializeControls(GenericContainer container)
{
//_initialisationDesControles();
}

private void _initialisationDesControles()
{

labLibelleProduit.Click += new EventHandler(actionEvent_Click);
labLibelleProduit.PostBackUrl = (m_instance.bSelectionCircuit) ? “~/detailCircuit” : “~/descriptif-de-votre-hotel”;

}

protected void actionEvent_Click(object sender, EventArgs e)
{
if (e != null) OnLinkButtonClick(sender, e);
//LinkButton _lb = (LinkButton)sender;

}

protected override string LayoutTemplateName
{
get { return “moduleSiteFinityMDC.Resources.Views.controleUtilisateur.ascx”; }
}
}
}

/////////////////////////////////////////////////
////////////////////////////////////////////////

In the other page when i call this control:

namespace moduleSiteFinityMDC
{
[RequireScriptManager]
public class accueil : SimpleView
{
private static controleMDC ctrlMDC;

protected System.Web.UI.WebControls.Table tableau
{
get
{
return this.Container.GetControl(“Table1″, true);
}
}

protected override void InitializeControls(GenericContainer container)
{
container.GetControl(“BtnRechercher”, true).Click += new EventHandler(BtnRechercher_Click);
}

protected override string LayoutTemplateName
{
get { return “moduleSiteFinityMDC.Resources.Views.AccueilMDC.ascx”; }
}

//private static LinkButton lB;
private void BtnRechercher_Click(object sender, EventArgs e)
{

if (this.Page.IsPostBack)
{

int icol = 0;// new int();
foreach (produit _p in m_listProduit)
{

System.Web.UI.WebControls.TableRow rw = new TableRow();
System.Web.UI.WebControls.TableCell cel = new TableCell();

icol += 1;
tableau.Rows.Add(rw);
tableau.Rows[icol - 1].Cells.Add(cel);

ctrlMDC = new controleMaisonDeChine();

tableau.Rows[icol - 1].Cells[0].Controls.Add(ctrlMDC);

}
}
}

}
}

So, i create the table in the parent.ascx as :

Misaotra mialoha o. (thank you.)

In the user Control i initialize this in the:

protected override void InitializeControls(GenericContainer container)
{
_initialisationDesControles();
}

mais pas dans le: OnLoad()

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (this.Page.IsPostBack)
_initialisationDesControles();
}

Sorry.

Hello M,

Sorry for responding so lately. Actually, there is fuzzy viewstate exception. I’ll dig into it later.

But currently I found out that instanciating the PersistentPanel in the code behind during the CreateChildControl() method fixes the issue :

public partial class Simple : System.Web.UI.Page
{
PersistentPanel ctlContent;

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

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

ctlContent = new PersistentPanel { ID = “ctlContent” };
ctlContainer.Controls.Add(ctlContent);
}

protected void ctlAddTextBox_Click(object sender, EventArgs e)
{
ctlContent.Controls.Add(new TextBox { Text = “Created on ” + DateTime.Now.ToString(“h:m:s”) });
}

protected void ctlAddLabel_Click(object sender, EventArgs e)
{
var label = new Label();
ctlContent.Controls.Add(label);
label.ID =
label.Text = “Created on ” + DateTime.Now.ToString(“h:m:s”);
}
}

This is strange and if I have a serious explanation about this, i’ll post it.

By the way, I updated the zip file so that the simple.aspx works with any controls as well.

Manitra.

no code available to download but just the thing i ws looking for .. great

Could you please post the updated code? I would like to see that solution…

Hi Manitra.. download link is not working.. could you please post the updated code.. thanks much! — Jee

Hello,

Really sorry about the download issue (IIS/worpress stuffs :p)

The download link is now working.

Manitra.

I created a button to capture the result for the textboxes. May I know how can I get the textbox value in the simple.aspx when I click on “submit”?

Hello Sylphy,

The textboxes are created in “ctlContent” so their content can be retrieved this way :

IEnumerable<TextBox> textBoxes = ctlContent.Controls.OfType<TextBox>();

Then you can iterate through this collection to get the Text property of each of them.

Hello Manitra,

It’s great example and exactly what i was looking for. Many Thanks for your work.

I have one query, great if you could give some hints?

In ScalarEditor control, How do I fill or retrieve data from dynamic controls to an object, what would be best place to do it?

To be more specific: Each time when following is executed in ScalarEditor control, I need to retrieve selected data in these dynamic controls into a simple object. Vice-versa i also want to fill data in these dynamic controls in case of edit mode.

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

ctlField = new DropDownList();
ctlField.Items.Add(new ListItem(“Name”));
ctlField.Items.Add(new ListItem(“Age”));
ctlField.Items.Add(new ListItem(“City”));
Controls.Add(ctlField);

ctlOperator = new DropDownList();
ctlOperator.Items.Add(new ListItem(“=”));
ctlOperator.Items.Add(new ListItem(“”));
Controls.Add(ctlOperator);

ctlValue = new TextBox();
Controls.Add(ctlValue);

CssClass += ” ScalarEditor”;
}

So if there are 5 instances of scalar editor then each instance will have a corresponding object that will represent data into respective scalar editor.

Could you please possibly add a remove function to remove the controls and how to retrieve the values of controls?
Thank you so much for sharing great implementation.

I’m extremely pleased to find this site. I wanted to thank you for ones time for this particularly fantastic read!! I definitely savored every little bit of it and i also have you book-marked to see new stuff on your site.

Thx to your submit. I would prefer to say that the cost of car or truck insurance will fluctuate
from a single plan to a different, due to the truth you will
find numerous different issues which give rise to the overall cost.
One example is, the model and make in the car could have an enormous bearing to the buy selling price.
A reliable aged family automobile can have a more cost-effective premium compared to the usual flashy
expensive vehicle.

Thanks for your posting. I’ve generally noticed that a majority of people are eager to lose weight when they wish to show up slim along with attractive. Nevertheless, they do not always realize that you will discover other benefits for losing weight also. Doctors assert that in excess of weight people have problems using a variety of disorders which can be perfectely attributed to their own excess weight. The good news is that people that happen to be overweight in addition to suffering from diverse diseases are ready to reduce the severity of their own illnesses by simply losing weight. You are capable to see a gradual but identifiable improvement with health while even a slight amount of weight reduction is achieved.

Your weblog is 1 of a kind, i really like the way you organize the topics. ededaeeegcef

Based on research, in 2008 there were 24 million users of VOIP and this figure
is expected to increase in 2 years time to 44 million.

Next point; how long is home phone service going to be around for.
Were only available in 2001, 5LINX can be a rapidly expanding telecommunications organization that gives services in the
United States and around the world.

Enjoying to be useful in some way .

Thanks for another great post. Where else may anybody get that type of info in such a perfect approach of
writing? I’ve a presentation next week, and I am on the search for such info.

Very quickly this web site will be famous among all blogging and
site-building visitors, due to it’s fastidious articles or reviews

Write a comment