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