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

5 Comments

  • Ahh excellent tip :) It's too bad the catch up game can't be paused through some API, allowing you to modify the control collection and then tell it 'go' when you're done.

    Come to think of it, can't this be a problem with non-templates as well? If you create your child controls as a composite control in CreateChildControls, and you one-by-one add them to your control collection, its a similar problem. The difference is CreateChildControls is usually called early enough that it doesn't hurt. I believe the latest it _can_ be called is OnPreRender, so the controls you add will not catch all the way up to PreRender yet anyway. But if a control depended on something existing in say, OnInit, it would be a problem (controls depending on others in OnInit is a no-no and a pet peeve of mine, but it happens).

  • Nice post Eilon :-). If you promote a bit more also programmatically loading of UserControls on post-back events this will be great for all 3rd party control vendors like telerik :-).

  • Great comments, everyone, especially about "when to create child controls."
    The answer to that should be in Nikhil Kothari's book Developing Microsoft ASP.NET Server Controls and Components.
    How good is this book? Well, every person on the ASP.NET team keeps a copy on their desk for reference. And that's not just because Nikhil gave us each a free copy :) It's really *that* good.
    - Eilon

  • Absolutely agree! Nikhil Kothari's book should be on the desk of every ASP.NET developer.

    Small question out of the templates and control execution lifecycle scope:
    Is there any known issue with UpdatePanel in Conditional UpdateMode and AsyncPostBackTrigger with GridView Sorting event? Here is the declaration:











    Sorting works fine however paging throws event validation exception.
    Any ideas?

  • I am currently overiding the CreateChildControls method & adding my templated controls in here.

    Is this "recommended" :) cos my stuff is working (for the time being, atleast)

    Regards,

    Sunny

Comments have been disabled for this content.