Недавно начал переводить старый самописный движок с 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:
В связи со вторым моментом возник нюанс, о котором я расскажу попозже.
Предположим, у нас есть проект MVC приложения, мы создали контроллер HomeController с одним методом Index, создали главную страницу Site1.Master, создали представление Views/Home/Index.aspx, наследующееся от главной страницы Site1.Master.
Затем меняем директиву Page в соответствии с нашим требованием:
MvcHelperApplication.Inc.Types.ViewPage это будущий класс страницы, который мы напишем чуть ниже.
Для начала напишем простенький класс-помощник, который делает всего одну вещь — обрабатывает событие PreInit странички и задает значение её атрибуту MasterPageFile на основе кастомного атрибута MasterPagePath.
Здесь все должно быть понятно, этот момент не содержит сюрпризов и подводных камней. Если в директиве Page встретился тег MasterPagePath, он обрабатывается и, если он равен "{CurrentMasterPath}", то свойству MasterPageFile задается путь к существующему masterpage.
Создаем класс ViewPage:
Разбираем по порядку.
1) MasterPagePath это свойство, через которое класс получит значение атрибута MasterPagePath из директивы Page.
2) ViewPage — конструктор, в нем к событию PreInit привязывается метод page_PreInit нашего класса-помощника.
3) ControlHelper — собственно свойство для доступа к объекту класса-помощника.
Запускаем отладку и видим на странице красивое слово Index. При желании можно проследить процесс работы скрипта.
Замечательно, мы можем теперь задавать собственный путь к MasterPageFile любым способом. Плюс к этому решился вопрос с доступом к темам, конфигурации и т.п. — достаточно расширить класс ControlHelper и обращаться к конфигу через него. Например, так:
и т.п.
Казалось бы, все замечательно. Ан нет, ASP.NET приготовил очень гадкую собаку, из-за которой пришлось потратить день на рытье в сети. Дело в том, что парсер ASP.NET по-умолчанию не поддерживает generic types в перегруженных классах. Это означает, что если в Index.aspx вместо Inherits=«MvcHelperApplication.Inc.Types.ViewPage» написать Inherits=«MvcHelperApplication.Inc.Types.ViewPage» (т.е. применить к представлению модель, из-за чего, собственно, и затевается весь сыр-бор с MVC), то мы получим ошибку:
Попробуем невозмутимо изменить описание класса ViewPage на следующее:
Компилируем, обновляем и получаем ту же ошибку.
В общем, для того чтобы подсунут�� парсеру универсальный тип, придется воспользоваться такой штукой как pageParserFilterType. В Web.config в секции <system.web> прописывается следующее:
Создаем ViewTypeParserFilter:
Меняем файл ViewPage.cs
Компилируем — видим замечательное слово Index в браузере.
Есть маленький нюанс, связанный с файлами Web.config в папках представлений. Файл Views/Web.config лучше удалить, т.к. он будет затирать изменения тега pages в основном Web.config сайта.
При помощи ViewTypeParserFilter можно обеспечить работу собственных классов ViewPage, ViewUserControl, ViewMaserPage. На всякий случай замечание — в теге pages в конфигурационном файле есть возможность задать базовые типы pagesBaseType и userControlBaseType, но нет возможности задать masterpageBaseType. Пугаться не стоит, работает и без него.
Во-первых, с самого начала возникла необходимость из шаблона обращаться к методам основного объекта веб-приложения (назовем его 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. Пугаться не стоит, работает и без него.
