一.摘要
本文讲解在Action中向View传递Model的几种方式.以及View获取Model以后如何编写显示逻辑.还详细的介绍了
ASP.NET MVC框架提供的Html Helper类的使用及如何为Html Helper类添加自定义扩展方法.
二.承上启下
上一篇文章中我们学习了Controller处理一次请求的全过程.在Controller的Action中, 会传递数据给View,
还会通知View对象开始显示.所以Model是在Action中获取的, 并由Action传递给View. View对象接到Action通
知后会使用自己的显示逻辑展示页面.
下面首先让我们学习如何将Model传递给View对象.
三.传递数据给View
在MVC中,Model对象是指包含了数据的模型. Controller将Model传递给View以后, View对象中不应该做任何
的业务逻辑处理, 仅仅根据Model对象做一些显示逻辑的处理.
传递Model对象时, 我们有两种选择:
1.传递一个弱类型的集合, 即成员为object类型的集合, 在View中需要将每个成员转换成我们需要的类型,比如
int, string,自定义类型等.
2.传递强类型对象, 这些类型是我们自定义的. 在View中直接使用我们传递的强类型对象, 不需要再转换类型.
如果让我们自己设计一个MVC框架, 我们也会想到上面两种实现方式,接下来看看在ASP.NET MVC中的实现.
1.传递弱类型的集合
(1) 如何传递
ASP.NET MVC框架定义了ViewContext类, 直译后是"View上下文", 其中保存和View有关的所有数据, 其中
Model对象也封装在了此类型中.
ViewContext对象包含三个属性:
- IView View
- ViewDataDictionary ViewData
- TempDataDictionary TempData
其中ViewData集合和TempData集合都是用来保存Model对象的.在一个Controller的Action中, 我们可以用
如下方式为这两个集合赋值:
///<summary>
/// 传递弱类型Model的Action示例
/// </summary>
/// <returns>ViewResult</returns>
public ActionResult WeakTypedDemo()
{
ViewData["model"] = "Weak Type Data in ViewData";
TempData["model"] = "Weak Type Data in TempData";
return View("WeakTypedDemo");
}
在页面中, 是用如下方式使用这两个集合:
<div>
<% = ViewData["model"] %><br />
<% = TempData["model"] %><br />
</div>
(2) 传递过程
请注意Action中的赋值语句实际上操作的是Controller类的ViewData和TempData属性, 此时并没有任
何的数据传递.上一篇文章中我们已经学到, return View语句会返回一个ViewResult对象, 并且接下来要
执行ViewResult的Executeresult方法. Controller的View方法会将Controller类的ViewData和
TempData属性值传递给ViewResult,代码如下:
protected internal virtual ViewResult 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 = new ViewContext(context, View, ViewData, TempData); View.Render(viewContext, context.HttpContext.Response.Output); //... }
ViewContext对象最终会传递给ViewPage, 也就是说ViewData和TempData集合传递到了ViewPage. 我这里简化了最后的传递流程, 实际上ViewData对象并不是通过ViewContext传递到ViewPage中的, ViewPage上的ViewData是一个单独的属性, 并没有像TempData一样其实访问的是 ViewContext.TempData. 这么做容易产生奇异, 本类ViewContext是一个很好理解职责十分清晰的类. 作为使用者的我们暂时可以忽略这点不足, 因为如此实现ViewData完全是为了下面使用强类型对象.
(3)ViewData和TempData的区别
虽然ViewData和TempData都可以传递弱类型数据,但是两者的使用是有区别的:
- ViewData的生命周期和View相同, 只对当前View有效.
- TempData保存在Session中, Controller每次执行请求的时候会从Session中获取TempData并
删除Session, 获取完TempData数据后虽然保存在内部的字典对象中,但是TempData集合的每
个条目访问一次后就从字典表中删除. 也就是说TempData的数据至多只能经过一次Controller传递.
(4) TempData的实现
TempData的类型是TempDataDictionary, 和一般的字典表没有明显的不同, TempData的生命周期
是由Controll决定的.
在所有Controll的基类ControllerBase中, 创建了类型为TempDataDictionary的TempData属性.
在ControllerBase的派生类Controller中, 重写了ExecuteCore()方法:
protected override void ExecuteCore() { TempData.Load(ControllerContext, TempDataProvider); try { string actionName = RouteData.GetRequiredString("action"); if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) { HandleUnknownAction(actionName); } } finally { TempData.Save(ControllerContext, TempDataProvider); } } 注意其中的TempData.Load和TempData.Save语句.
TempDataDictionary.Load用来从ControllerContext总读取TempData的数据.
TempDataDictionary.Save方法将发生了变化的TempData数据保存到ControllerContext中.
这两个方法都需要传递ITempDataProvider实例负责具体的读取和保存操作. 在Controll中默认的
TempDataProvider是SessionStateTempDataProvider, 也就是说读取和保存都使用Session. 我
们也可以扩展自己的TempDataProvider, 可以将临时数据保存在任何地方.比如制作
AspNetCacheTempDataProvider使用机器本地缓存来保存TempData.
为何TempData只能够在Controll中传递一次? 因为SessionStateTempDataProvider.LoadTempData
方法(在TempDataDictionary.Load中调用)在从ControllerContext的Session中读取了TempData
数据后, 会清空Session:
public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext) { HttpContextBase httpContext = controllerContext.HttpContext; if (httpContext.Session == null) { throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled); } Dictionary<string, object> tempDataDictionary = httpContext.Session[TempDataSessionStateKey] as Dictionary<string, object>; 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 Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); } }
注意上面加粗的部分. 一旦读取成功, 就会删除TempData的Session.
再回忆一下Controll的ExecuteCore方法, 是在Controll的Execute方法中调用的, 是每一次
Controll一定会执行的方法, 所以我们得出了"只能在Controll之间传递一次"的结论.
2.传递强类型对象
我在系统中建立了一个模型类:StrongTypedDemoDTO
从名字可以看出, 这个模型类是数据传输时使用的(Data Transfer Object).而且是我为一个View单独创建的.
添加一个传递强类型Model的Action,使用如下代码:
public ActionResult StrongTypedDemo() { StrongTypedDemoDTO model = new StrongTypedDemoDTO() { UserName="ziqiu.zhang", UserPassword="123456" }; return View(model); }
使用了Controller.View()方法的一个重载, 将model对象传递给View对象.下面省略此对象的传输过程, 先让我们来 看看如何在View中使用此对象.
在创建一个View时, 会弹出下面的弹出框:
勾选"Create a strongly-typed view"即表示要创建一个强类型的View,
在"View data class"中选择我们的数据模型类.
在"view content"中如下选项:
这里是选择我们的View的"模板", 不同的模板会生成不同的View页面代码. 虽然这些代码不一定满足我们需要,
但是有时候的确能节省一些时间,尤其是在写文章做Demo的时候. 比如我们的View是添加数据使用的,那就选
择"Create".如果是显示一条数据用的, 就选择"Detail".
以选择Detail为例, 自动生成了下列代码:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage
<DemoRC.Models.DTO.TransferModelController.StrongTypedDemoDTO>" %>
...
<body>
<fieldset>
<legend>Fields</legend>
<p>
UserName:
<%= Html.Encode(Model.UserName) %>
</p>
<p>
UserPassword:
<%= Html.Encode(Model.UserPassword) %>
</p>
</fieldset>
<p>
<%=Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) %> |
<%=Html.ActionLink("Back to List", "Index") %>
</p>
</body>
...
页面的Model属性就是一个强类型对象, 在这里就是StrongTypedDemoDTO类实例.page页面指令可以看出,
这里的页面继承自ViewPage<T>类, 而不是ViewPage, 用T已经确定为StrongTypedDemoDTO类型,
所以Model的类型就是StrongTypedDemoDTO.
3.传递数据的思考
使用强类型数据要优于弱类型数据, 老赵也曾提出过. 强类型有太多的好处, 智能提示, 语意明确,
提高性能,编译时发现错误等等.
所以在实际应用中, 我们应该传递强类型的数据.
目前ASP.NET MVC还存在一些不足. 将页面类型化, 导致了只能传递一种数据类型实例到页面上. 而且内
部代码的实现上并不十分完美.尤其是虽然我们已经知道传递的是StrongTypedDemoDTO类型, 页面的Model
属性也是StrongTypedDemoDTO类型, 但是仍然需要进行强制类型转换, 原因是Controller的View(object model)
方法重载接收的是一个object类型.
还有, 为每个View建立一个模型类, 也是一件繁琐的工作. 也许我们的业务模型中的两个类组合在一起就是View页
面需要的数据, 但是却不得不建立一个类将其封装起来.模型类的膨胀也是需要控制一个事情. 尤其是对于互谅网
应用而非企业内部的系统, 页面往往会有成百上千个.而且复用较少.
当然目前来说既然要使用强类型的Model, 我提出一些组织Model类型的实践方法.下面是我项目中的Model
类型组织结构:
这里Model是一个文件夹, 稍大一些的系统可以建立一个Model项目. 另外我们需要建立一个DTO文件夹,
用来区分Model的类型.
MVC中的Model应该对应DTO文件夹中的类.在DTO中按照Controller再建立文件夹,
因为Action和View大部分都是按照Controller组织的, 所以Model也应该按照Controller组织.
在Controller文件夹里放置具体的Model类. 其实两个Controller文件夹中可以同相同的类名称, 我们
通过命名空间区分同名的Model类:
使用时也要通过带有Controller的命名空间使用比如DTO.TransferModelController.StrongTypedDemoDTO,
或者建立一些自己的约定.
四.使用Model输出页面
View对象获取了Model以后, 我们可以通过两种方式使用数据:
1.使用内嵌代码
熟悉ASP,PHP等页面语言的人都会很熟悉这种直接在页面上书写代码的方式.但是在View中应该只书写和显示
逻辑有关的代码,而不要增加任何的业务逻辑代码.
假设我们创建了下面这个Action,为ViewData添加了三条记录:
/// <summary>
/// Action示例:使用内嵌代码输出ViewData
/// </summary>
/// <returns></returns>
public ActionResult ShowModelWithInsideCodeDemo()
{
ViewData["k1"] = @"<script type=""text/javascript"">";
ViewData["k2"] = @"alert(""Hello ASP.NET MVC !"");";
ViewData["k3"] = @"</script>";
return View("ShowModelWithInsideCode");
}
在ShowModelWithInsideCode中, 我们可以通过内嵌代码的方式, 遍历ViewData集合:
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>使用内嵌代码输出ViewData</title>
<% foreach(KeyValuePair<string, object> item in ViewData )
{%>
<% = item.Value %>
<% } %>
</head>
<body>
<div>
<div>此页面运行的脚本代码为:</div>
<fieldset>
<% foreach(KeyValuePair<string, object> item in ViewData )
{%>
<% = Html.Encode(item.Value) %> <br />
<% } %>
</fieldset>
</div>
</body>
</html>
页面上遍历了两遍ViewData,第一次是作为脚本输出的, 第二次由于进行了HTML编码,所以将作为文字显示在页面上.
使用这种方式, 我们可以美工做好的HTML页面的动态部分, 使用<% %>的形式转化为编码区域, 通过程序控制输出.
由于只剩下显示逻辑要处理,所以这种开发方式往往要比CodeBehind的编码方式更有效率, 维护起来一目了然.
最让我高兴是使用这种方式后,我们终于可以只使用HTML控件了.虽然ASP.NET WebFrom编程模型中自带了很多服
务器控件, 功能很好很强大, 但是那终究是别人开发的控件, 这些控件是可以随意变化的, 而且实现原理也对使用
者封闭. 使用原始的页面模型和HTML控件将使我们真正的做程序的主人.而且不会因为明天服务器控件换了个
用法就要更新知识, 要知道几乎
所有的HTML控件几乎是被所有浏览器支持且不会轻易改变的.
2.使用服务器控件
注意虽然我们同样可以在ASP.NET MVC中使用服务器端控件, 但是在MVC中这并不是一个好的使用方式.建议不要使用.
要使用服务器端控件, 我们就需要在后台代码中为控件绑定数据. ASP.NET MVC框架提供的添加一个View对象的方法已
经不能创建后台代码, 也就是说已经摒弃了这种方式.但是我们仍然可以自己添加.
首先创建一个带有后台代码的(.cs文件),一般的Web Form页面(aspx页面),然后修改页面的继承关系,
改为继承自ViewPage:
public partial class ShowModelWithControl : System.Web.Mvc.ViewPage
在页面上放置一个Repeater控件用来显示数据:
<body>
<form id="form1" runat="server">
<div>
<asp:Repeater ID="rptView" runat="server">
<ItemTemplate>
<%# ((KeyValuePair<string, object>)Container.DataItem).Value %><br />
</ItemTemplate>
</asp:Repeater>
</div>
</form>
</body>
在Page_Load方法中, 为Repeater绑定数据:
public partial class ShowModelWithControl : System.Web.Mvc.ViewPage
{
protected void Page_Load(object sender, EventArgs e)
{
rptView.DataSource = ViewData;
rptView.DataBind();
}
}
在Controller中创建Action, 为ViewData赋值:
/// <summary>
/// Action示例:使用服务器控件输出ViewData
/// </summary>
/// <returns></returns>
public ActionResult ShowModelWithControlDemo()
{
ViewData["k1"] = @"This";
ViewData["k2"] = @"is";
ViewData["k3"] = @"a";
ViewData["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 Helper类的大部分方法都是返回一个HTML控件的完整字符串, 所以可以直接在需要调用的地方使用
<% =Html.ActionLink() %>的形式输出字符串.
(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.RadioButton()
- Html.TextArea()
- Html.TextBox()
上面列举的常用的HtmlHelper类的方法,并不是完整列表.
下面的例子演示如何使用HtmlHelper类:
<div>
<% using (Html.BeginForm())
{ %>
<label style="width:60px;display:inline-block;">用户名:</label>
<% =Html.TextBox("UserName", "ziqiu.zhang", new { style="width:200px;" })%>
<br /><br />
<label style="width:60px;display:inline-block;">密码:</label>
<% =Html.Password("Psssword", "123456", new { style = "width:200px;" })%>
<% }%>
</div>
上面的代码使用Html.BeginForm输出一个表单对象, 并在表单对象中添加了两个Input, 一个使用
Html.TextBox输出, 另一个使用Html.Password输出,区别是Html.Password输出的input控件的type类型
为password.效果如图:
(2)扩展Html Helper类
我们可以自己扩展HtmlHelper类, 为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 helper,string id, string text)
{
return String.Format(@"<span id=""{0}"">{1}</span>", id, text);
}
}
}
上面的代码我们为HtmlHelper类添加了一个Span()方法, 能够返回一个Span的完整HTML字符串.
因为命名空间是System.Web.Mvc,所以页面使用的时候不需要再做修改,Visual Studio会自动识别出来:
请大家一定要注意命名空间, 如果不使用System.Web.Mvc命名空间, 那么一定要在页面上引用你的扩展方法所在
的命名空间, 否则我们的扩展方法将不会被识别.
接下来在页面上可以使用我们的扩展方法:
<div>
<!-- 使用自定义的Span方法扩展HTML Helper -->
<% =Html.Span("textSpan", "使用自定义的Span方法扩展HtmlHelper类生成的Span") %>
</div>
(3) 使用TagBuilder类创建扩展方法
上面自定义的Span()方法十分简单, 但是有时候我们要构造具有复杂结构的Html元素, 如果用字符串拼接的方法就有些笨拙.
ASP.NET MVC框架提供了一个帮助我们构造Html元素的类:TagBuilder
TagBuilder类有如下方法帮助我们构建Html控件字符串:
方法名称 |
用途 |
AddCssClass() |
添加class=””属性 |
GenerateId() |
添加Id, 会将Id名称中的"."替换为IdAttributeDotReplacement 属性值的字符.默认替换成"_" |
MergeAttribute() |
添加一个属性,有多种重载方法. |
SetInnerText() |
设置标签内容, 如果标签中没有再嵌套标签,则与设置InnerHTML 属性获得的效果相同. |
ToString() |
输出Html标签的字符串, 带有一个参数的重载可以设置标签的输出形式为以下枚举值:
- TagRenderMode.Normal -- 有开始和结束标签
- TagRenderMode.StartTag -- 只有开始标签
- TagRenderMode.EndTag -- 只有结尾标签
- TagRenderMode.SelfClosing -- 单标签形式,如<br/>
|
同时一个TagBuilder还有下列关键属性:
属性名称 |
用途 |
Attributes |
Tag的所有属性 |
IdAttributeDotReplacement |
添加Id时替换"."的目标字符 |
InnerHTML |
Tag的内部HTML内容 |
TagName |
Html标签名, TagBuilder只有带一个参数-TagName的构造函数.所以TagName是必填属性 |
下面在添加一个自定义的HtmlHelper类扩展方法,同样是输出一个<Span>标签:
public static string Span(this HtmlHelper helper, string id, string text, string css, object htmlAttributes)
{
//创意某一个Tag的TagBuilder
var builder = new TagBuilder("span");
//创建Id,注意要先设置IdAttributeDotReplacement属性后再执行GenerateId方法.
builder.IdAttributeDotReplacement = "-";
builder.GenerateId(id);
//添加属性
builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
//添加样式
builder.AddCssClass(css);
//或者用下面这句的形式也可以: builder.MergeAttribute("class", css);
//添加内容,以下两种方式均可
//builder.InnerHtml = text;
builder.SetInnerText(text);
//输出控件
return builder.ToString(TagRenderMode.Normal);
}
在页面上,调用这个方法:
<% =Html.Span("span.test", "使用TagBuilder帮助构建扩展方法", "ColorRed", new { style="font-size:15px;" })%>
生成的Html代码为:
<span id="span-test" class="ColorRed" style="font-size: 15px;">使用TagBuilder帮助构建扩展方法</span>
注意已经将id中的"."替换为了"-"字符.
五.总结
本来打算在本文中直接将ViewEngine的使用也加进来, 但是感觉本文已经写了很长的内容, (虽然不多,但是很长......)
所以将ViewEngine作为下一章单独介绍.
前些天 Scott Guthrie's的博客上放出了"ASP.NET MVC免费教程", 里面介绍了创建一名为"NerdDinner"项目的全过程,
使用LINQ+ASP.NET MVC, 但是其中对于技术细节没有详细介绍(和本系列文章比较一下就能明显感觉出来), 下面提供本书的
pdf文件下载地址以及源代码下载地址:
免费下载PDF版本
下载应用源码 + 单元测试
源代码是英文版本, 其实我最近有做一个中文的"Nerd Dinner"的想法, 但是因为要写文章而且还要工作已经让我焦头烂额,
写文章真的是一件体力活.如果有人同样有兴趣翻译代码, 我可以提供域名和服务器空间.
差点提供忘记本篇文章的示例源代码下载:
http://files.cnblogs.com/zhangziqiu/AspNet-Mvc-4-Demo.rar
作者:张子秋
|