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,讓支持從物理路徑的加載方法。這兩個類的代碼如下:

public class FileResourceLoaderEx : FileResourceLoader
{
    public FileResourceLoaderEx() : base() { }
    private Stream FindTemplate(string filePath)
    {
        try
        {
            FileInfo file = new FileInfo(filePath);
            return new BufferedStream(file.OpenRead());
        }
        catch (Exception exception)
        {
            base.runtimeServices.Debug(string.Format("FileResourceLoader : {0}", exception.Message));
            return null;
        }
    }


    public override long GetLastModified(global::NVelocity.Runtime.Resource.Resource resource)
    {
        if (File.Exists(resource.Name))
        {
            FileInfo file = new FileInfo(resource.Name);
            return file.LastWriteTime.Ticks;
        }
        return base.GetLastModified(resource);
    }
    public override Stream GetResourceStream(string templateName)
    {
        if (File.Exists(templateName))
        {
            return FindTemplate(templateName);
        }
        return base.GetResourceStream(templateName);
    }
    public override bool IsSourceModified(global::NVelocity.Runtime.Resource.Resource resource)
    {
        if (File.Exists(resource.Name))
        {
            FileInfo file = new FileInfo(resource.Name);
            return (!file.Exists || (file.LastWriteTime.Ticks != resource.LastModified));
        }
        return base.IsSourceModified(resource);
    }
}
public class NVelocityViewEngine : VirtualPathProviderViewEngine, IViewEngine
{
    public static NVelocityViewEngine Default = null;

    private static readonly IDictionary DEFAULT_PROPERTIES = new Hashtable();
    private readonly VelocityEngine _engine;

    static NVelocityViewEngine()
    {
        string targetViewFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "views");
        //DEFAULT_PROPERTIES.Add(RuntimeConstants.RESOURCE_LOADER, "file");
        DEFAULT_PROPERTIES.Add(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, targetViewFolder);
        DEFAULT_PROPERTIES.Add("file.resource.loader.class", "NVelocityEngine.FileResourceLoaderEx\\,NVelocityEngine");


        Default = new NVelocityViewEngine();
    }

    public NVelocityViewEngine()
        : this(DEFAULT_PROPERTIES)
    {
    }

    public NVelocityViewEngine(IDictionary properties)
    {
        base.MasterLocationFormats = new string[] { "~/Views/{1}/{0}.vm", "~/Views/Shared/{0}.vm" };
        base.AreaMasterLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.vm", "~/Areas/{2}/Views/Shared/{0}.vm" };
        base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.vm", "~/Views/Shared/{0}.vm" };
        base.AreaViewLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.vm", "~/Areas/{2}/Views/Shared/{0}.vm" };
        base.PartialViewLocationFormats = base.ViewLocationFormats;
        base.AreaPartialViewLocationFormats = base.AreaViewLocationFormats;
        base.FileExtensions = new string[] { "vm" };


        if (properties == null) properties = DEFAULT_PROPERTIES;

        ExtendedProperties props = new ExtendedProperties();
        foreach (string key in properties.Keys)
        {
            props.AddProperty(key, properties[key]);
        }

        _engine = new VelocityEngine();
        _engine.Init(props);
    }

    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
        Template viewTemplate = GetTemplate(viewPath);
        Template masterTemplate = GetTemplate(masterPath);
        NVelocityView view = new NVelocityView(controllerContext, viewTemplate, masterTemplate);
        return view;
    }
    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
        Template viewTemplate = GetTemplate(partialPath);
        NVelocityView view = new NVelocityView(controllerContext, viewTemplate, null);
        return view;
    }
    public Template GetTemplate(string viewPath)
    {
        if (string.IsNullOrEmpty(viewPath))
        {
            return null;
        }
        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); 。完全代碼如下:

public class NVelocityView : IViewDataContainer, IView
{
    private ControllerContext _controllerContext;
    private readonly Template _masterTemplate;
    private readonly Template _viewTemplate;

    public NVelocityView(ControllerContext controllerContext, string viewPath, string masterPath)
        : this(controllerContext, NVelocityViewEngine.Default.GetTemplate(viewPath), NVelocityViewEngine.Default.GetTemplate(masterPath))
    {

    }
    public NVelocityView(ControllerContext controllerContext, Template viewTemplate, Template masterTemplate)
    {
        _controllerContext = controllerContext;
        _viewTemplate = viewTemplate;
        _masterTemplate = masterTemplate;
    }

    public Template ViewTemplate
    {
        get { return _viewTemplate; }
    }

    public Template MasterTemplate
    {
        get { return _masterTemplate; }
    }

    private VelocityContext CreateContext(ViewContext context)
    {
        Hashtable entries = new Hashtable(StringComparer.InvariantCultureIgnoreCase);
        if (context.ViewData != null)
        {
            foreach (var pair in context.ViewData)
            {
                entries[pair.Key] = pair.Value;
            }
        }
        entries["viewdata"] = context.ViewData;
        entries["tempdata"] = context.TempData;
        entries["routedata"] = context.RouteData;
        entries["controller"] = context.Controller;
        entries["httpcontext"] = context.HttpContext;
        entries["viewbag"] = context.ViewData;
        CreateAndAddHelpers(entries, context);

        return new VelocityContext(entries);
    }

