Nuno Gomes /* ASP.NET Adventures */

var myInterests = new { language : "C#", technology: "ASP.NET" };

June 2008 - Posts

ASP.NET Controls - Improving automatic ID generation : The ShortIDs Naming Provider (Part 4)

In the previous posts on this subject I wrote about why automatic ID generation should be improved and how we can improve it. Now I will step forward and show you my own implementation of a specific naming provider.

As we saw in part 3, to create a specific Naming provider you only need to develop your own implementation of SetControlID method.

I named my naming provider ShortIDsProvider and it will have only one specification to meet:

  • it will create IDs in the form Txxx

where T denotes the 'T' character and xxx denotes an unique incremental integer value.

Why using the 'T' character? Well, since Control.ID must always start by an alphabetic character ... I choose on my own risk the 'T' char.

So ... here it is:

public class ShortIDsProvider : NamingProvider
{
    #region Private Fields
    private const string ID_PREFIX = "T";
    private static object KeepLongIDsAttributeValueKey = new object();
    #endregion Private Fields

    #region Public Methods
    private bool KeepOriginalID(System.Web.UI.Control control)
    {
        bool keepOriginalIDs = false;

        #region KeepLongIDs Attribute Value Management
        if (!this.KeepOriginalIDs)
        {
            if (HttpContext.Current.Items.Contains(KeepLongIDsAttributeValueKey))
            {
                keepOriginalIDs = (bool)HttpContext.Current.Items[KeepLongIDsAttributeValueKey];
            }
            else
            {
                string path = System.Web.HttpContext.Current.Request.Path.Replace(System.Web.HttpContext.Current.Request.ApplicationPath, string.Empty);

                if (this.ExceptionList != null && this.ExceptionList.Contains(path))
                {
                    keepOriginalIDs = true; ;
                }
                else
                {
                    if (control.Page != null)
                    {
                        object[] atts = control.Page.GetType().GetCustomAttributes(true);

                        foreach (Attribute att in atts)
                        {
                            if (att is KeepOriginalIDsAttribute)
                            {
                                keepOriginalIDs = ((KeepOriginalIDsAttribute)att).Enabled;
                                break;
                            }
                        }
                    }
                }
                HttpContext.Current.Items[KeepLongIDsAttributeValueKey] = keepOriginalIDs;
            }
        }
        #endregion KeepLongIDs Attribute Value Management

        return keepOriginalIDs;
    }
    #endregion Public Methods

    #region NamingProvider Implementation

    /// <summary>
    /// Generates the Control ID.
    /// </summary>
    /// <param name="name">The controls name.</param>
    /// <param name="control">The control.</param>
    /// <returns></returns>
    public override string SetControlID(string name, System.Web.UI.Control control)
    {
        if (this.KeepOriginalID(control))
        {
            return name;
        }
        if (control == null)
        {
            throw new ArgumentNullException("control");
        }
        if (control.NamingContainer == null)
        {
            return name;
        }
        NamingContainerControlCollection controlsCollection = control.NamingContainer.Controls as NamingContainerControlCollection;
        if (controlsCollection == null)
        {
            return name;
        }

        string shortid = null;
        if (!controlsCollection.ContainsName(name))
        {
            shortid = string.Format("{0}{1}", ID_PREFIX, controlsCollection.GetUniqueControlSufix());

            if (string.IsNullOrEmpty(name))
            {
                name = shortid;
            }
            controlsCollection.RegisterControl(shortid, name, control);
        }
        else
        {
            shortid = control.ID;
        }
        return shortid;
    }

    #endregion NamingProvider Implementation

}

As you can see it's not rocket science, and enable us to create any automatic id generation strategy.

Naturally this ShortIDsProvider is only valuable in conjugation with a set of improved web controls.

Making an improved web control is also very simple and straight forward. Here is the improved TextBox control:

public class TextBox : System.Web.UI.WebControls.TextBox
{
    #region Naming Management

    /// <summary>
    /// Creates a new <see cref="T:System.Web.UI.ControlCollection"></see> object to hold the child controls (both literal and server) of the server control.
    /// </summary>
    /// <returns>
    /// A <see cref="T:System.Web.UI.ControlCollection"></see> object to contain the current server control's child server controls.
    /// </returns>
    protected override ControlCollection CreateControlCollection()
    {
        return NamingConfiguration.Provider.CreateControlCollection(this);
    }

    /// <summary>
    /// Gets or sets the programmatic identifier assigned to the server control.
    /// </summary>
    /// <value></value>
    /// <returns>The programmatic identifier assigned to the control.</returns>
    public override string ID
    {
        get{ return NamingConfiguration.Provider.GetControlID(this, base.ID); }
        set { base.ID = NamingConfiguration.Provider.SetControlID(value, this); }
    }

    /// <summary>
    /// Searches the current naming container for a server control with the specified id and an integer, specified in the pathOffset parameter, which aids in the search. You should not override this version of the <see cref="Overload:System.Web.UI.Control.FindControl"></see> method.
    /// </summary>
    /// <param name="id">The identifier for the control to be found.</param>
    /// <param name="pathOffset">The number of controls up the page control hierarchy needed to reach a naming container.</param>
    /// <returns>
    /// The specified control, or null if the specified control does not exist.
    /// </returns>
    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;
    }

    /// <summary>
    /// Raises the <see cref="E:System.Web.UI.Control.Init"></see> event.
    /// </summary>
    /// <param name="e">An <see cref="T:System.EventArgs"></see> object that contains the event data.</param>
    protected override void OnInit(EventArgs e)
    {
        this.EnsureID();
        this.ID = base.ID;
        base.OnInit(e);
    }
    #endregion Naming Management

}

The last step is configuration: first to instruct asp.net to use our improved web controls instead of the usual ASP.NET controls. We do this thru TagMapping configuration, like this:

<system.web>
  <pages>
    <tagMapping>
      <add tagType="System.Web.UI.WebControls.TextBox, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
           mappedTagType="IDsSample.Controls.TextBox"/>
    </tagMapping>
  </pages>
</system.web>

And second, setting the default Naming provider. The web.config should look similar to this one:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="NamingConvention" type="IDsSample.Configuration.NamingSection"
          allowDefinition="MachineToApplication"
          restartOnExternalChanges="true" />

  </configSections>
  <appSettings/>
  <connectionStrings/>
  <system.web>
    <compilation debug="true"/>
    <authentication mode="Windows"/>

    <pages>
      <tagMapping>
        <add tagType="System.Web.UI.WebControls.TextBox, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
             mappedTagType="IDsSample.Controls.TextBox"/>
      </tagMapping>
    </pages>
  </system.web>

  <NamingConvention defaultProvider="ShortIDs">
    <providers>
      <add name="ShortIDs" type="IDsSample.Providers.ShortIDsProvider"
           exceptionlist="/Exception.aspx"
           keeporiginalids="false"/>
    </providers>
  </NamingConvention>

</configuration>

Please note that TagMapping will only work for controls declared in markup. If you need to create dynamic controls then use the DynamicControlBuilder class.

I'm currently working on a SiteKit that can be applied to any existing web application in order to increase performance by reducing the rendered html size.

While waiting for the SiteKit you can try the sample solution.

kick it on DotNetKicks.com

Posted: Jun 19 2008, 12:40 AM by nmgomes | with 7 comment(s)
Filed under:
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

Posted: Jun 04 2008, 12:51 AM by nmgomes | with 7 comment(s)
Filed under:
More Posts