UserControl Component Activation in Castle MicroKernel

Yesterday I announced to my large user base that I am working on an MVP Framework for ASP.Net.  I gave a little preview regarding the View and Presenter interfaces as well as an introduction to some of the internal plumbing of the Castle MicroKernel dependency injection framework.  Today I want to continue that trend and talk about how the Kernel will activate (instantiate) the web UserControl components.

When a component is initally added to the Kernel, meta data is collected on the constuctors, properties, attributes, and methods that make up the component.  An additional piece of information gathered is the class responsible for intantiating that component when an instance is requested from the Kernel.  The MicroKernel comes with a DefaultComponentActivator that eventually calls Activator.CreateInstance, supplying a "best guess" of constructor arguments based on other components loaded in the Kernel. 

Activator.CreateInstance won't do us much good in the context of creating an instance of a UserControl.  ASP.Net developers who dynamically load UserControls at runtime are familiar with Page.LoadControl.  In ASP.Net 1.1, LoadControl took a single argument, the path to the .ascx file for the UserControl.  LoadControl would then perform some magic under the covers, checking for a cached version, parsing and compiling the .ascx markup, then returning a Control object that was ready for you to include in your page.  ASP.Net 2.0 adds an overload to LoadControl that allows you to pass the Type of control you want to load along with any constructor arguments.  The only catch with the overloaded method is that the UserControl must be pre-compiled.  If the control is not pre-compiled, you will get an instance of the UserControl, but anything declared in markup in the .ascx portion of the control will be null and usesless e.g. TextBoxes, Labels, etc.  I addressed the precompilation issues in my previous post "Dependency Injection and UserControls with Castle MicroKernel".  To address the LoadControl issue I created the WebUserControlComponentActivator.

My custom activator is based on the DefaultComponentActivator so using it will still resolve Property based dependencies as well as constructor dependencies.  The main thrust of the activator is to get a reference to the currently executing Page and then call LoadControl passing the type of the component to be created.  The complete listing for WebUserControlComponentActivator is below:

using System;
using System.Web;
using System.Web.UI;
 
using Castle.Model;
using Castle.MicroKernel;
using Castle.MicroKernel.ComponentActivator;
 
namespace WCPierce.MicroKernel.ComponentActivator
{
  [Serializable]
  public class WebUserControlComonentActivator : DefaultComponentActivator
  {
    public WebUserControlComonentActivator(ComponentModel model, IKernel kernel, ComponentInstanceDelegate onCreation, ComponentInstanceDelegate onDestruction) 
      : base(model, kernel, onCreation, onDestruction)
    {
    }
 
    protected override object CreateInstance(object[] arguments, Type[] signature)
    {
      object instance;
 
      Type implType = Model.Implementation;
 
      if (Model.Interceptors.HasInterceptors)
      {
        try
        {
          instance = Kernel.ProxyFactory.Create(Kernel, Model, arguments);
        }
        catch (Exception ex)
        {
          throw new ComponentActivatorException("WebUserControlComponentActivator: could not proxy " + Model.Implementation.FullName, ex);
        }
      }
      else
      {
        try
        {
          HttpContext currentContext = HttpContext.Current;
          if (currentContext == null)
          {
            throw new InvalidOperationException("System.Web.HttpContext.Current is null.  WebUserControlComponentActivator can only be used in an ASP.Net environment.");
          }
 
          Page currentPage = currentContext.Handler as Page;
          if (currentPage == null)
          {
            throw new InvalidOperationException("System.Web.HttpContext.Current.Handler is not of type System.Web.UI.Page");
          }
 
          instance = currentPage.LoadControl(implType, arguments);
        }
        catch (Exception ex)
        {
          throw new ComponentActivatorException("WebUserControlComponentActivator: could not instantiate " + Model.Implementation.FullName, ex);
        }
      }
 
      return instance;
    }
  }
}

The question you all are asking is how do I tell the Kernel that I want to use the WebUserControlComponentActivator to activate all the UserControl components I put in the Kernel?  That's an excellent question and you did the right thing by calling.  I needed to create two more pieces to finish this little puzzle.

First, the ComponentActivatorInspector, allows you to specify an additional attribute ("componentActivatorType") on components when creating the .config file for your Kernel. 

<component id="Dashboard"
  lifestyle="transient"
  service="BatchTracker.Interfaces.IDashboardView, BatchTracker.Interfaces"
  type="BatchTracker.Views.DashboardView, BatchTracker.Views"
  componentActivatorType="WCPierce.MicroKernel.ComponentActivator.WebUserControlComonentActivator, WCPierce.MVP.Framework"
/>

If config files are not your style, you can also use a custom class attribute to specify which component activiator to use when you code up your component.

using System;
using BatchTracker.Interfaces;
using Castle.Model;
using WCPierce.MicroKernel.ComponentActivator;
using WCPierce.Model;
using WCPierce.MVP.Framework;
 
