ASP.NET Web Forms Extensibility: Control Builders
One of the most often ignored extensibility point in Web Forms is the Control Builder. Control Builders are subclasses of ControlBuilder (or other more specialized, such as FileLevelPageControlBuilder, for pages, or FileLevelMasterPageControlBuilder, for master pages) that can be specified per class. It controls some aspects of a control instance:
-
Whether or not it allows blank spaces inside its declaration (AllowWhitespaceLiterals);
-
If a closing tag is required (HasBody);
-
What is the type of its markup-declared child controls (GetChildControlType);
-
If the control supports code blocks (HasAspCode);
-
The type of the control’s binding container (BindingContainerType) and item (ItemType) (ASP.NET 4.5 only);
-
Etc;
It also allows overriding a couple of things:
-
The parameters specified in the markup (Init);
-
What to do when the control builder is added to a parent control builder (OnAppendToParentBuilder);
-
Modify the code that will be generated in the code-behind class that is produced by ASP.NET or the code that will be used to instantiate the control (ProcessGeneratedCode);
-
Change the tag’s inner textual content (SetTagInnerText);
-
Etc.
This is a powerful mechanism, which has even been used to allow generic control classes. We apply a control builder through a ControlBuilderAttribute (for regular controls) or FileLevelControlBuilderAttribute for pages, master pages or user controls.
I won’t go into many details, but instead I will focus on the Init and ProcessGeneratedCode methods.
Init let’s us do things such as:
public override void Init(TemplateParser parser, ControlBuilder parentBuilder, Type type, String tagName, String id, IDictionary attribs)
{
if (type == typeof(SomeBaseControl)
{
//replace the control's type for another one
type = typeof(SomeDerivedControl);
//convert an hypothetical Text property value to upper case
attribs["Text"] = (attribs["Text"] as String).ToUpper();
}
base.Init(parser, parentBuilder, type, tagName, id, attribs);
}
And ProcessGeneratedCode, messing with the generated page class:
public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
{
//add some interface to the generated page class
derivedType.BaseTypes.Add(typeof(ISomeInterface));
//add a property implementation to the generated page class
var prop = new CodeMemberProperty();
prop.Attributes = MemberAttributes.Public;
prop.Name = "SomeProperty";
prop.Type = new CodeTypeReference(typeof(String));
prop.GetStatements.Add(new CodeMethodReturnStatement(new CodePrimitiveExpression("Hello, World, from a generated property!")));
derivedType.Members.Add(prop);
base.ProcessGeneratedCode(codeCompileUnit, baseType, derivedType, buildMethod, dataBindingMethod);
}
But also something MUCH more fun! Imagine you are using an IoC container – I will use Unity, but you can use whatever you want. We might have something like this in Application_Start (or whatever method spawned from it);
var unity = new UnityContainer();
unity.RegisterInstance<MyControl>(new MyControl { Text = "Bla bla" });
ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(unity));
Notice I am using the Common Service Locator to abstract the IoC container and to make the code independent of it. Here, I am assigning a static instance to the MyControl type, in essence, a singleton.
Now, we can change our control builder so as to have the control build method return this instance:
public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
{
//return ServiceLocator.Current.GetInstance(typeof(MyControl));
var type = Type.GetType((buildMethod.Statements[0] as CodeVariableDeclarationStatement).Type.BaseType);
var currentProperty = new CodePropertyReferenceExpression(new CodeTypeReferenceExpression(typeof (ServiceLocator)), "Current");
var getInstance = new CodeMethodInvokeExpression(currentProperty, "GetInstance", new CodeTypeOfExpression(type));
var @cast = new CodeCastExpression(type, getInstance);
var @return = new CodeMethodReturnStatement(@cast);
buildMethod.Statements.Clear();
buildMethod.Statements.Add(@return);
base.ProcessGeneratedCode(codeCompileUnit, baseType, derivedType, buildMethod, dataBindingMethod);
}
In case you didn’t notice, what this does is, every time the MyControl control is instantiated in a page, for every request, ASP.NET will always return the same instance!
Now, I am not saying that you SHOULD do this, but only that you CAN do this!
Take care out there…