Generic Inherited ViewPage<> and new Property
Asked Answered
B

1

35

Setup:

  • CustomViewEngine
  • CustomController Base
  • CustomViewPage Base (in this base, a new property is added "MyCustomProperty")

Problem:

When a view is strongly typed such as: <@ Page Inherits="CustomViewPage<MyCustomObject" MyCustomProperty="Hello">, I get a compiler "Parser" error stating that MyCustomProperty is not a public property of System.Web.Mvc.ViewPage

I have done numerous trial and errors (see below) to see whats causing this error and have come to the following conclusions:

  • The error only occurs when I declare "MyCustomProperty" or any other property in the @Page directive of the view.
  • The error will always display "System.Web.Mvc.ViewPage" rather than the declared inherits=".." class.
Blinker answered 26/9, 2009 at 3:37 Comment(9)
Sorry, I can't help further and I'd love to know why this is happening. See the "How Input Controls Get Their Values" section of #1435234. This might help, it might not, but MVC does seem to search for values in an order that is not always obvious.Moolah
Can't see how I can apply the link to this situation. I tried all types of different class declaration with generic types and different inheritance patterns, nothing =(Blinker
I found this on Google: forums.asp.net/p/1432045/3408610.aspx#3408610 (Last Post on page) But why does it only work for if that user uses"raw .net format type specification"Blinker
Offhand, I'd say that it is reflecting over your assembly, and it sees a property that it doesn't expect.Glutathione
But I explicitly set the new property and it works perfectly with a regular ViewPage (no generic inheritance/model passed to it)Blinker
Are you using MVC 1.0 or the new MVC 2.0 Preview?Glutathione
The WebViewEngine only instantiates ViewPage(s). I think you would have to implement a custom view engine for this to work. https://mcmap.net/q/450107/-extending-webformview-in-mvcPedantry
@Pedantry I don't think thats the case since the ViewEngine renders MyCustomViewPage just fine for a view that is not strongly typed.Blinker
Is there a way to increase the bounty after creating it?Blinker
S
57

Update: Looks like Technitium found another way to do this that looks much easier, at least on newer versions of ASP.NET MVC. (copied his comment below)

I'm not sure if this is new in ASP.NET MVC 3, but when I swapped the Inherits attribute from referencing the generic in C# syntax to CLR syntax, the standard ViewPageParserFilter parsed generics correctly -- no CustomViewTypeParserFilter required. Using Justin's examples, this means swapping

<%@ Page Language="C#" MyNewProperty="From @Page directive!"
    Inherits="JG.ParserFilter.CustomViewPage<MvcApplication1.Models.FooModel>

to

<%@ Page Language="C#" MyNewProperty="From @Page directive!"` 
    Inherits="JG.ParserFilter.CustomViewPage`1[MvcApplication1.Models.FooModel]>

Original answer below:

OK, I solved this. Was a fascinating exercise, and the solution is non-trivial but not too hard once you get it working the first time.

Here's the underlying issue: the ASP.NET page parser does not support generics as a page type.

The way ASP.NET MVC worked around this was by fooling the underlying page parser into thinking that the page is not generic. They did this by building a custom PageParserFilter and a custom FileLevelPageControlBuilder. The parser filter looks for a generic type, and if it finds one, swaps it out for the non-generic ViewPage type so that the ASP.NET parser doesn't choke. Then, much later in the page compilation lifecycle, their custom page builder class swaps the generic type back in.

This works because the generic ViewPage type derives from the non-generic ViewPage, and all the interesting properties that are set in a @Page directive exist on the (non-generic) base class. So what's really happening when properties are set in the @Page directive is that those property names are being validated against the non-generic ViewPage base class.

Anyway, this works great in most cases, but not in yours because they hardcode ViewPage as the non-generic base type in their page filter implementation and don't provide an easy way to change it. This is why you kept seeing ViewPage in your error message, since the error happens in between when ASP.NET swaps in the ViewPage placeholder and when it swaps back the generic ViewPage right before compilation.

