TRULY Understanding Dynamic Controls (Part 3)
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
PART 3
Adding Dynamic Controls to the Control Tree
Creating a control obviously isn't enough to get it involved in the page life cycle. A control is just an instance of a class like any other class. You have to put it somewhere so the framework knows it exists. You also probably want to initialize it into some state that suits your needs. You may even want to add even more dynamic controls to it.
When should dynamic controls be added? OnInit? OnLoad? DataBind? What if the controls I create depend on ViewState? Should I initialize the controls before or after I add them? How and when do I databind a dynamic dropdown list? These are just some of the questions you may have (or should have) asked yourself if you have used dynamic controls.
All these questions combine with your particular situation to create a mind boggling amount of permutations. I can't cover them all without writing whole book on it, so it will be up to you to try and adapt this knowledge to your own needs. But I will cover many scenarios that should be similar to your own.
Simply having the knowledge of how to use dynamic controls doesn't mean you should! In a future episode I will cover some of these scenarios and how you can simplify your life by avoiding them altogether. But for the purposes of this section, I will be using examples that you wouldn't do in the "real world". For example, there's no good reason why you should create a label dynamically when a static one would suffice. So don't take these examples literally.
The Control Tree
The Control Tree is a data structure. Strictly speaking, it is an implementation of an ordered tree where the type of each node is System.Web.UI.Control, and the root node's type is System.Web.UI.Page. The actual types may be derived from those types, of course. The root node isn't really a special case since Page derives from Control. It's an ordered tree because the order of the child nodes matters. That is to say, the fact that the "First Name" label control occurs before the TextBox, for example, is important (it is important but not necessarily for the reason you think... more on that later).
From a particular node in the tree (a control), you can get its child nodes (child controls) through the Controls property, which returns type System.Web.UI.ControlCollection. On the surface, this collection is not unlike any other collection. It has methods like Add, AddAt, Remove, RemoveAt, Clear, etc. You can also get to a parent node through the Parent property, which returns type Control. The root node (the page) has no parent just as described in the definition of an ordered tree, and thusly returns null.
Manipulating the Tree
Thinking about ASP.NET's control tree from the perspective of a strict ordered tree is a good thing. If you take a look at the Wikipedia definition again, it defines the following typical operations for manipulating this data structure. Adjacent to each operation I have included sample code that demonstrates it:
- Enumerating all the items:
private void DepthFirstEnumeration(Control root) {
// deal with this control
// visit each child
foreach(Control control in root.Controls) {
this.DepthFirstEnumeration(control);
}
}
- Searching for an item:
// read about the control ID syntax later
this.FindControl("namingContainer$txtFirstName");
- Adding a new item at a certain position on the tree:
Control myControl = this.LoadControl("~/MyControl.ascx");
someParent.Controls.Add(myControl);
- Deleting an item:
someParent.Controls.Remove(myControl);
- Removing a whole section of a tree (called pruning):
// build a mini-tree
Control myControl = this.LoadControl("~/MyControl.ascx");
myControl.Controls.Add(new LiteralControl("First name:"));
myControl.Controls.Add(new TextBox());
someParent.Controls.Add(myControl);
// removes the entire mini-tree we just built
someParent.Controls.Remove(myControl);
- Adding a whole section to a tree (called grafting):
// build a mini-tree
Control myControl = this.LoadControl("~/MyControl.ascx");
myControl.Controls.Add(new LiteralControl("First name:"));
myControl.Controls.Add(new TextBox());
// adds the entire mini-tree just built
someParent.Controls.Add(myControl);
- Finding the root for any node:
// get a control's parent
Control parent = someControl.Parent;
// or... get the root to the tree given any node in it
while(someControl.Parent != null)
someControl = someControl.Parent;
Note that consistent with the definition of a tree, a node may only have one parent at a time. So lets say you tried to be sneaky by adding the same control to two places in the tree:
Control foo = this.LoadControl("~/foo.ascx");
this.Controls.Add(foo);
Control bar = this.LoadControl("~/bar.ascx");
this.Controls.Add(bar);
foo.Controls.Add(bar);
No. This isn't going to throw an error or anything though. When you add a control to another control, and that control already had a parent, it is first removed from the old parent. In other words, this code is essentially the same thing as doing this:
Control foo = this.LoadControl("~/foo.ascx");
this.Controls.Add(foo);
Control bar = this.LoadControl("~/bar.ascx");
this.Controls.Add(bar);
// remove it
this.Controls.Remove(bar);
// then add it to a different parent
foo.Controls.Add(bar);
There may be times where you would legitimately move a control from one parent to another, but it should be extremely rare.
Controls are notified when their child control collection is modified. As a control or page developer you can override these methods. This only applies to immediate children though.
protected override void AddedControl(Control control, int index) {
// someone added a control to this control's collection
base.AddedControl(control, index);
}
protected override void RemovedControl(Control control) {
// someone removed a control from this control's collection
base.RemovedControl(control);
}
Be Responsible for the Control Tree
Building the tree is your responsibility. Whether you do it dynamically or statically, it doesn't matter. One way or another, the controls of the tree must be completely reconstructed upon each and every request to the page. A common mistake made by developers is to add a dynamic control to the tree only in response to some event. For example, lets say you have a button on the form that reads, "Click here to enter your name", and when clicked you want to dynamically add a TextBox for them to enter it into:
private void cmdEnterName_Click(object sender, EventArgs e) {
TextBox txtFirstName = new TextBox();
this.Controls.Add(txtFirstName);
}
If you try it, you'll see the TextBox like you expect. But on the next postback, unless the user clicks this very same button a second time, the TextBox will cease to exist! Many developers who run into issues like this believe it to be due to broken ViewState. They scour the internet with queries like "how to enable ViewState for dynamic controls". It has nothing to do with ViewState. ViewState is responsible for maintaining the state of the controls in the control tree -- not the state of the control tree itself! That responsibility lies wholly on your shoulders. Dynamically created controls will not exist on the next postback unless you add them to the control tree once again. Once a control is a member of the tree, the framework takes over to maintain the control's ViewState. Of course, there are some gotchas with that, which will cover in more detail.
Okay. How then can we solve the problem stated in the example? As I've mentioned already -- and you will hear more about it later on -- when it comes to dynamic controls, just because you can doesn't mean you should! It's always better to statically declare controls whenever it is humanly possible. You will save yourself a lot of work. So the solution here is to avoid using dynamic controls in the first place.
Simply declare that TextBox on the form, but mark it as Visible="false". Then when you need it, just make it Visible. When you don't need it any more, make it not visible again. Simple as can be.
By the way, some of you may be aware of a little control out there called the DynamicPlaceHolder. I am reluctant to even mention it, because if you haven't heard of it you might go looking for it and then you will be tainted. Please, don't use it!!! I'm not saying there aren't times where a control like that could be useful -- but 99.999% of the time it is the wrong way go about solving a dynamic control problem. There are all sorts of disadvantages to using it, all of which can be avoided by solving the problem "correctly" to begin with. I believe this so firmly that if you find yourself using it, I will personally work with you via email to find a better way.
Revisiting the Repeater
This is somewhat off-topic but it's the perfect time to point this out, and it drives home the "responsibility" point. Remember in Part 2 we discussed how a Repeater uses it's ItemTemplate to repeatedly create controls dynamically? (If you skipped part 2 I highly recommend you go back and read it before continuing). If you stop and think about it for a moment, you might realize that the Repeater (or any other databound control) has a problem to deal with. When you call DataBind(), it creates the template one time for every data item. How is the Repeater going to follow the guideline that all controls must be recreated on every request? On a postback, you aren't databinding the repeater again (because you usually only do it when !IsPostback). So how can the repeater possibly rebuild the control tree, when it doesn't have any data to work with?
Simple. All it has to do is remember how many data items there were. When you call DataBind(), not only does it use the template for each data item, but it takes the number of data items there were and stores it in a ViewState field. Then on a postback, it waits for LoadViewState to occur, gets the count value back out of ViewState, and then uses the template count times.
Volia! As it creates each data item by using the template, the controls contained within it become "rooted" in the tree. Thanks to the framework, the instant they are rooted, their ViewState is loaded, and are restored to the state they were in when you first called DataBind() many requests ago. It's really a brilliant process. The repeater itself doesn't have to remember any state information about the controls it contains. All it has to remember is the number of items, and let the framework do the rest.
If you've ever dealt with the ItemCreated and ItemDataBound events on the Repeater (or again, any other databound control that has them), now you can appreciate the distinction, and why the DataItem is not always available.
The state of the Control Tree
We now have all the building blocks. We know how to create various types of dynamic controls. We know how they differ (or don't differ) from static controls. We know how to add these controls into the control tree. And we understand that it is up to us to rebuild that tree every request.
But the control tree isn't just a data structure. It's an evolving data structure. Throughout the page event sequence, the state of the tree and all the controls within it change. That has implications for dynamic controls, because your dynamic controls are going to be late-commers in the game. For example, if you add a control during the Load event, your control hasn't even had its Init event fired, but its older siblings will have already gone through Init, LoadViewState, and LoadPostData. If you are the youngest one in your family, you know how mean older siblings can be. Obviously, the framework has a way to regulate this situation (and it doesn't involve turning the car around).
Basically, dynamically added controls play "catch up" with the state of the tree. The instant a control becomes "rooted" in the control tree, the framework fast-forwards through the events in that control along with any children it contains until it has caught up with the page's current state. By "rooted" I mean that you can get to the root node of the tree by following the Parent properties. Here's an example:
PlaceHolder ph = new PlaceHolder();
ph.Controls.Add(new TextBox());
// at this point, neither the PlaceHolder or TextBox are "rooted"
this.Controls.Add(ph); // moment of rooting!
// now the PlaceHolder _and_ the TextBox are "rooted"
This example assumes that the control represented by "this" is already rooted in the tree. If the control that contains this code isn't rooted when this code runs, then those controls aren't rooted yet either.
So Controls.Add() isn't an innocent addition to a simple collection. It immediately has an impact on the control being added, as it "grows up" at an accelerated pace. But the steps through which the control is fast forwarded don't completely correspond to all of the events available on the page. The only events eligible for this process are (in order): Init, LoadViewState, Load, PreRender.
You must memorize that list...
Part 4, soon to come, will explore the different stages of the page event sequence in which you may be adding dynamic controls to the tree, and the scenarios they are meant for. Originally I planned for that to be part of this part, but it seems I'm so long winded I have reached Windows Live Writer's maximum length for a blog post!