Pull to refresh

Расширение функционала тегов Page/MasterPage/UserControl в ASP.NET MVC

Reading time8 min
Views3.3K
Недавно начал переводить старый самописный движок с PHP на ASP.NET и столкнулся с несколькими моментами, связанными с шаблонами Smarty и возможностями представлений ASP.NET MVC. Сразу оговорюсь, что подход можно применять и для веб-проектов, но там, возможно, потребуется допилка. Итак.

Во-первых, с самого начала возникла необходимость из шаблона обращаться к методам основного объекта веб-приложения (назовем его Main) — например, конфигурация, менеджер тем, к методам вызывающего контроллера и так далее. Стандартный класс System.Web.Mvc.ViewPage не предоставляет удобного функционала для этого. Конечно, можно добраться до свойства ViewContext.Controller, сделать приведение типа и работать в шаблоне с кодом вида <%=((IndexController)ViewContext.Controller).CurrentTheme.Name%>, но тут возникает вопрос читабельности кода и удобства его написания вообще. Я пошел по пути расширения функционала System.Web.Mvc.ViewPage (а заодно System.Web.Mvc.MasterPage и System.Web.Mvc.UserControl) и добавления в него свойства ControlHelper, которое возвращает объект-помощник, делающий доступными необходимые возможности.

Во-вторых, возникла необходимость в представлениях не задавать прямой путь MasterPageFile, а размечать его дополнительными тегами а-ля «CurrentTheme.SiteMaster», «UserTheme.SiteMaster» и т.п. К сожалению, при записи подобной строки в атрибут MasterPageFile директивы Page я получал ошибку синтаксического анализатора, ругавшегося на отсутствие файла "~/Views/{CurrentTheme.SiteMaster}". Единственное найденное решение — создание своего атрибута для директивы Page, например MasterPagePath:
<%@ Page Language="C#" MasterPagePath="{CurrentTheme.SiteMaster}" Inherits="..." %>


В связи со вторым моментом возник нюанс, о котором я расскажу попозже.

Предположим, у нас есть проект MVC приложения, мы создали контроллер HomeController с одним методом Index, создали главную страницу Site1.Master, создали представление Views/Home/Index.aspx, наследующееся от главной страницы Site1.Master.
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Site1.Master" Inherits="MvcHelperApplication.Inc.Types.ViewPage" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" runat="server"><h2>Index</h2></asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="head" runat="server">
</asp:Content>


Затем меняем директиву Page в соответствии с нашим требованием:
<%@ Page Title="" Language="C#" MasterPagePath="{CurrentMasterPath}" Inherits="MvcHelperApplication.Inc.Types.ViewPage" %>


MvcHelperApplication.Inc.Types.ViewPage это будущий класс страницы, который мы напишем чуть ниже.

Для начала напишем простенький класс-помощник, который делает всего одну вещь — обрабатывает событие PreInit странички и задает значение её атрибуту MasterPageFile на основе кастомного атрибута MasterPagePath.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MvcHelperApplication.Inc
{
    public class ControlHelper
    {
        private Types.ViewPage mPage = null;

        public ControlHelper(Types.ViewPage page)
        {
            mPage = page;
        }

        public void page_PreInit(object sender, EventArgs e)
        {
            try
            {
                if (mPage.MasterPagePath != null && mPage.MasterPagePath == "{CurrentMasterPath}")
                {
                    mPage.MasterPageFile = "~/Views/Site1.Master";
                }
            }
            catch (Exception ex)
            {

            }
        }

    }
}


Здесь все должно быть понятно, этот момент не содержит сюрпризов и подводных камней. Если в директиве Page встретился тег MasterPagePath, он обрабатывается и, если он равен "{CurrentMasterPath}", то свойству MasterPageFile задается путь к существующему masterpage.

Создаем класс ViewPage:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.UI;
using System.Diagnostics.CodeAnalysis;

namespace MvcHelperApplication.Inc.Types
{
    public class ViewPage : System.Web.Mvc.ViewPage
    {
        private string _masterpagepath = "";
        public string MasterPagePath
        {
            get { return _masterpagepath; }
            set { _masterpagepath = value; }
        }

        public ViewPage()
        {
            this.PreInit += new EventHandler(ControlHelper.page_PreInit);
        }

        private ControlHelper mControlHelper = null;
        public ControlHelper ControlHelper
        {
            get
            {
                if (mControlHelper == null) mControlHelper = new ControlHelper(this);
                return mControlHelper;
            }
        }
    }

}


Разбираем по порядку.
1) MasterPagePath это свойство, через которое класс получит значение атрибута MasterPagePath из директивы Page.
2) ViewPage — конструктор, в нем к событию PreInit привязывается метод page_PreInit нашего класса-помощника.
3) ControlHelper — собственно свойство для доступа к объекту класса-помощника.

Запускаем отладку и видим на странице красивое слово Index. При желании можно проследить процесс работы скрипта.

Замечательно, мы можем теперь задавать собственный путь к MasterPageFile любым способом. Плюс к этому решился вопрос с доступом к темам, конфигурации и т.п. — достаточно расширить класс ControlHelper и обращаться к конфигу через него. Например, так:
<%=ControlHelper.Config%>
<%=ControlHelper.Themes.Current%>

и т.п.

