ASP.NET pluggable module implementation

Matt Berther bio photo By Matt Berther Comment

For an ASP.NET project that I've been working on for quite a while, we wanted to have a pluggable module type of page architecture. We would have one main page (default.aspx) which would have a panel control which could get different controls loaded into it.

The way that we originally implemented this was to have a base control that all of our ASCX files would derive from. As detailed in a previous post, creating base classes is something I always do when creating a new project.

public class ControlBase : Control
{
    public SessionManager SessionManager
    {
        return SessionManager.Instance;
    }
}

Now, in our original implementation, we added all the modules to the web.config file.

<modules>
    <add key="myModule" value="~/path/to/myModule.ascx" />
</modules>

Modules were then loaded in the main page class like this:

protected override void OnInit(EventArgs e)
{
    base.OnInit();

    Hashtable modules = ConfigurationSettings.GetConfig("modules");
    ControlBase ctl = Page.LoadControl((string)modules[Request.QueryString["module"]]);
    myContainerPanel.Controls.Add(ctl);
}

The more I worked with this and added new controls into our framework, the more disgruntled I became with it. It always seemed like there was more to adding a new control that really should be there. I also had tied a dependency to using ASCX files for my controls, which I didnt really care for either. There are some wierd cases where you may want to have code completely render a control.

Because of this, I started to think about ways that I could improve this idea. Design patterns to the rescue...

The idea would be that a class would be responsible for creating the control, rather than the OnInit method of the page. This led to the realization that I would have some common interface to key and create a control.

public interface IControlFactory
{
    string Name { get; }
    ControlBase CreateControl();
}

Now, my derived controls have a code structure that looks something like this:

public class MyControl : ControlBase
{
    class Factory : IControlFactory
    {
        public Name { get { return "MyControl"; } }
        public ControlBase CreateControl()
        {
            Page page = (Page)HttpContext.Current.Handler;
            return (ControlBase)page.LoadControl("~/path/to/myControl.ascx");

            // note that this works with ascx files, but we could just as easily
            // do return new MyControl(); if our control is completely rendered with code.
        }
    }

    // rest of MyControl class goes here
}

The missing component now is a class that tracks these IControlFactory implementations and returns the appropriate instance based on a passed in key. Introducing the WebControlFactory class...

class WebControlFactory
{
    private static Hashtable factories;

    private WebControlFactory()
    {
    }

    public static void Register()
    {
        factories = new Hashtable();
        foreach(string file in Directory.GetFiles(HttpContext.Current.Server.MapPath("~/bin"), "*.dll"))
        {
            try
            {
                FileInfo fileInfo = new FileInfo(file);
                string assemblyPath = fileInfo.Name.Replace(fileInfo.Extension, "");
                Assembly asm = AppDomain.CurrentDomain.Load(assemblyPath);

                foreach(Type t in asm.GetTypes())
                {
                    if(!t.IsInterface &amp;&amp; !t.IsAbstract &amp;&amp; typeof(IControlFactory).IsAssignableFrom(t))
                    {
                        try
                        {
                            IControlFactory controlFactory = (IControlFactory)Activator.CreateInstance(t);
                            factories.Add(controlFactory.Name, controlFactory);
                        }
                        catch (Exception e)
                        {
                            Trace.WriteLine("Exception encountered loading type '" + t.FullName +  "': " + e.ToString());
                            // this is deliberately ignored, as any error in loading the type should just involve
                            // continuing on
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Trace.WriteLine("Exception encountered loading control: " + e.ToString());
                // this is deliberately ignored, as any error in loading the assembly should just involve
                // continuing on
            }
        }
    }

    public static ControlBase CreateWebControl(string name)
    {
        if (factories.ContainsKey(name))
        {
            IControlFactory factory = (IControlFactory)factories[name];
            return factory.CreateControl();
        }

        throw new InvalidOperationException("No factory registered to handle '" + name + "' controls.");
    }
}

Now, the final steps to implement this new method would be to add a call to WebControlFactory.Register() to the Application_Start method in Global.asax. The register method is responsible for scanning any assemblies in the bin folder, scanning their types for IControlFactory implementations, and adding them to a hashtable if they do.

The second thing to do here is to modify our OnInit method to take advantage of this new class:

protected override void OnInit(EventArgs e)
{
    base.OnInit();

    ControlBase ctl = WebControlFactory.CreateWebControl(Request.QueryString["module"]);
    myContainerPanel.Controls.Add(ctl);
}

This has solved my problems with the original implementation. I no longer am limited to ascx files for controls, and I also dont have to jump through the hoops of adding new modules to the web.config file. Using this object model, I am able to create web controls in a separate assembly, drop them in the bin folder and go.

I understand that this technique will be obsoleted by VS.NET 2005 and master pages, but for now, I think this is an elegant technique and I hope you find some value from it...