Part 1: Dynamic vs. Static
Part 2: Creating Dynamic Controls
Part 3: Adding Dynamic Controls to the Control Tree
Part 4: Because you don't know what to render at design time
UPDATE: Click here to download the Sample Code referenced in this article.
I started part 1 of this series so long ago. My original plan was to have all 4 parts done over four weeks. It's been a lot longer than that! Sorry folks!
The new plan is that four parts is not enough! I realized while writing this part that there's just too much to cover!
There are three main reasons I can think of why you would want to create controls dynamically instead of declaratively.
-
Because the types and/or number of controls to be rendered is not known at design time. The form may be driven by a database or through configuration, or it is dynamically determined based on other data the user has provided.
-
You know what controls may be rendered but there are a large number of possibilities and/or the controls are expensive to load, so you don't want to load them all.
-
Because you are creating a custom server control and you don't have a choice.
In this part, we will cover #1 and #2. Part 5, hopefully the last part, will cover #3.
Example: You don't know "what" or "how many" controls should be rendered at design time
First and foremost -- ask yourself whether the problem is that you don't know "what" controls should be rendered, or if it's just "how many" controls should be rendered. If it's a question of how many, you can still solve the problem declaratively thanks to Templating.
This is a real scenario that someone emailed me about. They were attempting to solve the problem with dynamic controls. They were having various problems with their implementation and asked for my help. And of course, it turned out to be much simpler with templating.
The scenario is as follows:
You want users to be able to upload files to your site. They do this from a file manager page, where they can see a listing of the files that already exist. Next to each file is a button to delete the file. At the bottom of the list is a file upload control they can use to upload a new file. Any time a file is deleted or uploaded, the listing is updated.
To display the files, we will add rows dynamically to a table that is declared on the form. When a file is deleted we'll give some feedback via a label.
<asp:Label ID="lblStatus" runat="server" EnableViewState="false" />
<table id="tblFiles" runat="server" cellpadding="4" cellspacing="4" border="1"></table>
Here is the implementation I was given. In red, because it has problems!
public partial class _Default : Page { protected override void OnLoad(EventArgs e) { string[] files = Directory.GetFiles(Server.MapPath("~/uploaded"));
foreach (string path in files) { // strips directory from path
string fileName = Path.GetFileName(path);
HtmlTableRow row = new HtmlTableRow();
// file name cell
HtmlTableCell cell = new HtmlTableCell();
cell.InnerText = fileName;
row.Cells.Add(cell);
// delete cell
LinkButton cmdDelete = new LinkButton();
cmdDelete.Text = "delete";
cmdDelete.CommandArgument = fileName;
cmdDelete.Command += new CommandEventHandler(cmdDelete_Command);
cell = new HtmlTableCell();
cell.Controls.Add(cmdDelete);
row.Cells.Add(cell);
// add row to table
this.tblFiles.Rows.Add(row);
}
base.OnLoad(e);
}
private void cmdDelete_Command(object sender, CommandEventArgs e) { // command argument contains the file name to delete
string fileName = Path.GetFileName((string)e.CommandArgument);
string path = Path.Combine(Server.MapPath("~/uploaded"), fileName);
File.Delete(path);
this.lblStatus.Text = "Deleted " + fileName;
// now remove the table row for the file
LinkButton cmdDelete = (LinkButton)sender;
// link button's parent = cell, parent.parent = row
HtmlTableRow row = (HtmlTableRow)cmdDelete.Parent.Parent;
this.tblFiles.Rows.Remove(row);
}
}
It seems pretty straight forward. The first cell shows the file name, the second shows a link we can click on to delete it. When deleting a file, we remove it from the file system and then remove the row from the table. And of course, we let the user know the file was deleted successfully. Let's see how it runs...

Cool -- let's delete "InfinitiesLoop.gif", who wants that thing anyway?

So far so good! Now, let's delete "TheFamily.jpg". I was blinking during that picture, and I don't appreciate the bunny ears over my head, sister. Bah...

