The impact of ClientIDMode=Predictable
ASP.NET 4 introduces a new property on all controls: ClientIDMode. It lets web form developers minimize the size of the id= attribute written into HTML tags. It also helps them dictate the actual form of the ID, avoiding the mangled naming of previous versions when controls are inside of naming containers.
As a commercial web control developer, I’ve studied my products when the user sets ClientIDMode to something other than AutoID (which is that mangled format.) Unfortunately, the Predictable setting can break things.
Predictable is documented 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 (_).
Since we are talking about the ClientID property, we really are focused on how our client-side javascript code interacts with the HTML generated by the web controls. My controls heavily use javascript, and always grabbing HTML elements by anticipating the id= attribute. For example, suppose you have this TextBox inside of a UserControl whose ID is “UserControl1”.
<asp:TextBox id="TextBox1" runat="server" />
ASP.NET generates this HTML when ClientIDMode=AutoID.
<input type='text' id="UserControl1_TextBox1" />
To get this DHTML element, the Javascript should do this:
var fld = document.getElementById("UserControl1_TextBox1");
This script breaks if the Textbox is inside of a ListView or FormView control when ClientIDMode=Predictable. It’s that ClientIDRowSuffix property, mentioned in the docs above, that is causing the problem. It appends a “_row#” pattern to the overall id= output, like this:
<input type='text' id="UserControl1_TextBox1_0" />
Note: MS has told me that FormView won’t be a problem in the RTM of ASP.NET 4, but for Beta 2, it is.
There is NO problem if you always get the value for the id from the ClientID property itself. For example:
var fld = document.getElementById('<% = FindControl("TextBox1").ClientID %>');
My web controls do something a little different. They have child controls and their IDs have a specific pattern. Take my DateTextBox control. It uses an Image control to toggle a popup calendar.
It is generated this way:
public class DateTextBox : System.Web.UI.WebControls.TextBox
{
protected override void CreateChildControls()
{
System.Web.UI.WebControls.Image image = new System.Web.UI.WebControls.Image();
image.ID = this.ID + "_Img";
this.Controls.Add(image);
}
}
Here’s where it breaks
var img = document.getElementById('<% = FindControl("TextBox1").ClientID %>' + "_Img");
This no longer works when using Predictable mode if the control is inside a ListView or FormView because the image tag has that suffix:
<img id="FormView1_UserControl1_TextBox1_Img_0" />
To solve this, I created the following function that I wanted to share. It handles getting the correct ID, whether or not there is that suffix. It should be used only when a child web control is named in the pattern shown here.
// When joining [ClientID] + "_const", ClientIDMode=Predictable causes problems
// It adds "_#" to the end ([ClientID]_const_0).
// It will move _# from the end of [ClientID] to the end of the combined id.
// This function detects the pattern and copies the "_#" from the ClientId to the end of the overall id
// It returns the new ID.
// pID – The ClientID of the parent control
// pExt – The string appended to the ClientID of the parent, such as “_Img”
// pMode - int.
// 0 = Normal. Move the ClientID extension to the end of the overall Id.
// 1 = use pId + pExt exactly.
// 2 = the HTML tag was hardcoded with an ID (Literal control written out)
// instead of being generated by a webcontrol's ID.
function PrepIdExt(pId, pExt, pMode)
{
if (pMode == 1)
return pId + pExt;
if (!gGBIRE)
gGBIRE = new RegExp("_\\d+$"); // _ followed by 1 or more digits followed by the end of text
var vM = gGBIRE.exec(pId);
if (vM != null) // found it. Revise the ID
{
pId = pId.substr(0, pId.length - vM[0].length) + pExt;
if (!pMode)
pId = pId + vM[0];
}
else
pId = pId + pExt;
return pId;
} // PrepIdExt
var gGBIRE;
Here’s how to use it:
var img = document.getElementById(PrepIdExt('<% = FindControl("TextBox1").ClientID %>', "_Img", 0));
Some concluding thoughts
- There are other ways to create IDs within the web controls to avoid this. Most common is to implement INamingContainer on your base control and establish child control IDs without adding the parent (image.ID = "_Img" as opposed to image.ID = this.ID + "_Img")
- If you always use the ClientID property to specify the exact child control, your code will work. I don’t like that because you have to pass every ID you need from the server side, and the ID strings can be lengthy, impacting the page transmission time. I like to pass a single ClientID of the base control and let the javascript know how to work from there.
- Peter’s soapbox: I do not feel its right for the end-user of my controls to take control over how my child controls are generated. My web control should have complete control over that. Right now, the end-user can set ClientIDMode and impact my scripts. I have no problem with their dictating the value of the ClientID of the main control’s HTML, just the child controls. Microsoft should change the design to allow web controls to ignore ClientIDMode’s impact on their children.
- If you cannot modify existing code to support ClientIDMode, you can either document to set AutoID on the containing NamingContainer’s ClientIDMode property or make your web control’s code set ClientIDMode=AutoID on every child control.