The fix is to create your own version of the following:

  1. page parser filter - this is almost an exact copy of ViewTypeParserFilter.cs in the MVC source, with the only difference being that it refers to your custom ViewPage and page builder types instead of MVC's
  2. page builder - this is identical to ViewPageControlBuilder.cs in the MVC source, but it puts the class in your own namespace as opposed to theirs.
  3. Derive your custom viewpage class directly from System.Web.Mvc.ViewPage (the non-generic version). Stick any custom properties on this new non-generic class.
  4. derive a generic class from #3, copying the code from the ASP.NET MVC source's implementation of ViewPage.
  5. repeat #2, #3, and #4 for user controls (@Control) if you also need custom properties on user control directives too.

Then you need to change the web.config in your views directory (not the main app's web.config) to use these new types instead of MVC's default ones.

I've enclosed some code samples illustrating how this works. Many thanks to Phil Haack's article to help me understand this, although I had to do a lot of poking around the MVC and ASP.NET source code too to really understand it.

First, I'll start with the web.config changes needed in your web.config:

<pages
    validateRequest="false"
    pageParserFilterType="JG.ParserFilter.CustomViewTypeParserFilter"
    pageBaseType="JG.ParserFilter.CustomViewPage"
    userControlBaseType="JG.ParserFilter.CustomViewUserControl">

Now, here's the page parser filter (#1 above):

namespace JG.ParserFilter {
    using System;
    using System.Collections;
    using System.Web.UI;
    using System.Web.Mvc;

    internal class CustomViewTypeParserFilter : PageParserFilter
    {

        private string _viewBaseType;
        private DirectiveType _directiveType = DirectiveType.Unknown;
        private bool _viewTypeControlAdded;

        public override void PreprocessDirective(string directiveName, IDictionary attributes) {
            base.PreprocessDirective(directiveName, attributes);

            string defaultBaseType = null;

            // If we recognize the directive, keep track of what it was. If we don't recognize
            // the directive then just stop.
            switch (directiveName) {
                case "page":
                    _directiveType = DirectiveType.Page;
                    defaultBaseType = typeof(JG.ParserFilter.CustomViewPage).FullName;  // JG: inject custom types here
                    break;
                case "control":
                    _directiveType = DirectiveType.UserControl;
                    defaultBaseType = typeof(JG.ParserFilter.CustomViewUserControl).FullName; // JG: inject custom types here
                    break;
                case "master":
                    _directiveType = DirectiveType.Master;
                    defaultBaseType = typeof(System.Web.Mvc.ViewMasterPage).FullName;
                    break;
            }

            if (_directiveType == DirectiveType.Unknown) {
                // If we're processing an unknown directive (e.g. a register directive), stop processing
                return;
            }


            // Look for an inherit attribute
            string inherits = (string)attributes["inherits"];
            if (!String.IsNullOrEmpty(inherits)) {
                // If it doesn't look like a generic type, don't do anything special,
                // and let the parser do its normal processing
                if (IsGenericTypeString(inherits)) {
                    // Remove the inherits attribute so the parser doesn't blow up
                    attributes["inherits"] = defaultBaseType;

                    // Remember the full type string so we can later give it to the ControlBuilder
                    _viewBaseType = inherits;
                }
            }
        }

        private static bool IsGenericTypeString(string typeName) {
            // Detect C# and VB generic syntax
            // REVIEW: what about other languages?
            return typeName.IndexOfAny(new char[] { '<', '(' }) >= 0;
        }

        public override void ParseComplete(ControlBuilder rootBuilder) {
            base.ParseComplete(rootBuilder);

            // If it's our page ControlBuilder, give it the base type string
            CustomViewPageControlBuilder pageBuilder = rootBuilder as JG.ParserFilter.CustomViewPageControlBuilder; // JG: inject custom types here
            if (pageBuilder != null) {
                pageBuilder.PageBaseType = _viewBaseType;
            }
            CustomViewUserControlControlBuilder userControlBuilder = rootBuilder as JG.ParserFilter.CustomViewUserControlControlBuilder; // JG: inject custom types here
            if (userControlBuilder != null) {
                userControlBuilder.UserControlBaseType = _viewBaseType;
            }
        }

        public override bool ProcessCodeConstruct(CodeConstructType codeType, string code) {
            if (codeType == CodeConstructType.ExpressionSnippet &&
                !_viewTypeControlAdded &&
                _viewBaseType != null &&
                _directiveType == DirectiveType.Master) {

                // If we're dealing with a master page that needs to have its base type set, do it here.
                // It's done by adding the ViewType control, which has a builder that sets the base type.

                // The code currently assumes that the file in question contains a code snippet, since
                // that's the item we key off of in order to know when to add the ViewType control.

                Hashtable attribs = new Hashtable();
                attribs["typename"] = _viewBaseType;
                AddControl(typeof(System.Web.Mvc.ViewType), attribs);
                _viewTypeControlAdded = true;
            }

            return base.ProcessCodeConstruct(codeType, code);
        }

        // 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;
            }
        }

        private enum DirectiveType {
            Unknown,
            Page,
            UserControl,
            Master,
        }
    }
}