Казалось бы, все замечательно. Ан нет, ASP.NET приготовил очень гадкую собаку, из-за которой пришлось потратить день на рытье в сети. Дело в том, что парсер ASP.NET по-умолчанию не поддерживает generic types в перегруженных классах. Это означает, что если в Index.aspx вместо Inherits=«MvcHelperApplication.Inc.Types.ViewPage» написать Inherits=«MvcHelperApplication.Inc.Types.ViewPage» (т.е. применить к представлению модель, из-за чего, собственно, и затевается весь сыр-бор с MVC), то мы получим ошибку:
Описание: Ошибка при разборе ресурса, требуемого для обслуживания этого запроса. Изучите следующие подробные сведения о данной ошибке разбора и измените исходный файл. 

 Сообщение об ошибке синтаксического анализатора: Ошибка при обработке атрибута 'masterpagepath': Тип 'System.Web.Mvc.ViewPage' не содержит свойство с именем 'masterpagepath'.

Ошибка источника: 

Строка 1:  <%@ Page Title="" Language="C#" MasterPagePath="{CurrentMasterPath}" Inherits="MvcHelperApplication.Inc.Types.ViewPage<dynamic>" %>


Попробуем невозмутимо изменить описание класса ViewPage на следующее:
    public class ViewPage<TModel> : System.Web.Mvc.ViewPage<TModel>


Компилируем, обновляем и получаем ту же ошибку.

В общем, для того чтобы подсунуть парсеру универсальный тип, придется воспользоваться такой штукой как pageParserFilterType. В Web.config в секции <system.web> прописывается следующее:
<pages validateRequest="false" pageParserFilterType="MvcHelperApplication.Inc.ViewTypeParserFilter" pageBaseType="MvcHelperApplication.Inc.Types.ViewPage">


Создаем ViewTypeParserFilter:

using System;
using System.Collections;
using System.Web.UI;
using System.Web.Mvc;
using System.CodeDom;
using System.Web.UI;

namespace MvcHelperApplication.Inc
{
    public class ViewTypeParserFilter : 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;

            switch (directiveName)
            {
                case "page":
                    _directiveType = DirectiveType.Page;
                    defaultBaseType = typeof(Types.ViewPage).FullName; 
                    break;
                case "control":
                    _directiveType = DirectiveType.UserControl;
                    defaultBaseType = typeof(System.Web.Mvc.ViewUserControl).FullName;
                    break;
                case "master":
                    _directiveType = DirectiveType.Master;
                    defaultBaseType = typeof(System.Web.Mvc.ViewMasterPage).FullName;
                    break;
            }

            if (_directiveType == DirectiveType.Unknown) return;

            string inherits = (string)attributes["inherits"];
            if (!String.IsNullOrEmpty(inherits))
            {
                if (IsGenericTypeString(inherits))
                {
                    attributes["inherits"] = defaultBaseType;
                    _viewBaseType = inherits;
                }
            }
        }

        private static bool IsGenericTypeString(string typeName)
        {
            return typeName.IndexOfAny(new char[] { '<', '(' }) >= 0;
        }

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

            ViewPageControlBuilder pageBuilder = rootBuilder as ViewPageControlBuilder;
            if (pageBuilder != null)
            {
                pageBuilder.PageBaseType = _viewBaseType;
            }
        }

        public override bool ProcessCodeConstruct(CodeConstructType codeType, string code)
        {
            if (codeType == CodeConstructType.ExpressionSnippet &&
                !_viewTypeControlAdded &&
                _viewBaseType != null &&
                _directiveType == DirectiveType.Master)
            {
                Hashtable attribs = new Hashtable();
                attribs["typename"] = _viewBaseType;
                AddControl(typeof(System.Web.Mvc.ViewType), attribs);
                _viewTypeControlAdded = true;
            }

            return base.ProcessCodeConstruct(codeType, code);
        }

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

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

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

}


Меняем файл ViewPage.cs


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

namespace MvcHelperApplication.Inc.Types
{
    [FileLevelControlBuilder(typeof(ViewPageControlBuilder))]
    public class ViewPage : System.Web.Mvc.ViewPage
    {
        private string _masterpagepath = "";
        public string MasterPagePath
        {
            get { return _masterpagepath; }
            set { _masterpagepath = value; }
        }

        public ViewPage()
        {
            this.PreInit += new EventHandler(ControlHelper.page_PreInit);
        }

        private ControlHelper mControlHelper = null;
        public ControlHelper ControlHelper
        {
            get
            {
                if (mControlHelper == null) mControlHelper = new ControlHelper(this);
                return mControlHelper;
            }
        }
    }

    [FileLevelControlBuilder(typeof(ViewPageControlBuilder))]
    public class ViewPage<TModel> : ViewPage
        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);
        }

    }

}


Компилируем — видим замечательное слово Index в браузере.
Есть маленький нюанс, связанный с файлами Web.config в папках представлений. Файл Views/Web.config лучше удалить, т.к. он будет затирать изменения тега pages в основном Web.config сайта.

При помощи ViewTypeParserFilter можно обеспечить работу собственных классов ViewPage, ViewUserControl, ViewMaserPage. На всякий случай замечание — в теге pages в конфигурационном файле есть возможность задать базовые типы pagesBaseType и userControlBaseType, но нет возможности задать masterpageBaseType. Пугаться не стоит, работает и без него.
Tags:
Hubs:
Total votes 27: ↑21 and ↓6+15
Comments76

Articles