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.