Single Instance Templates

A brief introduction to one of the lesser known Whidbey features for control developers: Single Instance Templates, and some context around the problems it solves: specifically the need to use FindControl to access controls inside templates.

Templates are a powerful mechanism that enable page developer's to customize controls and define repeatable content (eg. in data-bound controls) and have been around since ASP.NET v1. However, the templating and closely related INamingContainer infrastructure can get in the way of accessing controls defined within. Top-level controls can be accessed directly via associated member fields. However controls inside templates do not get corresponding fields. Instead, you have to use FindControl passing in the ID as a string argument and then cast the result to the right type to access controls placed within a template, which is sub-optimal. In Whidbey, we've made some incremental improvements in this area. One of the features we've added is the notion of single-instance templates.

Why FindControl?
At runtime, the page is basically a control tree, and each control needs to be identifiable uniquely for various reasons. Unfortunately the ID is not good enough, because templates may be repeated (as ItemTemplate is in DataList). This is the same reason that ID'd controls within templates do not have associated fields. The solution we chose to implement was to add the ability to define name scopes. A name scope is defined by a control that implements INamingContainer (as each item does in DataList). With this in place, controls must have a unique ID within a name scope rather than across the entire page. The actual unique ID is computed by concatenating the IDs of controls along the name scope hierarchy. One other thing this scheme for unique IDs provided was a mechanism to enable on-demand control creation in composite controls. Composite controls don't really know when to build their child controls. They need to be told to do so. So when FindControl is invoked (either by the page developer's code, or by the page when it tries to match controls to incoming post-back data), it gives a chance to each naming container to build its control hierarchy as needed. Hence the two reasons for FindControl: to ensure controls are created so they can be found, and the ability to define a name scope to search within (both of these requirements cannot be solved by direct references.

In reality some controls do know when to build their child controls. An example of this is the new LoginView control that uses the ambient user context to automatically switch views. So it would be nice if such controls could expose their template contents as top-level controls, and simulate direct access. Secondly, a lot of templated controls do not repeat their contents. In these cases, templates are just a customization mechanism, and are not meant to define a snippet of repeatable content. Again LoginView is a good example. The contents of the template are in fact unique across the page, and could be promoted top-level, and be accessed directly.

The Solution
This is a general pattern that would be useful elsewhere. We added the notion of "single instance templates" as a general solution. Control developers can define templates to be single instance using metadata which causes the ID'd controls within the template contents to be promoted to the page level, i.e. have associated fields, and therefore accessible directly without FindControl. Control developers simply need to mark their template properties appropriately. The parser and code-generator together work behind the scenes to add member fields, and initialize them at the right time. For example, LoginView defines its AnonymousTemplate as follows:


[TemplateInstance(TemplateInstance.Single)]
public ITemplate AnonymousTemplate {
    get { ... }
    set { ... }
}


So if you're a control developer, check to see if any of your control's template properties qualify for this metadata, if you are looking to simplify some usage patterns associated with your control. One thing to keep in mind: the fields associated with the contained control references remain null, until the template is in fact instantiated. Consequently, templated controls using this new mechanism ought to define an event that page developers can handle, and access the controls in that event handler. On the other hand, page developers ought to keep in mind that Page_Load may be too early to access these controls, and they instead should handle this event to be guaranteed that the template has been instantiated, and that it is safe to access the contained controls.

I believe access to controls can be simplified in multi-instance templates as well. I've been discussing some ideas in the context of v-next ideas around data-bound controls. So hopefully, they will completely eliminate the pain of FindControl and INamingContainer in terms of accessing controls with templates.

Posted on Monday, 10/17/2005 @ 6:01 PM | #ASP.NET


Comments

15 comments have been posted.

Sergio Pereira

Posted on 10/18/2005 @ 5:45 AM
Nikhil, that's something that has always bothered me since I started porting some of my projects to 2.0. I was using this ITemplate (single instance) approach a lot in v1.1 for both pages and user controls. Converting the pages to use a MasterPage was a snap but I was surptised that no similar concept existed for user controls (MasterControl ?). I had to keep my original template code. Knowing about this attribute will help a little, though.

pRASAD pATNAIK

Posted on 10/19/2005 @ 10:39 PM
i really wonder how it works, it generates a name which i cannot control, what if i want to change the the way it name the control, for example it name s a control _ctl0_ctl0_ctl1_textbox1 , can i make it to _c0_c0_c1_textbox1

Luis Abreu

Posted on 10/20/2005 @ 5:28 AM
Nikhilk, one question: what version of .net are you using? in my rc version the anonymoustemplate property of that control doesn't have that attribute...you can easilly override the control to add that attribute to the property though...

Nikhil Kothari

Posted on 10/20/2005 @ 9:38 AM
Prasad, To control the unique name "ctl0_ctl0_textBox1" - you have to ID all the controls in the name scope hierarchy. For controls on the page, you can obviously ID them as you like. For runtime-created controls (eg. rows in a GridView), its much harder. In GridView you'd have to handle RowCreated event, and ID the rows as they are created. Probably not worth the trouble. So in general, yes, the _ctl prefix is hardcoded. Whats the scenario you have in mind to reduce it to _c? Size reduction?

Luis, I made a mistake with my post - the AnonymousTemplate used to be annotated with this metadata. There were apparently some designer bugs, and to workaround those the metadata was removed. However, while the designer bugs are supposed to have been fixed, the template metadata was not restored (unfortunate!) The template properties on Zone controls in the WebParts framework do have this metadata.

Thomas Carpe

Posted on 10/21/2005 @ 2:25 AM
Hi folks! This is my first comment here, but I can see after reading this article that what I am trying to do is also happening amongst you all.

I had similar problems with getting controls inside of templates, especially as I learned to use GridView and DetailsView (which are gret controls BTW). I am equally perturbed that meta-data in DetailsView is not carried uphill.

My solution was not to use FindControl. Frankly, it sucks! Sometimes, it did not find controls even with the correct ID. Knowing the unique ID might help, but why guess and then HOPE your code does not change in a few months and break the string!

What I did was to use an OnInit event handler. For every template control I wished to track, I add an OnInit event to it. The event adds it to a NamedWebControlCollection (in my case its just a jazzed up name-object collection that only accepts WebControl objects). I pick a name for them when I do the call, and if they are initialized then they are in the collection for later use. I can just test _control == null for cases where they don't exist. This is great for me when working in ItemTemplate, EditItemTemplate, and InsertItemTemplate.

I don't think it will work very well in repeater groups like DataList or GridView (yet), but I could rewrite it to assign names in a uniform way. In that way it would become alot like a scaled back version of FindControl, yes?

This doesn't really provide me with compiler-time checking and meta-data, which would be nice, so I ended up having to write public/protected properties to call GetNamesTemplateControl(...) inside my UserControls (actually I inherit UserControl and called it something like TemplatedUSerControl, but "whatever").

The down side is of course that you have to wire up the OnInit event in your ascx files. Bummer! But I got so tired of writing event handlers that were like, "if (send is CheckBox && (CheckBox)sender.ID == "MyName") { /* do something - usually assign it to a protected property */ } ..." What a drag!

Anyway, I am always interested in other strategies for solving this problem. Perhaps my method could fall back on FindControl() when it doesn't find a match in my explicitly named list. I hope this helps folks to deal with all the extra baggage of dealing with templates, so we can exploit their work-saving features.

Thomas Carpe

Posted on 10/21/2005 @ 2:29 AM
I almost forgot. I would not have had to do this at all, if the DetailsView had supported this SingleInstanceTemplate thing you speak about. Let's hope maybe it makes it into the final version, but I am not holding my breath obviously!

:-)

