Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating an HTML Attribute Plug-in Framework

0.00/5 (No votes)
16 May 2007 1  
This article explains how to create your own HTML attributes using a plug-in framework.

Screenshot - MainImage.jpg

Introduction

This article is based on that of Javier Lozano. A portion of this code is from his article, and is reproduced with his express permission.

When I first read the article "Extending ASP.NET Web Controls With Custom HTML Attributes", I immediately saw the possible applications of this technology and started to play around with the code. I liked the way that it worked, and it solved a number of problems for me. One thing that bothered me though was this: Extensibility.

So naturally, I set out to turn this concept into a plug-in framework. This article is the result of that effort.

The rundown

How the work gets done

This code works as follows:

  1. We create a base page from which the pages in our application will inherit. This allows us to hook into the page lifecycle by overriding the appropriate events. This base page itself must inherit from System.Web.UI.Page.

    /// <summary>
    /// This is our BasePage, for our plugins to be applied,
    /// the page containing them need to inherit from this one.
    /// This page extends System.Web.UI.Page
    /// </summary>
    public abstract class BasePage : System.Web.UI.Page
  2. We then override the method System.Web.UI.Page.Render(HtmlTextWriter writer) in our base page. This means that we can grab our controls out of the page at the last minute before they are rendered to the browser. This is important because otherwise we will miss the custom attributes of controls on any dynamically added web user controls. The code below simply iterates through the server side controls on our page (both HTML and Web controls):
  3. /// <summary>
    /// The Render method is overrides our System.Web.UI.Page.Render(HtmlTextWriter writer)
    /// method, and is where we intercept the controls on our page.
    /// </summary>
    /// <param name="writer">Stream that writes to output page.</param>
    protected override void Render(HtmlTextWriter writer)
    {
        //********************************************************
        // The original version of this code overrode
        // System.Web.UI.Page.CreateChildControls().
        // I found that using this method occured
        // before custom web user controls and their
        // control collections were added to the page,
        // thus I opted to override 
        // System.Web.UI.Page.Render(HtmlTextWriter writer)
        // instead, as it get called after
        // our custom web user controls are added to the page.
        //********************************************************
        foreach(Control c in Controls)
        {
            if (c is HtmlForm)
            {
                ProcessControls((c as HtmlForm).Controls);
            }
        }
        base.Render (writer);
    }
  4. These controls are handed off to the ProcessControls(ControlCollection c) method that recursively scans all controls.
  5. /// <summary>
    /// This method checks if we are deealing with an HtmlControl
    /// or a WebControl and passes the control to the relevant method
    /// for processing.
    /// </summary>
    /// <param name="c">Our server sidee forms contrtol collection.</param>
    private void ProcessControls(ControlCollection c)        
    {
        foreach (Control cc in c)
        {
            if (cc is WebControl)
            {
                AttributeParser.ProcessControl(cc as WebControl);
            }
            if(cc is HtmlControl)
            {
                AttributeParser.ProcessControl(cc as HtmlControl);
            }
            if(cc.HasControls())
            {
                ProcessControls(cc.Controls);
            }
        }
    }
  6. The AttributeParser class has an overloaded ProcessControl method that takes either a Web or an HTML control as an argument. This method then checks our list of custom attributes and if the current attribute is in that list, passes it off to the relevant plug-in's Apply method. Since this method is overloaded, the two implementations of it pass their abstracted data to our ProcessProperty(string attributeValue, PropertyInfo pi, Control c) method where the actual processing of the attribute takes place.

