控件封装的部分说明

可能有人觉得应该前后端分离,我也承认这是应该的方向,我们也在考虑使用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. iOS之App Store上架被拒Legal - 5.1.5问题

    今天在看到App Store 上架过程中,苹果公司反馈的拒绝原因发现了这么一个问题: Legal - 5.1.5 Your app uses background location services ...

  2. CocoaPods的安装、使用、以及遇到的问题

    CocoaPods是什么? 当你开发iOS应用时,会经常使用到很多第三方开源类库,比如JSONKit,AFNetWorking等等.可能某个类库又用到其他类库,所以要使用它,必须得另外下载其他类库,而 ...

  3. windows 7(32/64位)GHO安装指南(U盘引导篇)~

    上一篇我们说了怎么制作U盘启动盘,那么这一篇让我们来看看如何进行正确的U盘引导启动. 现在的个人计算机一般分为台式机和笔记本,由于各厂商的喜好不同(开玩笑的啦),所以对于主板的BIOS设置各所不同.进 ...

  4. 第14章 Linux启动管理(2)_启动引导程序grub

    2. 启动引导程序grub 2.1 Grub配置文件 (1)grub中分区的表示 硬盘 分区 Linux设备文件名 Grub中设备文件名 第1块SCSI硬盘 第1个主分区 /dev/sda1 hd(0 ...

  5. 将MPM雪模拟移植到Maya

    同事实现了一个迪士尼的MPM雪模拟论文,我将其移植到Maya中 论文题目是 A material point method for snow simulation 代码在这里: https://git ...

  6. 【腾讯优测干货分享】如何降低App的待机内存(四)——进阶:内存原理

    本文来自于腾讯优测公众号(wxutest),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/3FTPFvZRqyAQnU047kmWJQ 1.4进阶:内存原理 在 ...

  7. 纯JS打造比QQ空间更强大的图片浏览器-支持拖拽、缩放、过滤、缩略图等

    在线演示地址(打开网页后,点击商家图册): http://www.sport7.cn/cc/jiangnan/football5.html 先看一看效果图: 该图片浏览器实现的功能如下: 1. 鼠标滚 ...

  8. ASP.NET Web API WebHost宿主环境中管道、路由

    ASP.NET Web API WebHost宿主环境中管道.路由 前言 上篇中说到ASP.NET Web API框架在SelfHost环境中管道.路由的一个形态,本篇就来说明一下在WebHost环境 ...

  9. atitit 商业项目常用模块技术知识点 v3 qc29

    atitit 商业项目常用模块技术知识点 v3 qc29 条码二维码barcodebarcode 条码二维码qrcodeqrcode 条码二维码dm码生成与识别 条码二维码pdf147码 条码二维码z ...

  10. python code

    执行动态语句 执行字符串中的代码 http://www.cnblogs.com/fanweibin/p/5418817.html #!usr/bin/env python #coding:utf-8 ...