One of the ASP.NET enhancements which first made an appearance in the Visual Studio 2010 PDC CTP release  is the ability to set a client Id which you can be confident will be used when the control renders it’s markup.
All of the samples in this post will use the following Server Control:

using System.Web.UI;

using System.Web.UI.WebControls;

namespace TestControls

{

    public class NamingPanel : Panel, INamingContainer

    { }

}

 

NamingPanel is very simple server control which just enables us to see the effect of ClientId without all the cruft that goes with using a more complex control (e.g, GridView).

How does the Control Id get defined now?

When used with a ‘nested’ NamingPanel, the following markup:

<tc:NamingPanel runat="server" id="rootPanel">
    <tc:NamingPanel runat="server" id="childPanel1">
    <asp:TextBox ID="TextBox" runat="server" Text="Hello!"></asp:TextBox>
    </tc:NamingPanel>
</tc:NamingPanel>

results in this rendered HTML:

  <div id="Div1"> 
       <div id="rootPanel_childPanel1">
        <input name="rootPanel$childPanel1$TextBox" type="text" value="Hello!" id="rootPanel_childPanel1_TextBox" />
    </div>
</div>

As you can see the id “TextBox” defined in the original markup gets mangled to  preserve ‘uniqueness’ by including the Ids of it’s parent controls within it’s own id attribute.
Whilst making the id attribute ‘unique’ does ensure that we can ensure that selecting a specific control client side using the id attribute will result in a single control, it does tend to get a bit verbose; the example above is only 2 levels deep, with more nesting; or even just using a control in  Nested MasterPages can result in hugely long values for the Client Id…

The second major problem with the ‘unique’ Client Ids is that you can’t guarantee that your client Id will remain the same if you move a control from one ‘NamingContainer’ to another.

NOTE: A NamingContainer is a control which either directly or indirectly implements the INamingContainer interface. In essence it defines a ‘NameSpace’ for controls…you’d most commonly implement this interface on Composite Server Controls to ensure that  any of the contained child controls are rendered with the parent ids prefixing their own Ids, ensuring they remain unique within the page.

What happens if you don’t specify an ‘id’ for one of these parent controls in markup? Given the following markup:

<tc:NamingPanel runat="server" id="rootPanel">
    <tc:NamingPanel runat="server">
    <asp:TextBox ID="TextBox" runat="server" Text="Hello!"></asp:TextBox>
    </tc:NamingPanel>
</tc:NamingPanel>

results in this rendered HTML:

<div id="rootPanel">
   <div>
       <input name="rootPanel$ctl00$TextBox" type="text" value="Hello!" id="rootPanel_ctl00_TextBox" />
   </div>
</div>

Taking a look at the id for the TextBox you can see that there’s an odd name inserted in the id attribute “ctl00”, where did this come form? Well, in the System.Web.UI.Control class there’s a declaration as follows (note, this and all other source in the post is from the ASP.NET 2.0 source using Reflector):

  private static readonly string[] automaticIDs = new string[] {

        "ctl00", "ctl01", "ctl02", "ctl03", "ctl04", "ctl05", "ctl06", "ctl07", "ctl08", "ctl09", "ctl10", "ctl11", "ctl12", "ctl13", "ctl14", "ctl15",

        "ctl16", "ctl17", "ctl18", "ctl19", "ctl20", "ctl21", "ctl22", "ctl23", "ctl24", "ctl25", "ctl26", "ctl27", "ctl28", "ctl29", "ctl30", "ctl31",

        "ctl32", "ctl33", "ctl34", "ctl35", "ctl36", "ctl37", "ctl38", "ctl39", "ctl40", "ctl41", "ctl42", "ctl43", "ctl44", "ctl45", "ctl46", "ctl47",

        "ctl48", "ctl49", "ctl50", "ctl51", "ctl52", "ctl53", "ctl54", "ctl55", "ctl56", "ctl57", "ctl58", "ctl59", "ctl60", "ctl61", "ctl62", "ctl63",

        "ctl64", "ctl65", "ctl66", "ctl67", "ctl68", "ctl69", "ctl70", "ctl71", "ctl72", "ctl73", "ctl74", "ctl75", "ctl76", "ctl77", "ctl78", "ctl79",

        "ctl80", "ctl81", "ctl82", "ctl83", "ctl84", "ctl85", "ctl86", "ctl87", "ctl88", "ctl89", "ctl90", "ctl91", "ctl92", "ctl93", "ctl94", "ctl95",

        "ctl96", "ctl97", "ctl98", "ctl99", "ctl100", "ctl101", "ctl102", "ctl103", "ctl104", "ctl105", "ctl106", "ctl107", "ctl108", "ctl109", "ctl110", "ctl111",

        "ctl112", "ctl113", "ctl114", "ctl115", "ctl116", "ctl117", "ctl118", "ctl119", "ctl120", "ctl121", "ctl122", "ctl123", "ctl124", "ctl125", "ctl126", "ctl127"

     };

