1、(四) View/Model 全解(mvc)转自:http:/ Action 中向 View 传递 Model 的几种方式.以及 View 获取 Model 以后如何编写显示逻辑.还详细的介绍了 ASP.NET MVC 框架提供的 Html Helper 类的使用及如何为 Html Helper 类添加自定义扩展方法.二.承上启下上一篇文章中我们学习了 Controller 处理一次请求的全过程.在 Controller 的 Action 中, 会传递数据给 View,还会通知 View 对象开始显示.所以 Model 是在 Action 中获取的, 并由Action 传递给 View. Vi
2、ew 对象接到 Action 通知后会使用自己的显示逻辑展示页面.下面首先让我们学习如何将 Model 传递给 View 对象.三.传递数据给 View在 MVC 中,Model 对象是指包含了数据的模型. Controller 将 Model 传递给 View 以后, View 对象中不应该做任何的业务逻辑处理 , 仅仅根据 Model 对象做一些显示逻辑的处理.传递 Model 对象时, 我们有两种选择:1.传递一个弱类型的集合, 即成员为 object 类型的集合, 在 View 中需要将每个成员转换成我们需要的类型,比如 int, string,自定义类型等.2.传递强类型对象, 这些
3、类型是我们自定义的. 在 View 中直接使用我们传递的强类型对象, 不需要再转换类型.如果让我们自己设计一个 MVC 框架, 我们也会想到上面两种实现方式,接下来看看在ASP.NET MVC 中的实现.1.传递弱类型的集合(1) 如何传递ASP.NET MVC 框架定义了 ViewContext 类, 直译后是“View 上下文“, 其中保存和 View 有关的所有数据, 其中 Model 对象也封装在了此类型中.ViewContext 对象包含三个属性: IView View ViewDataDictionary ViewData TempDataDictionary TempData其中
4、 ViewData 集合和 TempData 集合都是用来保存 Model 对象的.在一个 Controller 的Action 中, 我们可以用如下方式为这两个集合赋值 :/ / 传递弱类型 Model 的 Action 示例 / / ViewResult public ActionResult WeakTypedDemo() ViewData“model“ = “Weak Type Data in ViewData“; TempData“model“ = “Weak Type Data in TempData“; return View(“WeakTypedDemo“); 在页面中, 是用
5、如下方式使用这两个集合:(2) 传递过程请注意 Action 中的赋值语句实际上 操作的是 Controller 类的 ViewData 和 TempData属性, 此时并没有任何的数据传递.上一篇文章中我们已经学到, return View 语句会返回一个ViewResult 对象 , 并且接下来要执行 ViewResult 的 Executeresult 方法. Controller 的View 方法会将 Controller 类的 ViewData 和 TempData 属性值传递给 ViewResult,代码如下:protected internal virtual ViewResul
6、t View(IView view, object model) if (model != null) ViewData.Model = model; return new ViewResult View = view, ViewData = ViewData, TempData = TempData ; 然后在 ViewResult 中根据 ViewData 和 TempData 构建 ViewContext 对象:public override void ExecuteResult(ControllerContext context) /. ViewContext viewContext
7、= new ViewContext(context, View, ViewData, TempData); View.Render(viewContext, context.HttpContext.Response.Output); /. ViewContext 对象最终会传递给 ViewPage, 也就是说 ViewData 和 TempData 集合传递到了 ViewPage. 我这里简化了最后的传递流程, 实际上 ViewData 对象并不是通过ViewContext 传递到 ViewPage 中的, ViewPage 上的 ViewData 是一个单独的属性, 并没有像 TempDat
8、a 一样其实访问的是 ViewContext.TempData. 这么做容易产生奇异, 本类ViewContext 是一个很好理解职责十分清晰的类. 作为使用者的我们暂时可以忽略这点不足 , 因为如此实现 ViewData 完全是为了下面使用强类型对象 .(3)ViewData 和 TempData 的区别虽然 ViewData 和 TempData 都可以传递弱类型数据,但是两者的使用是有区别的: ViewData 的生命周期和 View 相同, 只对当前 View 有效. TempData 保存在 Session 中, Controller 每次执行请求的时候会从 Session 中获取
9、TempData 并删除 Session, 获取完 TempData 数据后虽然保存在内部的字典对象中,但是 TempData 集合的每个条目访问一次后就从字典表中删除. 也就是说TempData 的数据至多只能经过一次 Controller 传递.(4) TempData 的实现TempData 的类型是 TempDataDictionary, 和一般的字典表没有明显的不同, TempData的生命周期是由 Controll 决定的.在所有 Controll 的基类 ControllerBase 中, 创建了类型为 TempDataDictionary 的TempData 属性.在 Cont
10、rollerBase 的派生类 Controller 中, 重写了 ExecuteCore()方法:protected override void ExecuteCore() TempData.Load(ControllerContext, TempDataProvider); try string actionName = RouteData.GetRequiredString(“action“); if (!ActionInvoker.InvokeAction(ControllerContext, actionName) HandleUnknownAction(actionName); f
11、inally TempData.Save(ControllerContext, TempDataProvider); 注意其中的 TempData.Load 和 TempData.Save 语句.TempDataDictionary.Load 用来从 ControllerContext 总读取 TempData 的数据.TempDataDictionary.Save 方法将发生了变化的 TempData 数据保存到ControllerContext 中.这两个方法都需要传递 ITempDataProvider 实例负责具体的读取和保存操作. 在 Controll 中默认的 TempDataPr
12、ovider 是 SessionStateTempDataProvider, 也就是说读取和保存都使用 Session. 我们也可以扩展自己的 TempDataProvider, 可以将临时数据保存在任何地方.比如制作 AspNetCacheTempDataProvider 使用机器本地缓存来保存 TempData.为何 TempData 只能够在 Controll 中传递一次? 因为SessionStateTempDataProvider.LoadTempData 方法( 在 TempDataDictionary.Load中调用)在从 ControllerContext 的 Session
13、中读取了 TempData 数据后, 会清空 Session:public virtual IDictionary LoadTempData(ControllerContext controllerContext) HttpContextBase httpContext = controllerContext.HttpContext; if (httpContext.Session = null) throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled)
14、; Dictionary tempDataDictionary = httpContext.SessionTempDataSessionStateKey as Dictionary; if (tempDataDictionary != null) / If we got it from Session, remove it so that no other request gets it httpContext.Session.Remove(TempDataSessionStateKey); return tempDataDictionary; else return new Dictiona
15、ry(StringComparer.OrdinalIgnoreCase); 注意上面加粗的部分. 一旦读取成功, 就会删除 TempData 的 Session.再回忆一下 Controll 的 ExecuteCore 方法, 是在 Controll 的 Execute 方法中调用的, 是每一次 Controll 一定会执行的方法, 所以我们得出了“只能在 Controll 之间传递一次“的结论.2.传递强类型对象我在系统中建立了一个模型类:StrongTypedDemoDTO从名字可以看出, 这个模型类是数据传输时使用的(Data Transfer Object).而且是我为一个View 单
16、独创建的.添加一个传递强类型 Model 的 Action,使用如下代码:public ActionResult StrongTypedDemo() StrongTypedDemoDTO model = new StrongTypedDemoDTO() UserName=“ziqiu.zhang“, UserPassword=“123456“ ; return View(model); 使用了 Controller.View()方法的一个重载, 将 model 对象传递给 View 对象.下面省略此对象的传输过程, 先让我们来看看如何在 View 中使用此对象.在创建一个 View 时, 会弹
17、出下面的弹出框 :勾选“Create a strongly-typed view“即表示要创建一个强类型的 View, 在“View data class“中选择我们的数据模型类.在“view content“中如下选项:这里是选择我们的 View 的“ 模板“, 不同的模板会生成不同的 View 页面代码. 虽然这些代码不一定满足我们需要, 但是有时候的确能节省一些时间,尤其是在写文章做 Demo 的时候. 比如我们的 View 是添加数据使用的 ,那就选择“Create“. 如果是显示一条数据用的, 就选择“Detail“.以选择 Detail 为例, 自动生成了下列代码:“ % . Fi
18、elds UserName: UserPassword: | .页面的 Model 属性就是一个强类型对象, 在这里就是 StrongTypedDemoDTO 类实例.page页面指令可以看出, 这里的页面继承自 ViewPage类, 而不是 ViewPage, 用 T 已经确定为 StrongTypedDemoDTO 类型, 所以 Model 的类型就是 StrongTypedDemoDTO.3.传递数据的思考使用强类型数据要优于弱类型数据, 老赵也曾提出过. 强类型有太多的好处, 智能提示, 语意明确, 提高性能,编译时发现错误等等.所以在实际应用中, 我们应该传递强类型的数据.目前 AS
19、P.NET MVC 还存在一些不足. 将页面类型化, 导致了只能传递一种数据类型实例到页面上. 而且内部代码的实现上并不十分完美.尤其是虽然我们已经知道传递的是StrongTypedDemoDTO 类型, 页面的 Model 属性也是 StrongTypedDemoDTO 类型, 但是仍然需要进行强制类型转换, 原因是 Controller 的 View(object model)方法重载接收的是一个 object 类型.还有, 为每个 View 建立一个模型类 , 也是一件繁琐的工作. 也许我们的业务模型中的两个类组合在一起就是 View 页面需要的数据 , 但是却不得不建立一个类将其封装起
20、来.模型类的膨胀也是需要控制一个事情. 尤其是对于互谅网应用而非企业内部的系统, 页面往往会有成百上千个.而且复用较少.当然目前来说既然要使用强类型的 Model, 我提出一些组织 Model 类型的实践方法.下面是我项目中的 Model 类型组织结构:这里 Model 是一个文件夹, 稍大一些的系统可以建立一个 Model 项目. 另外我们需要建立一个 DTO 文件夹, 用来区分 Model 的类型. MVC 中的 Model 应该对应 DTO 文件夹中的类.在DTO 中按照 Controller 再建立文件夹, 因为 Action 和 View 大部分都是按照 Controller 组织的
21、, 所以 Model 也应该按照 Controller 组织.在 Controller 文件夹里放置具体的 Model 类. 其实两个 Controller 文件夹中可以同相同的类名称, 我们通过命名空间区分同名的 Model 类:namespace DemoRC.Models.DTO.TransferModelController / / Action 为 StrongTypedDemo 的数据传输模型 / public class StrongTypedDemoDTO / / 用户名 / public string UserName get; set; / / 用户密码 / public
22、string UserPassword get; set; 使用时也要通过带有 Controller 的命名空间使用比如DTO.TransferModelController.StrongTypedDemoDTO, 或者建立一些自己的约定.四.使用 Model 输出页面View 对象获取了 Model 以后, 我们可以通过两种方式使用数据:1.使用内嵌代码熟悉 ASP,PHP 等页面语言的人都会很熟悉这种直接在页面上书写代码的方式.但是在 View 中应该只书写和显示逻辑有关的代码,而不要增加任何的业务逻辑代码.假设我们创建了下面这个 Action,为 ViewData 添加了三条记录 :/
23、/ Action 示例:使用内嵌代码输出 ViewData / / public ActionResult ShowModelWithInsideCodeDemo() ViewData“k1“ = “; ViewData“k2“ = “alert(“Hello ASP.NET MVC !“);“; ViewData“k3“ = “; return View(“ShowModelWithInsideCode“); 在 ShowModelWithInsideCode 中, 我们可以通过内嵌代码的方式, 遍历 ViewData 集合:使用内嵌代码输出 ViewData item in ViewDat
24、a ) % 此页面运行的脚本代码为: item in ViewData ) % 页面上遍历了两遍 ViewData,第一次是作为脚本输出的, 第二次由于进行了 HTML 编码,所以将作为文字显示在页面上.使用这种方式, 我们可以美工做好的 HTML 页面的动态部分 , 使用的形式转化为编码区域, 通过程序控制输出.由于只剩下显示逻辑要处理,所以这种开发方式往往要比CodeBehind 的编码方式更有效率, 维护起来一目了然.最让我高兴是使用这种方式后,我们终于可以只使用 HTML 控件了.虽然 ASP.NET WebFrom编程模型中自带了很多服务器控件, 功能很好很强大, 但是那终究是别人开
25、发的控件, 这些控件是可以随意变化的, 而且实现原理也对使用者封闭. 使用原始的页面模型和 HTML 控件将使我们真正的做程序的主人.而且不会因为明天服务器控件换了个用法就要更新知识, 要知道几乎所有的 HTML 控件几乎是被所有浏览器支持且不会轻易改变的.2.使用服务器控件注意虽然我们同样可以在 ASP.NET MVC 中使用服务器端控件, 但是在 MVC 中这并不是一个好的使用方式.建议不要使用.要使用服务器端控件, 我们就需要在后台代码中为控件绑定数据. ASP.NET MVC 框架提供的添加一个 View 对象的方法已经不能创建后台代码 , 也就是说已经摒弃了这种方式.但是我们仍然可以
26、自己添加.首先创建一个带有后台代码的(.cs 文件),一般的 Web Form 页面(aspx 页面), 然后修改页面的继承关系, 改为继承自 ViewPage:public partial class ShowModelWithControl : System.Web.Mvc.ViewPage在页面上放置一个 Repeater 控件用来显示数据:)Container.DataItem).Value % 在 Page_Load 方法中, 为 Repeater 绑定数据:public partial class ShowModelWithControl : System.Web.Mvc.View
27、Page protected void Page_Load(object sender, EventArgs e) rptView.DataSource = ViewData; rptView.DataBind(); 在 Controller 中创建 Action, 为 ViewData 赋值:/ / Action 示例:使用服务器控件输出 ViewData / / public ActionResult ShowModelWithControlDemo() ViewData“k1“ = “This“; ViewData“k2“ = “is“; ViewData“k3“ = “a“; View
28、Data“k4“ = “page“; return View(“ShowModelWithControl“); 运行结果:再次强调, 在 ASP.NET MVC 中我们应该尽量避免使用这种方式.3.使用 HTML Helper 类生成 HTML 控件HTML Helper 类是 ASP.NET MVC 框架中提供的生成 HTML 控件代码的类. 本质上与第一种方式一样, 只是我们可以不必手工书写 HTML 代码,而是借助 HTML Helper 类中的方法帮助我们生成 HTML 控件代码 .同时,我们也可以使用扩展方法为 HTML Helper 类添加自定义的生成控件的方法.HTML Help
29、er 类的大部分方法都是返回一个 HTML 控件的完整字符串, 所以可以直接在需要调用的地方使用的形式输出字符串.(1)ASP.NET MVC 中的 HtmlHelper 类在 ViewPage 中提供了 Html 属性, 这就是一个 HtmlHelper 类的实例. ASP.NET MVC 框架自带了下面这些方法: Html.ActionLink() Html.BeginForm() Html.CheckBox() Html.DropDownList() Html.EndForm() Html.Hidden() Html.ListBox() Html.Password() Html.Radi
30、oButton() Html.TextArea() Html.TextBox()上面列举的常用的 HtmlHelper 类的方法,并不是完整列表.下面的例子演示如何使用 HtmlHelper 类:用户名 : 密码:上面的代码使用 Html.BeginForm 输出一个表单对象, 并在表单对象中添加了两个 Input, 一个使用 Html.TextBox 输出, 另一个使用 Html.Password 输出,区别是 Html.Password 输出的 input 控件的 type 类型为 password.效果如图:(2)扩展 Html Helper 类我们可以自己扩展 HtmlHelper 类
31、, 为 HtmlHelper 类添加新的扩展方法, 从而实现更多的功能.在项目中建立 Extensions 文件夹, 在其中创建 SpanExtensions.cs 文件.源代码如下:using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace System.Web.Mvc public static class SpanExtensions public static string Span(this HtmlHelper
32、 helper,string id, string text) return String.Format(“1“, id, text); 上面的代码我们为 HtmlHelper 类添加了一个 Span()方法 , 能够返回一个 Span 的完整HTML 字符串 .因为命名空间是 System.Web.Mvc,所以页面使用的时候不需要再做修改 ,Visual Studio 会自动识别出来:请大家一定要注意命名空间, 如果不使用 System.Web.Mvc 命名空间, 那么一定要在页面上引用你的扩展方法所在的命名空间, 否则我们的扩展方法将不会被识别.接下来在页面上可以使用我们的扩展方法:(3)
33、 使用 TagBuilder 类创建扩展方法上面自定义的 Span()方法十分简单, 但是有时候我们要构造具有复杂结构的 Html 元素, 如果用字符串拼接的方法就有些笨拙.ASP.NET MVC 框架提供了一个帮助我们构造 Html 元素的类 :TagBuilderTagBuilder 类有如下方法帮助我们构建 Html 控件字符串:方法名称 用途AddCssClass() 添加 class=”属性GenerateId() 添加 Id, 会将 Id 名称中的“.“替换为 IdAttributeDotReplacement 属性值的字符.默认替换成“_“MergeAttribute() 添加一
34、个属性,有多种重载方法.SetInnerText() 设置标签内容, 如果标签中没有再嵌套标签,则与设置 InnerHTML 属性获得的效果相同.ToString() 输出 Html 标签的字符串, 带有一个参数的重载可以设置标签的输出形式为以下枚举值: TagRenderMode.Normal - 有开始和结束标签 TagRenderMode.StartTag - 只有开始标签 TagRenderMode.EndTag - 只有结尾标签 TagRenderMode.SelfClosing - 单标签形式,如同时一个 TagBuilder 还有下列关键属性:属性名称 用途Attributes
35、Tag 的所有属性IdAttributeDotReplacement 添加 Id 时替换“.“的目标字符InnerHTML Tag 的内部 HTML 内容TagName Html 标签名, TagBuilder 只有带一个参数-TagName 的构造函数.所以 TagName是必填属性下面在添加一个自定义的 HtmlHelper 类扩展方法,同样是输出一个标签:public static string Span(this HtmlHelper helper, string id, string text, string css, object htmlAttributes) /创意某一个 Ta
36、g 的TagBuilder var builder = new TagBuilder(“span“); /创建 Id,注意要先设置IdAttributeDotReplacement 属性后再执行 GenerateId 方法. builder.IdAttributeDotReplacement = “-“; builder.GenerateId(id); /添加属性 builder.MergeAttributes(new RouteValueDictionary(htmlAttributes); /添加样式 builder.AddCssClass(css); /或者用下面这句的形式也可以: bu
37、ilder.MergeAttribute(“class“, css); /添加内容, 以下两种方式均可 /builder.InnerHtml = text; builder.SetInnerText(text); /输出控件 return builder.ToString(TagRenderMode.Normal); 在页面上,调用这个方法:生成的 Html 代码为 :使用TagBuilder 帮助构建扩展方法 注意已经将 id 中的“.“替换为了 “-“字符.五.总结本来打算在本文中直接将 ViewEngine 的使用也加进来, 但是感觉本文已经写了很长的内容 , (虽然不多,但是很长) 所以将 ViewEngine 作为下一章单独介绍 .前些天 Scott Guthries 的博客上放出了 “ASP.NET MVC 免费教程“, 里面介绍了创建一名为“NerdDinner“项目的全过程, 使用 LINQ+ASP.NET MVC, 但是其中对于技术细节没有详细介绍(和本系列文章比较一下就能明显感觉出来), 下面提供本书的 pdf 文件下载地址以及源代码下载地址: 免费下载 PDF 版本 下载 应用源码 + 单元测试