MVC 用扩展方法执行自定义视图,替代 UIHint
项目中用了 Bootstrap , 这样就不用写太多的CSS了,省去很多事情。
但是这个业务系统需要输入的地方很多,每个表都有100多个字段,每个页面需要大量的表单。
把这些表单按 bootstrap 的格式写出来,也是件头痛的事情。
我想到模板,EditorTemplates UIHint, 但是 UIHint 需要用 Metadata 标注,一个一个的加,也是不现实的。
还有别外一种办法,就是扩展 HtmlHelper。
要用HtmlHelper ,大家可能就想到了 TagBuilder 了,TagBuilder 基本全是 Hard code 了,不方便调整显示格式。
最终我用了另外一种办法:
在 HtmlHelper 扩展里,取自定义的视图,视图可以随时改,又不用每个字段去加 UIHint.
1 public static MvcHtmlString EditorBlockAFor<TModel, TProperty>(this HtmlHelper<TModel> helper, string template, Expression<Func<TModel, TProperty>> property, bool withLabel, string containerClass = "col-xs-4", object htmlAttributes = null) { 2 3 var body = (MemberExpression)property.Body; 4 if (body == null) 5 throw new ArgumentException(); 6 7 var ctx = helper.ViewContext.Controller.ControllerContext; 8 var result = ViewEngines.Engines.FindPartialView(helper.ViewContext.Controller.ControllerContext, template); 9 if (result.View != null) { 10 11 var metadata = ModelMetadata.FromLambdaExpression(property, helper.ViewData); 12 //var model = metadata.Model; 13 14 var attrs = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); 15 16 using (var writer = new StringWriter(CultureInfo.CurrentCulture)) { 17 var vctx = new ViewContext(ctx, result.View, helper.ViewData, helper.ViewContext.TempData, writer); 18 vctx.ViewBag.PropertyName = string.Join(".", body.ToString().Split(new char[] { '.' }).Skip(1));//body.Member.Name; 19 20 vctx.ViewBag.ContainerClass = containerClass; 21 vctx.ViewBag.IsRequired = metadata.IsRequired; 22 vctx.ViewBag.HtmlAttributes = attrs; 23 vctx.ViewBag.WithLabel = withLabel; 24 25 vctx.ViewBag.DisplayName = metadata.DisplayName ?? metadata.PropertyName; 26 27 result.View.Render(vctx, writer); 28 29 return MvcHtmlString.Create(writer.ToString()); 30 } 31 } else { 32 throw new InvalidOperationException(string.Format("particle view {0} not found", template)); 33 } 34 }
这个是核心,其它的扩展都是调用这个方法,比如:
1 public static MvcHtmlString TextBlockFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> property, string containerClass = "col-lg-4 col-md-4 col-xs-4", object htmlAttributes = null, bool withLabel = true) { 2 return EditorBlockAFor(helper, "TextBlock", property, withLabel, containerClass, htmlAttributes); 3 }
在 EditorBlockAFor 这个方法里,有一句:
var result = ViewEngines.Engines.FindPartialView(helper.ViewContext.Controller.ControllerContext, template);
这个是去查找指定的自定义视图,它就是整个思路的关键。
自定义视图 TextBlock.cshtml
@model object @{ this.Layout = null; string propertyExpression = ViewBag.PropertyName; string containerClass = ViewBag.ContainerClass; RouteValueDictionary htmlAttributes = SharedTemplatesHelper.MargeClass(ViewBag); } <div class="@containerClass"> @SharedTemplatesHelper.Label(ViewBag) @Html.TextBox(propertyExpression, null, htmlAttributes) </div>
注意,模型是object , 因为不确定模型的类型。
这个文件需要放到 Shared 下,这个是简单的结构。
调用:
@Html.TextBlockFor(m => m.VesselInfo.VESSEL_NAME_CN, "col-lg-2 col-md-2")
最终会生成这样一段HTML:
<div class="col-lg-2 col-md-2"> <span class="help-block">中文名称 <span class="red">*</span> </span> <input class="form-control input-sm" data-val="true" data-val-length="The field 中文名称 must be a string with a maximum length of 500." data-val-length-max="500" data-val-required="The 中文名称 field is required." id="VesselInfo_VESSEL_NAME_CN" name="VesselInfo.VESSEL_NAME_CN" type="text" value="" /> </div>
长这个样子:
--------------分隔线内是废话,有兴趣可以了解一下我的崩溃经历-------------------
一切都按照设想的样子,直到。。。。
有一天,同事说明明是 Required 的,为什么没有执行验证?
我简单的看了一下,是因为没有生成 data-xxx 这样的验证属性。看了一下 Action ,就是 Return View(); 没有传递 model 到视图。
声明了一个 model 传给视图 (return View(XXX);) 后,一切正常。
我做MVC3的时候,不给 Model 都会输出验证属性,当时赶时间,没有去深入研究为什么,还以为 MVC 5 的新特性呢。
国庆过7天猪一样的生活,项目也进行的七七八八了,终于有时间回头看看了。
下了MVC的最新源码:
http://aspnetwebstack.codeplex.com/SourceControl/latest
版本号是 5.2.3.0, 我们项目中用的是 5.2.0.0 ,差别不大。
新建了一个测试项目,
编译了一份MVC的DLL,连同PDB一起放到项目的引用目录下,改了一下MVC的配置、引用,在 InputExtensions 下加了断点,但是却无法断点。
按照网上的搜到的调试 MVC 源码的方法去做,很不幸,没有一个适用的。
搞到晚上8点多,还是没有办法调试进源码。真是崩溃至极了。
又找到了篇 pdb 符号服务器的博文:
http://weblogs.asp.net/gunnarpeipman/stepping-into-asp-net-mvc-source-code-with-visual-studio-debugger
但是需要下载这些符号,电脑没关,回去了。
今天按照博文的说明去做,仍然不能调试进源码。
很有幸,找到另外一篇文:
http://blogs.msdn.com/b/micl/archive/2014/06/07/how-to-debug-your-code-with-mvc-fresh-source-code.aspx
Before each version of MVC launch, the contribute team always strong name each MVC related assembly by a specified keyfile 35MSSharedLib1024.snk which is located in tools folder to prevent assembly tamper. But the snk file that you get doesn't contain private key, that you can only delay signed all assemblies if you compile directly. Unfortunately, delay signed assembly doesn't support debug feature.
简单的翻译一下:
因为我们下载到的 MVC 源码是经过签名的,源码里提供的密钥不包含私钥,只能是延迟签名。很不幸,延迟答名是不能DEBUG 的。
(这方面我没有经验,不懂)
按照博文的做法,将相关的项目都去掉了签名:
System.Web.Helpers
System.Web.Mvc
System.Web.Razor
System.web.WebPages
等
然后将 System.Web.WebPages 下的
AssemblyInfo.cs (在 Properties 下) 中的 InternalsVisibleTo 参数换成不带版本号的。
[assembly: InternalsVisibleTo("System.Web.Mvc")]
[assembly: InternalsVisibleTo("System.Web.Helpers")]
编译,修改测试项目的的 web.config ,将 System.Web.Mvc 的版本改成编译的版本号,删除原来的相关引用,添加为刚刚编译过的相关DLL
加断点,运行,调试进去了。
--------------分隔线-------------------
上面都是废话。
调了一圈,发现在 ModelMetadata.cs 第 382 行:
ModelMetadata propertyMetadata = viewData.ModelMetadata.Properties.Where(p => p.PropertyName == expression).FirstOrDefault();
匹配不到 property, propertyMetadata 的结果是 null.
属性明明是有的,就死活就是没有属性的 Metadata.
当快速监视 viewData.ModelMetadata 后,这个值又有了,说明问题出在这个 viewData.ModelMetadata 上。
在 ViewDataDictionary.cs ,定义的是 ViewDataDictionary , 第82行,
if (_modelMetadata == null && _model != null)
_model 无疑是传到视图里的 model
当 _modelMetadata = null 且 _model 不为 null 的时候,会去取模型的Metadata ,这就解释了为什么当传 Model 到视图的时候,会输出验证属性了。
当是不传 Model 到视图,就返回 null 了。
进入 ViewDataDictionaryOfModel.cs
定义的是 ViewDataDictionary<TModel> ,继承自 ViewDataDictionary
在 第35行,取父类的 ModelMetadata, 因为返回的是 null, 又去按 模型(Model) 的类型去取 Metadata.
接上面的自定义视图,TextBlock.cshtml
@model object
...
...
指定的模型类型是 object , 这是因为它是 Shared 的,不确定模型的确切类型,所以我用了 object.
好了,问题来了,挖掘机哪家强?按 object 去取 Metadata 当然是取不到的!
转了一转,又把我逼到当初处理这个东西的原点上。
想到快速监视后,是可以取到想要的结果的,我把上面的扩展方法改加一句:
。。。
var vctx = new ViewContext(ctx, result.View, helper.ViewData, helper.ViewContext.TempData, writer);
vctx.ViewData.ModelMetadata = helper.ViewData.ModelMetadata;//////必须的,调了很长时间,定位问题在这个 ModelMetadata 上
。。。
运行,通过!