第5 章 视 图 本章导读: 在ASP.NET MVC中,只需要处理两种主要类型的组件:一种是控制器,它负责执 行请求并为原始输入生成原始结果;另一种是视图引擎,它负责生成基于由控制器计算出 的结果的任何预期的HTML响应。本章将介绍ASP.NET MVC中能见度最高、最靠近 终端用户的层面,即View。View 通过应用程序在Action 中返回ViewResult 或 PartialViewResult,在运行阶段内部调用ExecuteResult()方法而产生模板转换 (transformation),将运行阶段运算后产生的Model经由模板引擎进行转换,从而生成 HTML页面代码,输出到浏览器中。因为View 用来生成网页,因此可以说它是ASP. NET 所有内建的ActionResult中使用率最高的一种类型。 在一般的应用程序开发上,View几乎服务了ASP.NET MVC程序一切的界面设计 (或称接口设计)与运算结果呈现,在隐性的应用中还包含数据验证等API的行为,View 是在构建网络应用产品或服务时最不可或缺的技术。 本章要点: 视图引擎(ViewEngine)负责处理ASP.NET 内容,并查找有关指令,这些指令典型 的是将动态内容插入发送给浏览器的输出,而Razor是MVC框架视图引擎的名称,因 此,首先介绍视图的作用和类型,然后简要地论述视图引擎的内部结构,并对如何为引擎 提供视图模板和数据进行实际的考量。 5.1 视图的作用 视图的职责是向用户提供用户界面,不像基于文件的Web框架,如ASP.NET Web Forms和PHP,视图本身不会被直接访问,浏览器不能直接指向一个视图并渲染它。相 反,视图总是被控制器渲染,因为控制器为它提供了要渲染的数据。 在标准ASP.NET MVC项目结构中,View的位置被设计存储在应用程序根目录中 名为Views的子目录下,在一些简单的情况中,视图不需要或需要很少控制器提供的信 息。更常见的情况是控制器需要向视图提供一些信息,所以它会传递一个数据转移对象, 叫作模型。视图将这个模型转换为一种适合显示给用户的格式。在ASP.NET MVC中, 完成这一过程有两部分操作:一个是检查由控制器提交的模型对象;另一个是将其内容 转换为HTML格式。 当创建新的项目模板时,将会注意到,项目以一种非常具体的方式包含了一个结构化 第5 章 视图1 05 的Views目录。按照约定,每个控制器在Views目录下都有一个对应的文件夹,其名称 和控制器一样,只是没有Controller后缀名。例如,控制器HomeController在Views目 录下就会对应一个名为Home的文件夹。在每个控制器的View 文件夹中,每个操作方 法都有一个同名的视图文件与其对应,这就提供了视图与操作方法关联的基础。 ASP.NET MVC的Razor视图引擎具有非常好的扩展性,使用RazorC#语法的 ASP.NET网页的扩展名为cshtml,使用RazorVB语法的ASP.NET 文件的扩展名为 vbhtml,Razor视图引擎将编写视图模板所需的代码量降至最少。例如,控制器中的操作 方法通过View()方法返回ViewResult对象,代码如下所示。 public class HomeController : Controller { public ActionResult Index() { ViewBag.Message = "Asp.NET MVC application"; return View(); } } 注意,这个控制器操作没有指定视图的名称,当不指定视图名称时,操作方法返回的 ViewResult对象将按照约定确定视图。它会在目录“/View/ControllerName”(这里的 ControllerName不带Controller后缀)下查找与Action名称相同的视图,在这种情况下 选择的视图便是“/Views/Home/Index.cshtml”。 到目前为止介绍的控制器操作简单地调用returnView()渲染视图,还不需要指定视 图的文件名。可以这么做,是因为它们利用了ASP.NET MVC框架的一些隐式约定,这 些约定定义了视图选择逻辑。与ASP.NET MVC中大部分约定的设置一样,这一约定是 可以重写的。如果想让Index()操作方法渲染一个不同的视图,可以向其提供一个不同 的视图名称,代码如下所示。 public ActionResult Index() { ViewBag.Message = "Asp.NET MVC application"; return View("Welcome"); } 这样编码后,虽然操作的方法仍然在“/Views/Home”目录中查找视图,但是选择的 不再是Index.cshtml,而是Welcome.cshtml。在其他一些应用中,可能还需要定位到位 于完全不同目录结构中的视图。针对这种情况,可以使用带有~符号的语法提供视图的 完整路径,代码如下。 public ActionResult Index() { ViewBag.Message = "Asp.NET MVC application"; 1 06 ASP. NET MVC 程序开发实战 return View("~/Views/Manage/Index.cshtml"); } 注意,为了在查找视图时避开视图引擎的内部查找机制,使用这种语法时必须提供视 图的文件扩展名。 5.2 视图类型 View是扩展名为.cshtml(或.vbhtml)的程序代码,广义上说,一个View 可解释为代 表着一个页面内容,但实际上View可细分为多种功能,赋予不一样的责任。 视图可以分为以下3种类型。 常规视图:常存放于对应Controller名称的目录下。 分部视图:放置在Controller名称的目录下或集中放置在Shared目录中。 布局页:也叫母版页,是指可被其他页面作为模板引用的特殊网页。 接下来进一步说明和解析上述3种View类型。 5.2.1 常规视图 虽然可以手动创建视图文件,把它添加到Views目录下,但是VisualStudio中的 ASP.NET MVC工具的AddView对话框使得创建视图非常容易。可以在Views文件 夹或者子文件夹上右击,从弹出的快捷菜单中选择“新建”→“视图”命令,也可以在控制器 方法上右击,从弹出的快捷菜单中选择“添加视图”命令。 下面通过新建学生说明如何创建视图。首先在Models文件夹下新建一个Student 模型,然后在HomeController中添加Create动作。Student模型和Create动作的代码如 下所示。 public class Student { public string Name { get; set; } public string Number { get; set; } public string Sex { get; set; } public int Age { get; set; } }p ublic ActionResult Create() { return View(); } 然后在操作方法Create()上右击,从弹出的快捷菜单中选择“新建视图”菜单项,打开 “添加视图”对话框,如图5.1所示。 下面对每个菜单项进行详细描述。 (1)视图名称:如果在操作方法上打开这个对话框时,视图的名称默认被填充为操 第 5 章 视图 107 图5.“ 对话框 1 添加视图” 作方法的名称,则视图的名称是必填项。 (2)模板:一旦选择一个模型类型,就可以选择一个基架模板。这些模板利用Visual Studio模板系统生成基于选择模型类型的视图。 视图模板包括以下类型。 .Create:创建一个视图,其中带有创建模型新实例的表单,并为模型类型的每个属 性生成一个标签和输入框。 .Delete:创建一个视图,其中带有删除现有模型实例的表单,并为模型的每个属性 显示一个标签,以及当前该属性的值。 .Details:创建一个视图,它显示了模型类型的每个属性的标签及其相应值。 .Edit:创建一个视图,其中带有编辑现有模型实例的表单,并为模型类型的每个属 性生成一个标签和输入框。 .Empty:创建一个空视图,使用@model语法指定模型类型。 .Empty(不具有模型): 与Empty基架一样,创建一个空视图。但是,由于这个基 架没有模型,因此在选择此基架时不需要选择模型类型,这是唯一不需要选择模 型类型的一个基架类型。 .List:创建一个带有模型实例表的视图。为模型类型的每个属性生成一列。确保 操作方法向视图传递的是Enumerable<ModelType> 类型,同时,为了执行创 建、编辑、删除操作,视图中还包含指向操作的链接。 (3)模型类:除了选用“Empty(不具有模型)”类型的模板外,其他模板都需要指定与 视图关联的模型类。 (4)引用脚本库:这个选项用来指示要创建的视图是否应该包含指向JavaScript库 (如果对视图有意义)的引用。默认情况下,Lyu.stml文件既不引用jQury aotcheValidation库,也不引用UnobtrusivejQueryValidation库,只引用主jQuery库。 当创建一个包含数据条目表单的视图(如Edit视图或Create视图)时,选择这个选项 yg.p 会添加对jquerval(在BundleConfics中,主要用来压缩JavaScrit和CSS)捆绑的脚本 1 08 ASP. NET MVC 程序开发实战 引用。如果要实现客户端验证,那么这些库就是必需的。除这种情况外,完全可以忽略这 个复选框。 (5)创建为分部视图:选择这个选项意味着要创建的视图不是一个完整的视图,因 此,Layout选项是不可用的。生成的分部视图除在其顶部没有<html>标签和<head> 标签外,很像一个常规的视图。 (6)使用布局页:这个选项决定了要创建的视图是否引用布局,还是成为一个完全 独立的视图。如果选择使用默认布局,就没必要指定一个布局了,因为在_ViewStart. cshtml文件中已经指定了布局,这个选项是用来重写默认布局文件的。 单击“添加”按钮,会创建出基于Student模型的Create视图,该视图也称为强类型视 图,代码如下所示。 @model WebApplication1.Models.Student @{ ViewBag.Title = "Create"; } <h2>Create</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-horizontal"> <h4>Student</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="form-group"> @Html.LabelFor(model = > model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @ class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model = > model.Number, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model = > model.Number, new { htmlAttributes = 第5 章 视图1 09 new { @class = "form-control" } }) @Html.ValidationMessageFor(model = > model.Number, "", new { @ class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Sex, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Sex, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Sex, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Age, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Age, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Age, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" value="Create" class="btn btn-default" /> </div> </div> </div> } <div> @Html.ActionLink("Back to List", "Index") </div> 第一行@model定义了该视图对应的模型为Student,当使用模型创建视图时,会使 视图创建非常简单,通过模板基架会自动创建出合适的视图直接使用。 5.2.2 分部视图 何为“分部视图”? 在WebForms开发中经常用到用户自定义控件,其作用是提高代 1 10 ASP. NET MVC 程序开发实战 码的复用性,减少代码的冗余,使程序更加模块化。在ASP.NET MVC中,对应地引入基 于Razor结构的分布页,其作用与WebForms开发中的用户自定义控件差不多。 分部视图(PartialView)是指可应用于View 中以作为其组织部分的View 的部分 (片段),分部视图可以像类一样,编写一次,然后在其他View 中被多次反复应用。由于 分部视图需要被多个不同的View所引用,所以分部视图一般放在Views/Shared文件夹 中以共享。 PartialView与View几乎完全相同,细微的差别是Action返回PartialViewResult 时,不会引用_ViewStart.cshtml中默认的指定,在PartialView 中除了自行指定使用哪 份Layout,不会引用前述_ViewStart.cshtml中的默认值。Action执行结束时,Controller 类型中的PartialView()方法成员返回一个PartialViewResult给ActionInvoker,以便后 续PartialView的执行。 public ActionResult GetData ( ) { return PartialView("_GetDataPartial"); } 在命名习惯上,PartialView 的文件名部分会以Partial为结束,如_GetTimePartial. cshtml,虽然这不是一定要遵守的命名规则,但是这个命名方式的确有助于在解决方案资 源管理器中寻找View文件。 在视图中要想渲染一个分部视图,可以使用Html.Partial和Html.RenderPartial辅 助方法,在学习HTML辅助方法时将会详细介绍。 5.2.3 布局页 当创建一个默认的ASP.NET MVC项目时,在Views目录下会自动添加一个Razor 布局使应用程序中的多个视图保持一致的外观。如果熟悉WebForms,其中母版页和布 局的作用是相同的,但是布局提供了更简捷的语法和更大的灵活性。可使用布局为网站 定义公共模板(或只是其中的一部分)。公共模板包含一个或多个占位符,应用程序中的 其他视图为它(们)提供内容。从某些角度看,布局很像视图的抽象基类。 指定母版视图是比较容易的,可以使用视图引擎所支持的规则,也可以在控制器中选 择下一个视图时将母版视图的名称作为参数传递给View()方法。注意,与普通视图相 比,布局页可能会遵循不同的规则。例如,ASPX 视图引擎要求母版模板的扩展名为 .master,且需要放在Shared文件夹中。而Razor视图引擎则要求添加.cshtml扩展名,并 需要在Views文件夹根目录下的一个专用的_ViewStart.cshtml文件中指定路径。 _ViewStart.cshtml文件代码如下所示。 @{ Layout="~/Views/Shared/_Layout.cshtml"; } _ViewStart.cshtml页面可用来消除多个视图使用统一布局的冗余,这个文件中的代 第5 章 视图1 11 码先于同目录下任何视图代码的执行,可以递归地应用到子目录下的任何视图,所以子视 图可以重写Layout属性的默认值,从而重新选择一个不同的布局。如果一组视图拥有共 同的设置,那么使用_ViewStart.cshtml文件对共同的视图配置进行统一设置。如果有视 图需要覆盖统一的设置,那么只修改对应视图的属性值即可。 下面看Shared文件夹下项目生成的默认布局_Layout.cshtml文件代码。 <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - 我的ASP.NET 应用程序</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") </head> <body> <div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle= "collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> @Html.ActionLink("应用程序名称", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("主页", "Index", "Home")</li> <li>@Html.ActionLink("关于", "About", "Home")</li> <li>@Html.ActionLink("联系方式", "Contact", "Home")</li> </ul> </div> </div> </div> <div class="container body-content"> @RenderBody() <hr /> <footer> <p>© @DateTime.Now.Year - 我的ASP.NET 应用程序</p> </footer> 1 12 ASP. NET MVC 程序开发实战 </div> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") @RenderSection("scripts", required: false) </body> </html> 该视图看起来像一个标准的Razor视图,但需要注意以下几个问题。 1.RenderBody @RenderBody是布局页中比较重要的元素,这是一个占位符,用于在布局页的 <body>与</body>之间的某个位置定义视图页或视图的位置。 呈现引用此布局页的视图或视图页时,MVC会自动将视图或视图页的内容合并到 布局页中调用RenderBody()方法的位置处,多个Razor视图可以利用这个布局显示一致 的外观。 2.RenderSection @RenderSection用于在布局页指定在视图中(注意不是指分部视图)用Section定义 的占位符。将视图中定义的Section嵌入布局页中指定的位置后,当呈现引用此布局页 的视图时,MVC会自动将所定义的Section的内容合并到布局页中调用RenderSection() 方法的位置处。 @RenderSection有一个必要参数作为区段名称,并且有一个选择性参数required, 省略required参数或者设置required:true用来指定套用这份Layout的View 是否必须 满足这个区段,如果没有提供该区段,则会导致执行时弹出错误,用以下程序代码说明。 如果在布局页中添加下面的语句: @RenderSection("SectionOne", false) 或 @RenderSection("SectionOne", required :false) 在视图中就可以用下面的办法定义: @section SectionOne{ … } 如果在布局页中通过Razor语法设置了required:false的RenderSection()方法,但 是又希望当所有没有实现sectionName的视图呈现的相关内容有默认值,则可通过 IsSectionDefined实现。IsSectionDefined(sectionName)用于判断视图页中是否已经定 义了用sectionName指定的名称,如果视图页中没有定义该名称,就在布局页中定义呈现 的内容。 在布局页中添加下面的语句: 第5 章 视图1 13 @if(IsSectionDefined("SectionOne")) { @RenderSection("SectionOne") }e lse { <p>SectionOne Section is not defined!</p> } 在需要设置的视图中就可以用下面的办法定义: @section SectionOne{ … } 若没有设置名为SectionOne的RenderSection视图,则显示的内容可写在布局页的 else()方法体中。 3.Layout与View 的执行顺序 在执行顺序上,View会被优先执行,然后被View 引用的Layout执行,这一点是经 常容易被误解为相反的情况,因此产生不正确的程序编写结果。例如,在程序中以 ViewBag、ViewData在View 与Layout之间进行变量的传递,就会有执行顺序的问题需 要考虑,因此一开始需要特别留意概念的建立。 下面在目录“~/Views/Shared”下定义一个名为_MyLayout的布局页面,并修改布 局页代码如下。 <!DOCTYPE html> <html> <head><tit1e>@ViewBag.Title</title></head> <body> <h1>@ViewBag.Title</h1> <div id="main-content">@RenderBody()</div> </body> </html> 5.3 ASP.NET视图引擎 ViewEngine是隐藏在Controller/Action和View 之间的黏合剂,当Action执行结 束并且返回ViewResult(或PartialViewResult)后,ActionInvoker调用ActionResult中 定义的ExecuteResult()方法启动了系统中ViewEngine的工作。ViewEngine的职责 是根据ActionInvoker提供的context获取合适的View,将从ViewResult得到的数据给 模板程序执行,并将结果输出为网页。因历史关系,现在的ASP.NET MVC内建了两份 ViewEngine的实现,即Controller中已经概略提到的WebFormsViewEngine与Razor