How to instantiate templates (properly)
As part of my work on the ASP.NET team I've worked directly with several 3rd party control vendors and have spoken to hundreds of customers at conferences such as PDC and TechEd as well as presented on topics related to building controls and using ASP.NET in general. I've looked at the source code for literally hundreds of controls and although the controls are usually pretty darn cool, sometimes I spot some snippets code that just don't look right. That is, they don't look right to me. I'm sure they look right to whoever wrote them.
I can't tell you how many times I've seen code for a templated control such as this:
public ITemplate MyTemplate { ... }
public override void OnPreRender(EventArgs e) {
if (MyTemplate != null) {
MyTemplate.InstantiateIn(this);
}
}
I mean, it looks pretty good, doesn't it? It checks if the template is set and if so it instantiates it within the container control. This code is wrong. Very wrong!
Here's the correct way to instantiate a template:
public ITemplate MyTemplate { ... }
public override void OnPreRender(EventArgs e) {
if (MyTemplate != null) {
Control templateContainer = new Control();
MyTemplate.InstantiateIn(templateContainer);
Controls.Add(templateContainer);
}
}
What's the point of this templateContainer thing? The answer: control lifecycle catch-up, and it's one of the most important concepts you need to understand in ASP.NET, especially if you're a control developer.
Every time a control is added to a parent control the child control will immediately "catch up" to the lifecycle point of the parent control. The control lifecycle includes the familiar Init, Load, PreRender, and a number of other lifecycle stages related to state management. When a template is instantiated through a call to InstantiateIn, all that happens (typically) is that the controls in the template are instantiated one by one, and added to the container that you passed in to InstantiateIn, again one by one. Imagine the template had two controls, call them GridView1 and SqlDataSource1. When InstantiateIn is called, this is the lifecycle of the child controls:
Instantiate GridView1 and add to live control tree at the PreRender point in the lifecycle
GridView1.Init
GridView1.Load
GridView1.PreRender
Instantiate SqlDataSource1 and add to live control tree at the PreRender point in the lifecycle
SqlDataSource1.Init
SqlDataSource1.Load
SqlDataSource1.PreRender
This scenario is now broken since in GridView1's PreRender it will look for SqlDataSource1, which doesn't exist yet, so it will throw an exception. Each control is doing a full lifecycle catch-up on its own.
How do we fix it? Easy: Add the controls to an unparented template container, and then add the entire container to the live control tree. Using the corrected code, this is the lifecycle of the child controls:
Instantiate GridView1 and add to unparented template container control tree before any lifecycle happens
Instantiate SqlDataSource1 and add to unparented template container control tree before any lifecycle happens
Add template container to live control tree at the PreRender point in the lifecycle
GridView1.Init
SqlDataSource1.Init
GridView1.Load
SqlDataSource1.Load
GridView1.PreRender
SqlDataSource1.PreRender
This time around the child controls do their lifecycle catch-up at the same time so in GridView1's PreRender it can do a FindControl to locate SqlDataSource1 and start grabbing its data. This is much closer to what happens to controls that are directly on the page and not inside templates, which is why it works so well.
This very subtle bug is very hard to notice and even harder to find. I once spent about three days debugging such a bug in a control until I discovered that it simply wasn't using a template container.
Bonus: If you want the controls in the template to be in their own naming container, instead of instantiating a regular System.Web.UI.Control, write a derived control that also implements the INamingContainer interface and use that instead.
- Eilon