Here's the page builder class (#2 above):

namespace JG.ParserFilter {
    using System.CodeDom;
    using System.Web.UI;

    internal sealed class CustomViewPageControlBuilder : FileLevelPageControlBuilder {
        public string PageBaseType {
            get;
            set;
        }

        public override void ProcessGeneratedCode(
            CodeCompileUnit codeCompileUnit,
            CodeTypeDeclaration baseType,
            CodeTypeDeclaration derivedType,
            CodeMemberMethod buildMethod,
            CodeMemberMethod dataBindingMethod) {

            // If we find got a base class string, use it
            if (PageBaseType != null) {
                derivedType.BaseTypes[0] = new CodeTypeReference(PageBaseType);
            }
        }
    }
}

And here's the custom view page classes: the non-generic base (#3 above) and the generic derived class (#4 above):

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

namespace JG.ParserFilter
{
    [FileLevelControlBuilder(typeof(JG.ParserFilter.CustomViewPageControlBuilder))]
    public class CustomViewPage : System.Web.Mvc.ViewPage //, IAttributeAccessor 
    {
        public string MyNewProperty { get; set; }
    }

    [FileLevelControlBuilder(typeof(JG.ParserFilter.CustomViewPageControlBuilder))]
    public class CustomViewPage<TModel> : CustomViewPage
        where TModel : class
    {
        // code copied from source of ViewPage<T>

        private ViewDataDictionary<TModel> _viewData;

        public new AjaxHelper<TModel> Ajax
        {
            get;
            set;
        }

        public new HtmlHelper<TModel> Html
        {
            get;
            set;
        }

        public new TModel Model
        {
            get
            {
                return ViewData.Model;
            }
        }

        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public new ViewDataDictionary<TModel> ViewData
        {
            get
            {
                if (_viewData == null)
                {
                    SetViewData(new ViewDataDictionary<TModel>());
                }
                return _viewData;
            }
            set
            {
                SetViewData(value);
            }
        }

        public override void InitHelpers()
        {
            base.InitHelpers();

            Ajax = new AjaxHelper<TModel>(ViewContext, this);
            Html = new HtmlHelper<TModel>(ViewContext, this);
        }

        protected override void SetViewData(ViewDataDictionary viewData)
        {
            _viewData = new ViewDataDictionary<TModel>(viewData);

            base.SetViewData(_viewData);
        }

    }
}