How the plug-ins get loaded

  1. Our PluginFramework.dll contains the following class:
  2. /// <summary>
    /// This class scans our bin directory
    /// for plugins of type PluginFramework.IAttribute.
    /// This scan only happens once on Application_Start.
    /// </summary>
    public sealed class AttributeFactory
  3. This class possesses a static constructor that scans our bin folder for plug-ins of type PluginFramework.IAttribute.
  4. /// <summary>
    /// Scan for and record plugins and their attribute names.
    /// This scan only happens once on Application_Start.
    /// </summary>
    static AttributeFactory()
    {
        //***********************************************************
        // This is not the most processor friendly way to do this!!
        // This section could load plugins from a file or db.
        //***********************************************************
        string[] files = Directory.GetFiles(Path.GetDirectoryName(
          Assembly.GetExecutingAssembly().CodeBase).Replace("file:\\", ""));
        foreach(string s in files)
        {
            if(Path.GetExtension(s).ToLower() == ".dll")
            {
                Assembly asm                      = Assembly.LoadFile(s);
                Type interfaceType                = null;
                int interfaceCount                = 0;
                foreach(Type t in asm.GetTypes())
                {
                    Type iType = t.GetInterface("IAttribute");
                    if(iType != null)
                    {
                        interfaceType    = t;
                        interfaceCount++;
                    }
                }
    
                if(interfaceType == null)
                {
                    Debug.Write("Interface not found in plugin - " + asm.FullName);
                }
                else if(interfaceCount > 1)
                {
                    Debug.Write("More that one interface found - " + asm.FullName);
                }
                else
                {
                    IAttribute myAttrib             = 
                          (IAttribute)Activator.CreateInstance(interfaceType);
                    MethodInfo InvokeMethodInfo     = 
                           interfaceType.GetMethod("GetIdentifier");
                    ParameterInfo[] InvokeParamInfo = 
                           InvokeMethodInfo.GetParameters();
                    string identifier               = 
                           InvokeMethodInfo.Invoke(myAttrib, 
                           InvokeParamInfo).ToString().ToLower();
                    asmLocations.Add(identifier, myAttrib);
                }
            }
        }
        //***********************************************************
    }
  5. Our plug-ins are then stored in a Hashtable along with their attribute names.
  6. /// <summary>
    /// This hashtablecontains all of our plugins and their attribute name.
    /// </summary>
    private static Hashtable asmLocations   = new Hashtable();
  7. Our GetAttribute(string type) method is then responsible for providing the correct plug-in for our attribute.

Points of interest

A crucial point to make is that our plug-ins communicate through the following interface:

using System.Reflection;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;

namespace PluginFramework
{
    /// <summary>
    /// This is the interface that our plugins must adhere
    /// to in ordedr to be able to make use of our framework.
    /// </summary>
    public interface IAttribute
    {
        /// <summary>
        /// This method gets the plugin identifier.
        /// This identifier becomes the recognised attribute name,
        /// and as such should remain unique.
        /// </summary>
        /// <returns>String - The plugin identifier</returns>
        string GetIdentifier();

        /// <summary>
        /// This method gets the property info for our control and
        /// passes it of to ProcessProperty(string attributeValue, PropertyInfo pi, Control c)
        /// for processing.
        /// </summary>
        /// <param name="attributeValue">The value of our attribute.</param>
        /// <param name="wc">Our WebControl</param>
        void Apply(string attributeValue, WebControl wc);

        /// <summary>
        /// This method gets the property info for our control and
        /// passes it of to ProcessProperty(string attributeValue, PropertyInfo pi, Control c)
        /// for processing.
        /// </summary>
        /// <param name="attributeValue">The value of our attribute.</param>
        /// <param name="wc">Our HtmlControl.</param>
        void Apply(string attributeValue, HtmlControl wc);

        /// <summary>
        /// This method performs the logic necessary to do the actuall work
        /// that is required by our plugin.
        /// </summary>
        /// <param name="attributeValue">The value of our attribute.</param>
        /// <param name="pi">Our PropertyInfo.</param>
        /// <param name="c">Our Control.</param>
        void ProcessProperty(string attributeValue, PropertyInfo pi, Control c);
    }
}

The GetIdentifier() method is responsible for returning a unique identifier/name for the plug-in. This identifier is what will be used as the attribute name. I.e.:

/// <summary>
/// This method gets the plugin identifier.
/// This identifier becomes the recognised attribute name,
/// and as such should remain unique.
/// </summary>
/// <returns>String - The plugin identifier</returns>
public string GetIdentifier()
{
    return "Translate";
}

History

  • 2007/01/26 - Article created.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here