ASP.NET Controls - Improving automatic ID generation : Architectural Changes ( Part 3)

Naming container controls are a subclass of standard controls, that differ in the ability to manage child controls' ID, in fact, these naming container controls are the key to unique ID generation. To become a namingcontainer a regular control must implement the INamingContainer interface.

In order to override ASP.NET ID generation we will have to work in two fronts:

  • override regular controls' behavior to decouple the Control.UniqueID property from the Control.ID property
  • override naming container controls to allow us to control how ID generation is done and how to find a control 

To decouple the Control.UniqueID property from the Control.ID property without changing the ASP.NET paradigm I chose to set the Control.ID property base value with a computed value and map it to the original ID value. Naturally, this computed value is composed by its Control.NamingContainer.

Since settings the value of Control.ID has changed, getting its value must change to act accordingly.

public override string ID
{
    get { return NamingConfiguration.Provider.GetControlID(this, base.ID); }
    set { base.ID = NamingConfiguration.Provider.SetControlID(value, this); }
}

As you can notice, there's a provider working here, but the key that makes this work is:

protected override void OnInit(EventArgs e)
{
    this.EnsureID();
    this.ID = base.ID;
    base.OnInit(e);
}

This little code is used to ensure the correct initialization of ID mapping, i.e, that the initial value of Control.ID is generated from value set on markup, if present.

In the naming container control I want to change the way child controls' IDs are managed, so I decided to create a new ControlCollection that will aggregate all child control ID management logic.

protected override ControlCollection CreateControlCollection()
{
    return NamingConfiguration.Provider.CreateControlCollection(this);
}

Naturally, if the ControlCollection changes, the FindControl method needs to change too.

protected override Control FindControl(string id, int pathOffset)
{
    Control ctrl = base.FindControl(id, pathOffset);
    if (ctrl == null)
    {
        ctrl = NamingConfiguration.Provider.FindControl(this, id, pathOffset);
    }
    return ctrl;
}

What we can see here is that FindControl method can find controls either by computed Id (for internal purposes) or by control name (for human purposes).

These are the only members that  any control needs to override. The implementation details are left to the NamingProvider and NamingContainerControlCollection classes.

The NamingProvider

The NamingProvider class has methods, one of those methods is abstract and its goal is to compose/generate an automatic Id.

public abstract string SetControlID(string name, System.Web.UI.Control control);

The other methods are:

/// <summary>
/// Creates a controls collection.
/// </summary>
/// <param name="control">The owner control.</param>
/// <returns></returns>
public ControlCollection CreateControlCollection(System.Web.UI.Control control)
{
    return new NamingContainerControlCollection(control);
}

/// <summary>
/// Gets the control name given the control's id.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="id">The controls id</param>
/// <returns></returns>
public string GetControlID(System.Web.UI.Control control, string id)
{
    if (control == null)
    {
        throw new ArgumentNullException("control");
    }
    if (control.NamingContainer != null)
    {
        NamingContainerControlCollection namingContainerCollection = control.NamingContainer.Controls as NamingContainerControlCollection;
        if (namingContainerCollection != null)
        {
            return namingContainerCollection.GetName(id);
        }
    }
    return id;
}

/// <summary>
/// Finds a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="id">The id.</param>
/// <param name="pathOffset">The path offset.</param>
/// <returns></returns>
public Control FindControl(System.Web.UI.Control control, string id, int pathOffset)
{
    if (control == null)
    {
        throw new ArgumentNullException("control");
    }

    NamingContainerControlCollection controlsCollection = control.Controls as NamingContainerControlCollection;
    if (controlsCollection == null)
    {
        return null;
    }

    return controlsCollection.FindControl(id, pathOffset);
}

This provider model also allows us to control when to keep original IDs by setting the following attributes in provider configuration:

  • keeporiginalids - specifies whether to keep original IDs
  • exceptionlist - set a list of pages that will allways render the original IDs ( comma separated )

Now, the only core entity that's missing is NamingContainerControlCollection.

The NamingContainerControlCollection

The NamingContainerControlCollection class extends the  ControlCollection class and manages child controls' names and IDs. This is done by using two collections:

  • Dictionary<string, string> m_linkDictionary - to provide the link between the Id and Name;
  • Dictionary<string, System.Web.UI.Control> m_nameDictionary - to provide a collection of controls by name.

The following methods are also added:

/// <summary>
/// Determines whether a control is in the <see cref="NamingContainerControlCollection"></see>.
/// </summary>
/// <param name="name">The controls name.</param>
/// <returns>
///     <c>true</c> if a control is found; otherwise, <c>false</c>.
/// </returns>
public bool ContainsName(string name)
{
    if (string.IsNullOrEmpty(name))
    {
        return false;
    }
    return m_nameDictionary.ContainsKey(name);
}

/// <summary>
/// Gets the control name given the control id.
/// </summary>
/// <param name="id">The control's name.</param>
/// <returns></returns>
public string GetName(string id)
{
    if (string.IsNullOrEmpty(id))
    {
        return id;
    }
    if (ContainsID(id))
    {
        string name = m_linkDictionary[id];
        //if (baseid == shortid)
        //{
        //    return null;
        //}
        return name;
    }
    return id;
}

/// <summary>
/// Registers the pair [name, control] and link id to name.
/// </summary>
/// <param name="id">The id value.</param>
/// <param name="name">The name value.</param>
/// <param name="control">The control.</param>
public void RegisterControl(string id, string name, Control control)
{
    m_nameDictionary.Add(name, control);
    m_linkDictionary.Add(id, name);
}

With all this changes in place and ready to be used we only need to create a specific NamingProvider, in fact, we only need to implement the GenerateAutomaticID method, but that ... will came soon.

kick it on DotNetKicks.com

6 Comments

  • I'm eager for the rest... Keep up the good work.

  • hello

    is there a way to preventing Generation of Client id in Server side controls to improve of loading of page?

  • Hi karami,

    the Control.ClientID is only render when Control.ID is not null. In order to set the internal _id to null you must Control.ID = string.Empty (it's strange but its true).

    But remember, by doing this you wont be able to use the document.getElementByID to find the rendered html element.

  • It looks like some methods and properties are missing between your classes defined in this part 3 and part 4. Perhaps you could add them to your post, or provide a zip download of the whole thing that builds?

    Thanks.

  • Pretty section of content. I just stumbled upon your site and in accession capital to assert that I acquire in fact enjoyed account your blog posts. Anyway I will be subscribing to your feeds and even I achievement you access consistently rapidly.

  • Asp net controls improving automatic id generation architectural changes part 3.. Awful :)

Comments have been disabled for this content.