Asked by:
Dynamically creating controls

Question
-
User-760709272 posted
In this thread I'm going to show you two ways of dynamically adding controls to forms. The first method does everything itself, the second method leverages a UserControl to make life simpler.
I can't stress enough that this code works
You might not believe it works because I am simply creating the controls and expecting them to magically re-create themselves from thin air...but that is exactly what happens. After we have finished creating our page, .net goes through all of our controls and stores their state in the ViewState. It does this automatically as part of the framework. After calling the Load event of your page, .net then goes through all of your controls and sees if they have state in the ViewState. As long as we keep the IDs of our controls exactly the same as when they were created, .net will find their state in the ViewState and update our controls for us so that they are exactly as they were when the page was rendered. Again all of this happens under the covers as part of the asp.net framework. This work happens after the Load event and before control events (such as button clicks etc).
Anyway, here is the code.
Mark-up
<%@ Page Language="C#" AutoEventWireup="true" Inherits="DynamicControls" Codebehind="DynamicControls.aspx.cs" %> <!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></title> </head> <body> <form id="form1" runat="server"> <asp:Button ID="ButtonAdd" Text= "Add" runat="server" OnClick="ButtonAdd_Click"/> <asp:Button ID="ButtonSave" Text= "Save" runat="server" OnClick="ButtonSave_Click"/> <asp:PlaceHolder runat="server" ID="PlaceControls" /> </form> </body> </html>
Code-behind
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; public partial class DynamicControls : System.Web.UI.Page { protected void ButtonAdd_Click(object sender, EventArgs e) { // The user has specified they want to add a new form section so let's get it done // As these controls are added dynamically it is up to us to maintain a list of everything // we've added so that it can be re-created. // Each form section consists of a panel that holds a textbox and a dropdown list. Each control // will have a unique string tacked onto it to make the whole control ID unique. I have chosen to // add a Guid to the ID of my controls to make them unique. Each panel that I dynamically create // is then added to a PlaceHolder that is in the aspx mark-up; this is where the controls will // appear on the page // As all of my controls are inside a panel, I only really need to keep track of the panels // as each panel contains the same controls I know how to re-create it. So I need to keep a list // of all the Guids I have added so far. I'm holding this as a comma separated list in the // ViewState, but the ControlIDs property used in this method has some nice code that automatically // turns that string of Guids into a List<Guid> structure that is much better to work with // Get the list of Guids already added List<Guid> ids = this.ControlIDs; // Create a new Guid for the form section we're about to add Guid guid = Guid.NewGuid(); // Add it to the list ids.Add(guid); // CreateControlSection will take the Guid and make the Panel with the textbox and dropdown list // in it, and and it to the control container for us Panel newSection = CreateControlSection(guid, true); // Store the new list of Guids this.ControlIDs = ids; } protected void ButtonSave_Click(object sender, EventArgs e) { // This method just runs through all of our dynamically created controls and extracts their // data. You'd save this in a database, or do something with it, I'm just writing it // to the output stream // All of our dynamically added controls are inside the PlaceControls placeholder // Using Controls.OfType is a nice way of only getting controls of the type we're interested in foreach (Panel panelControls in PlaceControls.Controls.OfType<Panel>()) { // We need to extract the id for this forum section. We know the parent panel // has an ID of "panelControls[ourGuidHere]" so by just removing the "panelControls" // text we're left with the id. // (I am deeply ashamed of this line of code) string id = panelControls.ID.Replace("panelControls", string.Empty); // Now we have the id we know what Id the txtName and dllGender contols will have // so get a handle on them via FindControl TextBox txtName = (TextBox)panelControls.FindControl("txtName" + id); DropDownList ddlGender = (DropDownList)panelControls.FindControl("ddlGender" + id); // Now we can get at their data System.Diagnostics.Debug.WriteLine("Name = " + txtName.Text); System.Diagnostics.Debug.WriteLine("Gender = " + ddlGender.SelectedValue); } } private Panel CreateControlSection(Guid guid, bool giveDefaultValues) { // This is the function that creates the dynamic controls that are inside our forum section // It looks like // <br/>Name: [textbox] // <br/>Gender: [dropdownlist] // [Remove button] // <hr/> // First turn the Guid into a string that can be used for naming controls. ToString("N") returns the // Guid without any curly brackets or hyphens. // {21EC2020-3AEA-1069-A2DD-08002B30309D} becomes 21EC20203AEA1069A2DD08002B30309D string newId = guid.ToString("N"); // First we create the panel that all of our controls will sit inside. It is this panel that will be added // to the PlaceHolder on the aspx page Panel panel = new Panel(); SetID(panel, "panelControls", newId); // SetID is just a helper function (code below) that gives the control (first parameter) an ID of the // text you supply (second parameter) with the newId stuck on the end // We have our parent Panel, now start creating the controls. // Hopefully this code will be fairly self-explanatory so I won't comment it. Literal literalName = CreateLiteral("literalName", newId, "<br/>Name:"); panel.Controls.Add(literalName); TextBox txtName = new TextBox(); SetID(txtName, "txtName", newId); if (giveDefaultValues) { txtName.Text = "Enter full name..."; } panel.Controls.Add(txtName); Literal literalGender = CreateLiteral("literalGender", newId, "<br/>Gender:"); panel.Controls.Add(literalGender); DropDownList ddlGender = new DropDownList(); SetID(ddlGender, "ddlGender", newId); ddlGender.Items.Add(new ListItem("Male", "m")); ddlGender.Items.Add(new ListItem("Female", "f")); ddlGender.Items.Add(new ListItem("Prefer not to say", "")); if (giveDefaultValues) { ddlGender.SelectedIndex = 2; } panel.Controls.Add(ddlGender); Literal literalRemoveButton = CreateLiteral("literalRemoveButton", newId, "<br/>"); panel.Controls.Add(literalRemoveButton); Button buttonRemove = new Button(); SetID(buttonRemove, "buttonRemove", newId); buttonRemove.Text = "Remove"; // We need to do something different with the button. We need to know *which* panel to remove when // the user clicks it. There is only one buttonRemove_Click event that all the buttons will call, // so we put the id of this form section (newId) in the CommandArgument property. We will use this // property in the buttonRemove_Click function to work out which form section to remove buttonRemove.CommandArgument = newId; buttonRemove.Click += new EventHandler(buttonRemove_Click); // Yes, we even have to attach the events! panel.Controls.Add(buttonRemove); Literal literalEndOfSection = CreateLiteral("literalEndOfSection", newId, "<hr/>"); panel.Controls.Add(literalEndOfSection); // The form section has now been created, so add it to the PlaceHolder so it will show on the page PlaceControls.Controls.Add(panel); // I'm returning the panel we've just created in case you want to do something with it // Our code doesn't, but it's a good idea to return anything that a function creates. return panel; } private Literal CreateLiteral(string name, string uniqueID, string value) { // This is just a helper function to create a literal, give it an ID and set its value // all in one hit Literal literal = new Literal(); SetID(literal, name, uniqueID); literal.Text = value; return literal; } private void SetID(Control control, string name, string uniqueID) { // A helper function to give a control an ID consisting of the name and the uniqueID stuck together control.ID = string.Format("{0}{1}", name, uniqueID); } protected void Page_Load(object sender, EventArgs e) { // Normally I wouldn't have the Page_Load this far down the page, but // if you've understood all of the code above, you'll hopefully now understand this // We need to re-create all of our controls on every postback, so get the IDs of the controls // we've already got List<Guid> ids = this.ControlIDs; // And re-create each panel foreach (Guid id in ids) { // The second parameter is false so that we just create the controls and don't amend their values (state) CreateControlSection(id, false); } } private void buttonRemove_Click(object sender, EventArgs e) { // Get the id of the form section to remove string id = ((Button)sender).CommandArgument.ToString(); // Our IDs are stored as Guids so we need to turn it into a Guid too Guid guid = Guid.Parse(id); // Find the parent panel of this section Panel panelControls = (Panel)PlaceControls.FindControl("panelControls" + id); // Remove it from the controls. This event happens *after* Page_Load so the control has already been created. PlaceControls.Controls.Remove(panelControls); // Get the stored list of IDs List<Guid> ids = this.ControlIDs; // Remove the id of the section we're deleting ids.Remove(guid); // Store the new list this.ControlIDs = ids; } /// <summary> /// This propery abstracts the fact that the IDs are stored in a comma separated list /// </summary> private List<Guid> ControlIDs { get { List<Guid> guids = new List<Guid>(); if (ViewState["IDs"] != null && !string.IsNullOrWhiteSpace((string)ViewState["IDs"])) { // Split the ids on the "," string[] vals = ((string)ViewState["IDs"]).Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (string val in vals) { // Add each one to the collection as a Guid guids.Add(Guid.Parse(val.Trim())); } } return guids; } set { // string.Join just collapses the List into a comma separated string string vals = string.Join(",", value.ToArray<Guid>()); ViewState["IDs"] = vals; } } }
Wednesday, February 5, 2014 6:15 PM
All replies
-
User-760709272 posted
The above code works, but it's not pretty. That CreateControlSection function is an ugly beast and hard to maintain. If I had to implement this kind of functionality I wouldn't do it the way it is done above. Below I have re-architected the code to use a user control instead (ascx control). Rather than dynamically creating everything, we are going to encapsulate the form section as a user control and that will be the only control we dynamically manage.
I hope you agree the re-worked solution is much nicer and cleaner.
The user control
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="DynamicControlsSection.ascx.cs" Inherits="WebTest.DynamicControlsSection" %> <table> <tr> <td> Name: </td> <td> <asp:TextBox ID="txtName" runat="server" Text="Enter full name..." /> </td> </tr> <tr> <td> Gender: </td> <td> <asp:DropDownList ID="ddlGender" runat="server"> <asp:ListItem Value="m" Text="Male" /> <asp:ListItem Value="f" Text="Female" /> <asp:ListItem Value="" Text="Prefer not to say" Selected="True" /> </asp:DropDownList> </td> </tr> </table> <asp:Button ID="buttonRemove" Text="Remove" runat="server" OnClick="buttonRemove_Click" /> <hr />
Its code behind
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace WebTest { public partial class DynamicControlsSection : System.Web.UI.UserControl { public delegate void RemoveHandler(Guid id); public event RemoveHandler Remove; public enum GenderEnum { PreferNotToSay, Male, Female } public Guid DynamicID { get { return (Guid)ViewState["DynamicID"]; } set { ViewState["DynamicID"] = value; } } public string Name { get { return txtName.Text.Trim(); } } public GenderEnum Gender { get { switch (ddlGender.SelectedValue) { case "m": return GenderEnum.Male; case "f": return GenderEnum.Female; default: return GenderEnum.PreferNotToSay; } } } protected void buttonRemove_Click(object sender, EventArgs e) { if (this.Remove != null) { this.Remove.Invoke(this.DynamicID); } } } }
The page (it's markup is the same as the example above)
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="DynamicControlsUC.aspx.cs" Inherits="WebTest.DynamicControlsUC" %> <!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></title> </head> <body> <form id="form1" runat="server"> <asp:Button ID="ButtonAdd" Text= "Add" runat="server" OnClick="ButtonAdd_Click"/> <asp:Button ID="ButtonSave" Text= "Save" runat="server" OnClick="ButtonSave_Click"/> <asp:PlaceHolder runat="server" ID="PlaceControls" /> </form> </body> </html>
Code-behind
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace WebTest { public partial class DynamicControlsUC : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { List<Guid> ids = this.ControlIDs; foreach (Guid id in ids) { CreateControlSection(id); } } protected void ButtonAdd_Click(object sender, EventArgs e) { List<Guid> ids = this.ControlIDs; Guid guid = Guid.NewGuid(); ids.Add(guid); DynamicControlsSection newSection = CreateControlSection(guid); this.ControlIDs = ids; } protected void ButtonSave_Click(object sender, EventArgs e) { foreach (DynamicControlsSection section in PlaceControls.Controls.OfType<DynamicControlsSection>()) { // Isn't this code a million times better? Because we have written our user control properly // it is just a very nice, easy, collection of strongly-typed properties. // No need to do any FindControls or casting, or null checking. Guid id = section.DynamicID; string name = section.Name; DynamicControlsSection.GenderEnum gender = section.Gender; System.Diagnostics.Debug.WriteLine("Name = " + name); System.Diagnostics.Debug.WriteLine("Gender = " + gender); } } private DynamicControlsSection CreateControlSection(Guid guid) { string newId = guid.ToString("N"); // Create a new instance of our user control DynamicControlsSection section = (DynamicControlsSection)Page.LoadControl("~/DynamicControlsSection.ascx"); // Tell it its unique Guid and give the control an ID section.DynamicID = guid; SetID(section, "section", newId); // Our user control implements a Remove event that fires when an item is // to be removed. We will listen to this event as this is something we're // interested in handling. The event passes the ID of the control being removed // as a parameter // Your user controls should be a "black box". They should expose their data via // properties, and communicate to their parents via events. Your parent code // should never be getting direct access to the user control's inner controls // and the user control should never be trying to access its parent section.Remove += new DynamicControlsSection.RemoveHandler(section_Remove); // Add it to the PlaceHolder so it is shown PlaceControls.Controls.Add(section); return section; } private void section_Remove(Guid id) { // This event comes from the user control and it tells us the ID of the control to remove DynamicControlsSection panelControls = (DynamicControlsSection)PlaceControls.FindControl("section" + id.ToString("N")); PlaceControls.Controls.Remove(panelControls); List<Guid> ids = this.ControlIDs; ids.Remove(id); this.ControlIDs = ids; } private void SetID(Control control, string name, string uniqueID) { control.ID = string.Format("{0}{1}", name, uniqueID); } private List<Guid> ControlIDs { get { List<Guid> guids = new List<Guid>(); if (ViewState["IDs"] != null && !string.IsNullOrWhiteSpace((string)ViewState["IDs"])) { string[] vals = ((string)ViewState["IDs"]).Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (string val in vals) { guids.Add(Guid.Parse(val.Trim())); } } return guids; } set { string vals = string.Join(",", value.ToArray<Guid>()); ViewState["IDs"] = vals; } } } }
Wednesday, February 5, 2014 6:18 PM