One of the more anticipated features of ASP.NET 4.0 – at least for me - is the new ClientIDMode property, which can be used to force controls to generate clean Client IDs that don’t follow ASP.NET’s munged NamingContainer ID conventions. To be clear, NamingContainer IDs are necessary for complex controls and pages that nest many things inside of a single context, but for most application level Web design scenarios those munged IDs get in the way and don’t provide a lot of value.
The munged IDs affect all sorts of development scenarios from design and CSS styling where ID haven’t been predictable for #id styling:
#ctl00_content_txtName
{
font-weight: bold;
}
vs the more expected:
#txtName
{
font-weight: bold;
}
And even more so there’s the whole nightmare of ClientIds in script pages where code like this:
<script type="text/javascript">
var txtName = $("<%= txtName.ClientID %>");
var txtTitle = $("[id$=_txtTitle]");
</script>
or some other pre-generation code is necessary to get client id’s properly referenced in script code.
ClientIDMode
In prior versions of ASP.NET you had to live with the problem or work around with various hacks. In ASP.NET 4.0 things get a little easier via the new ClientIDMode property which allows you to specify exactly how a ClientID is generated.
The idea is simple: you get a new ClientIDMode property on all ASP.NET controls which can be set either at the actual control or somewhere up the NamingContainer chain. If the ClientIDMode property is not set on a control it inherits the setting from the nearest NamingContainer with the exception of the <asp:Content> placeholder which doesn’t participate in ClientIDMode processing (bummer!).
This means if you choose to you can set ClientIDMode on the Page level and it will trickle down into all the child controls on the page, but a custom naming container like a User Control can still override the ClientIDMode to enforce it’s naming container requirements. It’ll be interesting to see how much existing code might break based on this inheritance scheme as custom controls may lose controls over their ClientID naming if the ClientIDMode property isn’t set explicitly.
Anyway let’s take a look at a very simple example and how the ClientIDMode property affects the ID rendering. Imagine you have page that uses a master page and is set up like this:
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="WebForm2.aspx.cs"
Inherits="WebApplication1.WebForm2"
ClientIDMode="Predictable"
%>
<asp:Content ID="content" ContentPlaceHolderID="content" runat="server" ClientIDMode="Static" >
<asp:TextBox runat="server" ID="txtName" ClientIDMode="Static" />
</asp:Content>
Here’s what the various values for the txtName property can look like:
AutoID
This is the existing behavior in ASP.NET 1.x-3.x where full naming container munging takes place.
<input name="ctl00$content$txtName" type="text" id="ctl00_content_txtName" />
In Beta 2 this is the default value for the Page. According to the latest VS 2010 documentation however, the default behavior by release time will be Predictable.
Static
This option forces the control’s ClientID to use its ID value directly. No naming container naming at all is applied and you end up with clean client ids:
<inputname="ctl00$content$txtName"type="text"id="txtName" />
This option is what most of us want to use, but you have to be clear on that this can potentially cause conflicts with other control on the page. If there are several instances of the same naming container (several instances of the same user control for example) there can easily be a client ID naming conflict. It’s basically up to you to decide whether this is a problem or not.
Predictable
The previous two values are pretty self-explanatory. Predictable however, requires some explanation. To me at least it's not in the least bit predictable :-}.
MSDN defines this enum value as follows:
This algorithm is used for controls that are in data-bound controls. The ClientID value is generated by concatenating the ClientID value of the parent naming container with the ID value of the control. If the control is a data-bound control that generates multiple rows, the value of the data field specified in the ClientIDRowSuffix property is added at the end. For the GridView control, multiple data fields can be specified. If the ClientIDRowSuffix property is blank, a sequential number is added at the end instead of a data-field value. Each segment is separated by an underscore character (_).
The key that makes this value a bit confusing is that it relies on the parent’s ClientID value to build it’s own client ID value. Which effectively means that the value is not predictable at all but rather very tightly coupled to the parent naming container’s ClientIDMode setting.
For our simple textbox example, if the ClientIDMode property of the parent naming container (Page in this case) is set to “Predictable” you’ll get this:
<input name="ctl00$content$txtName" type="text" id="content_txtName" />
If on the other hand the Page is set to “AutoID” you get:
<input name="ctl00$content$txtName" type="text" id="ctl00_content_txtName" />
The latter is effectively the same as if you specified AutoID in this scenario because it inherits the AutoID naming from the Page and Content control of the page. But again – predictable behavior always depends on the parent naming container and how it generates its id so the ID may not always be exactly the same as the AutoID generated value because somewhere in the NamingContainer chain the ClientIDMode setting may be set to a different value. For example if you had another naming container in the middle that was set to Static you’d end up effectively with an ID that starts with the NamingContainers' ID rather than the whole ctl000_content munging.
Predictable is useful, but only if all naming containers down the chain use this setting. Otherwise you’re right back to the munged Ids that are pretty unpredictable.
Inherit
The final setting is Inherit which is the default for all controls except Page (AFAIK). This means that controls by default inherit the naming container’s ClientIDMode setting.
Inheritance
The explicit values are pretty obvious in what they do. AutoID is classic behavior, Static is what we want in typical client centric apps, and Predictable is what you typically will want to use for list controls and anything that has possible naming conflicts.
Things get a little more tricky with inheritance of these settings. Specicfically the ClientIDMode property inherits from a NamingContainer down. Unlike most other ASP.NET controls the inheritance is based on the NamingContainer not on the Parent Container.
What this means is that if you set ClientIDMode="Static" on a Page or MasterPage all controls inherit that settings:
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="WebForm2.aspx.cs" Inherits="WebApplication1.WebForm2"
ClientIDMode="Static" %>
If you don’t set the ClientIDMode on any other controls the entire page will use this ClientIDMode. Any UserControls can override the setting but all controls will use this setting.
Now imagine I do:
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="WebForm2.aspx.cs" Inherits="WebApplication1.WebForm2"
ClientIDMode="Predictable" %>
and I’m running inside of a master page and I put in a block of controls like this:
<asp:Panel runat="server" ClientIDMode="Static">
This is a test:
<asp:TextBox runat="server" ID="txtName" />
<asp:Button ID="btnSubmit" runat="server" Text="Go" />
</asp:Panel>
Quick what should you see? Unfortunately not what I would have expected – although it’s true to what the documentation advertises. The block above renders into:
<div>
This is a test:
<input name="ctl00$content$txtName" type="text" id="content_txtName" />
<input type="submit" name="ctl00$content$btnSubmit" value="Go" id="content_btnSubmit" />
</div>
What’s happening here is that even though we specified Static naming on the Panel (which is not a NamingContainer) Predictable naming is used which runs up to the nearest naming container – in this case the Master Page Content control and using that as its base for the name. If I want those controls to render with clean ids I need to use:
<asp:Panel runat="server" ClientIDMode="Static">
This is a test:
<asp:TextBox runat="server" ID="txtName" ClientIDMode="Static" />
<asp:Button ID="btnSubmit" runat="server" Text="Go" ClientIDMode="Static" />
</asp:Panel>
with explicit static extensions on the control – or by changing the Page’s ClientIDMode to Static:
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="WebForm2.aspx.cs" Inherits="WebApplication1.WebForm2"
ClientIDMode="Static" %>
With either of those in place I now get:
<div id="Panel1">
This is a test:
<input name="ctl00$content$txtName" type="text" id="txtName" />
<input type="submit" name="ctl00$content$btnSubmit" value="Go" id="btnSubmit" />
</div>
Notice that when page level ClientIDMode="Static" the <div> tag now renders with an explicit ID which is rather inconsistent (it’s not there if I just have ClientIDMode=”Static” on the Panel alone).
Note that you unfortunately cannot use an <asp:Content> control and specify the ClientIDMode like this:
<asp:Content ID="content" ContentPlaceHolderID="content" runat="server" ClientIDMode="Static">
which would be the ideal situation for many applications. Unfortunately ClientIDMode on the Content control has no effect on its children.
Unfortunately this means that there’s no real clean way for page code to isolate ClientIDMode rendering to just a few controls in a page. In typical pages you often don’t care about generated IDs but for a few isolated controls/areas of the page, but there’s no easy way to isolate just that block of controls with its own naming container to force the ClientID hierarchy to be affected.
The only way around this would be to build a control that creates a naming container (<my:NamingContainerPlaceHolder> maybe?).
ClientIDRowSuffix
Another feature of the ClientID improvements in ASP.NET 4.0 is the ClientIDRowSuffix which can be applied to DataBound/List controls. This setting basically determines how ID values for template controls are generated, but it requires that the ClientIDMode is set to Predictable.
For example if I have GridView that looks like this:
<asp:GridView runat="server" ID="gvProducts" AutoGenerateColumns="false"
ClientIDRowSuffix="id" ClientIDMode="Predictable"
DataSourceID="xmlDataSource">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:Label runat="server" id="txtTitle" Text='<%# Eval("title") %>' />
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
<ItemTemplate>
<asp:Label runat="server" id="txtId" Text='<%# Eval("id") %>' />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
ASP.NET will generate the labels inside of the rows with IDs that include the name of the
<table cellspacing="0" rules="all" border="1" id="content_gvProducts" style="border-collapse:collapse;">
<tr>
<th scope="col"> </th><th scope="col"> </th>
</tr><tr>
<td>
<span id="content_gvProducts_txtTitle_32">West Wind West Wind Web Toolkit</span>
</td><td>
<span id="content_gvProducts_txtId_32">32</span>
</td>
</tr><tr>
<td>
<span id="content_gvProducts_txtTitle_33">West Wind West Wind Web Store</span>
</td><td>
<span id="content_gvProducts_txtId_33">33</span>
</td>
</tr>
</table>
Too bad that this doesn’t work with ClientIDMode=”Static” – that way you’d get short IDs instead of the long naming container values, but this is better than nothing. I’d much prefer txtTitle_33 for example.
Wouldn’t it be nice if Client Row Ids could be generated?
Another feature that I wish certain controls had was the ability to easily generate a ClientIDs onto the each item – in the case of the GridView each <tr> row element. It sure would be get output like this:
<tr id="content_gvProducts_33">
...
</tr>
In almost any client scenario I need to know the ID of the row in order to handle navigation and while you can get this done in a GridView with ItemCreated events it’s quite a pain to do this. For example for the gridview I’m using with a simple XmlDataSource control I have to do this:
protected void gvProducts_RowCreated(object sender, GridViewRowEventArgs e)
{
object dataItem = e.Row.DataItem;
if (dataItem != null)
{
XPathNavigator nav = ((IXPathNavigable)dataItem).CreateNavigator();
if (nav != null)
e.Row.Attributes.Add("id", this.gvProducts.ID + "_" + nav.GetAttribute("id",""));
}
}
to produce output like this:
<tr id="gvProducts_33"> ... </tr>
which is anything but intuitive for such a common scenario (although this IS a bit easier to grab the data if you use Entity list or DataTable binding).
Oh well – guess I don’t use GridView’s much anyway anymore with almost everything I do now using ListView that do allows more control during generation, but still .
What does this all mean?
I suspect it’s going to take some time to figure out all the little nuances of the new ClientIDMode features. At the very least the Static option on individual controls allows you to explicitly force controls to use the name you want it to and that’s a win any way you look at it.
For now I’m thinking ahead and I think my typical usage scenario likely looks like this:
- Add ClientIDMode="Static" to each Page
- Add ClientIDMode="Predictable" explicitly to each List Control on a Page
- Override explicitly to Predictable where necessary only
- For Control Development leave at default behavior if possible (Inherit)
- Override only when necessary and preferrably on individual subcontrols
Also for now I think it’s a good idea to EXPLICITLY specify a ClientIDMode on each page (or in your project) or explicitly declare the value in your web.config file:
<pages clientIDMode="Static" />
to ensure you get a predictable setting since the current Beta 2 implementation and the documentation are at odds of what the default value actually is.
It’s funny to think that such simple functionality should cause such complex workarounds and dependent behaviors but I suspect with a consistent regimen of CLientIDMode settings you can achieve output that works for any scenario.
© Rick Strahl, West Wind Technologies, 2005-2009

