ASP.NET Web Forms Extensibility: Page Parser Filters

Introduction

ASP.NET includes a valuable yet not well known extension point called the page parser filter. I once briefly talked about it in the context of SharePoint, but I feel a more detailed explanation is in order.

A page parser filter is a class with a public parameterless constructor that inherits from PageParserFilter and is registered on the Web.config file, in the pages section, pageParserFilterType attribute. It is called when ASP.NET is compiling a page, the first time it is called, for every page. There can be only one page parser filter per web application.

Parser

Why is it a parser? Well, it parses – or, better, receives a notification for - every control declared on the markup of a page (those with runat=”server” or contained inside of), as well as all of the page’s directives (<%@ … %>). The control declarations include all of its attributes and properties, the recognized control type and any complex properties that the markup contains. This allows us to do all kinds of crazy stuff:

  • Inspect, add and remove page directives;
  • Setting the page’s compilation mode;
  • Insert or remove controls or text literals dynamically at specific places;
  • Add/change/remove a control’s properties or attributes;
  • Even (with some reflection magic) change a control’s type or tag.

So, how do we all this? First, the parser part. We can inspect all page directives by overriding the PreprocessDirective method. This is called for all page directives:

   1: public override void PreprocessDirective(String directiveName, IDictionary attributes)
   2: {
   3:     if (directiveName == "page")
   4:     {
   5:         //make a page asynchronous
   6:         attributes["async"] = "true";
   7:     }
   8:  
   9:     base.PreprocessDirective(directiveName, attributes);
  10: }

The page’s compilation mode is controlled by GetCompilationMode:

   1: public override CompilationMode GetCompilationMode(CompilationMode current)
   2: {
   3:     return (base.GetCompilationMode(current));
   4: }

As for adding controls dynamically, we make use of the ParseComplete method:

   1: public override void ParseComplete(ControlBuilder rootBuilder)
   2: {
   3:     if (rootBuilder is FileLevelPageControlBuilder)
   4:     {
   5:         this.ProcessControlBuilder(rootBuilder);
   6:     }
   7:  
   8:     base.ParseComplete(rootBuilder);
   9: }
  10:  
  11: private void ProcessControlBuilder(ControlBuilder builder)
  12: {
  13:     this.ProcessControlBuilderChildren(builder);
  14:  
  15:     if (builder.ControlType == typeof(HtmlForm))
  16:     {
  17:         //add a Literal control inside the form tag
  18:         var literal = ControlBuilder.CreateBuilderFromType(null, builder, typeof(Literal), "asp:Literal", "literal", new Hashtable { { "Text", "Inserted dynamically I" } }, -1, null);
  19:         builder.AppendSubBuilder(literal);
  20:  
  21:         //add an HTML snippet inside the form tag
  22:         builder.AppendLiteralString("<div>Inserted dynamically II</div>");
  23:     }
  24: }
  25:  
  26: private void ProcessControlBuilderChildren(ControlBuilder parentBuilder)
  27: {
  28:     foreach (var builder in parentBuilder.SubBuilders.OfType<ControlBuilder>())
  29:     {
  30:         this.ProcessControlBuilderChildren(builder);
  31:     }
  32: }

Same for changing a control’s properties:

   1: private static readonly FieldInfo simplePropertyEntriesField = typeof(ControlBuilder).GetField("_simplePropertyEntries", BindingFlags.Instance | BindingFlags.NonPublic);
   2:  
   3: private void SetControlProperty(ControlBuilder builder, String propertyName, String propertyValue)
   4: {
   5:     var properties = (simplePropertyEntriesField.GetValue(builder) as IEnumerable);
   6:  
   7:     if (properties == null)
   8:     {
   9:         properties = new ArrayList();
  10:         simplePropertyEntriesField.SetValue(builder, properties);
  11:     }
  12:  
  13:     var entry = properties.OfType<SimplePropertyEntry>().SingleOrDefault(x => x.Name == propertyName) ?? simplePropertyEntryConstructor.Invoke(null) as SimplePropertyEntry;
  14:     entry.Name = propertyName;
  15:     entry.UseSetAttribute = (builder.ControlType != null && builder.ControlType.GetProperties().Any(x => x.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) == false);
  16:     entry.PersistedValue = propertyValue;
  17:     entry.Value = entry.PersistedValue;
  18:     entry.Filter = String.Empty;
  19:  
  20:     if (properties.OfType<SimplePropertyEntry>().Any(x => x.Name == propertyName) == false)
  21:     {
  22:         (properties as ArrayList).Add(entry);
  23:     }
  24: }
  25:  
  26: private void ProcessControlBuilder(ControlBuilder builder)
  27: {
  28:     if (typeof(IEditableTextControl).IsAssignableFrom(builder.ControlType) == true)
  29:     {
  30:         this.SetControlProperty(builder, "Text", "Injected dynamically!");
  31:     }
  32:  
  33:     this.ProcessControlBuilderChildren(builder);
  34: }

