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

[more]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 SetControlID method, but that … will came soon.

ASP.NET – Dynamic Control Mapping

I already posted here about Tag Mapping and how helpful it can be, but naturally there’s are a few improvements that I would like to see available in future framework release.

The one I expect the most is about the capability of mapping dynamic created controls using the same rules as Tag Mapping uses when interpreting the page markup.

Without this capability we can never use widely the tag mapping because whenever we need to create dynamic controls they will be strongly coupled to a specific control implementation.

Imagine this scenario:

  1. First you have built an web application that use standard ASP.NET TextBox  control, some of them dynamically created.
  2. Now, imagine that you want to reuse that application, as is, but instead of ASP.NET Textbox control you want to use your own Textbox implementation.

This task could be easily accomplished using Tag Mapping if no dynamic controls were used, but in this scenario ASP.NET give us no solution, so the application cannot be reused without modifications.

Naturally, you can copy/paste your application and make the necessary changes, or even add a few if statements, but that will only increase complexity and maintenance effort.

Until the .NET team provide us such capability we must do the magic ourselves.

My proposal is an help class (DynamicControlBuilder) that provide us two methods: GetMappedType and CreateControl.


/// <summary>
/// Gets the mapped <see cref="System.Web.UI.Control"/> type.
/// </summary>
/// <param name="type">The <see cref="System.Web.UI.Control"/> type to be mapped</param>
/// <param name="prefix">The namespace prefix.</param>
/// <returns>A <see cref="System.Type"/> object.</returns>
public static Type GetMappedType(Type type, string prefix)
{
    if (!typeof(Control).IsAssignableFrom(type))
    {
		throw new ArgumentOutOfRangeException("type", "Must inherit from Control.");
	}
    Type mappedtype;
    if (!string.IsNullOrEmpty(prefix))
    {
		TagPrefixInfo prefixinfo;
        if (!m_prefixes.TryGetValue(prefix, out prefixinfo))
        {
			throw new ArgumentException("prefix", "No prefix found.");
		}
        else
        {
			type = BuildManager.GetType(string.Format("{0}.{1}, {2}", prefixinfo.Namespace, type.UnderlyingSystemType.Name, prefixinfo.Assembly), false);
            if (type == null)
            {
				throw new ArgumentException("type", "Control not found within specified prefix.");
			}
		}
	}
	if (m_tagMappings.TryGetValue(type.UnderlyingSystemType, out mappedtype))
	{
		return mappedtype;
	}
    return type;
}
/// <summary>
/// Creates a dynamic mapped <see cref="System.Web.UI.Control"/>.
/// </summary>
/// <param name="type">The <see cref="System.Web.UI.Control"/> type to be mapped</param>
/// <param name="prefix">The namespace prefix.</param>
/// <returns>A <paramref name="T"/> object.</returns>
public static Control CreateControl(Type type, string prefix)
{
    Type mappedType = GetMappedType(type, prefix);
    return (Control)Activator.CreateInstance(mappedType);
}

The main goal is to enable any of the following usages:


this.Page.Controls.Add(DynamicControlBuilder.CreateControl("foo"));
this.Page.Controls.Add(DynamicControlBuilder.CreateControl(typeof(System.Web.UI.WebControls.TextBox), "foo"));
this.Page.Controls.Add(DynamicControlBuilder.CreateControl(typeof(System.Web.UI.WebControls.TextBox)));

Try it !

DynamicControlBuilder.cs