📜 ⬆️ ⬇️

Extension of Page / MasterPage / UserControl tag functionality in ASP.NET MVC

Recently, I began to translate an old samopisny engine with PHP to ASP.NET and faced with several points related to Smarty templates and ASP.NET MVC presentation capabilities. At once I will make a reservation that the approach can also be applied to web projects, but there may need to be finished. So.

First, from the very beginning it became necessary to refer to the methods of the main object of the web application (let's call it Main) from the template — for example, the configuration, the manager of the object, the methods of the calling controller, and so on. The standard class System.Web.Mvc.ViewPage does not provide convenient functionality for this. Of course, you can get to the ViewContext.Controller property, do a type conversion and work in a template with the code like <% = ((IndexController) ViewContext.Controller) .CurrentTheme.Name%>, but then the question arises about the readability of the code and the convenience of writing it in general. I went along the path of expanding the functionality of System.Web.Mvc.ViewPage (along with System.Web.Mvc.MasterPage and System.Web.Mvc.UserControl) and adding the ControlHelper property to it, which returns a helper object that makes the necessary features available.

Secondly, it became necessary for representations not to set the direct path of MasterPageFile, but to mark it with additional tags a la “CurrentTheme.SiteMaster”, “UserTheme.SiteMaster”, etc. Unfortunately, when writing a similar string to the MasterPageFile attribute of the Page directive, I received an error from the parser, complaining about the absence of the file "~ / Views / {CurrentTheme.SiteMaster}". The only solution found is to create your own attribute for the Page directive, for example MasterPagePath:
<%@ Page Language="C#" MasterPagePath="{CurrentTheme.SiteMaster}" Inherits="..." %> 

')
In connection with the second point a nuance arose, which I will discuss later.

Suppose we have an MVC application project, we created the HomeController controller with one Index method, created the Site1.Master home page, created Views / Home / Index.aspx view inheriting from the Site1.Master home page.
 <%@ 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> 


Then we change the Page directive in accordance with our requirement:
 <%@ Page Title="" Language="C#" MasterPagePath="{CurrentMasterPath}" Inherits="MvcHelperApplication.Inc.Types.ViewPage" %> 


MvcHelperApplication.Inc.Types.ViewPage is the future class of the page, which we will write below.

First, let's write a simple helper class that does only one thing - it handles the PreInit event of the page and sets the value of its MasterPageFile attribute based on the custom attribute 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) { } } } } 


Everything should be clear here, this moment does not contain surprises and pitfalls. If there is a MasterPagePath tag in the Page directive, it is processed and, if it is equal to "{CurrentMasterPath}", then the MasterPageFile property is set to the path to the existing masterpage.

Create a ViewPage class:
 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; } } } } 


Parse in order.
1) MasterPagePath is the property through which the class will get the value of the MasterPagePath attribute from the Page directive.
2) ViewPage is a constructor, in it the page_PreInit method of our helper class is bound to the PreInit event.
3) ControlHelper is the property itself for accessing the object of the helper class.

We start debugging and see on the page the beautiful word Index. If you wish, you can follow the process of the script.

Great, we can now set our own path to MasterPageFile in any way. Plus, the issue was resolved with access to themes, configuration, etc. - it is enough to expand the ControlHelper class and access the configuration through it. For example:
 <%=ControlHelper.Config%> <%=ControlHelper.Themes.Current%> 

etc.

It would seem that everything is great. But no, ASP.NET prepared a very ugly dog, because of which it was necessary to spend a day digging on the net. The fact is that the ASP.NET parser does not support generic types in overloaded classes by default. This means that if in Index.aspx instead of Inherits = "MvcHelperApplication.Inc.Types.ViewPage" write Inherits = "MvcHelperApplication.Inc.Types.ViewPage" (i.e., apply the model to the view, which is why and all the fuss stuffed with MVC is started, then we get an error:
 :    ,     .            .     :     'masterpagepath':  'System.Web.Mvc.ViewPage'      'masterpagepath'.  :  1: <%@ Page Title="" Language="C#" MasterPagePath="{CurrentMasterPath}" Inherits="MvcHelperApplication.Inc.Types.ViewPage<dynamic>" %> 


Let’s try to calmly change the description of the ViewPage class to the following:
  public class ViewPage<TModel> : System.Web.Mvc.ViewPage<TModel> 


We compile, update and get the same error.

In general, in order to slip a generic type to the parser, you will have to use such a thing as pageParserFilterType. In the Web.config section in the <system.web> section the following is written:
 <pages validateRequest="false" pageParserFilterType="MvcHelperApplication.Inc.ViewTypeParserFilter" pageBaseType="MvcHelperApplication.Inc.Types.ViewPage"> 


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


Change the file 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); } } } 


Compile - see the wonderful word Index in the browser.
There is a small nuance associated with Web.config files in view folders. The Views / Web.config file is best removed, because it will overwrite the changes to the pages tag in the main website web.config.

With the help of ViewTypeParserFilter, you can use your own ViewPage, ViewUserControl, ViewMaserPage classes. Just in case, the remark - in the pages tag in the configuration file there is an opportunity to set the basic types pagesBaseType and userControlBaseType, but there is no possibility to set the masterpageBaseType. You should not be afraid, it works without it.

Source: https://habr.com/ru/post/134843/


All Articles