文章非常长,仅仅用于记录自己学习。
创建自定义视图引擎
创建自定义视图引擎的价值是,演示请求处理管道如何工作,并完善关于MVC架构如何操作的知识,视图引擎实现IViewEngine接口,如下图所示:
public interface IViewEngine{ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);void ReleaseView(ControllerContext controllerContext, IView view);}
视图引擎的作用是将对视图的请求转化成ViewEngineResult对象。 这个接口中两个方法是FindView 和 FindPartialView,给它们传递的是描述请求的参数:处理该请求的控制器(ControllerContext对象)、视图名及其布局,以及是否允许视图引擎重用其缓存结果。当框架对ViewResult进行处理时,会调用这个两个方法。最后一个方法是ReleaseView(释放视图),当视图不在需要时被调用(从其名称不难看出,该方法的作用是释放视图所占用的资源)。
当请求一个视图时,ViewEngineResult类使视图引擎能够对MVC框架做出响应。代码如下图所示:
using System.Collections.Generic;namespace System.Web.Mvc
{public class ViewEngineResult{public IEnumerable<string> SearchedLoactions{get;private set;}public IView View {get;private set;}public IViewEngine ViewEngine{get;private set;}public ViewEngineResutlt(IEnumerable<string> searchedLoactions){if(searchedLoactions == null){throw new ArgumentNullException("searchedLocations");}SearchedLoactions = searchedLoactions}public ViewEngineResutlt(IView view,IViewEngine viewEngine){if(view == null){throw new ArgumentNullException("view");}if(viewEngine == null){throw new ArgumentNullException("viewEngine");}View = view;ViewEngine = viewEngine;}}
}
可以通过两个构造器中的其中一个来表示一个结果,如果视图引擎能够对请求提供视图,那么可以用以下构造器创建一个ViewEngineResult:
public ViewEngineResutlt(IView view,IViewEngine viewEngine)
如果视图引擎不能对请求提供视图,那么可以使用如下构造器: 这一版本是查找视图位置的一个枚举。如果找不到视图,就会将枚举的信息显示给用户。
public ViewEngineResutlt(IEnumerable<string> searchedLoactions)
视图引擎的最后一个构造块是IView接口,如下图所示:
public interface IView{void Render(ViewContext viewContext, TextWriter writer);}
把一个IView实现传递给ViewEngineResult对象的构造器,然后它会被视图引擎方法所返回。MVC框架会调用Render方法(把IView实现传递给ViewEngineResult对象构造器时,自然便会调用IView接口中的这个Render方法)。 到目前为止,ViewContext对象定义了一些属性,这些属性给你提供请求信息以及MVC框架如何处理它的细节。
名称 | 描述 |
Controller | 返回处理当前请求的IController实现 |
RequestContext | 返回当前请求的细节 |
RouteData | 为当前请求返回的路有数据 |
TempData | 返回和请求相关的临时数据 |
View | 返回将要处理请求的IView接口的实现。很明显,如果你正在创建一个自定义视图实现,它将是当前类。 |
ViewBag | 返回一个表示视图的object |
ViewData | 返回一个包含模式视图包和元数据的视图模型数据字典。 |
这些属性中最有趣的是ViewData,它返回一个ViewDataDictionary对象。定义了一些有用的属性,如下图所示:
名称 | 描述 |
keys | 为字典中的数据返回键值集合,他们可用来访问视图包属性 |
Model | 为请求返回视图模型对象 |
ModelMetadata | 返回一个可以用来反映模型类型的ModelMetadata对象 |
ModelState | 返回有关模型的状态信息 |
根据以上知识,创建一个自定义视图引擎, 首先新建一个空白项目,创建一个Home控制器,如下图所示:
public class HomeController : Controller{// GET: Homepublic ActionResult Index(){ViewBag.Messge = "Hello,World";ViewBag.Time = DateTime.Now.ToShortTimeString();return View("DebugData");}public ActionResult List(){return View();}}
创建一个自定义IView,在项目中新建一个Infrastructure的文件夹,创建一个DebugDataView.cs的新的类文件,如下图所示:
namespace WebApplication1.Infrastructure
{public class DebugDataView : IView{public void Render(ViewContext viewContext, TextWriter writer){Write(writer, "---Routing Data---");foreach (string key in viewContext.RouteData.Values.Keys){Write(writer, "key:{0},value:{1}", key, viewContext.RouteData.Values[key]);}Write(writer, "---View Data---");foreach (string key in viewContext.ViewData.Keys){Write(writer,"key:{0},value:{1}",key,viewContext.ViewData[key]);}}private void Write(TextWriter writer, string template, params object[] values){writer.Write(string.Format(template,values) + "<p>");}}
}
视图引擎的目的是产生一个ViewEngineResult对象,它或者包含一个IView,或者是一个用于搜索适当视图的位置列表。现在已经有了IView的实现,于是可以创建视图引擎,在Infrastructure的文件夹下新建一个DebugDataViewEngine.cs的类文件,代码如下图所示:
namespace WebApplication1.Infrastructure
{public class DebugDataViewEngine : IViewEngine{public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache){return new ViewEngineResult(new string[] { "No View (Debug Data View Engine)" });}public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache){if (viewName == "DebugData"){return new ViewEngineResult(new DebugDataView(), this);}else{return new ViewEngineResult(new string[] { "No view (Debug Data View Engine)" });}}public void ReleaseView(ControllerContext controllerContext, IView view){}}
}
注册自定义视图引擎
视图引擎需要在 Global.asax 的 Application_Start 方法中进行注册,代码如下图所示:
ViewEngines.Engines.Add(new DebugDataViewEngine());
静态的ViewEngine.Engines集合含有一组在应用程序中安装的视图引擎。MVC框架支持在一个单一的应用程序中安装多个引擎。当处理一个ViewResult时,动作调用器获取这组已经安装的视图引擎,并依次调用它们的FindView方法。
一旦动作调用接收到一个含有IView方法的ViewEngineResult对象。便会停止调用FindView方法。如果有两个或者多个引擎能够对同视图的请求进行服务,这意味着在ViewEngines.Engines集合中添加引擎的顺序是很重要的,代码如下图所示:
ViewEngines.Engines.Insert(0,new DebugDataViewEngine());
测试视图引擎
运行程序,效果如下图所示:
如果导航到/Home/List,该动作方法调用View方法请求其默认视图,这是一个我们不支持的视图,就会报错,如下图所示:
从上述报错来看,Razor和ASPX视图也在列表中,可以清除其他类型的视图引擎, 代码如下:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new DebugDataViewEngine());
理解Razor视图渲染
Razor视图引擎会编译应用程序中的视图,以改善性能。视图会被转化成C#类,然后被编译。这是在视图中能够如此方便地包含C#代码片段的原因。
程序启动前,视图不会被编译。只有程序启动后才能触发视图编译过程。出于方便,会将视图文件生成的类写成磁盘上的C#代码,然后进行编译,这意味着能够看到一个视图的C#语句。这些语句可以在C盘的某些隐蔽的目录下被找到,并且文件名和它们所包含的类名不对应。
配置视图搜索位置
在查找视图时,Razor视图引擎遵循MVC框架早期版本建立起来的约定。例如,如果请求与Home控制器先关的Index视图时,Razor会审查一下视图列表:
/Views/Home/Index.cshtml
/Views/Home/Index.vbhtml
/Views/Shared/Index.cshtml
/Views/Shared/Index.vbhtml
Razor实际上不会在磁盘上查找这些视图文件,因为它们还没有被编译成C#文件。Razor查找的是表示这些视图的编译类。.cshtml文件是含有C#语句的模板,而.vbhtml文件含有Visual Basic语句。
属性 | 描述 | 默认值 |
ViewLoactionFormats MasterLocationFormatsPartiaView LocationFormats | 查找视图、分部视图、以及布局的位置 | ~/ Views / {1} / {0}.cshtml, ~/ Views/ {1} / {0}.vbhtml, ~/ Views/ Shared / {0}.cshtml, ~/ Views/ Shared/ {0}.vbhtml, |
AreaViewLocationFormats AreaMasterLocationFormats AreaPartialViewLocationForms | 为一个区域查找视图、分部视图,以及布局位置 | ~/ Areas / {2} / Views / {1} / {0}.cshtml, ~/ Areas / {2} / Views / {1} / {0}.vbhtml, ~/ Areas / {2} / Views / Shared / {0}.cshtml, ~/ Areas / {2} / Views / Shared / {0}.vbhtml |
这些属性是在Razor之前引入的,这是每组三个属性具有相同值的原因。每个属性都是一个字符串数组,它们是用合成字符串格式化符号来表示的。以下是与占位符对应的参数值:
{0} 表示视图名。
{1} 表示控制器名。
{2} 表示区域名。
通过创建一个RazorViewEngine子类,可以改变Razor搜索的视图文件。 在Infrastructrue文件夹中,创建了一个名称为CustomerLocationViewEngine的视图引擎,代码如下图所示:
public class CustomLocationViewEngine:RazorViewEngine{public CustomLocationViewEngine(){ViewLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml","~/Views/Common/{0}.cshtml"};}}
以上代码对 ViewLoactionFormats设置了一个新值。新数组只包含用于.cshtml文件的条目。此外,已经将查找共享视图的位置修改为了View/Common,而不是Views/Shared。并且在Global.asax的Application_Start方法中,注册此引擎:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomLocationViewEngine());
最后创建/View/Common文件夹,并添加一个名称为List.cshtml的视图文件。 代码如下图所示:
@{Layout = null;
}<!DOCTYPE html><html>
<head><meta name="viewport" content="width=device-width" /><title>List</title>
</head>
<body><div> <h3>This is the /Views/Common/List.cshtml View</h3></div>
</body>
</html>
启动程序,并导航到/Home/List时,执行效果如下图所示:
使用分段
Razor引擎支持分段的概念,Razor分段能够灵活的控制将视图的哪一个部分插入到布局之中,以及把他们插入到哪里。
下面对/Views/Home/Index.cshtml文件进行编辑,如下图所示:
@model string[]
@{Layout = "~/Views/Shared/_Layout.cshtml";
}@section Header{<div class="view">@foreach (string str in new[] {"Home","List","Edit" }){@Html.ActionLink(str,str,null,new { style = "margin:5px"})}</div>
}<div class="view">This is a lists of fruit names@foreach (string name in Model){<span><b>@name</b></span>}
</div>@section Footer
{<div class="view">This is the footer</div>
}
在上图代码中,创建了名称为“Header” 和 “Footer”的分段。分段的内容可以混用HTML标记和Razor标签。你可以在布局中使用@RenderSection 辅助器方法来指定分段要插入的位置。修改Views/Shared/_Layout.cshtml文件,代码如下图所示:
<!DOCTYPE html><html>
<head><meta name="viewport" content="width=device-width" /><style type="text/css">div.layout {background-color:lightgray;}div.view {border:thin solid black;margin:10px 0;}</style><title>@ViewBag.Title</title>
</head>
<body>@RenderSection("Header")<div class="layout">This is part of the layout</div>@RenderBody()<div class="layout">This is part of Layout</div>@RenderSection("Footer")<div class="layout">This is part of Layout</div>
</body>
</html>
在Razor对布局进行解析时,RenderSection辅助器方法会显示视图中指定名称的分段内容。视图中未包含在分段中的内容会插入布局中使用RenderBody辅助器方法。 运行程序,结果如下图所示:
注意:一个视图只能定义在布局中被引用的分段。如果视图在视图布局中午没有对应的@RendSection辅助器调用分段,MVC框架会报错。
一般情况下,不要分段与视图的其余的部分混杂在一起。其约定是,在视图的开始或者结尾部分定义分段,以便更容易看到哪些内容区域被处理成分段,对于那些需要RenderBody辅助器捕捉的内容,可以把这些定义成一个独立的分段,如下图所示:
@model string[]
@{Layout = "~/Views/Shared/_Layout.cshtml";
}@section Header{<div class="view">@foreach (string str in new[] {"Home","List","Edit" }){@Html.ActionLink(str,str,null,new { style = "margin:5px"})}</div>
}@section Body
{<div class="view">This is a lists of fruit names@foreach (string name in Model){<span><b>@name</b></span>}</div>
}@section Footer
{<div class="view">This is the footer</div>
}
以上这种办法有利于建立更清晰的视图,并减少了RenderBody捕捉无关内容的情况。为了使用这种方法,我们得用Rend而Section("Body")替换对RenderBody辅助器的调用。 代码如下图所示:
<!DOCTYPE html><html>
<head><meta name="viewport" content="width=device-width" /><style type="text/css">div.layout {background-color:lightgray;}div.view {border:thin solid black;margin:10px 0;}</style><title>@ViewBag.Title</title>
</head>
<body>@RenderSection("Header")<div class="layout">This is part of the layout</div>@RenderSection("Body")<div class="layout">This is part of Layout</div>@RenderSection("Footer")<div class="layout">This is part of Layout</div>
</body>
</html>
对分段进行测试
检查一个视图是否已经定义了布局中的一个特定的分段,可以使用如下代码,修改了_layout.cshtml:
@if (IsSectionDefined("Footer"))
{@RenderSection("Footer")
}
else
{<h4> This is the deafault footer </ht>
}
渲染可选分段
默认情况下,视图中必须含有布局中的RendSection的所有分段。如果缺少分段,MVC框架将会报错,修改_layout.cshtml代码并运行,结果如下图所示:
定义可选分段,只需加上一个false值即可:如果视图定义了它,其内容将被插入到结果中,否则也不会抛出异常,代码如下图所示:
@RenderSection("scripts",false)
使用分部视图
通常需要在应用程序中多个不同的地方,使用同样的Razor标签 和 HTML 标记片段。采取的办法不是重复这些标记,而是采用分部视图(Partial View).
新建一个名称为MyPartial.cshtml分部视图,代码如下图所示:
<div>This is the massage from the partial view.@Html.ActionLink("This is a link to the Index action", "Index");
</div>
修改List.cshtml页面,代码如下图所示:
@{Layout = null;
}<!DOCTYPE html><html>
<head><meta name="viewport" content="width=device-width" /><title>List</title>
</head>
<body><div> <h3>This is the /Views/Common/List.cshtml View</h3></div>
</body>
</html>
@Html.Partial("MyPartial")
提示:Razor视图引擎对分部视图的查找方式,于规则视图相同 (即在~/Views/<controller> 和 ~/Views/Shared文件夹中进行查找)。这意味着,可以创建控制器专用的的特殊版本的分部视图,它会覆盖Shared文件夹下进行查找。
运行程序,效果如下:
使用强类型分部视图
可以创建强类型分部视图,然后在渲染这个分部视图时,传递要使用的视图模型对象。
新建一个名为MyStrongTypedPartial.cshtml的分部视图,代码如下图所示:
@model IEnumerable<string><div>This is message from the partial view.<ul>@foreach (string str in Model){<li>@str</li>}</ul>
</div>
并修改/View/Common/List.cshtml文件,代码如下所示:
@{Layout = null;
}<!DOCTYPE html><html>
<head><meta name="viewport" content="width=device-width" /><title>List</title>
</head>
<body><div> <h3>This is the /Views/Common/List.cshtml View</h3></div>
</body>
</html>
@Html.Partial("MyStronglyTypedPartial",new[] {"Apple","Orange","Pear" })
运行效果如下图所示:
使用子动作
子动作是通过视同调用的动作方法。当你希望将某种控制器逻辑用于应用程序的多个地方时,子动作可以让你避免重复的控制器逻辑。子动作和动作之间的关系,如同分部视图和视图的关系一样。无论何时,当希望显示某些数据驱动的“小部件”,这些“小部件”要出现在多个页面上,而且含有与主动作无关的数据时,你可能就会希望使用子动作。
创建一子动作,代码如下图所示:
[ChildActionOnly]public ActionResult Time(){return PartialView(DateTime.Now);}
新增Time.cshtml页面,代码如下图所示:
@model DateTime<p> The time is @Model.ToShortDateString()</p>
启动程序,再次导航到/Home/List,效果如下图所示:
通过提供一个匿名类型对象,其属性对应于子动作方法的参数名,可以将参数传递给动作方法,代码如下图所示:
[ChildActionOnly]public ActionResult Time(DateTime time){return PartialView(DateTime.Now);}
@Html.Action("Time",new { time = DateTime.Now})