Diego Vega

Posted on 11/1/2005 @ 1:49 PM
Thanks for the hint Nikhil, I was trying to find a way to do this yes just yesterday, so I will be checking single instance templates tomorrow.

I hope single instance templates will allow for greater "componentization", as the other alternative (nested master pages) is not supported at design time. I love whatever let’s us reduce the amount of code the front end programmer needs to write.

By the way, if you ask me why this is important, I think the most frustrating consequence of having all controls wrapped in INamingContainers is that declarative databinding does not work as expected when a control that is wrapped is supposed to serve as source for a ControlParameter.

I have found a solution very similar to what Thomas does; just I don't add the controls to any collection. After all, I want to keep the amount of code to a minimum! What I do is capture the Init event of the control and then assign the sender to a page level control variable. Then, on the Filtering event of the DataSource, I will find those fields already filled (crossed fingers).

It is not a lot of code because I only do this for the controls that I really need, but if you find a solution that will help me assign a wrapped control to a control parameter, then the promise of declarative databinding will be restored.

prasad patnaik

Posted on 1/2/2006 @ 10:42 AM
yes nikhil, the problem is that i have multiple nesting of controls in my page, which results to increase my pagesize to some where more then 100k and most of the bytes of the page size is occupied by the control names ie: [ctl0_ctl0_textBox1], just need to get rid of them if i can.