And here are the corresponding classes for user controls (#5 above) :

namespace JG.ParserFilter
{
    using System.Diagnostics.CodeAnalysis;
    using System.Web.Mvc;
    using System.Web.UI;

    [FileLevelControlBuilder(typeof(JG.ParserFilter.CustomViewUserControlControlBuilder))]
    public class CustomViewUserControl : System.Web.Mvc.ViewUserControl 
    {
        public string MyNewProperty { get; set; }
    }

    public class CustomViewUserControl<TModel> : CustomViewUserControl  where TModel : class
    {
        private AjaxHelper<TModel> _ajaxHelper;
        private HtmlHelper<TModel> _htmlHelper;
        private ViewDataDictionary<TModel> _viewData;

        public new AjaxHelper<TModel> Ajax {
            get {
                if (_ajaxHelper == null) {
                    _ajaxHelper = new AjaxHelper<TModel>(ViewContext, this);
                }
                return _ajaxHelper;
            }
        }

        public new HtmlHelper<TModel> Html {
            get {
                if (_htmlHelper == null) {
                    _htmlHelper = new HtmlHelper<TModel>(ViewContext, this);
                }
                return _htmlHelper;
            }
        }

        public new TModel Model {
            get {
                return ViewData.Model;
            }            
        }

        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public new ViewDataDictionary<TModel> ViewData {
            get {
                EnsureViewData();
                return _viewData;
            }
            set {
                SetViewData(value);
            }
        }

        protected override void SetViewData(ViewDataDictionary viewData) {
            _viewData = new ViewDataDictionary<TModel>(viewData);

            base.SetViewData(_viewData);
        }
    }
}

namespace JG.ParserFilter {
    using System.CodeDom;
    using System.Web.UI;

    internal sealed class CustomViewUserControlControlBuilder : FileLevelUserControlBuilder {
        internal string UserControlBaseType {
            get;
            set;
        }

        public override void ProcessGeneratedCode(
            CodeCompileUnit codeCompileUnit,
            CodeTypeDeclaration baseType,
            CodeTypeDeclaration derivedType,
            CodeMemberMethod buildMethod,
            CodeMemberMethod dataBindingMethod) {

            // If we find got a base class string, use it
            if (UserControlBaseType != null) {
                derivedType.BaseTypes[0] = new CodeTypeReference(UserControlBaseType);
            }
        }
    }
}

Finally, here's a sample View which shows this in action:

<%@ Page Language="C#" MyNewProperty="From @Page directive!"  Inherits="JG.ParserFilter.CustomViewPage<MvcApplication1.Models.FooModel>" %>
    <%=Model.SomeString %>
    <br /><br />this.MyNewPrroperty = <%=MyNewProperty%>
</asp:Content>
Speedball answered 2/10, 2009 at 22:8 Comment(5)
You my friend are you my hero. If I could reach thru the screen and hug you I would. I would've never solved this with my experience.. ever! I stopped coding for a week because of this issue and finally got an answer. If I could, I would give you the remainder of my 226 reputation, but I can't edit bounties. Thank you again, very thorough answer, with a perfect explanation. Hopefully this answer will help out others who try to get this working.Blinker
Just found this article and it saved me a load of time too. Thanks Justin! Just one thing to point out that needs correcting in your example code. In the "corresponding classes for user controls": public class CustomViewUserControl<TModel> : ViewUserControl where TModel : class should be public class CustomViewUserControl<TModel> : CustomViewUserControl where TModel : class Thanks again.Ghazi
Thanks for this! MS code is such a beauty that it stands even on their own way :(Zee
I'm not sure if this is new in ASP.NET MVC 3, but when I swapped the Inherits attribute from referencing the generic in C# syntax to CLR syntax, the standard ViewPageParserFilter parsed generics correctly -- no CustomViewTypeParserFilter required. Using Justin's examples, this means swapping <%@ Page Language="C#" MyNewProperty="From @Page directive!" Inherits="JG.ParserFilter.CustomViewPage<MvcApplication1.Models.FooModel> to <%@ Page Language="C#" MyNewProperty="From @Page directive!" Inherits="JG.ParserFilter.CustomViewPage`1[MvcApplication1.Models.FooModel]>.Pretender
I'm converting an ASP.NET MVC 2 application that used a Generic base view-page class with two type-arguments. I tried using the IL syntax (Inherits="MyNamespace.MyBasePage2[Type1,Type2]"`) but it gives me an error because it assumes the comma is prefixing an assembly name (as though it were a fully-qualified type-name). Do you know if your updated answer works with 2 arguments somehow?Underbody

© 2022 - 2024 — McMap. All rights reserved.