Недавно начал переводить старый самописный движок с 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. Пугаться не стоит, работает и без него.