As you can see this defines an array containing some predefined IDs to be used when no id is specified for a control; again, this retains uniqueness for control names. At runtime the ID for the control is generated by reading the ‘UniqueID’ property of the Control class.

public virtual string UniqueID

    {

        get

        {

            if (this._cachedUniqueID == null)

            {

                Control namingContainer = this.NamingContainer;

                if (namingContainer == null)

                {

                    return this._id;

                }

                if (this._id == null)

                {

                    this.GenerateAutomaticID();

                }

                if (this.Page == namingContainer)

                {

                    this._cachedUniqueID = this._id;

                }

                else

                {

                    string uniqueIDPrefix = namingContainer.GetUniqueIDPrefix();

                    if (uniqueIDPrefix.Length == 0)

                    {

                        return this._id;

                    }

                    this._cachedUniqueID = uniqueIDPrefix + this._id;

                }

            }

            return this._cachedUniqueID;

        }

    }

 

As you can see this code has the following check:

 

    if (this._id == null)

    {

            this.GenerateAutomaticID();

    }

 

Simply, if there’s no id already in existence for the control at render time then one is generated using a call to the following method:

 

 

private void GenerateAutomaticID()

{

    this.flags.Set(0x200000);

    this._namingContainer.EnsureOccasionalFields();

    int index = this._namingContainer._occasionalFields.NamedControlsID++;

    if (this.EnableLegacyRendering)

    {

        this._id = "_ctl" + index.ToString(NumberFormatInfo.InvariantInfo);

    }

    else if (index < 0x80)

    {

        this._id = automaticIDs[index];

    }

    else

    {

        this._id = "ctl" + index.ToString(NumberFormatInfo.InvariantInfo);

    }

    this._namingContainer.DirtyNameTable();

}

As you can see, this method simply looks at the specified array of control names for the next ’ctlXXX’ value until it runs out then it generates one. The ctlXXX ‘counter’ restarts for each NamingContainer…so it’s pretty unlikely you’ll get to the end of the static array of control names. The ‘automaticIDs’ array is used simply as a performance optimization…saving even this very simple piece of code from running on each control rendering.

So What’s With the Control’s ‘Name’

In the examples in the previous section you can see that there’s not just an Id attribute, rather there’s also a ‘name’ attribute for each control, e.g., “rootPanel$childPanel1$TextBox". You’ll see that this looks really similar to the Id property (and is in fact generated using almost an identical code-path), so why are there two different attributes. In the simplest explanation, consider the ‘name’ attribute to be the ‘server side’ name and the ‘id’ attribute to be the client side one.
When you “post’ or ‘get’ a form in HTML the ‘name’ attribute links the specific HTML control to the value which gets posted back; this is where the ‘uniqueness’ part comes in. By being unique it’s possible to hook up the posted value back to the Control on the server, in addition this also specifies the correct event to call on postback.

NOTE: While in current ASP.NET, it’s possible to translate from the name attribute to the id attribute this is not guaranteed  and will most likely break when using the new ASP.NET 4.0 functionality. We will not alter the ‘name’ attribute in ASP.NET 4.0…

Client Ids in DataBound Controls

The most critical reason for ensuring rendered controls have unique id attributes is controls within Data Bound controls. As an example the following markup defines a simple ListView hooked up to the Northwind database:

 

<tc:NamingPanel runat="server" id="rootPanel">
      <tc:NamingPanel runat="server">        

          <asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:ConnectionString %>"
              SelectCommand="SELECT [ProductName] FROM [Alphabetical list of products]"></asp:SqlDataSource>
          <asp:ListView ID="ListView1" runat="server" DataSourceID="SqlDataSource1"
              onselectedindexchanged="ListView1_SelectedIndexChanged">
              <ItemTemplate>
                  <tr style="">
                      <td>
                          <asp:Label ID="ProductNameLabel" runat="server"
                              Text='<%# Eval("ProductName") %>' />
                      </td>
                  </tr>
              </ItemTemplate>
                  <LayoutTemplate>
                  <table runat="server">
                      <tr runat="server">
                          <td runat="server">
                              <table ID="itemPlaceholderContainer" runat="server" border="0" style="">
                                  <tr runat="server" style="">
                                      <th runat="server">
                                          ProductName</th>
                                  </tr>
                                  <tr ID="itemPlaceholder" runat="server">
                                  </tr>
                              </table>
                          </td>
                      </tr>
                      <tr runat="server">
                          <td runat="server" style="">
                          </td>
                      </tr>
                  </table>
              </LayoutTemplate>
          </asp:ListView>
      </tc:NamingPanel>
  </tc:NamingPanel>

 

