zoukankan      html  css  js  c++  java
  • 9.2.2 .net framework下的MVC 控件的封装(下)

    控件封装的部分说明

    可能有人觉得应该前后端分离,我也承认这是应该的方向,我们也在考虑使用ng2等简化前端。但是,我们封装控件还是因为如下原因综合考虑的:

    • 我们这是个框架,上面支撑了许多个应用,包含几百个页面,每个页面都去写一堆的jscsshtml标签可能对开发人员来说非常麻烦,且每个人写的都可能不一样。为了更简化中、低级开发人员的工作才提供的这种封装,个人认为这样才是简化和标准化开发的做法
    • 像我们这里有datatable、文件上传等的控件,datatable就包含分页、超链、排序、格式化等等,js非常复杂,附件上传更复杂,这个不做封装实在不方便使用
    • 我们后面有自定义表单、自定义数据查询等功能,控件都是通过拖拽生成的,必须使用封装方式

    其实呢,mvc也提供了html.textfor等写法,其中有的也封装了js的,甚至校验也是封装的js。本节内容进阶二,是直接使用cshtml,部分做到了前后端分离。当然了,如果有更好的建议和做法,欢迎提出来。

    看本篇之前,建议先看一下上一篇9.2.1 .net framework下的MVC 控件的封装(上)

    进阶一:For类型控件的做法

    我们在上一篇的最开始样例中,写了MVC控件的三种写法。

    1 @model UserInfo
    2 
    3  
    4 <input type="text" id="t2" value="t2Value" /> <!—第一种写法 -->
    5 
    6 @Html.TextBox("t1", "t1value"); <!—第二种写法 -->
    7 
    8 @Html.TextBoxFor(user => user.EMail) <!—第三种写法 -->

    第一种是html的原始写法,控件的封装不会用这种方法(.net core的taghelper就可以写成这样了)。在上一篇的介绍中,我们讲解了按照第二种样子来做控件的封装。但是当我们的控件要绑定cshtml页面的model属性时,要写成第三种方式,也就是写成类似@Html.TextboxFor(user => user.Email)的For写法。

    这里的user是指的UserInfo,这个写法就是在页面中生成Id为Email的input元素,值也自动填充UserInfo的实例的电子邮件地址EMail,并且如果UserInfo类的Email属性上有校验、显示名等Attribute时,可以自动生成校验脚本和标签。例如下面就是对UserName和Age两个属性增加显示名称和校验。

    1 [Display(Name="用户名")]
    2 [Required(ErrorMessage = "*姓名必填")]
    3 public string UserName { get; set; }
    4 
    5 [Display(Name = "年龄")]
    6 [Required(ErrorMessage = "*年龄必填")]
    7 public int Age { get; set; }

     这种写法,会在最终生成的html代码中,自动添加用户名和年龄的标签和校验脚本。

    既然这种写法可以自动绑定model,也可以自动生成标签和校验等,我们该如何实现For的写法呢?下面我们仍旧以下拉多选控件为例,进行说明。

    同样的,我们需要写一个MultiSelectFor的控件,这个For控件是个泛型类

     1     public class MultiSelectFor<TModel, TProperty> : MvcControlForBase<TModel, TProperty>
     2     {
     3         /// <summary>
     4         /// 初始化
     5         /// </summary>
     6         /// <param name="htmlHelper"></param>
     7         /// <param name="expression"></param>
     8         /// <param name="htmlAttributes"></param>
     9         public MultiSelectFor(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
    10             : base(htmlHelper, expression, htmlAttributes)
    11         {
    12             this.DataSource = new Dictionary<string, string>();
    13         }
    14         ……
    15     }

    继承泛型的控件基类MvcControlForBase<TModel, TProperty>,一样的也要有一个泛型的基控件构造类

    public abstract class MvcControlForBuilderBase<TModel, TProperty, TMvcControl, TBuilder>

      where TMvcControl : MvcControlForBase<TModel, TProperty>

      where TBuilder : MvcControlForBuilderBase<TModel, TProperty, TMvcControl, TBuilder>

    这两个类的实现与上一篇很类似。我们不再仔细介绍了,唯一需要注意的是控件基类MvcControlForBase<TModel, TProperty>的Name和Id是从表达式中获取的,而不是通过构造函数的参数传入的。因此构造函数传入的是Expression<Func<TModel, TProperty>>。控件的Name可以自动从Expression中获得,因此可以直接改为只读属性。

     1     protected string Name
     2     {
     3         get
     4         {
     5             if (Attributes.ContainsKey("name"))
     6             {
     7                 return Attributes["name"].ToString();
     8             }
     9             else
    10             {
    11                 return ExpressionHelper.GetExpressionText(Expression);
    12             }
    13         }
    14     }

    MultiSelectFor因为能够自动生成Id、Name、Value、Label等,因此构造函数中也就不再传入Label、value等参数,而是传入表达式Expression,这些数据应该直接从for的表达式中获取。为了实现获取value,我们需要在render方法中,增加下面的内容

    ModelMetadata metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(this.Expression, this.Helper.ViewData);

    List<string> value = metadata.Model as List<string>;

    以及生成select控件时,写法也更简单了,直接从表达式生成

    divTag.InnerHtml = Helper.DropDownListFor(Expression, selectList, HtmlAttributes).ToHtmlString();

    同样的,按照上一篇的做法,如果在cshtml页面中按照如下的方式使用multiselect控件(控件的含义是:选择一个人的多个职责,Duty是职责,一个人可能有多个职责,例如项目经理、高级开发工程师)

    @model UserInfo

    @HtmlHelper.MultiSelectFor(p => p.Duty).SetDataSource().Render()

    HtmlHelper扩展方法也需要增加一个:

    1     public static MultiSelectForBuilder<TModel, TProperty> MultiSelectFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes = null)
    2     {
    3         return new MultiSelectForBuilder<TModel, TProperty>(new MultiSelectFor<TModel, TProperty>(helper, expression, htmlAttributes));
    4     }

    进阶二:资源性视图的应用

    为了更简化WriteHtml和WriteScript的写法,我们将脚本和html元素都写到一个cshtml中,这样代码看起来更简洁,也更易读,也可以实现前后端的分离。我们以日期选择控件DatePicker为例,DataPicker.cshtml内容如下:

     1 @{
     2     string id = this.ViewData.GetString("Id");
     3     string name = this.ViewData.GetString("Name");
     4     string dateFormat = this.ViewData.GetString("DateFormat");
     5     bool readOnly = this.ViewData.GetBool("ReadOnly");
     6 }
     7 @if (!readOnly)
     8 {
     9     <script>
    10         require(['jquery', 'jquery.ui', 'ready!'], function ($) {
    11             var options = {
    12                 changeYear: true,
    13                 changeMonth: true,
    14                 dateFormat: "@(dateFormat.ToLower().Replace("yyyy", "yy"))"
    15             };
    16             $("#@(id)").datepicker(options);
    17         });
    18     </script>
    19 }

    写成这样的好处就是不要在WriteHtml和WriteScript中写一大堆的脚本、html标签的拼接程序。

    再回到DataPicker控件来,这样datapicker不需要再写WriteScript方法,仅仅重写WriteHtml方法,这个方法主要工作也简化为:传入ViewData,然后调用部分视图DataPicker.cshtml:

    1 ViewDataDictionary vdata = new ViewDataDictionary();
    2 
    3 vdata["Id"] = this.Id;
    4 vdata["Name"] = this.Name;
    5 vdata["DateFormat"] = this.DateFormat;
    6 vdata["ReadOnly"] = (this.DisplayStatus == FieldDisplayStatus.ReadOnly);
    7 
    8 writer.Write(Helper.Partial("MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker", vdata));

    这里需要注意的就是Helper.Partial方法,我们这里调用的是这个重载方法

     1 //
     2 // 摘要:
     3 //     以 HTML 编码字符串的形式呈现指定的分部视图。
     4 //
     5 // 参数:
     6 //   htmlHelper:
     7 //     此方法扩展的 HTML 帮助器实例。
     8 //
     9 //   partialViewName:
    10 //     要呈现的分部视图的名称。
    11 //
    12 //   viewData:
    13 //     用于分部视图的视图数据字典。
    14 //
    15 // 返回结果:
    16 //     以 HTML 编码字符串形式呈现的分部视图。
    17 public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, ViewDataDictionary viewData);

    Helper.Partial的参数是分部视图名称,但是为什么是MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker这样的写法呢?

    我们前面提到,我们又更近一步做了封装,将这些控件都放到一个项目中,打包成应用程序集给各个项目使用。控件使用的cshtml也会被打包进应用程序集中,做法就是将cshtml文件属性设置为嵌入的资源,编译时就会以资源的方式直接打包在dll中。当然了,也可以将控件的cshtml复制到Web项目中,但是这种做法确实有些土,我们用的是更高大上的做法J

    打包进应用程序集的文件的写法就是这样的,类名的全名称,例如这里的DataPicker.cshtml就是MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker。

    但是怎么通过MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker来定位DataPicker.cshtml呢?这涉及到页面查找的问题,说来话长了。

    一般情况下,Razor视图引擎的基类是RazorViewEngine,继承于抽象类型BuildManagerViewEngine,再继承自VirtualPathProviderViewEngine。ViewEngine中的方法FindView和FindPartialView是按照如下的目录进行搜索的:

    • ~/Views/{ControllerName}/{ViewName}.cshtml
    • ~/Views/Shared/{ViewName}.cshtml
    • ~/Areas/{AreaName}/Views/{ControllerName}/{ViewName}.cshtml
    • ~/Areas/{AreaName}/Views/ Shared /{ViewName}.cshtml

    我们可以继承VirtualPathProviderViewEngine,重写FindView和FindPartialView修改上面列表,以适应自己系统的目录搜索要求。

    这种方式对于文件系统方式下的目录搜索是可以的,但是如果视图文件存放在数据库、Dll的资源中,这种方式就不好使了。这时候,应该是使用更底层的VirtualPathProvider。MSDN中的说明是这样的:VirtualPathProvider提供了一组可让 Web 应用程序从虚拟文件系统中检索资源的方法。我们这里就是通过修改它来完成从Dll的资源中获取视图文件。

    在Global.asax中的写法如下:

    1 var embeddedViewResolver = new EmbeddedViewResolver();
    2 var viewTable = embeddedViewResolver.GetEmbeddedViews();
    3 var embeddedProvider = new EmbeddedViewVirtualPathProvider(viewTable);
    4 HostingEnvironment.RegisterVirtualPathProvider(embeddedProvider);

    第一行,创建资源性视图的解析类,这个类初始化时,先找到控件所在的程序集。

    1     public class EmbeddedViewResolver : IEmbeddedViewResolver
    2     {
    3         private IList<Assembly> assemblies;
    4 
    5         public EmbeddedViewResolver() {
    6             this.assemblies = new List<Assembly>() { Assembly.GetAssembly(typeof(MvcControlBase)) };
    7         }
    8     }

    第二行,解析类从控件所在程序集中找到所有的资源行视图,存放到EmbeddedViewTable中。EmbeddedViewTable相当与一个Dictionary,存放了ViewName和ViewMetaData的键值对。EmbeddedViewTable和ViewMetaData相对比较简单,不做介绍了。

     1         public EmbeddedViewTable GetEmbeddedViews()
     2         {
     3             if (assemblies == null || assemblies.Count == 0) return null;
     4 
     5             var table = new EmbeddedViewTable();
     6 
     7             foreach (var assembly in assemblies)
     8             {
     9                 var names = GetNamesOfAssemblyResources(assembly);
    10                 if (names == null || names.Length == 0) continue;
    11 
    12                 foreach (var name in names)
    13                 {
    14                     var key = name.ToLowerInvariant();
    15                    
    16                     if (!key.Contains(".views.")) continue; //要求所有的资源性视图都应该在views目录下
    17 
    18                     table.AddView(name, assembly.FullName);
    19                 }
    20             }
    21 
    22             return table;
    23         }
    24 
    25         private string[] GetNamesOfAssemblyResources(Assembly assembly)
    26         {
    27             try
    28             {
    29                 return assembly.GetManifestResourceNames();
    30             }
    31             catch
    32             {
    33                 return new string[] { };
    34             }
    35         }

    第三行,进入正题了,创建继承于VirtualPathProvider的资源性视图提供器EmbeddedViewVirtualPathProvider。重写GetFile,如果是资源性视图,去EmbeddedViewTable中获取视图,否则调用Previous.GetFile(virtualPath)继续系统缺省方式。同时也重写FileExists和GetCacheDependency方法。

     1     public class EmbeddedViewVirtualPathProvider : VirtualPathProvider
     2     {
     3         private readonly EmbeddedViewTable _embeddedViews;
     4 
     5         public EmbeddedViewVirtualPathProvider(EmbeddedViewTable embeddedViews)
     6         {
     7             MicroLibraryExceptionHelper.IsNull(embeddedViews, this.GetType().FullName, TraceLogType.Error, "embeddedViews为空");
     8 
     9             this._embeddedViews = embeddedViews;
    10         }
    11 
    12         private bool IsEmbeddedView(string virtualPath)
    13         {
    14             if (string.IsNullOrEmpty(virtualPath))
    15                 return false;
    16 
    17             string virtualPathAppRelative = VirtualPathUtility.ToAppRelative(virtualPath);
    18             if (!virtualPathAppRelative.StartsWith("~/Views/", StringComparison.InvariantCultureIgnoreCase))
    19                 return false;
    20             
    21             var fullyQualifiedViewName = virtualPathAppRelative.Substring(virtualPathAppRelative.LastIndexOf("/") + 1, virtualPathAppRelative.Length - 1 - virtualPathAppRelative.LastIndexOf("/"));
    22 
    23             bool isEmbedded = _embeddedViews.ContainsEmbeddedView(fullyQualifiedViewName);
    24             return isEmbedded;
    25         }
    26 
    27         public override bool FileExists(string virtualPath)
    28         {
    29             return (IsEmbeddedView(virtualPath) ||
    30                     Previous.FileExists(virtualPath));
    31         }
    32 
    33         public override VirtualFile GetFile(string virtualPath)
    34         {
    35             if (IsEmbeddedView(virtualPath))
    36             {
    37                 string virtualPathAppRelative = VirtualPathUtility.ToAppRelative(virtualPath);
    38                 var fullyQualifiedViewName = virtualPathAppRelative.Substring(virtualPathAppRelative.LastIndexOf("/") + 1, virtualPathAppRelative.Length - 1 - virtualPathAppRelative.LastIndexOf("/"));
    39 
    40                 var embeddedViewMetadata = _embeddedViews.FindEmbeddedView(fullyQualifiedViewName);
    41                 return new EmbeddedResourceVirtualFile(embeddedViewMetadata, virtualPath);
    42             }
    43 
    44             return Previous.GetFile(virtualPath);
    45         }
    46 
    47         public override CacheDependency GetCacheDependency(
    48             string virtualPath,
    49             IEnumerable virtualPathDependencies,
    50             DateTime utcStart)
    51         {
    52             return IsEmbeddedView(virtualPath)
    53                 ? null : Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    54         }
    55     }

    大家可能注意到GetFile返回的是EmbeddedResourceVirtualFile这个类,这个类继承自VirtualFile,主要的方法是Open,就是从应用程序集的资源中返回当前资源性视图的stream。

     1     public class EmbeddedResourceVirtualFile : VirtualFile
     2     {
     3         private readonly EmbeddedViewMetadata _embeddedViewMetadata;
     4 
     5         public EmbeddedResourceVirtualFile(EmbeddedViewMetadata embeddedViewMetadata, string virtualPath)
     6             : base(virtualPath)
     7         {
     8             MicroLibraryExceptionHelper.IsNull(embeddedViewMetadata, this.GetType().FullName, TraceLogType.Error, "embeddedViewMetadata 为空");
     9 
    10             this._embeddedViewMetadata = embeddedViewMetadata;
    11         }
    12 
    13         public override Stream Open()
    14         {
    15             Assembly assembly = GetResourceAssembly();
    16             return assembly == null ? null : assembly.GetManifestResourceStream(_embeddedViewMetadata.Name);
    17         }
    18 
    19         private Assembly GetResourceAssembly()
    20         {
    21             return AppDomain.CurrentDomain.GetAssemblies()
    22                 .Where(assembly => string.Equals(assembly.FullName, _embeddedViewMetadata.AssemblyFullName, StringComparison.InvariantCultureIgnoreCase))
    23                 .FirstOrDefault();
    24         }
    25     }

    第四行,在系统中注册我们的EmbeddedViewVirtualPathProvider。

    通过上面的四个步骤,就可以完成资源性视图的解析工作了,通过MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker来定位cshtml也得以实现。

    面向云的.net core开发框架

  • 相关阅读:
    静态主席树
    uva 11107 Life Forms
    codeforce 605B. Lazy Student
    codeforce 606B Testing Robots
    codeforce 606C
    codeforce 606A
    uva 11019 Matrix Matcher
    uva 11468 Substring
    uvalive 4670 Dominating Patterns
    codeforce 603B
  • 原文地址:https://www.cnblogs.com/BenDan2002/p/6129395.html
Copyright © 2011-2022 走看看