Hacking the ASP.NET Parser

A while ago I blogged about using external templates in an asp.net application. The problem with that was it wasn’t natively supported, so I had to create a derived SpecialRepeater in order to take advantage of the external template system.

In this latest parser hack I’ve managed to “trick” the asp.net parser into letting me declaratively set any template property to be a virtual path in order to load a user control into that template at runtime.

How it works

The ASP.NET compliation system is pretty complex. There are all kinds of extensibility points, including BuildProviders, PageParserFilters, ControlBuilders, ExpressionBuilders and the list goes on. One unknown *feature* of the parser is it’s ability to generate code from something called an InstanceDescriptor.

The parser has a special way of dealing with ITemplate properties so if we try to do this:

<asp:Repeater ID="repeater" runat="server" ItemTemplate="~/MyUserControl.ascx">
</asp:Repeater>

It fails because the there is no way to convert the string “~/MyUserControl.ascx” into an ITemplate.

The parser uses the TypeConverter attribute defined on properties it parses to aid in the conversion.  Enter TypeDescriptionProvider. These complex beasts are used at the heart of all designers in Visual Studio. There are used for things like populating the property grid, and adding and removing properties dynamically, basically a general purpose metadata API (think of it as an abstraction on top of reflection).

VirtualPathTemplate

The code we are going to generate will instantiate a VirtualPathTemplate with a virtual path pointing to a user control on our site. Normally when you define a template in markup, a special type called CompiledTemplateBuilder (which points to a delegate that builds the template at runtime) is assigned to it. We want to replace a line of code that looks like this:

repeater.ItemTemplate = new CompiledTemplateBuilder(BuildTemplate);

to this

repeater.ItemTemplate = new VirtualPathTemplate("~/MyUserControl.ascx");

TypeDescriptionProvider

After overriding about 4 classes (TypeDescriptionProvider, CustomTypeDescriptor, PropertyDescriptor, and finally TypeConverter) we are able to control what happens when the parser asks, “can we convert “~/MyUserControl.ascx” to an ITemplate?”.

Here is the code for the TemplateTypeConverter:

 

public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) {
    // Allow InstanceDescriptor so that the code gen engine can use it to generate the correct
    // code for the ITemplate property
    return destinationType == typeof(InstanceDescriptor) || _converter.CanConvertTo(context, destinationType);
}

public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) {
    var descriptorProvider = value as IInstanceDescriptorProvider;
    if (descriptorProvider != null) {
        return descriptorProvider.Descriptor;
    }
    return _converter.ConvertTo(context, culture, value, destinationType);
}

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) {
    return sourceType == typeof(string) || _converter.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) {
    string stringValue = value as string;
    
    if (stringValue != null) {
        if (stringValue.StartsWith("~/")) {                    
            // Assume this is a virtual path and return the instance description provider
            // for it
            return new VirtualPathInstanceDescriptorProvider(stringValue);
        }                
    }
    return _converter.ConvertFrom(context, culture, value);
}

 

First the parser asks the converter if it can convert from a string so we always say yes. In ConvertFrom we try to convert the value to a string and check if the path starts with “~/” in order to determine if it’s a virtual path. If it is a virtual path we return an object that knows how to get an InstanceDescriptor from the virtual path (VirtualPathInstanceDescriptorProvider). Now we have successfully parsed the control.

 

Next the code generator tries to generate code for the ITemplate property. The code generator will eventually ask if it can convert the object we returned earlier (VirtualPathInstanceDescriptorProvider) to an InstanceDescriptor. The implementation of VirtualPathInstanceDescriptorProvider returns an instance descriptor that wraps a constructor info for a custom template we are going to use:

internal class VirtualPathInstanceDescriptorProvider : IInstanceDescriptorProvider {
    private string _virtualPath;
    private static ConstructorInfo s_Constructor = GetConstructor();

    private static ConstructorInfo GetConstructor() {
        return typeof(VirtualPathTemplate).GetConstructor(new[] { typeof(string) });
    }

    public InstanceDescriptor Descriptor {
        get {
            return new InstanceDescriptor(s_Constructor, new[] { _virtualPath });
        }
    }

    public VirtualPathInstanceDescriptorProvider(string virtualPath) {            
        _virtualPath = virtualPath;
    }        
}

The code generation engine then generates the resulting code we wanted to specify above using the constructor info and virtual path.

At runtime we use BuildManager.CreateInstanceFromVirtualPath(“~/MyUserControl.ascx”) to create an instance of the user control and add it to the control’s collection and we’re done.

public class VirtualPathTemplate : ITemplate {
    private string _virtualPath;
    public VirtualPathTemplate(string virtualPath) {
        _virtualPath = virtualPath;
    }

    public void InstantiateIn(Control container) {
        // Try to create the control from the virtual path
        Control control = (Control)BuildManager.CreateInstanceFromVirtualPath(_virtualPath, typeof(Control));

        if (control != null) {
            // Add it to the controls collection
            container.Controls.Add(control);
        }            
    }
}

Caveats

In order to get the parser to use your TypeDescriptionProvider you have to make one call (which is nicely wrapped up in an API in the sample ParserConfiguration.RegisterTemplateOverride()) which registers a TypeDescriptionProvider for the control type so that it will work with any control that has an ITemplate property. If you are writing custom controls you can also specify the TypeConverter attribute and use this same trick to support this without the global override.

Also design time support is busted with this, but hey its a hack what would you expect :).

Check out the sample here.

Note: Code sample was built using vs2010 beta 2 but nothing should stop it from working on 3.5 sp1.

3 Comments

Comments have been disabled for this content.