    private void CreateAndAddHelpers(Hashtable entries, ViewContext context)
    {
        entries["html"] = entries["htmlhelper"] = new HtmlExtensionDuck(context, this);
        entries["url"] = entries["urlhelper"] = new UrlHelper(context.RequestContext);
        entries["ajax"] = entries["ajaxhelper"] = new AjaxHelper(context, this);
    }

    public void Render(ViewContext viewContext, TextWriter writer)
    {
        this.ViewData = viewContext.ViewData;

        bool hasLayout = _masterTemplate != null;

        VelocityContext context = CreateContext(viewContext);

        if (hasLayout)
        {
            StringWriter sw = new StringWriter();
            _viewTemplate.Merge(context, sw);

            context.Put("childContent", sw.GetStringBuilder().ToString());

            _masterTemplate.Merge(context, writer);
        }
        else
        {
            _viewTemplate.Merge(context, writer);
        }
    }

    private ViewDataDictionary _viewData;
    public ViewDataDictionary ViewData
    {
        get
        {
            if (_viewData == null)
            {
                return _controllerContext.Controller.ViewData;
            }
            return _viewData;
        }
        set
        {
            _viewData = value;
        }
    }
}

ExtensionDuck

ExtensionDuck就是對IDuck接口的實現,它是我們需要提供擴展方法支持的Duck對象的基類。所有需要接供擴展方法的對象,通過繼承該方法可以簡化大部分的工作:

public class ExtensionDuck : IDuck
{
    private readonly object _instance;
    private readonly Type _instanceType;
    private readonly Type[] _extensionTypes;
    private Introspector _introspector;

    public ExtensionDuck(object instance)
        : this(instance, Type.EmptyTypes)
    {
    }

    public ExtensionDuck(object instance, params Type[] extentionTypes)
    {
        if(instance == null) throw new ArgumentNullException("instance");

        _instance = instance;
        _instanceType = _instance.GetType();
        _extensionTypes = extentionTypes;
    }

    public Introspector Introspector
    {
        get
        {
            if(_introspector == null)
            {
                _introspector = RuntimeSingleton.Introspector;
            }
            return _introspector;
        }
        set { _introspector = value; }
    }

    public object GetInvoke(string propName)
    {
        throw new NotSupportedException();
    }

    public void SetInvoke(string propName, object value)
    {
        throw new NotSupportedException();
    }

    public object Invoke(string method, params object[] args)
    {
        if(string.IsNullOrEmpty(method)) return null;

        MethodInfo methodInfo = Introspector.GetMethod(_instanceType, method, args);
        if(methodInfo != null)
        {
            return methodInfo.Invoke(_instance, args);
        }

        object[] extensionArgs = new object[args.Length + 1];
        extensionArgs[0] = _instance;
        Array.Copy(args, 0, extensionArgs, 1, args.Length);

        foreach(Type extensionType in _extensionTypes)
        {
            methodInfo = Introspector.GetMethod(extensionType, method, extensionArgs);
            if(methodInfo != null)
            {
                return methodInfo.Invoke(null, extensionArgs);
            }
        }

        return null;
    }
}

接下,我們就可以來實現一個HtmlExtensionDuck,指定一下,View中可以調用到HtmlHelper的哪些擴展方法,需要被開放的擴展方法可以在HTML_EXTENSION_TYPES中提供擴展方法所在的靜態類名:

public class HtmlExtensionDuck : ExtensionDuck
{
    public static readonly Type[] HTML_EXTENSION_TYPES =
        new Type[]
            {
                typeof(DisplayExtensions),
                typeof(DisplayTextExtensions),
                typeof(EditorExtensions),
                typeof(FormExtensions), 
                typeof(InputExtensions), 
                typeof(LabelExtensions),
                typeof(LinkExtensions), 
                typeof(MvcForm),
                typeof(PartialExtensions),
                typeof(RenderPartialExtensions),
                typeof(SelectExtensions),
                typeof(TextAreaExtensions),
                typeof(ValidationExtensions)
            };

    public HtmlExtensionDuck(ViewContext viewContext, IViewDataContainer container)
        : this(new HtmlHelper(viewContext, container))
    {
    }

    public HtmlExtensionDuck(HtmlHelper htmlHelper)
        : this(htmlHelper, HTML_EXTENSION_TYPES)
    {
    }

    public HtmlExtensionDuck(HtmlHelper htmlHelper, params Type[] extentionTypes)
        : base(htmlHelper, extentionTypes)
    {
    }
}

完整的NVelocity for ASP.NET MVC的實現就是以上幾個類就可以完成。然後,我們就可以直接註冊到系統中來。我們不需要重寫任何Conroller,我們直接把ViewEngine註冊到MVC中來就可以被用到,也可以支持一個程序支持多種視圖引擎共存的和諧場面。簡單的註冊代碼放在Global.asax文件中:

ViewEngines.Engines.Add(NVelocityViewEngine.Default);

詳細的使用示例,下載附件查看詳細。

最後總結一下,NVelocity確實是一種簡單實用的模板引擎,特別是它的語法非常簡潔,而且API的可擴展性還是挺強的。Razor的語法的基本風格應該是有借鑑了它的語法風格。我現在雖然也很喜歡NVelocity,但如果不是特殊情況的特殊需要,在普通的ASP.NET MVC程序中,我還是會側向於使用Razor。它更自由一點,由於是直接的C#編譯支持,所以我們可以做任何的事情,這對於很多開發人員來說很重要。另一個不可忽視的就是它的IDE支持,特別是代碼提示確實是讓人相當的舒服。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章