WHAT?! I swear I clicked delete on TheFamily.jpg, but it deleted my TODO list! Nooooo.....! Oh well I guess I can just go home now. Dare I try to delete something else? Who knows what it's going to do next...
So, what went wrong?
When the page first renders, there are 4 rows with 4 link buttons. We never specified an ID for those link buttons when we created them. That means they get automatically generated IDs. The rows and cells do, too, but only the link button actually requires an ID to be rendered, because it must cause a postback via javascript (since links don't naturally do postbacks). If you view the html source, you will see the link button's href looks like "javascript:__doPostBack('ctl01', '')". In this case, ctl01 is the LinkButton ID.
So let's say the link buttons have IDs ctl01, ctl02, ctl03, and ctl04 (the actual IDs are different because of the controls between them, but you get the idea). We click on ctl02 to delete "InfinitiesLoop.jpg". When the postback is processed, first we recreate the table with all 4 rows again. The link buttons will still have the same IDs they did last time. ctl02 raises its command event, we delete the file, and then remove the row from the table. But the link buttons have already determined what their IDs are going to be -- the rendered table will now have link buttons with these IDs: ctl01, ctl03, ctl04. Hmmm. ctl03 is the link button the represents TheFamily.jpg. It will now appear 2nd in the table.
Now we click to delete TheFamily.jpg (ctl03). First the table gets recreated -- only, this time there is no InfinitiesLoop.jpg in the Upload directory. So we only create 3 link buttons this time -- ctl01, ctl02, ctl03. At this stage, ctl03 is the link button that corresponds to TODOList.txt. See the problem yet?
ASP.NET knows who raised the postback event via its ID. But we've pulled the rug out from under it -- the controls on this request have different IDs than they did on the previous request. There happens to be a control with the ID that was posted, but it isn't the one we intended it to be. ASP.NET finds ctrl03, and we delete TODOList.txt. Oops.
This problem can manifest itself in many different ways. If we had textboxes in this table, you might find that they lose their values after certain postbacks, or that their values seem to shift in position.
What to do about it
It's important that the structure of the control tree be the same after a postback as it was before the postback. We actually were not really violating that rule in this case -- we rendered out 3 rows when InfinitiesLoop.jpg was deleted, and we created 3 rows after the postback. But we cheated -- we created 4 rows and removed one, and then on the next request we just created 3 right off the bat. One way we could solve this is by giving each LinkButton a specific ID that will never change. In this case the simplest way would be to make it's ID equal to the file name. There's no way you can confuse one link button with another then. But it's far from ideal to have to stuff the file name into the control ID. The name might be really long. And if we were accessing multiple directories, we could have duplicate file names, so now we'd have to include path information, too. There may be other controls in each row, such as checkboxes that show the file's attributes like whether it is marked as ReadOnly. We'd have to apply this trick to those controls, too. Yuck. Yuck. Yuck.
Really what you want is to be able to "reset" the ID scheme of all the controls in the table. When we remove a row, all the controls after it should 'shift up' with their IDs, so that they will render with the same ID they will have on the next postback. So, what if when we deleted a row, we completely rebuilt the table from the start? Just clear out all the rows and then rebuild it like we originally did...
protected override void OnLoad(EventArgs e) { BuildTable();
base.OnLoad(e);
}
private void cmdDelete_Command(object sender, CommandEventArgs e) { // command argument contains the file name to delete
string fileName = Path.GetFileName((string)e.CommandArgument);
string path = Path.Combine(Server.MapPath("~/uploaded"), fileName);
File.Delete(path);
this.lblStatus.Text = "Deleted " + fileName;
// removed a row, throw away the table and rebuild it
this.tblFiles.Rows.Clear();
BuildTable();
}
private void BuildTable() { string[] files = Directory.GetFiles(Server.MapPath("~/uploaded"));
foreach (string path in files) { // strips directory from path
string fileName = Path.GetFileName(path);
HtmlTableRow row = new HtmlTableRow();
// file name cell
HtmlTableCell cell = new HtmlTableCell();
cell.InnerText = fileName;
row.Cells.Add(cell);
// delete cell
LinkButton cmdDelete = new LinkButton();
cmdDelete.Text = "delete";
cmdDelete.CommandArgument = fileName;
cmdDelete.Command += new CommandEventHandler(cmdDelete_Command);
cell = new HtmlTableCell();
cell.Controls.Add(cmdDelete);
row.Cells.Add(cell);
// add row to table
this.tblFiles.Rows.Add(row);
}
}
"A" for effort (affort?), but there's still a problem. Now when we click to delete TheFamily.jpg after deleting InfinitiesLoop.jpg, this is what we get.

Nothing happens except the postback. If you examine the control IDs this time, you will see that instead of resetting the IDs by clearing out the table and rebuilding it, the rows simply continued the ID sequence. That's because Controls.Clear() does not reset the automatic ID counter unless the control implements INamingContainer. HtmlTable does not implement INamingContainer.
Aside: What is INamingContainer?
INamingContainer is a marker interface, meaning it has no methods to implement. A control "implements" this interface to let the framework know that it plans on giving it's child controls really specific IDs. It's important to the framework, because if a two instances of the same control are on the same page, and the control gives its child controls some specific ID, there'd end up being multiple controls with the same ID on the page, which is going to cause problems. So when a Control is a naming container, the UniqueID for all controls within it will have the parent's ID as a prefix. This scopes it to that control. So while a child control might have ID "foo", its UniqueID will be "parentID$foo" (where parentID = the ID of the parent). Now even if this control exists twice on the page, everyone will still have a UniqueID.
INamingContainer also has the property that any controls within it that do not have a specific ID will have its ID automatically determined based on a counter that is scoped to it. So if there were two naming containers, foo and bar, they might have child controls with UniqueIDs foo$ctl01 and bar$ctl01. Each naming container gets its own little counter.
Note that the ID "foo$ctl01" does not necessarily imply that ctl01 is a direct child control of foo! All it means is that foo is ctl01's naming container (control.NamingContainer). It's parent might be another control which is not a naming container.
So we can solve this problem once and for all by using a custom HtmlTable control that implements INamingContainer. I won't bother showing that... because there's an even better solution.
Remember this scenario is that you don't know "how many" controls there should be at design time. We do, however, know "what" the controls are. You can do it dynamically, if you manage the control tree correctly. But when you know what the controls will be ahead of time, Templating can step in and take care of all of this for you automatically!
<asp:Label ID="lblStatus" runat="server" EnableViewState="false" />
<asp:Repeater ID="rptFiles" runat="server" EnableViewState="false">
<HeaderTemplate>
<table cellpadding="4" cellspacing="4" border="1">
</HeaderTemplate>
<ItemTemplate>
<tr>
<td><%# Path.GetFileName((string)Container.DataItem) %></td>
<td>
<asp:LinkButton runat="server" Text="delete"
CommandArgument="<%# Path.GetFileName((string)Container.DataItem) %>" />
</td>
</tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
Using a repeater, we declare what each row will look like. This is what I mean by "what" vs "how many". We know what, so we can express "what" via the repeater ItemTemplate. The "how many" will come from databinding the repeater, which will repeat the template once for each item we bind to it.
protected override void OnLoad(EventArgs e) { this.rptFiles.ItemCommand += new RepeaterCommandEventHandler(rptFiles_ItemCommand);
BindRepeater();
base.OnLoad(e);
}
private void rptFiles_ItemCommand(object source, RepeaterCommandEventArgs e) { // command argument contains the file name to delete
string fileName = Path.GetFileName((string)e.CommandArgument);
string path = Path.Combine(Server.MapPath("~/uploaded"), fileName);
File.Delete(path);
this.lblStatus.Text = "Deleted " + fileName;
// removed a row, rebind the repeater
BindRepeater();
}
private void BindRepeater() { string[] files = Directory.GetFiles(Server