《Pro ASP.NET MVC 3 Framework》学习笔记之二十七【视图1】

在前面很多的章节里面的,最常用的action result是视图呈现并返回给客户端的ViewResult类型。本章会专注于视图的原理,首先展示MVC框架是如何使用视图引擎处理ViewResults的,包括阐释如何创建一个视图引擎。接着介绍使用Razor视图引擎的一些技术。最后是关于创建和使用部分视图,子actions,以及Razor片段,这些都是涉及高效MVC开发的本质话题。

创建一个自定义视图引擎(Creating a Custom View Engine)

MVC框架包含了两个内置的,功能完善,容易理解的视图引擎:

a.Razor引擎:MVC3里面引入的一个新的视图引擎,具有更加简洁和优雅的语法。
b.ASPX引擎:使用的是webform里面的标签语法”<%..%>”,用于维护兼容旧的MVC程序。

这里创建一个自定义的视图引擎的目的是为了阐释请求管道的运行原理以及完善我们对MVC框架运行原理的认识,包含了理解视图引擎在转化一个ViewResult到响应客户端的输出有多少自主性。

视图引擎实现了IViewEngine接口,如下所示:

namespace System.Web.Mvc
{
public interface IViewEngine
{
ViewEngineResult FindView(ControllerContext controllerContext, string viewName,
string masterName, bool useCache);
ViewEngineResult FindPartialView(ControllerContext controllerContext,
string partialViewName, bool useCache);

void ReleaseView(ControllerContext controllerContext, IView view);
}
}

视图引擎扮演的角色是将请求转换成ViewEngineResult对象,前面两个方法包含的参数分别是:描述请求,处理请求的控制器,视图及其布局,以及是否允许从它的缓存里面取出前一次的结果。这些方法在ViewResult被处理时调用。最后一个方法当视图不在需要时被调用。

注:MVC框架对视图引擎的支持是通过ControllerActionInvoker类实现的,这个类是IActionInvoker接口的实现。如果我们实现的是自己的action调用者或控制器工厂的话,这样是不会自动访问视图引擎的功能。

ViewEngineResult类允许视图引擎在一个视图被请求时响应给MVC框架,我们通过选择两个构造器中的一个来表达结果。

如果视图引擎能够提供一个针对请求的视图,那么我们可以使用的构造器是:public ViewEngineResult(IView view, IViewEngine viewEngine)
否则,能使用的构造器为:public ViewEngineResult(IEnumerable<string> searchedLocations) ,参数提供能够找到视图的位置。
视图引擎系统最后一块是IView接口,如下:

using System.IO;

namespace System.Web.Mvc
{
public interface IView {
void Render(ViewContext viewContext, TextWriter writer);
}
}

我们传递一个IView接口的实现给ViewEngineResult对象的构造器,然后从视图引擎的方法返回。MVC框架调用Render方法,ViewContext参数包含了关于请求的信息和action方法的输出。TextWriter参数将输出写到客户端。下面会创建一个视图引擎,只返回一种视图,该视图会呈现包含请求信息的结果以及由action方法产生的视图数据。从创建视图引擎的过程里面能让我们了解视图引擎运作的方式,这里是没有涉及到解析视图模版的。

创建一个自定义的IView接口(Creating a Custom IView)

namespace Views.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/>”);
}
}
}

创建IViewEngine实现(Creating an IViewEngine Implementation)

我要明确视图引擎的目的是生产一个ViewEngineResult对象,该对象包含一个IView接口或者是一个包含视图位置的列表。上面创建了IView接口,下面接着创建视图引擎,如下:

using System.Web.Mvc;

namespace Views.Infrastructure
{
public class DebugDataViewEngine : IViewEngine
{
public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
return new ViewEngineResult(new string[] { “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[] { “Debug Data View Engine” });
}
}

public void ReleaseView(ControllerContext controllerContext, IView view)
{

}
}
}

这里创建的视图引擎支持DebugData视图,当有一个针对该视图的请求时,则返回一个IView接口实现的实例,像这样:return new ViewEngineResult(new DebugDataView(), this);

如果我们要实现更加正式的视图引擎,需要利用这个时机寻找模版,并考虑布局和提供缓存设置。这里的例子仅仅需要创建一个DataDebugView类的实例,如果接收到其他的请求,会返回如:return new ViewEngineResult(new string[] { “Debug Data View Engine” }); IViewEngine接口假定视图引擎具有它需要用来寻找视图的位置信息,这是非常合理的假设,因为视图是典型的存储在项目里的模版文件。这里并不需要去找,我们返回是一个虚拟的位置。这里也没有支持部分视图,也没有实现ReleaseView方法,因为没有资源需要释放。