The markup above has the ListView inside the NamingPanels we had previously. This markup generates the following HTML (or a sinppet of it…)

    <table>
            <tr>
                <td>
                                <table id="rootPanel_ctl00_ListView1_itemPlaceholderContainer" border="0" style="">
                    <tr style="">
                        <th> ProductName</th>
                    </tr>
                    <tr style="">
                        <td>
                            <span id="rootPanel_ctl00_ListView1_ctrl0_ProductNameLabel">Chai</span>
                        </td>
                    </tr>

 

As you can see, the controls in the ListView are rendered using a mix of user-defined as well as auto-generated ids…this is great for ensuring the names are unique but pretty useless if you need to know the ids of the controls at render time…As an example it’s really difficult to ensure that client side code such as Javascript can easily identify a specific control within the page without mixing in the classic <%=Control.ClientId%> server side markup into the JS.

NOTE: Databound controls are more difficult than usual when trying to ensure unique control Ids you only have access to some of the container controls at design-time (e.g., ctrl0 in the above html is not accessible in the designer…so you can’t change the id without hooking into backend events).

More Control over Control Ids…ASP.NET 4.0 Client Ids

So, what are we doing in ASP.NET 4.0 to let you define the ids?

NOTE: The information below is different to the Visual Studio 10 PDC CTP VPC (love those acronyms!). Following the CTP release we refactored to remove the ‘set’ on the ClientId property…this is to improve the behavior of the API, previously the value you got back from ClientId would almost certainly not match the value you set. In the new API you set the Id parameter and can then ‘get’ the ClientId property…

IMPORTANT: The new Client Id functionality has no impact on the server side name of the control, so string myString = TextBox1.Text; is completely unaffected by the Client Id changing.

In order to enable you to have more control over Client Ids we added a of property on ‘System.Web.UI.Control’:

ClientIdMode

  • Legacy – This is exactly equivalent to the ASP.NET 2.0 Client Id behavior. This is also the default if no ClientIdMode property is set in the current control’s hierarchy.
  • Static – You set it, you get it…most controllable but potentially the least ‘safe’. If a control is set to ‘static’ ClientIdMode then exactly what you set for Id is used as the client id, no matter what naming container the control sits in.
  • Predictable – Mostly for use in DataBound controls, only uses ‘set’ Id attributes of parent Naming Containers (so, no automatic id generation using ‘ctlXXX’ names). This also works in conjunction with the DataBound control property RowClientIdSuffix to allow you to define the ‘uniquefying’ item for the specific row. Previously, the auto-generated name ctrl0…ctrl1…ctrl2…etc…was used to provide this uniqueifying function for controls in the rows of DataBound controls.
  • Inherit -  Essentially the ‘default’ behavior for controls, explicitly setting ClientIdMode = ‘Inherit’ essentially clears the ClientIdMode for the current control and allows this and any child controls (which have either ‘Inherit’ as ClientIdMode or ClientIdMode not set) will take the ClientIdMode of any parent control (including Page and Config…see below)

Page

You can also set the ClientIdMode at Page level, this defines the default ClientIdMode for all controls within the current page…

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" ClientIdMode="Static"%>

Config

It’s also possible to set the ClientIdMode in the config section at either machine or application level…this defines the default ClientIdMode for all controls within all pages in the application.

<system.web>
  <pages clientIdMode="Predictable"></pages>
</system.web>

So what can I do with that?

Restarting Control Naming

As mentioned earlier the client id for a control is derived from the NamingContainers in which the control sits in the Control Hierarchy, normally this is only the actual controls within the page (e.g., in DataBound controls), however when using MasterPages you can end up with ids as found in the following HTML:

    <div id="ctl00_ContentPlaceHolder1_ParentPanel">
        <div id="ctl00_ContentPlaceHolder1_ParentPanel_NamingPanel1">
            <input name="ctl00$ContentPlaceHolder1$ParentPanel$NamingPanel1$TextBox1" type="text" value="Hello!" id="ctl00_ContentPlaceHolder1_ParentPanel_NamingPanel1_TextBox1" />
    </div>

Even though the TextBox shown in the HTML is only within two NamingContainers within the page, due to the way MasterPages hook together you wind up with a control id like the following: ctl00_ContentPlaceHolder1_ParentPanel_NamingPanel1_TextBox1

Obviously this is a pretty long id…guaranteed unique within the page but unnecessarily long for most purposed. In this example we now want to reduce the length of the rendered id and make it more user defined (so, shortened, no ctlXXX etc…). The easiest way to achieve this is the following

