35 votes

ViewPage<> générique héritée et nouvelle propriété

Mise en place :

  • CustomViewEngine
  • Base CustomController
  • CustomViewPage Base (dans cette base, une nouvelle propriété est ajoutée "MyCustomProperty")

Problème :

Lorsqu'une vue est fortement typée, par exemple : <@ Page Inherits="CustomViewPage<MyCustomObject" MyCustomProperty="Hello"> J'obtiens une erreur de compilation "Parser" indiquant que MyCustomProperty n'est pas une propriété publique de System.Web.Mvc.ViewPage.

J'ai effectué de nombreux essais et erreurs (voir ci-dessous) pour déterminer la cause de cette erreur et je suis arrivé aux conclusions suivantes :

  • L'erreur ne se produit que lorsque je déclare "MyCustomProperty" ou toute autre propriété dans la directive @Page de la vue.
  • L'erreur affichera toujours "System.Web.Mvc.ViewPage" plutôt que la classe déclarée inherits="..".

57voto

Justin Grant Points 25644

Mise à jour : On dirait que Technitium J'ai trouvé une autre façon de procéder qui semble beaucoup plus facile, du moins sur les nouvelles versions d'ASP.NET MVC. (copié son commentaire ci-dessous)

Je ne sais pas si cela est nouveau dans ASP.NET MVC 3, mais lorsque j'ai échangé l'attribut Inherits de la référence au générique dans la syntaxe C# à la syntaxe CLR la syntaxe standard ViewPageParserFilter analyser correctement les génériques -- pas de CustomViewTypeParserFilter requis. En utilisant les exemples de Justin, cela signifie échanger

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

a

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

Réponse originale ci-dessous :

OK, j'ai résolu le problème. C'était un exercice fascinant, et la solution n'est pas triviale mais pas trop difficile une fois que vous l'avez fait fonctionner la première fois.

Voici le problème sous-jacent : l'analyseur de pages ASP.NET ne prend pas en charge les génériques comme type de page.

La façon dont ASP.NET MVC a contourné ce problème était de tromper l'analyseur de page sous-jacent en lui faisant croire que la page n'est pas générique. Pour ce faire, ils ont construit un fichier PageParserFilter et un FileLevelPageControlBuilder . Le filtre de l'analyseur syntaxique recherche un type générique et, s'il en trouve un, le remplace par le type non générique ViewPage afin que l'analyseur syntaxique ASP.NET ne s'étrangle pas. Puis, bien plus tard dans le cycle de vie de la compilation de la page, la classe de construction de page personnalisée réintroduit le type générique.

Cela fonctionne parce que le type ViewPage générique dérive de la ViewPage non générique, et que toutes les propriétés intéressantes qui sont définies dans une directive @Page existent sur la classe de base (non générique). Ainsi, ce qui se passe réellement lorsque des propriétés sont définies dans la directive @Page, c'est que les noms de ces propriétés sont validés par rapport à la classe de base non générique ViewPage.

Quoi qu'il en soit, cela fonctionne très bien dans la plupart des cas, mais pas dans le vôtre, car ils codent en dur ViewPage comme type de base non générique dans leur implémentation de filtre de page et ne fournissent pas un moyen facile de le changer. C'est pourquoi vous avez continué à voir ViewPage dans votre message d'erreur, puisque l'erreur se produit entre le moment où ASP.NET insère l'espace réservé ViewPage et celui où il réinsère le ViewPage générique juste avant la compilation.

La solution consiste à créer votre propre version de ce qui suit :

  1. filtre d'analyseur de pages - il s'agit d'une copie presque exacte de ViewTypeParserFilter.cs dans le code source MVC, la seule différence étant qu'il fait référence à vos types personnalisés ViewPage et page builder au lieu de ceux de MVC.
  2. page builder - il est identique à ViewPageControlBuilder.cs dans la source MVC, mais il place la classe dans votre propre espace de noms plutôt que dans le leur.
  3. Faites dériver votre classe Viewpage personnalisée directement de System.Web.Mvc.ViewPage (la version non générique). Collez toutes les propriétés personnalisées sur cette nouvelle classe non-générique.
  4. dériver une classe générique à partir de #3, en copiant le code de l'implémentation de ViewPage de la source ASP.NET MVC.
  5. Répétez les étapes 2, 3 et 4 pour les contrôles utilisateur (@Control) si vous avez également besoin de propriétés personnalisées sur les directives de contrôle utilisateur.

Vous devez ensuite modifier le web.config de votre répertoire views (pas le web.config de l'application principale) pour utiliser ces nouveaux types au lieu des types par défaut de MVC.

J'ai joint quelques exemples de code illustrant comment cela fonctionne. Un grand merci à l'article de Phil Haack pour m'avoir aidé à comprendre cela, même si j'ai dû beaucoup fouiller dans le code source de MVC et ASP.NET pour vraiment comprendre.

Tout d'abord, je vais commencer par les changements nécessaires dans votre web.config :

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

Maintenant, voici le filtre de l'analyseur de page (#1 ci-dessus) :

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,
        }
    }
}

Voici la classe du constructeur de pages (#2 ci-dessus) :

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

Et voici les classes de pages de vue personnalisées : la classe de base non générique (#3 ci-dessus) et la classe dérivée générique (#4 ci-dessus) :

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

    }
}

Et voici les classes correspondantes pour les contrôles utilisateurs (#5 ci-dessus) :

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

Enfin, voici un exemple de vue qui montre cette méthode en action :

<%@ Page Language="C#" MyNewProperty="From @Page directive!"  Inherits="JG.ParserFilter.CustomViewPage<MvcApplication1.Models.FooModel>" %>
    <%=Model.SomeString %>
    <br /><br />this.MyNewPrroperty = <%=MyNewProperty%>
</asp:Content>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X