mostlylucid

STATIC ARCHIVE of mostlylucid.co.uk of old
posts - 916, comments - 758, trackbacks - 11

My Links

News

Archives

Post Categories

Misc. Coding

ASP.NET 4.0: Way too much information on Control IDs and ASP.NET 4.0 Client Id Enhancements

Of 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).

 

Message from TestKing

Testking offers the featured resources for providing mcitp training , mcts courses and a+ certification materials.

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="TextBox1" runat="server" Text="Hello!"></asp:TextBox>

  </tc:NamingPanel>

</tc:NamingPanel>

results in this rendered HTML:

  <div id="Div2">

    <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="SqlDataSource2" runat="server" ConnectionString="<%$ ConnectionStrings:ConnectionString %>"

                    SelectCommand="SELECT [ProductName] FROM [Alphabetical list of products]"></asp:SqlDataSource>

                <asp:ListView ID="ListView2" 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 id="Table2" runat="server">

                            <tr id="Tr4" runat="server">

                                <td id="Td3" runat="server">

                                    <table ID="itemPlaceholderContainer" runat="server" border="0" style="">

                                        <tr id="Tr5" runat="server" style="">

                                            <th id="Th2" runat="server">

                                                ProductName</th>

                                        </tr>

                                        <tr ID="itemPlaceholder" runat="server">

                                        </tr>

                                    </table>

                                </td>

                            </tr>

                            <tr id="Tr6" runat="server">

                                <td id="Td4" 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 snippet of it…)

      <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>

      </table>

             

      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:

        • Shorten the Client Ids for Controls
          Make the Client Id predictable
          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” ClientIdRowSuffix=”ProductID”>

                      <ItemTemplate>

                          <tr style="">

                              <td>

                                  <asp:Label ID="ProductNameLabel" runat="server"

                                      Text='<%# Eval("ProductName") %>' />

                              </td>

                          </tr>

                      </ItemTemplate>

                          <LayoutTemplate>

                          <table id="Table1" runat="server">

                              <tr id="Tr1" runat="server">

                                  <td id="Td1" runat="server">

                                      <table ID="itemPlaceholderContainer" runat="server" border="0" style="">

                                          <tr id="Tr2" runat="server" style="">

                                              <th id="Th1" runat="server">

                                                  ProductName</th>

                                          </tr>

                                          <tr ID="itemPlaceholder" runat="server">

                                          </tr>

                                      </table>

                                  </td>

                              </tr>

                              <tr id="Tr3" runat="server">

                                  <td id="Td2" runat="server" style="">

                                  </td>

                              </tr>

                          </table>

                      </LayoutTemplate>

                  </asp:ListView>

              </tc:NamingPanel>

          </tc:NamingPanel>

        In the markup above, we have used the ClientIdMode and ClientIdRowSuffix properties. ClientIdRowSuffix 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 ClientIdRowSuffix 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 ClientIdRowSuffix 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 ClientIdRowSuffix 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 ClientIdRowSuffix 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!

        Matthew Osborn from our QA team has also posted on this stuff…

        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.

Print | posted on Monday, November 03, 2008 1:41 PM | Filed Under [ ASP.NET Long & Rambling ASP.NET 4.0 ]

Feedback

Gravatar

# re: Way too much information on Control IDs and ASP.NET 4.0 Client Id Enhancements

Fantastic! This will be a pleasant improvement for those of us coders who appreciate a little control over their markup. (Not to mention are accustomed to a little determinism when writing Javascript or CSS!)
11/28/2008 7:48 AM | Jordan Gray
Gravatar

# re: Way too much information on Control IDs and ASP.NET 4.0 Client Id Enhancements

Great post! Thanks for explaining this new feature in such detail. I can't wait for v4.0!
12/19/2008 2:08 PM | Kevin Babcock
Gravatar

# re: Way too much information on Control IDs and ASP.NET 4.0 Client Id Enhancements

wow. i just was wondering if there was some code that could force this to happen in 3.5, did a search and found this. I sure could use this right now in 3.5 tho...
1/20/2009 8:12 PM | boomhauer
Gravatar

# re: ASP.NET 4.0: Way too much information on Control IDs and ASP.NET 4.0 Client Id Enhancements

boomhauer: Kind of...I wrote an early prototype which works but requires extending all the existing controls; this new stuff is baked in to the Control class...
1/28/2009 5:46 PM | scott

# re: ASP.NET 4.0: Way too much information on Control IDs and ASP.NET 4.0 Client Id Enhancements

If .Net 2.0 had this ability I think I could have saved myself a lot of work! I spent about two days getting Master Pages to work with AJAX, javascript and postbacks all working as they did before I began using Master Pages. Basically I had to defeat the effects of the MasterPage and ContentPlaceHolder being NamingContainers.

I eventually got all that working by deriving my own MasterPage and ContentPlaceHolder controls and by juggling the control hierarchy during the page PreInit. I ended up writing about it and providing the source code necessary (C#).

Hope this helps. Feel free to copy the code:

www.netquarry.com/.../master-pages-ajax-and-jav...
3/3/2009 5:08 PM | Cam Woods

# re: ASP.NET 4.0: Way too much information on Control IDs and ASP.NET 4.0 Client Id Enhancements

What about the name? can you control it as well?
3/19/2009 3:40 AM | Yosii

# re: ASP.NET 4.0: Way too much information on Control IDs and ASP.NET 4.0 Client Id Enhancements

I'd been struggling with my jqueries failing when a page contains system generated ids. I don't know the mechanics behind when they get generated but they always seem to change after a recompile of an aspx page (or maybe a control).

So instead of doing something like
$("#_ctl0_some_page_control").css("font-weight","bold");

I'm using the $= function to match the last part of the id instead:
$("[ID$=_some_page_control]").css("font-weight","bold");

I've not seen a remarked decline in performance using this technique so it appears sound :)
4/29/2009 9:31 AM | Jason Salter
Gravatar

# re: ASP.NET 4.0: Way too much information on Control IDs and ASP.NET 4.0 Client Id Enhancements

Jason, yeah this technique is sound. The ID of the control will change if anything in it's naming hierarchy (naming containers above that control) change. A common way around this in JQuery is to use marker classes on the element. E.g. , <input type="text" class="identifier style", in this case both identifier and style are classes, but the identifier class is used to provide a unique name to the element. You can set these in ASP.NET by using the CssClass property of the control. The JQuery syntax would then be:
$("input[class*='identifier']"),
docs.jquery.com/.../attributeContains
4/29/2009 11:26 PM | scott
Comments have been closed on this topic.

Powered by: