控件封装的部分说明

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

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

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

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

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

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

 @model UserInfo

 <input type="text" id="t2" value="t2Value" /> <!—第一种写法 -->

 @Html.TextBox("t1", "t1value"); <!—第二种写法 -->

 @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两个属性增加显示名称和校验。

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

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

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

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

     public class MultiSelectFor<TModel, TProperty> : MvcControlForBase<TModel, TProperty>
{
/// <summary>
/// 初始化
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="expression"></param>
/// <param name="htmlAttributes"></param>
public MultiSelectFor(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
: base(htmlHelper, expression, htmlAttributes)
{
this.DataSource = new Dictionary<string, string>();
}
……
}

继承泛型的控件基类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中获得,因此可以直接改为只读属性。

     protected string Name
{
get
{
if (Attributes.ContainsKey("name"))
{
return Attributes["name"].ToString();
}
else
{
return ExpressionHelper.GetExpressionText(Expression);
}
}
}

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扩展方法也需要增加一个:

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

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

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

 @{
string id = this.ViewData.GetString("Id");
string name = this.ViewData.GetString("Name");
string dateFormat = this.ViewData.GetString("DateFormat");
bool readOnly = this.ViewData.GetBool("ReadOnly");
}
@if (!readOnly)
{
<script>
require(['jquery', 'jquery.ui', 'ready!'], function ($) {
var options = {
changeYear: true,
changeMonth: true,
dateFormat: "@(dateFormat.ToLower().Replace("yyyy", "yy"))"
};
$("#@(id)").datepicker(options);
});
</script>
}

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

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

 ViewDataDictionary vdata = new ViewDataDictionary();

 vdata["Id"] = this.Id;
vdata["Name"] = this.Name;
vdata["DateFormat"] = this.DateFormat;
vdata["ReadOnly"] = (this.DisplayStatus == FieldDisplayStatus.ReadOnly); writer.Write(Helper.Partial("MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker", vdata));

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

 //
// 摘要:
// 以 HTML 编码字符串的形式呈现指定的分部视图。
//
// 参数:
// htmlHelper:
// 此方法扩展的 HTML 帮助器实例。
//
// partialViewName:
// 要呈现的分部视图的名称。
//
// viewData:
// 用于分部视图的视图数据字典。
//
// 返回结果:
// 以 HTML 编码字符串形式呈现的分部视图。
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中的写法如下:

 var embeddedViewResolver = new EmbeddedViewResolver();
var viewTable = embeddedViewResolver.GetEmbeddedViews();
var embeddedProvider = new EmbeddedViewVirtualPathProvider(viewTable);
HostingEnvironment.RegisterVirtualPathProvider(embeddedProvider);

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

     public class EmbeddedViewResolver : IEmbeddedViewResolver
{
private IList<Assembly> assemblies; public EmbeddedViewResolver() {
this.assemblies = new List<Assembly>() { Assembly.GetAssembly(typeof(MvcControlBase)) };
}
}

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

         public EmbeddedViewTable GetEmbeddedViews()
{
if (assemblies == null || assemblies.Count == ) return null; var table = new EmbeddedViewTable(); foreach (var assembly in assemblies)
{
var names = GetNamesOfAssemblyResources(assembly);
if (names == null || names.Length == ) continue; foreach (var name in names)
{
var key = name.ToLowerInvariant(); if (!key.Contains(".views.")) continue; //要求所有的资源性视图都应该在views目录下 table.AddView(name, assembly.FullName);
}
} return table;
} private string[] GetNamesOfAssemblyResources(Assembly assembly)
{
try
{
return assembly.GetManifestResourceNames();
}
catch
{
return new string[] { };
}
}

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

     public class EmbeddedViewVirtualPathProvider : VirtualPathProvider
{
private readonly EmbeddedViewTable _embeddedViews; public EmbeddedViewVirtualPathProvider(EmbeddedViewTable embeddedViews)
{
MicroLibraryExceptionHelper.IsNull(embeddedViews, this.GetType().FullName, TraceLogType.Error, "embeddedViews为空"); this._embeddedViews = embeddedViews;
} private bool IsEmbeddedView(string virtualPath)
{
if (string.IsNullOrEmpty(virtualPath))
return false; string virtualPathAppRelative = VirtualPathUtility.ToAppRelative(virtualPath);
if (!virtualPathAppRelative.StartsWith("~/Views/", StringComparison.InvariantCultureIgnoreCase))
return false; var fullyQualifiedViewName = virtualPathAppRelative.Substring(virtualPathAppRelative.LastIndexOf("/") + , virtualPathAppRelative.Length - - virtualPathAppRelative.LastIndexOf("/")); bool isEmbedded = _embeddedViews.ContainsEmbeddedView(fullyQualifiedViewName);
return isEmbedded;
} public override bool FileExists(string virtualPath)
{
return (IsEmbeddedView(virtualPath) ||
Previous.FileExists(virtualPath));
} public override VirtualFile GetFile(string virtualPath)
{
if (IsEmbeddedView(virtualPath))
{
string virtualPathAppRelative = VirtualPathUtility.ToAppRelative(virtualPath);
var fullyQualifiedViewName = virtualPathAppRelative.Substring(virtualPathAppRelative.LastIndexOf("/") + , virtualPathAppRelative.Length - - virtualPathAppRelative.LastIndexOf("/")); var embeddedViewMetadata = _embeddedViews.FindEmbeddedView(fullyQualifiedViewName);
return new EmbeddedResourceVirtualFile(embeddedViewMetadata, virtualPath);
} return Previous.GetFile(virtualPath);
} public override CacheDependency GetCacheDependency(
string virtualPath,
IEnumerable virtualPathDependencies,
DateTime utcStart)
{
return IsEmbeddedView(virtualPath)
? null : Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}

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

     public class EmbeddedResourceVirtualFile : VirtualFile
{
private readonly EmbeddedViewMetadata _embeddedViewMetadata; public EmbeddedResourceVirtualFile(EmbeddedViewMetadata embeddedViewMetadata, string virtualPath)
: base(virtualPath)
{
MicroLibraryExceptionHelper.IsNull(embeddedViewMetadata, this.GetType().FullName, TraceLogType.Error, "embeddedViewMetadata 为空"); this._embeddedViewMetadata = embeddedViewMetadata;
} public override Stream Open()
{
Assembly assembly = GetResourceAssembly();
return assembly == null ? null : assembly.GetManifestResourceStream(_embeddedViewMetadata.Name);
} private Assembly GetResourceAssembly()
{
return AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => string.Equals(assembly.FullName, _embeddedViewMetadata.AssemblyFullName, StringComparison.InvariantCultureIgnoreCase))
.FirstOrDefault();
}
}

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

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

面向云的.net core开发框架

9.2.2 .net framework下的MVC 控件的封装(下)的更多相关文章

  1. 9.2.1 .net framework下的MVC 控件的封装(上)

    在写.net core下mvc控件的编写之前,我先说一下.net framework下我们MVC控件的做法. MVC下控件的写法,主要有如下三种,最后一种是泛型的写法,mvc提供的控件都是基本控件. ...

  2. 用MVC的辅助方法自定义了两个控件:“可编辑的下拉框控件”和“文本框日历控件”

    接触MVC也没多长时间,一开始学的时候绝得MVC结构比较清晰.后来入了门具体操作下来感觉MVC控件怎么这么少还不可以像ASP.net form那样拖拽.这样设计界面来,想我种以前没学过JS,Jquer ...

  3. 如何自定义MVC控件?

    今天公司要写学习总结,想着想着还是先写一篇关于MVC内部什么东东的博客整理整理再发表吧,一举两得. 之前写过了路由.过滤器等.今天就研究一下怎么自定义MVC控件吧. 本人技术小菜,不喜勿喷.....( ...

  4. android官方下拉刷新控件SwipeRefreshLayout的使用

    可能开发安卓的人大多数都用过很多下拉刷新的开源组件,但是今天用了官方v4支持包的SwipeRefreshLayout觉得效果也蛮不错的,特拿出来分享. 简介:SwipeRefreshLayout组件只 ...

  5. 分享一个 C# Winfrom 下的 OutlookBar 控件的使用

    最近在上网的时候,发现了这个C# 下的 OutlookBar 控件,看了一下感觉还真不错,特此记录一下. using System; using System.Drawing; using Syste ...

  6. [Android]下拉刷新控件RefreshableView的实现

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4172483.html 需求:自定义一个ViewGroup,实现 ...

  7. android SwipeRefreshLayout google官方下拉刷新控件

    下拉刷新功能之前一直使用的是XlistView很方便我前面的博客有介绍 SwipeRefreshLayout是google官方推出的下拉刷新控件使用方法也比较简单 今天就来使用下SwipeRefres ...

  8. bootstrap日期控件在火狐下的模态框中选择时间下拉菜单无效的解决办法

    今天收到程序组提交的一个兼容BUG,在火狐中使用模态框加载日期控件时选择时间下拉菜单没有效果(不能点击),而在谷歌中却是好的, 排错思路:1,在当前页面主层放置一个时间控件,测试通过 2,在ajax加 ...

  9. c# 遍历子控件,比如Form下的group,或者panel

    方法很好用.目的是遍历所有容器的子控件... 方法1private void GetControl(Control.ControlCollection ctc, ref int checkNull) ...

随机推荐

  1. CSS3 @keyframes 动画

    CSS3的@keyframes,它可以取代许多网页动画图像,Flash动画,和JAVAScripts. CSS3的动画属性 下面的表格列出了 @keyframes 规则和所有动画属性: 浏览器支持 表 ...

  2. arcgis api for js入门开发系列四地图查询(含源代码)

    备注:由于实现本篇功能的需求,修改了地图数据的dlsearch.mxd,然后更新了地图服务,需要的在文章最后有提供最新的mxd以及源代码下载的 上一篇实现了demo的地图工具栏,本篇新增地图查询功能, ...

  3. Android开发学习—— Broadcast广播接收者

    现实中:电台要发布消息,通过广播把消息广播出去,使用收音机,就可以收听广播,得知这条消息.Android中:系统在运行过程中,会产生许多事件,那么某些事件产生时,比如:电量改变.收发短信.拨打电话.屏 ...

  4. 超全面的.NET GDI+图形图像编程教程

    本篇主题内容是.NET GDI+图形图像编程系列的教程,不要被这个滚动条吓到,为了查找方便,我没有分开写,上面加了目录了,而且很多都是源码和图片~ (*^_^*) 本人也为了学习深刻,另一方面也是为了 ...

  5. 洛谷P1547 Out of Hay

    题目背景 奶牛爱干草 题目描述 Bessie 计划调查N (2 <= N <= 2,000)个农场的干草情况,它从1号农场出发.农场之间总共有M (1 <= M <= 10,0 ...

  6. iOS开发系列文章(持续更新……)

    iOS开发系列的文章,内容循序渐进,包含C语言.ObjC.iOS开发以及日后要写的游戏开发和Swift编程几部分内容.文章会持续更新,希望大家多多关注,如果文章对你有帮助请点赞支持,多谢! 为了方便大 ...

  7. 自己开发实现OAuth做webapi认证

    看到园子里面有人写的OAuth,就想把自己实现的OAuth也分享一下,关于OAuth协议这里就不再赘述. 一.作为认证服务器,首先需要提供一个可以通过appid/appsecret来获取token这样 ...

  8. ABP源码分析十六:DTO的设计

    IDTO:空接口,用于标注Dto对象. ComboboxItemDto:用于combobox/list中Item的DTO NameValueDto<T>/NameValueDto:用于na ...

  9. 有意思的记录-Java

    1.文件读取 项目外的绝对路径或相对路径文件读取 String path = "/xx/xx.txt"; BufferedReader reader = new BufferedR ...

  10. 用Hibernate和Struts2+jsp实现分页查询、修改删除

    1.首先用get的方法传递一个页数过去 2.通过Struts2跳转到Action 3.通过request接受主页面index传过的页数,此时页数是1, 然后调用service层的方法获取DAO层分页查 ...