And even changing the control’s output tag or instance type:

   1: private static readonly FieldInfo controlTypeField = typeof(ControlBuilder).GetField("_controlType", BindingFlags.Instance | BindingFlags.NonPublic);
   2: private static readonly FieldInfo tagNameField = typeof(ControlBuilder).GetField("_tagName", BindingFlags.Instance | BindingFlags.NonPublic);
   3:  
   4: private void SetControlType(ControlBuilder builder, Type controlType)
   5: {
   6:     controlTypeField.SetValue(builder, controlType);
   7: }
   8:  
   9: private void SetTagName(ControlBuilder controlBuilder, String tagName)
  10: {
  11:     tagNameField.SetValue(controlBuilder, tagName);
  12: }
  13:  
  14: private void ProcessControlBuilder(ControlBuilder builder)
  15: {
  16:     if (builder.TagName != null)
  17:     {
  18:         this.SetTagName(builder, builder.TagName.ToUpper());
  19:     }
  20:  
  21:     if (builder.ControlType == typeof(MyControl))
  22:     {
  23:         this.SetControlType(builder, typeof(MyDerivedControl));
  24:     }
  25:  
  26:     this.ProcessControlBuilderChildren(builder);
  27: }

Why would we want to change a control’s type? Well, thing about generics, for once.

Filter

And now the filtering part: why is it a filter? Because it allows us to filter and control a number of things:

  • The allowed master page, base page class and source file;
  • The allowed controls;
  • The total number of controls allowed on a page;
  • The total number of direct and otherwise references on a page;
  • Allow or disallow code and event handler declarations;
  • Allow or disallow code blocks (<%= … %>, <%: … %>, <% … %>);
  • Allow or disallow server-side script tags (<script runat=”server”>…</script>);
  • Allow, disallow and change data binding expressions (<%# … %>);
  • Add, change or remove event handler declarations.

All of the filtering methods and properties described below return a Boolean flag and its base implementation may or may not be called, depending on the logic that we want to impose.

Allowing or disallowing a base page class is controlled by the AllowBaseType method (the default is to accept):

   1: public override Boolean AllowBaseType(Type baseType)
   2: {
   3:     return (baseType == typeof(MyBasePage));
   4: }

For master pages, user controls or source files we have the AllowVirtualReference virtual method (again, the default is true):

   1: public override Boolean AllowVirtualReference(String referenceVirtualPath, VirtualReferenceType referenceType)
   2: {
   3:     if (referenceType == VirtualReferenceType.Master)
   4:     {
   5:         return (referenceVirtualPath == "AllowedMaster.Master");
   6:     }
   7:     else if (referenceType == VirtualReferenceType.UserControl)
   8:     {
   9:         return (referenceVirtualPath != "ForbiddenControl.ascx");
  10:     }
  11:  
  12:     return (base.AllowVirtualReference(referenceVirtualPath, referenceType));
  13: }

Controls are controlled (pun intended) by AllowControl, which also defaults to accept:

   1: public override Boolean AllowControl(Type controlType, ControlBuilder builder)
   2: {
   3:     return (typeof(IForbiddenInterface).IsAssignableFrom(controlType) == false);
   4: }

This may come in handy to disallow the usage of controls in ASP.NET MVC ASPX views!

The number of controls and dependencies on a page is defined by NumberOfControlsAllowed, NumberOfDirectDependenciesAllowed and TotalNumberOfDependenciesAllowed. Interesting, the default for all these properties is 0, so we have to return –1:

   1: public override Int32 NumberOfControlsAllowed
   2: {
   3:     get
   4:     {
   5:         return (-1);
   6:     }
   7: }
   8:  
   9: public override Int32 NumberOfDirectDependenciesAllowed
  10: {
  11:     get
  12:     {
  13:         return (-1);
  14:     }
  15: }
  16:  
  17: public override Int32 TotalNumberOfDependenciesAllowed
  18: {
  19:     get
  20:     {
  21:         return (-1);
  22:     }
  23: }

Direct dependencies are user controls directly declared in the page and indirect ones are those declared inside other user controls.

Code itself, including event handler declarations, are controlled by AllowCode (default is true):

   1: public override Boolean AllowCode
   2: {
   3:     get
   4:     {
   5:         return (true);
   6:     }
   7: }

If we want to change a data binding expression, we resort to ProcessDataBindingAttribute, which also returns true by default:

   1: public override Boolean ProcessDataBindingAttribute(String controlId, String name, String value)
   2: {
   3:     if (name == "Text")
   4:     {
   5:         //Do not allow binding the Text property
   6:         return (false);
   7:     }
   8:  
   9:     return (base.ProcessDataBindingAttribute(controlId, name, value));
  10: }

For intercepting event handlers, there’s the ProcessEventHook, which likewise returns true by default:

   1: public override Boolean ProcessEventHookup(String controlId, String eventName, String handlerName)
   2: {
   3:     if (eventName == "SelectedIndexChanged")
   4:     {
   5:         //Remove event handlers for the SelectedIndexChanged event
   6:         return (false);
   7:     }
   8:  
   9:     return (base.ProcessEventHookup(controlId, eventName, handlerName));
  10: }

And finally, for code blocks, server-side scripts and data binding expressions, there’s the ProcessCodeConstruct method, which likewise also allows everything by default:

Conclusion

This was in no means an in-depth description of page parser filters, I just meant to give you an idea of its (high) potential. It is very useful to restrict what end users can place on their pages (SharePoint style) as well as for adding dynamic control programmatically in specific locations of the page, before it is actually built.

As usual, let me hear your thoughts! Winking smile

                             

2 Comments

Add a Comment

As it will appear on the website

Not displayed

Your website