Chandra

Posted on 2/16/2006 @ 2:19 PM
Nikhil,
I am seeing a very odd behavior with Repeater control after we ported it to .Net 2.0.
The repeater has an itemtemplate and there are some textboxes in the template. The IDs of these are not getting scoped in some of the items when it is rendered. In other words they are just "control" and in others they are "ctl0_ctl0_control". This has been causing problems with the FindControl method. Has anybody seen this?

Parthiban

Posted on 2/17/2006 @ 5:36 AM
Hi,
We are using the system.web.caching.cache namespace for caching the some specific data set. We use the following sysntax to add the dataset into the cache
Cache.Insert("vsCustomer" & Session.SessionID, dsCustomer) the cache key name differentiate the users.
assigning this dataset to the datagrid. it works fine for the first time, when we take data from the datasource.
next time we are not getting the cached dataset from the caching. It works for some time and does not work for some time.
Please help us to fix this bug. Is there any settings/configurations to be modified for this feature.

we use the following syntax to take the value from the cache Cache.Get("vsCustomer" & Session.SessionID)
Thank you,
Parthiban.

Les Matheson

Posted on 2/23/2006 @ 4:25 PM
One solution which is simple -- if it fits your problem -- is to derive your custom control from Control instead of from TemplateControl or UserControl. Both of the latter classes implement INamingContainer, which is what stops FindControl() from penetrating into a user control's children. But in most cases, I've found that deriving directly from Control gives me what I want: some wrapper class into which I can put additional controls while still letting the page logic manage and name them -- I don't really want a naming container usually.

Rinu Rajan

Posted on 2/27/2006 @ 1:02 AM
Hi,
We are using DataGrid to export dataset to excel, we are using the well known method of changing the content type to "application/vnd.xls" and then writing using a HtmlTextWriter, we are doing this is a popup on click of a button, but the problem is that we are unable to close the popup after the user saves the excel, we are using javascript window.close() function, we also tried changing the content type to "text/html" still it does not work.

Thanks,
Rinu

miloush

Posted on 4/15/2006 @ 10:05 AM
Hi,
How could I implement template with parameter? I am developing a server custom control and want to provide the developer the ability to define more versions of the same template. Which version of the template will be used is determined during the run time.
Something like:
<ItemTemplate forcase="case1"> ... </ItemTemplate>
<ItemTemplate forcase="case2"> ... </ItemTemplate>
<ItemTemplate forcase="case3"> ... </ItemTemplate>

Thanks,
Jan

Andrew

Posted on 4/16/2006 @ 7:05 PM
This has been exactly what I was looking for. (Mind you I didn't even know to search on this attribute until I started reflecting over the ControlBuilder code using Reflector).

Nik, I was wondering if you know of any other resources dealing with when the templates will be instantiated?

My problem is I have implemented a server control which uses the single template instance....so far so good. I then have hosted that server control in a user control which has placed an asp:Panel inside of the template of the original server control....again all good. But then I wired up on of the properties exposed by the Panel control (which resides in the template) to a public property of the UserControl.....and thats where she breaks....because if I try to access that property using declarative syntax, I get a NullReferenceException due to the templated control not being instantiated yet.

I know you mentioned going down the raising event path, but I didn't really want the users of my usercontrol to have to worry about that sort of stuff. I'd prefer to use the whole EnsureChildControls methodology if possible.

TIA for any links to topics dealling with this issue.

Thendral

Posted on 6/28/2006 @ 11:07 PM
hi,
i created usercontrol with gridview, when i drag and drop into webform i want to customize the gridview by adding columns in HTML page, it works fine but when i switch to design it shows that error creating control .. stating that system.web.ui.usercontrol does not have a public property named Columns
The discussion on this post has been closed. Please use my contact form to provide comments.