注册自定义的视图引擎(Registering a Custom View Engine)

注册视图引擎的方法有很多,首选的是在Global的Application_Start方法里面添加 ViewEngines.Engines.Add(new DebugDataViewEngine());
静态的ViewEngine.Engines集合包含了一套安装在应用程序的视图引擎。当一个ViewResult正被处理时,action调用者获得已经安装的视图引擎的集合并轮流调用它们的FindView方法。当action调用者收到一个包含IView接口的ViewEngineResult对象时停止调用FindView方法。这意味着添加到ViewEngines.Engines里面的引擎的顺序非常重要,特别是在有多个引擎服务同名的视图时。

如果想要我们的视图引擎优先,可以利用集合的插入方法如下:

ViewEngines.Engines.Insert(0, new DebugDataViewEngine());
还有一种方法是使用全局依赖解析器,当应用程序启动时,依赖解析器会请求任何可用的IViewEngine接口实现。如果使用NinjectDependencyResolver类,我们可以在AddBinds方法里注册视图引擎,

如:private void AddBindings()
{
//绑定
Bind<IViewEngine>().To<DebugDataViewEngine>();
}

使用这种方式不能控制视图引擎被处理的顺序,如果我们要关注视图引擎会相互竞争,最好还是选择ViewEngines.Engines集合里面的Insert方法来实现。下面测试我们自定义的视图引擎,创建一个HomeController类如下:

namespace Views.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData[“Message”] = “Hello,World”;
ViewData[“Time”] = DateTime.Now.ToShortTimeString();
return View(“DebugData”);
}
}
}

这时可以运行程序,结果如下:

《Pro ASP.NET MVC 3 Framework》学习笔记之二十七【视图1】-attach
出现上面的结果是因为针对我们处理的视图的FindView被调用了。如果我们改变下Index方法以至于ViewResult请求一个视图时没有安装的引擎能够响应,如下:
return View(“No_Such_View”); 这时我们运行程序,action调用者会去遍历每一个可用的视图引擎并调用它们的FindView方法。没有一个能够服务这个请求,并且会返回它们寻找过位置集合。如图:

《Pro ASP.NET MVC 3 Framework》学习笔记之二十七【视图1】-attach

使用Razor引擎(Working with the Razor Engine)

在前面我们通过实现两个接口创建了自定义的视图引擎,当然是非常简单的,只是为了说明其原理。视图引擎的复杂性来自于视图模版系统,包含了代码分片,支持布局,可编译来优化性能。这些在我们自定义的视图引擎里面是没有的,也不需要,因为Razor引擎考虑了所有的。

理解Razor视图渲染(Understanding Razor View Rendering)

Razor视图引擎通过编译视图来改善性能,视图会被转变为C#类,然后编译。这也是为什么我们能够在视图里面包含C#代码片段的原因。看下Razor视图创建的源码会非常有益的,因为它帮助我们放置了很多Razor功能在上下文Context里面。下面是一个简单的Razor视图,获取了一个字符串数组作为视图模型对象:

@model string[]
@{
ViewBag.Title = “Index”;
}

This is a list of fruit names:

@foreach (string name in Model) {
<span><b>@name</b></span>
}

在应用程序启动时,视图才会被编译,也只有在这个时候才能看到由Razor创建的C#类,我们需要启动应用程序并导航到一个action方法。然后一个actin方法都行,因为初始化请求时会触发视图编译过程。非常方便的是,创建的C#类会作为C#代码文件写入磁盘然后编译,这意味着我们能够查看到C#具体的代码。如果是Windows7系统,查看路径为:c:\Users\yourLoginName\AppData\Local\Temp\Temporary ASP.NET Files。下面是一个示例文件:

namespace ASP {
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Helpers;
using System.Web.Security;
using System.Web.UI;
using System.Web.WebPages;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
using System.Web.Mvc.Html;
using System.Web.Routing;

public class _Page_Views_Home_Index_cshtml : System.Web.Mvc.WebViewPage<string[]> {

public _Page_Views_Home_Index_cshtml() {
}

public override void Execute() {

WriteLiteral(“\r\n”);
ViewBag.Title = “Index”;

WriteLiteral(“\r\nThis is a list of fruit names:\r\n\r\n”);

foreach (string name in Model) {
WriteLiteral(” <span><b>”);
Write(name);
WriteLiteral(“</b></span>\r\n”);
}
}
}

首先,可以注意到这个类是从WebViewPage<T>派生的,T表示model的类型,这也是能够处理强类型视图的原因。也可以发现视图文件的路径是怎样被编码为类名的。这也是Razor映射视图的请求到已编译的类的实例的原理。在Execute方法里,我们能够看到视图里面语句和元素是怎样被处理的。HTML元素被WirteLiteral方法处理,这个方法可以将参数的内容写到结果里面。这刚好跟Write方法相反,Write方法对C#变量使用并对字符串的值编码让它们能够在HTML页面安全使用。

WriteLiteral和Write方法都是将内容写到一个TextWriter对象里面。这个对象同样被传递给了IView.Render方法。一个已经编译的Razor视图的目的是为了创建静态和动态的内容并通过TextWriter发送给客户端。

添加依赖注入到Razor视图(Adding Dependency Injection to Razor Views)

MVC请求处理管道的每一个部分都支持DI(Dependency Injection),Razor也不例外。然而所不同的是,这里扩展了类和视图之间的边界。下面是一个例子来说明:
创建一个接口ICalculator如下:

namespace Views.Models
{
public interface ICalculator
{
int Sum(int x, int y);
}
}

如果我们想要一个视图能够使用ICalculator并且具有注入的实现,那么我们需要创建一个派生自WebViewPage的抽象类,如下:

using System.Web.Mvc;
using Ninject;

namespace Views.Models.ViewClasses
{
public abstract class CalculatorView : WebViewPage
{
[Inject]
public ICalculator Calculator { get; set; }
}
}

支持DI最简便的方式就是使用属性注入(property injection),即:我们的DI容器注入依赖到一个属性里面,而不是最开始时使用的构造器注入。这里使用的了Inject特性,为了在视图里面利用它,我们使用Razor继承元素,如下所示:

//Controller代码
namespace Views.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
string[] fruitNames = { “Apple”, “Cherry”, “Pear”, “Mango”, “<script>do something</script>” };
return View(fruitNames);
}

public ViewResult Calculate()
{
ViewBag.X = 10;
ViewBag.Y = 20;
return View();
}
}
}

//Calculate视图代码,也就是这里要用@inherits元素
@inherits Views.Models.ViewClasses.CalculatorView
@{
ViewBag.Title = “Calculate”;
}
<h2>
Calculate</h2>
The calculator result for @ViewBag.X and @ViewBag.Y is @Calculator.Sum(ViewBag.X, ViewBag.Y)

//Index视图代码
@model string[]
@{
ViewBag.Title = “Index”;
}
@helper GetNow()
{
<blockquote>@DateTime.Now</blockquote>
}
this is a list of fruit names:
@foreach (string name in Model)
{
<span><b>@name</b></span>
}

我们能够使用@inherits指定基类,这样就能够访问Calculator属性并接收ICalculator接口的实现,所有这一切都没有创建view和ICalculator接口之间的依赖。使用了@inherits元素的效果就是改变了实体编译创建的类的基类,如:public class _Page_Views_Home_Calculate_cshtml : Views.Models.ViewClasses.CalculatorView {…}
由于创建的类是从我们的抽象类派生的,所以能够访问我们自己定义的Calculator属性。剩下的就是使用依赖解析器注册ICalculator接口实现,以至于Ninject将被用来创建视图类并且有机会实施属性注入。如下:

//SimpleCalculator类,实现ICalculator接口
namespace Views.Infrastructure
{
public class SimpleCalculator : ICalculator
{
public int Sum(int x, int y)
{
return x + y;
}
}
}

//NinjectDependencyResolver,实施属性依赖
namespace Views.Infrastructure
{
public class NinjectDependencyResolver : IDependencyResolver
{
private IKernel kernel;
public NinjectDependencyResolver()
{
kernel = new StandardKernel();
AddBindings();
}

public object GetService(Type serviceType)
{
return kernel.TryGet(serviceType);
}

public IEnumerable<object> GetServices(Type serviceType)
{
return kernel.GetAll(serviceType);
}

public IBindingToSyntax<T> Bind<T>()
{
return kernel.Bind<T>();
}

public IKernel Kernel
{
get { return kernel; }
}

private void AddBindings()
{
Bind<IViewEngine>().To<DebugDataViewEngine>();
Bind<ICalculator>().To<SimpleCalculator>();
}
}
}

//在galobal里面设置解析器
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ViewEngines.Engines.Insert(0, new DebugDataViewEngine());
DependencyResolver.SetResolver(new NinjectDependencyResolver());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}

这时可以运行程序得到如下结果:

