[转载]NVelocity for ASP.NET MVC – 阿不 – 博客园.
在我的这篇博文中,有这么一段话:“我一直在想,有没有办法可以单独限制View中的代码的访问权限,类似于trust level,只是这个trust level是用来限制模板中的代码。”。有读者johngeng问, 为什么要用trust level来锁住view,他不是很理解。我的本意是,希望在view中,开发人员只能写某一些特定功能的代码,调用某一些特定开放的API,对于大部分 安全级比较高的代码,比如读写文件等API或类库,不允许在view当中使用。这对于我们将模板开放出来,在线提供给我们的用户去修改的需求下是非常重要 的。而目前,不管WebForm还是Razor,都是非常自由的模板,在View能做的事情等同于Controller或其它地方所写的代码,这样 View就不允许开放出来由用户在线修改。
在相同的博文里面,还是那位读者johngeng提到它更喜欢$而不是@,由于我之前并不了解NVelocity,所以我误解为它是在说客户端开发包JQuery。现在看来,他说的应该就是NVelocity,也许他觉得此人不可教,他并没有直接回复我的疑问,这也只能怪自己知识面太窄了。
若不是最近在为项目添加多模板引擎的支持,或许我永远也无法得到以上两个问题的答案,而这两个答案都与NVelocity有关。虽然我平常肯定也见过NVelocity这个词,但到要选择除WebForm以外的模板引擎,我还是完完全全没有记起他,还是同事@浪子提 醒我NVelocity这个模板引擎值得一试。看了官方的语法介绍后,我不得不说它是一种非常简洁且实用的模板,同时又不失它的灵活性和安全性。我所指的 灵活性是它不像StringTemplate那样,限制的那么死,连个对象的函数都不允许调用。安全性方面又可以满足我希望模板上限制开发人员只能在模板 上调用指定的API。到目前为止,NVelocity仍然让我非常满意。
在ASP.NET MVC切换视图引擎非常简单,在ASP.NET MVC1.0出来以后,MvcContrib就曾经提供了多种视图引擎的切换选择,但是在最近的版本中,我却始终没有找到相关的代码,应该是这些代码已经被移出去了,但它的介绍文档中还没有删掉相关的主题。还好在@重典童鞋的博客上找到了他从MvcContrib中提取出来的实现。 但是这个实现相对于MVC3来说,已经相对过时了,有些接口已经改变或被移除了,比如IViewLocator这个接口就已经不存在了。还有就是,它去掉 了原先支持的调用HtmlHelper扩展方法的功能,而我最重要的就是要支持扩展函数,因为我自定义了一些必须的扩展方法。下面我们就来看看 NVelocity for ASP.NET MVC几个类的详细情况:
NVelocityViewEngine
在 之前的实现中,直接实现了IViewEngine这个接口,查找View的路径是通过实现IViewLocator来定位。在MVC2当中,修改了这部分 的实现,MVC内部提供了VirtualPathProviderViewEngine这个模板方法类,在子类当中,我们中需要设置一下我们要查找的路径 格式,其它的事件就可以交给模板方法类来完成,这样一方面可以简化我们的实现,另一方面还可以和默认的路径查找方式统一。
同时,由于我使 用Nvelocity内置的相对文件路径的方式来查找模板,而使用VirtualPath的风格,因此在找到VirtualPath后,我们需要转换成实 际的物理路径,直接通过物理路径来加载模板内容,而内置的FileResourceLoader并不支持从物理路径加载模板,所以我们还要额外实现一下 FileResourceLoader,让支持从物理路径的加载方法。这两个类的代码如下:
001 |
public class FileResourceLoaderEx : FileResourceLoader |
003 |
public FileResourceLoaderEx() : base () { } |
004 |
private Stream FindTemplate( string filePath) |
008 |
FileInfo file = new FileInfo(filePath); |
009 |
return new BufferedStream(file.OpenRead()); |
011 |
catch (Exception exception) |
013 |
base .runtimeServices.Debug( string .Format( "FileResourceLoader : {0}" , exception.Message)); |
019 |
public override long GetLastModified(global::NVelocity.Runtime.Resource.Resource resource) |
021 |
if (File.Exists(resource.Name)) |
023 |
FileInfo file = new FileInfo(resource.Name); |
024 |
return file.LastWriteTime.Ticks; |
026 |
return base .GetLastModified(resource); |
028 |
public override Stream GetResourceStream( string templateName) |
030 |
if (File.Exists(templateName)) |
032 |
return FindTemplate(templateName); |
034 |
return base .GetResourceStream(templateName); |
036 |
public override bool IsSourceModified(global::NVelocity.Runtime.Resource.Resource resource) |
038 |
if (File.Exists(resource.Name)) |
040 |
FileInfo file = new FileInfo(resource.Name); |
041 |
return (!file.Exists || (file.LastWriteTime.Ticks != resource.LastModified)); |
043 |
return base .IsSourceModified(resource); |
046 |
public class NVelocityViewEngine : VirtualPathProviderViewEngine, IViewEngine |
048 |
public static NVelocityViewEngine Default = null ; |
050 |
private static readonly IDictionary DEFAULT_PROPERTIES = new Hashtable(); |
051 |
private readonly VelocityEngine _engine; |
053 |
static NVelocityViewEngine() |
055 |
string targetViewFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "views" ); |
057 |
DEFAULT_PROPERTIES.Add(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, targetViewFolder); |
058 |
DEFAULT_PROPERTIES.Add( "file.resource.loader.class" , "NVelocityEngine.FileResourceLoaderEx\\,NVelocityEngine" ); |
061 |
Default = new NVelocityViewEngine(); |
064 |
public NVelocityViewEngine() |
065 |
: this (DEFAULT_PROPERTIES) |
069 |
public NVelocityViewEngine(IDictionary properties) |
071 |
base .MasterLocationFormats = new string [] { "~/Views/{1}/{0}.vm" , "~/Views/Shared/{0}.vm" }; |
072 |
base .AreaMasterLocationFormats = new string [] { "~/Areas/{2}/Views/{1}/{0}.vm" , "~/Areas/{2}/Views/Shared/{0}.vm" }; |
073 |
base .ViewLocationFormats = new string [] { "~/Views/{1}/{0}.vm" , "~/Views/Shared/{0}.vm" }; |
074 |
base .AreaViewLocationFormats = new string [] { "~/Areas/{2}/Views/{1}/{0}.vm" , "~/Areas/{2}/Views/Shared/{0}.vm" }; |
075 |
base .PartialViewLocationFormats = base .ViewLocationFormats; |
076 |
base .AreaPartialViewLocationFormats = base .AreaViewLocationFormats; |
077 |
base .FileExtensions = new string [] { "vm" }; |
080 |
if (properties == null ) properties = DEFAULT_PROPERTIES; |
082 |
ExtendedProperties props = new ExtendedProperties(); |
083 |
foreach ( string key in properties.Keys) |
085 |
props.AddProperty(key, properties[key]); |
088 |
_engine = new VelocityEngine(); |
092 |
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) |
094 |
Template viewTemplate = GetTemplate(viewPath); |
095 |
Template masterTemplate = GetTemplate(masterPath); |
096 |
NVelocityView view = new NVelocityView(controllerContext, viewTemplate, masterTemplate); |
099 |
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) |
101 |
Template viewTemplate = GetTemplate(partialPath); |
102 |
NVelocityView view = new NVelocityView(controllerContext, viewTemplate, null ); |
105 |
public Template GetTemplate( string viewPath) |
107 |
if ( string .IsNullOrEmpty(viewPath)) |
111 |
return _engine.GetTemplate(System.Web.Hosting.HostingEnvironment.MapPath(viewPath)); |
NVelocityView
主要实现IView接口,实现Render方法来将模板和当前的上下文结合之后输出出来。这个类还实现了,IViewDataContainer好 像不是特别必要。NVelocity的Render也很简单,只是把所需要的对像塞到NVelocity执行的上下文当中,然后调用一下Merge方法就 OK了。这里要特别说明的是,在NVelocity模板上面,我们可以调用上下文对象的中的任何属性和方法,但是没有办法调用到对象上的扩展方法,这时 候,我们就需要借助NVelocity所提供的IDuck这个接口来提供扩展方法的支持,如下代码的:new HtmlExtensionDuck(context, this); 。完全代码如下:
01 |
public class NVelocityView : IViewDataContainer, IView |
03 |
private ControllerContext _controllerContext; |
04 |
private readonly Template _masterTemplate; |
05 |
private readonly Template _viewTemplate; |
07 |
public NVelocityView(ControllerContext controllerContext, string viewPath, string masterPath) |
08 |
: this (controllerContext, NVelocityViewEngine.Default.GetTemplate(viewPath), NVelocityViewEngine.Default.GetTemplate(masterPath)) |
12 |
public NVelocityView(ControllerContext controllerContext, Template viewTemplate, Template masterTemplate) |
14 |
_controllerContext = controllerContext; |
15 |
_viewTemplate = viewTemplate; |
16 |
_masterTemplate = masterTemplate; |
19 |
public Template ViewTemplate |
21 |
get { return _viewTemplate; } |
24 |
public Template MasterTemplate |
26 |
get { return _masterTemplate; } |
29 |
private VelocityContext CreateContext(ViewContext context) |
31 |
Hashtable entries = new Hashtable(StringComparer.InvariantCultureIgnoreCase); |
32 |
if (context.ViewData != null ) |
34 |
foreach (var pair in context.ViewData) |
36 |
entries[pair.Key] = pair.Value; |
39 |
entries[ "viewdata" ] = context.ViewData; |
40 |
entries[ "tempdata" ] = context.TempData; |
41 |
entries[ "routedata" ] = context.RouteData; |
42 |
entries[ "controller" ] = context.Controller; |
43 |
entries[ "httpcontext" ] = context.HttpContext; |
44 |
entries[ "viewbag" ] = context.ViewData; |
45 |
CreateAndAddHelpers(entries, context); |
47 |
return new VelocityContext(entries); |
50 |
private void CreateAndAddHelpers(Hashtable entries, ViewContext context) |
52 |
entries[ "html" ] = entries[ "htmlhelper" ] = new HtmlExtensionDuck(context, this ); |
53 |
entries[ "url" ] = entries[ "urlhelper" ] = new UrlHelper(context.RequestContext); |
54 |
entries[ "ajax" ] = entries[ "ajaxhelper" ] = new AjaxHelper(context, this ); |
57 |
public void Render(ViewContext viewContext, TextWriter writer) |
59 |
this .ViewData = viewContext.ViewData; |
61 |
bool hasLayout = _masterTemplate != null ; |
63 |
VelocityContext context = CreateContext(viewContext); |
67 |
StringWriter sw = new StringWriter(); |
68 |
_viewTemplate.Merge(context, sw); |
70 |
context.Put( "childContent" , sw.GetStringBuilder().ToString()); |
72 |
_masterTemplate.Merge(context, writer); |
76 |
_viewTemplate.Merge(context, writer); |
80 |
private ViewDataDictionary _viewData; |
81 |
public ViewDataDictionary ViewData |
85 |
if (_viewData == null ) |
87 |
return _controllerContext.Controller.ViewData; |
ExtensionDuck
ExtensionDuck就是对IDuck接口的实现,它是我们需要提供扩展方法支持的Duck对象的基类。所有需要接供扩展方法的对象,通过继承该方法可以简化大部分的工作:
01 |
public class ExtensionDuck : IDuck |
03 |
private readonly object _instance; |
04 |
private readonly Type _instanceType; |
05 |
private readonly Type[] _extensionTypes; |
06 |
private Introspector _introspector; |
08 |
public ExtensionDuck( object instance) |
09 |
: this (instance, Type.EmptyTypes) |
13 |
public ExtensionDuck( object instance, params Type[] extentionTypes) |
15 |
if (instance == null ) throw new ArgumentNullException( "instance" ); |
18 |
_instanceType = _instance.GetType(); |
19 |
_extensionTypes = extentionTypes; |
22 |
public Introspector Introspector |
26 |
if (_introspector == null ) |
28 |
_introspector = RuntimeSingleton.Introspector; |
32 |
set { _introspector = value; } |
35 |
public object GetInvoke( string propName) |
37 |
throw new NotSupportedException(); |
40 |
public void SetInvoke( string propName, object value) |
42 |
throw new NotSupportedException(); |
45 |
public object Invoke( string method, params object [] args) |
47 |
if ( string .IsNullOrEmpty(method)) return null ; |
49 |
MethodInfo methodInfo = Introspector.GetMethod(_instanceType, method, args); |
50 |
if (methodInfo != null ) |
52 |
return methodInfo.Invoke(_instance, args); |
55 |
object [] extensionArgs = new object [args.Length + 1]; |
56 |
extensionArgs[0] = _instance; |
57 |
Array.Copy(args, 0, extensionArgs, 1, args.Length); |
59 |
foreach (Type extensionType in _extensionTypes) |
61 |
methodInfo = Introspector.GetMethod(extensionType, method, extensionArgs); |
62 |
if (methodInfo != null ) |
64 |
return methodInfo.Invoke( null , extensionArgs); |
接下,我们就可以来实现一个HtmlExtensionDuck,指定一下,View中可以调用到HtmlHelper的哪些扩展方法,需要被开放的扩展方法可以在HTML_EXTENSION_TYPES中提供扩展方法所在的静态类名:
01 |
public class HtmlExtensionDuck : ExtensionDuck |
03 |
public static readonly Type[] HTML_EXTENSION_TYPES = |
06 |
typeof (DisplayExtensions), |
07 |
typeof (DisplayTextExtensions), |
08 |
typeof (EditorExtensions), |
09 |
typeof (FormExtensions), |
10 |
typeof (InputExtensions), |
11 |
typeof (LabelExtensions), |
12 |
typeof (LinkExtensions), |
14 |
typeof (PartialExtensions), |
15 |
typeof (RenderPartialExtensions), |
16 |
typeof (SelectExtensions), |
17 |
typeof (TextAreaExtensions), |
18 |
typeof (ValidationExtensions) |
21 |
public HtmlExtensionDuck(ViewContext viewContext, IViewDataContainer container) |
22 |
: this ( new HtmlHelper(viewContext, container)) |
26 |
public HtmlExtensionDuck(HtmlHelper htmlHelper) |
27 |
: this (htmlHelper, HTML_EXTENSION_TYPES) |
31 |
public HtmlExtensionDuck(HtmlHelper htmlHelper, params Type[] extentionTypes) |
32 |
: base (htmlHelper, extentionTypes) |
完整的NVelocity for ASP.NET MVC的实现就是以上几个类就可以完成。然后,我们就可以直接注册到系统中来。我们不需要重写任何Conroller,我们直接把ViewEngine注 册到MVC中来就可以被用到,也可以支持一个程序支持多种视图引擎共存的和谐场面。简单的注册代码放在Global.asax文件中:
1 |
ViewEngines.Engines.Add(NVelocityViewEngine.Default); |
详细的使用示例,下载附件查看详细。
最后总结一下,NVelocity确实是一种简单实用的模板引擎,特别是它的语法非常简洁,而且API的可扩展性还是挺强的。Razor的语法的基 本风格应该是有借鉴了它的语法风格。我现在虽然也很喜欢NVelocity,但如果不是特殊情况的特殊需要,在普通的ASP.NET MVC程序中,我还是会侧向于使用Razor。它更自由一点,由于是直接的C#编译支持,所以我们可以做任何的事情,这对于很多开发人员来说很重要。另一 个不可忽视的就是它的IDE支持,特别是代码提示确实是让人相当的舒服。