Introduction
If you are in the business to display data on the web using ASP.NET, you probably used templates. All data bound controls, which used to display tabular data (like TreeView, DataList, GridView, Repeater and ListView) use template to make your work easier. You define the template only once and the data-binding mechanism will repeat it for each and every row of the data for you. This idea works with great efficiency, but there is a little misunderstanding when it comes to access the controls inside the data rows using the ID of the template. It can be confusing as you only declared one control with that ID, but now there are tens (hundreds or thousands) of repetitions of the same block. What can make it more confusing is the rendering of the repeated IDs to the client; as a web developer you probably aware of the fact that in HTML ID must be unique...
Background
The trigger to this article was a couple of Q&A questions about 'Why I am getting the error: "Object reference not set to an instance of an object"' after running FindControl on a control with template. Or 'How to get selected value in GridView using jQuery' on the client side for the same kind of control.
All those questions came from the lack of understanding ID creation in controls with template...
In this article I will explain the problems and give example how to access the controls after all - on the server and the client...
Let see the facts
In the image you can see two columns, that represent the object hierarchy of a DataList control bounded to an XML file with data of 5 books in it. The left side column shows the IDs on the server side, while the right-side column shows the client side ID's. The DataList control (and the template!) used to create this object tree looks like this:
<asp:DataList ID="idBookList" runat="server">
<ItemTemplate>
<asp:Panel runat="server" ID="idGenre" />
<asp:Panel runat="server" ID="idTitle" />
<asp:Panel runat="server" ID="idAuthor" />
<asp:Panel runat="server" ID="idPrice" />
</ItemTemplate>
</asp:DataList>
As you can see the server-side IDs are follow the template - that creates multiple controls with the same ID. At the same time the ASP.NET rendering engine created different IDs on the client, that for resolve the problem of unique ID in HTML...
Now either on server or client, if you try to select control (element) using it's ID from the template - let say idPrice - you have some problems. On the server, FindControl should return multiple controls, but can't, so you get null. On the client there are no such control exist...
Now that you saw how a template expands itself at run time I will scan the most common error and give some solution.
FindControl returns null
Let see an example (the most common as I see):
You have a web page with a CheckBox and a DataList (the one from the previous sample) on it. The CheckBox used to control the display of the price inside the DataList.
protected override void OnLoad(EventArgs e)
{
base.OnLoad( e );
idBookList.FindControl("idPrice").Visible = idShowPrice.Checked;
}
The purpose is clear - hide the price part if the CheckBox is off and show it if CheckBox is on. The problem is that FindControl returns null!!! Now if you stop for a moment and think about it that was expected. You have now numerous controls with the ID idPrice, so which one do you want to retrieve? The answer of course - all of them, the problem is that FindControl can return only one!
There are two possible solutions for it:
On...DataBound event
For all the controls using template, exists an event that fires on each and every item created from the template during the data binding process. For DataList its OnItemDataBound, for GridView it's OnRowDataBound and so on... This event puts you in the context of a single data row, and in that context there is only on control with any given ID (as a single data row creates a single instance of the template), so FindControl will work happily...
Let see it with our sample
protected override void OnLoad(EventArgs e)
{
base.OnLoad( e );
idBookList.ItemDataBound += idBookList_ItemDataBound;
}
void idBookList_ItemDataBound(object sender, DataListItemEventArgs e)
{
e.Item.FindControl("idPrice").Visible = idShowPrice.Checked;
}
Now that's the most common way to manipulate controls from a template, and probably the one you will use.
A smart extension method
In case that the above solution does not fit you and you want to handle all the controls of the same ID, I give you an extension method that can solve all your problems...
public static class Extensions
{
public static List<Control> FindAllControls ( this Control Parent, string ID )
{
List<Control> oControls = null;
Stack oStack = new Stack( );
oStack.Push( Parent );
while ( oStack.Count != 0 )
{
Control oCheckControl = ( Control )oStack.Pop( );
if ( oCheckControl.ID == ID )
{
if ( oControls == null )
{
oControls = new List<Control>( );
}
oControls.Add( oCheckControl );
}
if ( oCheckControl.HasControls( ) )
{
foreach ( Control oChildControl in oCheckControl.Controls )
{
oStack.Push( oChildControl );
}
}
}
return ( oControls );
}
}
And here a sample how to use it:
protected override void OnLoad(EventArgs e)
{
base.OnLoad( e );
List<Control> oPriceControls = idBookList.FindAllControls("idPrice");
foreach(Control oPriceControl in oPriceControls)
{
oPriceControl.Visible = idShowPrice.Checked;
}
}
These two methods of accessing controls created from a template can help you, and even more can help you the understanding the way IDs created!
$('#idPrice') is an empty selector
[I took the liberty to use jQuery for the client side as it makes my life easier :-). All the sample code can be written in plain JavaScript, but as my point is not to teach you JavaScript but explain template in ASP.NET you will have to accept this...]
I believe that after all what you saw it is clear why searching for element(s) with idPrice will not gain any result! There is NO element with ID 'idPrice' on the client. There is idBookList_idPrice_0, idBookList_idPrice_1 and so on. There is a certain pattern that used to create those IDs and you can probably write some loop that creates the proper IDs to scan all elements, but I want to show you a better way that fit much better into the HTML/CSS/JavaScript word of the client...
<asp:DataList ID="idBookList" runat="server">
<ItemTemplate>
<asp:Panel runat="server" ID="idGenre" CssClass="genre" />
<asp:Panel runat="server" ID="idTitle" CssClass="title" />
<asp:Panel runat="server" ID="idAuthor" CssClass="author" />
<asp:Panel runat="server" ID="idPrice" CssClass="price" />
</ItemTemplate>
</asp:DataList>
As you can see I added css classes to every item - I believe that you already have some css for formatting, you can use it too...
That css class name is common for every instance of the same control, created from the template, and we will use it to access them together...
$(document).ready(function () {
$('#idShowPrice').click(function (e) {
if ($(this).is(':checked')) {
$('.price').show();
}
else {
$('.price').hide();
}
});
Without understanding exact jQuery syntax, you can see, what happened here: On the click event of the CheckBox we hide or show the price elements. $('.price') returns to us all the elements created by from template, and have the css class 'price'!
Another option is handle elements of a single row in the context of the row, let say on row-click...
$(document).ready(function () {
$('#idBookList TR').click(function (e) {
$(this).find('TD div.price').toggle();
});
});
In this sample we toggle the visibility of the 'price' column on the row click. This is the very same idea as before, except, that we look for the 'price' css class in the context of the row we just clicked on - $(this).
Summary
As you see, accessing controls/elements that were created from an ASP.NET template is non-trivial. On server you can use the common FindControl method only if you are in the right context (context of a data row), or you have to write your own method to traverse the object tree and find all controls you interested in.
On the client, there is no practical way (there is a few non-practical) to find the elements, created from the template, using their IDs. So we used css classes to identify the elements of the same group, select and manipulate them...
Using the code
The sample attached has a page for all the four solutions you saw here. There is also an implementation of the FindAllControls extension method.