namespace BatchTracker.Views
{
  [Transient]
  [ComponentActivator(typeof(WebUserControlComonentActivator))]
  public partial class DashboardView : ViewBase<IDashboardPresenter>, IDashboardView
  {
  }
}

The code for the ComponentActivator attribute is trivial, if you really want to see it let me know and I will email it to you.  The code for the ComponentActivatorInspector is based direcly on the LifestyleModelInspector performing a very similar function.  First, the code looks to the configuration information, if any.  If no configuration information is found we then look for the ComponentActivator attribute.

using System;
using System.Configuration;
 
using Castle.Model;
using Castle.MicroKernel;
using Castle.MicroKernel.ModelBuilder;
 
using WCPierce.Model;
 
namespace WCPierce.MicroKernel.ModelBuilder.Inspectors
{
  [Serializable]
  public class ComponentActivatorInspector : IContributeComponentModelConstruction
  {
    #region IContributeComponentModelConstruction Members
 
    public void ProcessModel(IKernel kernel, ComponentModel model)
    {
      if (!ReadComponentActivatorFromConfiguration(model))
      {
        ReadComponentActivatorFromType(model);
      }
    }
 
    protected virtual bool ReadComponentActivatorFromConfiguration(ComponentModel model)
    {
      if (model.Configuration != null)
      {
        string componentActivatorType = model.Configuration.Attributes["componentActivatorType"];
 
        if (componentActivatorType != null)
        {
          try
          {
            Type customComponentActivator = Type.GetType(componentActivatorType, true, false);
 
            this.ValidateComponentActivator(customComponentActivator);
 
            model.CustomComponentActivator = customComponentActivator;
          }
          catch (Exception ex)
          {
            string message = String.Format("The Type '{0}' specified  in the componentActivatorType attribute could not be loaded.", componentActivatorType);
            throw new ConfigurationErrorsException(message, ex);
          }
 
          return true;
        }
      }
 
      return false;
    }
 
    protected virtual void ReadComponentActivatorFromType(ComponentModel model)
    {
      object[] attributes = model.Implementation.GetCustomAttributes(typeof(ComponentActivatorAttribute), true);
 
      if (attributes.Length != 0)
      {
        ComponentActivatorAttribute attribute = (ComponentActivatorAttribute)attributes[0];
 
        this.ValidateComponentActivator(attribute.ComponentActivatorType);
 
        model.CustomComponentActivator = attribute.ComponentActivatorType;
      }
    }
 
    private void ValidateComponentActivator(Type customComponentActivator)
    {
      if (!typeof(IComponentActivator).IsAssignableFrom(customComponentActivator))
      {
        string message = String.Format("The Type '{0}' specified  in the componentActivatorType attribute must implement Castle.MicroKernel.IComponentActivator", customComponentActivator.FullName);
        throw new InvalidOperationException(message);
      }
    }
 
    #endregion
  }
}
One more problem solved on the way to our MVP Framework.

posted @ Friday, August 18, 2006 4:59 PM


Print

Comments on this entry:

# re: UserControl Component Activation in Castle MicroKernel

Left by paul landers at 12/20/2006 1:00 PM

Can you please provide an working example?<br /><br />I can't get a reference to my ascx-assembly (tried it via web.config) and so Windsor/Microkernel is unable to resolve the service...

# re: UserControl Component Activation in Castle MicroKernel

Left by Bill Pierce at 12/24/2006 2:48 PM

Hey Paul,<br />I posted an updated example here:<br />http://blogs.meetandplay.com/WPierce/archive/2006/12/24/Revisited_MVP_Framework.aspx<br /><br />Enjoy,<br />-Bill

# re: Would like to help you with GMap control

Left by William SerGio at 9/14/2006 11:30 AM
Gravatar

I am trying to create create a SQL Sever database for zip code boundaries and I would be glad to make that database and the code I create to convert shapefile to pairs of latitude and longitude available to you for your GMap project.<br />I want to write a program that will convert the data from the census bureau at:<br />http://www.census.gov/geo/www/cob/z52000.html#shp<br /><br />and convert it into a SQL Server table that can be used with your control to create zip code boundaries. The only reference I have found so far on converting shape files into text is at:<br />http://googlemapsbook.com/2006/08/30/shapefiles/<br /><br />I was wondering if you had any other references on the easiest way to get simple lat/long pairs into a table like the table you used for county boundaries?<br />

# re: UserControl Component Activation in Castle MicroKernel

Left by Matt at 2/15/2007 11:53 PM
Gravatar

Bill,<br /><br />I am porting a MVP pattern using castle windsor IOC fron nHibernmate to a webservice and I'm not sure where to start configuring the container to consume a webservice, have you done this or know of an example of how to configure this? btw I love your blog.<br /><br />cheers<br /><br />Matt

Your comment:



 (will not be displayed)


 
 
 
Please add 7 and 7 and type the answer here:
 

Live Comment Preview:

 
«August»
SunMonTueWedThuFriSat
272829303112
3456789
10111213141516
17181920212223
24252627282930
31123456