<tc:NamingPanel runat="server" ID="ParentPanel" ClientIdMode="Static">
    <tc:NamingPanel runat="server" ID="NamingPanel1" ClientIdMode=”Predictable">
        <asp:TextBox ID="TextBox1" runat="server" Text="Hello!"></asp:TextBox>
    </tc:NamingPanel>
</tc:NamingPanel>

In this sample (identical to earlier markup) we’ve set the ClientIdMode to ‘Static’ on the outermost NamingPanel as well as setting the next ‘Child’ control to ‘Predictable’. This results in this markup (note, the rest of the page, MasterPages etc,…is identical to the previous example)

         <div id="ParentPanel">
        <div id="ParentPanel_NamingPanel1">
            <input name="ctl00$ContentPlaceHolder1$ParentPanel$NamingPanel1$TextBox1" type="text" value="Hello!" id="ParentPanel_NamingPanel1_TextBox1" />
    </div>

Here we’ve essentially restarted the naming hierarchy for Controls to the outermost NamingPanel , eliminating the ContentPlaceHolder and MasterPage names from the id (note: the ‘name’ attribute is unaffected…meaning we retain the normal ASP.NET functionality for events, ViewState etc…). A nice side-effect of restarting the naming hierarchy is that even if the markup defining the NamingPanels  is moved to a different ContentPlaceholder, the rendered Client Ids remain the same.

NOTE: The developer does now take more responsibility for ensuring that rendered Control Ids are unique…not doing so can break functionality which expect to find unique HTML elements for each Id (e.g., Javascript’s GetElementById()).

Predictable DataBound Client Ids

As we showed previously, the Client Ids  generated for Controls within DataBound list controls are pretty messy and not really predictable…How does the new Client Id functionality help?

We want to achieve the following:

  1. Shorten the Client Ids for Controls
  2. Make the Client Id predictable
  3. Make the Client Id unique across pages (rather than ‘within’ pages)

So, how do we do this?

<tc:NamingPanel runat="server" id="rootPanel">
      <tc:NamingPanel runat="server">        

          <asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:ConnectionString %>"
              SelectCommand="SELECT [ProductName], [ProductID] FROM [Alphabetical list of products]"></asp:SqlDataSource>
          <asp:ListView ID="ListView1" runat="server" DataSourceID="SqlDataSource1"
              onselectedindexchanged="ListView1_SelectedIndexChanged" ClientIdMode=”Predictable” RowClientIdSuffix=”ProductID”>
              <ItemTemplate>
                  <tr style="">
                      <td>
                          <asp:Label ID="ProductNameLabel" runat="server"
                              Text='<%# Eval("ProductName") %>' />
                      </td>
                  </tr>
              </ItemTemplate>
                  <LayoutTemplate>
                  <table runat="server">
                      <tr runat="server">
                          <td runat="server">
                              <table ID="itemPlaceholderContainer" runat="server" border="0" style="">
                                  <tr runat="server" style="">
                                      <th runat="server">
                                          ProductName</th>
                                  </tr>
                                  <tr ID="itemPlaceholder" runat="server">
                                  </tr>
                              </table>
                          </td>
                      </tr>
                      <tr runat="server">
                          <td runat="server" style="">
                          </td>
                      </tr>
                  </table>
              </LayoutTemplate>
          </asp:ListView>
      </tc:NamingPanel>
  </tc:NamingPanel>

In the markup above, we have used the ClientIdMode and RowClientIdSuffix properties. RowClientIdSuffix is a property which can only be used in DataBound controls and actually differs based on the DataBound control it’s used with:

GridView: You can specify the name of a column in the DataSource or multiple columns which are then combined at runtime. As an example if you specified RowClientIdSuffix as “ProductName, ProductId” in a GridView  then the rendered control Id would be "rootPanel_GridView1_ProductNameLabel_Chai_1”.

ListView: You can specify a single column in the DataSource which will be appended to the Client Id. As an example if you specified RowClientIdSuffix as “ProductName” in a ListViewthen the rendered control Id would be "rootPanel_ListView1_ProductNameLabel_1”. In this case the last ‘1’ comes from the ProductId of the DataItem.

Repeater: No RowClientIdSuffix property is allowed. In a Repeater the index of the Row is used. In the case above, you would wind up with "rootPanel_Repeater1_ProductNameLabel_0”. The ‘0’ is simply the index of the current row.

Note: FormView, DetailsView do not have multiple rows so do not have a RowClientIdSuffix property.

 

Conclusion

So, there you have it…I dare say I’ll post again in future about this topic, we have a sample app which will find it’s way onto Codeplex in the near future!

 

NOTE: In current CTP builds you cannot use UpdatePanels with controls when you use the new Client Id functionality. This is fixed internally and will work correctly in future public releases.