《Pro ASP.NET MVC 3 Framework》学习笔记之二十七【视图1】-attach
配置视图搜索位置(Configuring the View Search Locations)

Razor视图引擎遵循早期MVC版本里面寻找视图的建立的约定,如果请求与Home控制器关联的Index视图,Razor按照下面的列表寻找:
• ~/Views/Home/Index.cshtml
• ~/Views/Home/Index.vbhtml
• ~/Views/Shared/Index.cshtml
• ~/Views/Shared/Index.vbhtml

Razor不是真的在磁盘上寻找视图文件,因为视图这个时候已经编译成了C#类。Razor实际上是寻找的这些代表视图的编译后的C#类,.cshtml文件是包含C#语句的模版。我们可以通过创建RazorViewEngine的子类来改变Razor搜寻的视图文件。它是建立在一序列的基类之上的,这些基类包含了决定哪一个视图文件被搜寻的一套属性。如下:

《Pro ASP.NET MVC 3 Framework》学习笔记之二十七【视图1】-attach
占位符{0},{1},{2}分别表示:视图名,控制器名,区域名。
为了改变搜寻的位置,我们创建一个从RazorViewEngine派生的类并改变属性的值,下面是一个示例:

using System.Web.Mvc;

namespace Views.Infrastructure
{
public class CustomRazorViewEngine : RazorViewEngine
{
public CustomRazorViewEngine()
{
ViewLocationFormats = new string[] {
“~/Views/{1}/{0}.cshtml”,”~/Views/Common/{0}.cshtml”
};
}
}
}

代码里面我们为ViewLocationFormats设置了新的值,新的数组包含了全部的.cshtml文件。另外,还改变了公共视图的寻找位置,不再是Views/Shared而是Views/Common。接着需要在Application_Start方法里面注册:

protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomRazorViewEngine());
//ViewEngines.Engines.Insert(0, new DebugDataViewEngine());
//DependencyResolver.SetResolver(new NinjectDependencyResolver());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}

记住一点:action调用者轮流去每一个视图引擎寻找视图,等到我们添加视图到集合里面时,它已经包含了标准的Razor视图引擎。为了避免跟其他的视图引擎竞争,我们调用Clear方法移除任何可能已经注册的视图引擎并调用Add方法注册我们自己实现的视图引擎。

添加动态的内容到Razor视图(Adding Dynamic Content to a Razor View)

视图的全部意图就是允许我们将领域模型作为用户接口呈现出来。为了达到这个目的,我们需要添加动态的内容,动态内容是在运行时创建的,并且每一次请求创建的内容都可以不同。这个跟静态内容是相反的,像HTML这些静态的内容在每次请求时都是一样的。添加动态内容的方式有四种:内嵌代码(Inline code),HTML辅助方法(HTML helper methods),部分视图(Partial views),子action(Child actions).下面分别对每一种方式进行介绍:

使用内嵌代码(Using Inline Code)

创建动态内容最简便的方式就是使用内嵌代码——一行或多行以@符合开始的C#代码。这是Razor视图引擎的核心。
内嵌代码与分解关注点:学过ASP.NET WebForms的人可能非常想知道为什么会对内嵌代码如此着迷,毕竟在WebForms里面的约定是尽可能的将代码放在代码隐藏文件里面。这很容易混淆,特别是当我们认为代码隐藏文件常常是对分解关注点的维护时。这个分歧的出现是因为MVC和WebForms对哪一个关注点应该被分解以及分界线应该在哪有不同的概念。WebForms将陈述性的HTML标记和程序逻辑分开,ASPX包含HTML标签,代码隐藏文件包含逻辑。与之相对的是MVC,它是将展示逻辑和应用逻辑分开,控制器和领域模型分别负责应用逻辑和领域逻辑,视图包含展示逻辑。所以内嵌代码更好的适应了MVC框架。Razor句法让我们很容易的创建可维护,可扩展的视图。

内嵌代码功能的灵活性很容易模糊应用程序的边界,并且让分解关注点的思想瓦解。作为一种纪律约束:必须通过某种方式来维护分解关注点的有效性,那就是与MVC设计模式保持一致。当我们着手使用MVC框架时,建议将关注点放在理解功能上。当我们有了更多的经验时,会自然形成将视图和MVC组件的职责分开的感觉。

好了,今天的笔记就到这里,关于这章的笔记在下一篇接着做完。
晚安!

作者:张雪飞
出处:https://zhangxuefei.site/p/185
版权说明:欢迎转载,但必须注明出处,并在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

发表评论

电子邮件地址不会被公开。 必填项已用*标注