Microsoft .NET MVC 4 Framework: How to Add a Custom Attribute to <%@ Page %> Directive of a Generic ViewPage Class

In Windows Forms .NET projects, adding a custom attribute to the <%@ Page %> directive is easy – all you have to do is simply create your base class that inherits from the System.Web.UI.Page class and add a public property like this:

using System.Web.UI;

namespace My.Namespace
{
	public class MyBasePage : Page
	{
		public string CustomAttribute { get; set; }
	}
}

and then inherit your page from that class:

<%@ Page Language="C#" AutoEventWireup="true" Inherits="My.Namespace.MyBasePage" CustomAttribute="Hello World!" %>

When you create a .NET MVC project, you would think it should be as simple as above. In MVC you do not get pages, you get views which are inherited from the System.Web.Mvc.ViewPage class. So, should be easy, right? Use the same logic and there is your attribute:

using System.Web.Mvc;

namespace My.Namespace
{
	public class MyBaseViewPage : ViewPage
	{
		public string CustomAttribute { get; set; }
	}

	public class MyBaseViewPage<TModel> : MyBaseViewPage where TModel : class
	{
	}
}

And then, in your view, you would simply add the attribute to the <%@ Page %> directive that references your MyBaseViewPage class and uses the MyModel class as a model:

<%@ Page Language="C#" Inherits="My.Namespace.ViewPage<My.Namespace.Models.MyModel>" CustomAttribute="Hello World!" %>

Wait… – Surprise! You get the yellow screen of death notifying you that there was an error:

Error parsing attribute 'customattribute': Type 'System.Web.Mvc.ViewPage' does not have a public property named 'customattribute'

So, what’s the deal here? – MVC works differently than from Windows Forms and the culprit here is that the MVC parser assumes that your view is going to be inheriting from System.Web.Mvc.ViewPage, therefore when it instantiates the view it is not aware of any extra properties you put in there in your MyBaseViewPage class. Ok, let me count how many times I said Oops!. Wait, I’ll be too rich!

How does one solve this problem, then? – the solution is far from apparent but fairly easy to implement, less the part that you do have to write rather more code than when using Windows Forms. The good part is that these are slight modifications to the open source MVC code from Microsoft. If you are interested, you can download it here: http://aspnet.codeplex.com/releases/view/58781 – I am using MVC 4 but the source code is not available yet. The bad part is, of course, that as their MVC code evolves and new versions come out, you’ll have to keep tabs on it and make sure your custom code is up to date.

The first class we will need to create is a subclass of ViewTypeParserFilter – the class responsible for handling what kind of view is being parsed. Here is the original class from MVC 3:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Web.UI;

namespace com.luefher.arms.Web.Mvc
{
	internal class ArmsViewTypeParserFilter : PageParserFilter
	{

		private static Dictionary _directiveBaseTypeMappings = new Dictionary {
            { "page",    typeof(ArmsViewPage)        },
            { "control", typeof(ArmsViewUserControl) },
            { "master",  typeof(ArmsViewMasterPage)  },
        };

		private string _inherits;

		[SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "System.Web.Mvc is SecurityTransparent and requires medium trust to run, so this downstream link demand is fine")]
		public ArmsViewTypeParserFilter()
		{
		}

		[SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "System.Web.Mvc is SecurityTransparent and requires medium trust to run, so this downstream link demand is fine")]
		public override void PreprocessDirective(string directiveName, IDictionary attributes)
		{
			base.PreprocessDirective(directiveName, attributes);

			Type baseType;
			if (_directiveBaseTypeMappings.TryGetValue(directiveName, out baseType))
			{
				string inheritsAttribute = attributes["inherits"] as string;

				// Since the ASP.NET page parser doesn't understand native generic syntax, we
				// need to swap out whatever the user provided with the default base type for
				// the given directive (page vs. control vs. master). We stash the old value
				// and swap it back in inside the control builder. Our "is this generic?"
				// check here really only works for C# and VB.NET, since we're checking for
				// < or ( in the type name.
				//
				// We only change generic directives, because doing so breaks back-compat
				// for property setters on @Page, @Control, and @Master directives. The user
				// can work around this breaking behavior by using a non-generic inherits
				// directive, or by using the CLR syntax for generic type names.

				if (inheritsAttribute != null && inheritsAttribute.IndexOfAny(new[] { '<', '(' }) > 0)
				{
					attributes["inherits"] = baseType.FullName;
					_inherits = inheritsAttribute;
				}
			}
		}

		//[SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "System.Web.Mvc is SecurityTransparent and requires medium trust to run, so this downstream link demand is fine")]
		//public override void ParseComplete(ControlBuilder rootBuilder)
		//{
		//    base.ParseComplete(rootBuilder);

		//    IMvcControlBuilder builder = rootBuilder as IMvcControlBuilder;
		//    if (builder != null)
		//    {
		//        builder.Inherits = _inherits;
		//    }
		//}

		// Everything else in this class is unrelated to our 'inherits' handling.
		// Since PageParserFilter blocks everything by default, we need to unblock it

		public override bool AllowCode
		{
			get
			{
				return true;
			}
		}

		public override bool AllowBaseType(Type baseType)
		{
			return true;
		}

		public override bool AllowControl(Type controlType, ControlBuilder builder)
		{
			return true;
		}

		public override bool AllowVirtualReference(string referenceVirtualPath, VirtualReferenceType referenceType)
		{
			return true;
		}

		public override bool AllowServerSideInclude(string includeVirtualPath)
		{
			return true;
		}

		public override int NumberOfControlsAllowed
		{
			get
			{
				return -1;
			}
		}

		public override int NumberOfDirectDependenciesAllowed
		{
			get
			{
				return -1;
			}
		}

		public override int TotalNumberOfDependenciesAllowed
		{
			get
			{
				return -1;
			}
		}
	}
}

When modifying any classes that need modifications you are going to be interested in references to the original MVC class names, in the case of this class, it’s the dictionary created in the beginning. Now note that it’s a good practice to create your own classes inheriting from the ones you extensively use, such as Page, MasterPage and UserControl in Windows Forms, or ViewMasterPage, Controller and ViewUserControl in MVC, even if you don’t have any extra properties to add. This way you can always extend them later easily if needed for your particular application. So, let’s change that dictionary and save it as My.Namespace.CustomViewTypeParserFilter:

private static Dictionary _directiveBaseTypeMappings = new Dictionary 
{
            { "page",    typeof(MyBaseViewPage)        },
            { "control", typeof(MyBaseViewUserControl) },
            { "master",  typeof(MyBaseViewMasterPage)  },
};

We now need to create a custom control builder class, which is going to be exactly the same as the System.Web.Mvc.ViewPageControlBuilder class with the exception that we are going to update our MyBaseViewPage with the custom MyBaseViewPage type:

using System;
using System.CodeDom;
using System.Web.UI;

namespace My.Namespace
{
	internal sealed class CustomViewPageControlBuilder : FileLevelPageControlBuilder
	{
		public string Inherits { get; set; }

		public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
		{
			if (!String.IsNullOrWhiteSpace(Inherits))
			{
				derivedType.BaseTypes[0] = new CodeTypeReference(typeof(MyBaseViewPage));
			}
		}
	}
}

All in all, we’re almost done at this point. If you run your website now, it will still give you the same error. That is because the site still does not know to use the classes you have just created. So, on to the web.config file. Find the <pages> tag which, in all likeliness, looks like this:


	
		
	

Now what you want to do is replace the pageParserFilterType, the pageBaseType and the userControlBaseType attributes (if you created one) to their respective class names:


	
		
	

Now, go ahead and hit that magic F5 button!

One of the things I do want to note is that I’m using MVC 4 Developer Edition, for which the source is not yet available. So please note how the ParseComplete method is commented out since there is no IMvcControlBuilder interface anymore.

I hope you enjoyed the reading and it helped you! Feel free to ask questions!

This entry was posted in MVC and tagged , , , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *