熟悉asp.net mvc的朋友都知道,mvc中,默認情況下視圖都在views文件夾下放着。要想改變文件必須重寫WebFormViewEngine,也就是從WebFormViewEngine繼承。對於razor模板是從RazorViewEngine繼承。
首選我們來回顧一下,傳統的做法。假若一個網站有前台、個人中心、企業中心、后台組成(對於類似博客系統不同用戶不同主題的現象問題一樣),那么,每個功能都需要定義一個視圖引擎。個人中心代碼如下:

1 public class UserViewEngine : WebFormViewEngine 2 3 { 4 public UserViewEngine() 5 { 6 MasterLocationFormats = new[]{ 7 "~/Views/Users/{1}/{0}.master", 8 "~/Views/Users/shared/{0}.master" 9 }; 10 11 ViewLocationFormats = new[]{ 12 "~/Views/Users/{1}/{0}.aspx", 13 "~/Views/Users/{1}/{0}.ascx", 14 "~/Views/Users/shared/{0}.aspx", 15 "~/Views/Users/shared/{0}.ascx" 16 }; 17 18 PartialViewLocationFormats = new[]{ 19 "~/Views/Users/{1}/{0}.ascx", 20 "~/Views/Users/shared/{0}.ascx" 21 }; 22 } 23 24 public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) 25 { 26 return base.FindView(controllerContext, viewName, masterName, useCache); 27 } 28 29 public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) 30 { 31 return base.FindPartialView(controllerContext, partialViewName, useCache); 32 } 33 }
企業中心代碼如下:

1 public class MemberViewEngine : WebFormViewEngine 2 3 { 4 public MemberViewEngine() 5 { 6 MasterLocationFormats = new[]{ 7 "~/Views/Member/{1}/{0}.master", 8 "~/Views/Member/shared/{0}.master" 9 }; 10 11 ViewLocationFormats = new[]{ 12 "~/Views/Member/{1}/{0}.aspx", 13 "~/Views/Member/shared/{0}.aspx" 14 }; 15 16 PartialViewLocationFormats = new[]{ 17 "~/Views/Member/{1}/{0}.ascx", 18 "~/Views/Member/shared/{0}.ascx", 19 "~/Views/shared/{0}.ascx" 20 }; 21 } 22 23 public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) 24 { 25 return base.FindView(controllerContext, viewName, masterName, useCache); 26 } 27 28 public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) 29 { 30 return base.FindPartialView(controllerContext, partialViewName, useCache); 31 } 32 }
對於前台和后台的模板引擎,可以自己通過簡單的修改去實現 。
視圖引擎能被調用的地方有三處,第一處是在globals中,也就是在application_start中注冊視圖引擎;第二個地方時在父類的構造函數中初始化;第三個是在view函數被調用前,也就是利用過濾器。
application_start中代碼如下:

protected void Application_Start() { ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(new UserViewEngine()); AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); }
構造函數中如下:

public ctor() { ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(new UserViewEngine()); }
過濾器中如下:

1 public override void OnActionExecuting(ActionExecutingContext filterContext) 2 3 { 4 //其他操作 5 ViewEngines.Engines.Clear(); 6 ViewEngines.Engines.Add(new UserViewEngine()); 7 }
對於第一種調用方法,顯然滿足不了我們的需求。這樣只有求助於第二種和第三種調用方法。 也就是在view函數被調用之前進行對當前視圖引擎的替換。view函數一般如下:

1 public ActionResult About() 2 3 { 4 return View(); 5 }
這樣便實現了我們的需求。
但是,目前實現的只能在單線程下使用,或者在同時都訪問前台或者都放我用戶中心的情況下是對的。假若同時出現並發現象。也就是一個用戶訪問企業中心,一個用戶訪問用戶中心。為了說明問題(簡單代碼實現),貼代碼如下,用戶中心類似代碼:

1 public ActionResult List() 2 3 { 4 ViewEngines.Engines.Clear(); 5 ViewEngines.Engines.Add(new UserViewEngine()); 6 7 Thread.Sleep(500000) 8 return View(); 9 }
企業中心類似代碼

1 public ActionResult List() 2 3 { 4 ViewEngineCollection.Clear(); 5 ViewEngineCollection.Add(new MemberViewEngine()); 6 7 Thread.Sleep(10000) 8 return View(); 9 }
為了說明問題,我故意在上面加了線程睡眠時間。
現在,個人中心先被訪問,然后企業中心再被訪問。這樣運行之后是會報錯的,說找不到視圖。
究其原因,是由於個人中心運行慢,企業中心運行快。那么用戶中心的視圖引擎就變成了企業中心的視圖引擎。但我們明明都在函數里加了如下代碼,為啥還報錯呢?

1 ViewEngines.Engines.Clear(); 2 3 ViewEngines.Engines.Add(new UserViewEngine());

1 ViewEngines.Clear(); 2 3 ViewEngines.Add(new MemberViewEngine());
我抱着究根問底的態度去研究程序的源碼,發覺微軟的實現如下:

1 // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. 2 3 4 namespace System.Web.Mvc 5 { 6 public static class ViewEngines 7 { 8 private static readonly ViewEngineCollection _engines = new ViewEngineCollection 9 { 10 new WebFormViewEngine(), 11 new RazorViewEngine(), 12 }; 13 14 public static ViewEngineCollection Engines 15 { 16 get { return _engines; } 17 } 18 } 19 }
看到上面的代碼,大家應該都明白了問題出現的根源,是靜態類造成的。
那么,我們有沒解決問題的辦法呢?幸運的是,微軟給出了另外一個調用辦法,這個方法是在controller中有一個非靜態變量,定義如下:

1 private ViewEngineCollection _viewEngineCollection; 2 3 [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This entire type is meant to be mutable.")] 4 public ViewEngineCollection ViewEngineCollection 5 { 6 get { return _viewEngineCollection ?? ViewEngines.Engines; } 7 set { _viewEngineCollection = value; } 8 }
看到這里,大家是不是感覺到微軟考慮問題是很周全了呢?
別急,我們看看 get { return _viewEngineCollection ?? ViewEngines.Engines; }
也就是說 _viewEngineCollection 為空的時候取的是ViewEngines.Engines。那么,我們在每個view函數被調用前都把ViewEngineCollection賦值不就行了嗎?類似於下面的代碼:

1 ViewEngineCollection.Clear(); 2 3 ViewEngineCollection.Add(new MemberViewEngine());
有的朋友可能發覺,這樣實現是不對的,他們會提出下面的實現辦法:

1 ViewEngineCollection = new ViewEngineCollection { new MemberViewEngine() };
這樣的代碼一運行,確實正確,我們ok!
但這是在我們沒用到用戶控件的情況下,假若一個母版頁中加載了用戶控件,類似下面的代碼

1 <%Html.RenderPartial("Menu");%>
上面的代碼在運行中還是會報錯的。
這個問題困擾了我一段時間,我想微軟不會錯吧。是不是我的程序出錯了,郁悶了好久。
然后,我就想到了看看RenderPartial 是怎么實現的。一查源碼,發覺代碼如下:

1 // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. 2 3 4 namespace System.Web.Mvc.Html 5 { 6 public static class RenderPartialExtensions 7 { 8 // Renders the partial view with the parent's view data and model 9 public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName) 10 { 11 htmlHelper.RenderPartialInternal(partialViewName, htmlHelper.ViewData, null /* model */, htmlHelper.ViewContext.Writer, ViewEngines.Engines); 12 } 13 14 // Renders the partial view with the given view data and, implicitly, the given view data's model 15 public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, ViewDataDictionary viewData) 16 { 17 htmlHelper.RenderPartialInternal(partialViewName, viewData, null /* model */, htmlHelper.ViewContext.Writer, ViewEngines.Engines); 18 } 19 20 // Renders the partial view with an empty view data and the given model 21 public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, object model) 22 { 23 htmlHelper.RenderPartialInternal(partialViewName, htmlHelper.ViewData, model, htmlHelper.ViewContext.Writer, ViewEngines.Engines); 24 } 25 26 // Renders the partial view with a copy of the given view data plus the given model 27 public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, object model, ViewDataDictionary viewData) 28 { 29 htmlHelper.RenderPartialInternal(partialViewName, viewData, model, htmlHelper.ViewContext.Writer, ViewEngines.Engines); 30 } 31 } 32 }
以上是微軟asp.net mvc 4中實現的源碼,我們看到了,微軟用的還是ViewEngines.Engines,也就是說用的還是靜態變量。這樣,我們便找到了問題的根源。
找到問題的根源后,我就在想,能不能把該函數重寫了呢?我就跟蹤到源碼中去,結果發覺它的實現如下:

1 internal virtual void RenderPartialInternal(string partialViewName, ViewDataDictionary viewData, object model, TextWriter writer, ViewEngineCollection viewEngineCollection) 2 3 { 4 if (String.IsNullOrEmpty(partialViewName)) 5 { 6 throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName"); 7 } 8 9 ViewDataDictionary newViewData = null; 10 11 if (model == null) 12 { 13 if (viewData == null) 14 { 15 newViewData = new ViewDataDictionary(ViewData); 16 } 17 else 18 { 19 newViewData = new ViewDataDictionary(viewData); 20 } 21 } 22 else 23 { 24 if (viewData == null) 25 { 26 newViewData = new ViewDataDictionary(model); 27 } 28 else 29 { 30 newViewData = new ViewDataDictionary(viewData) { Model = model }; 31 } 32 } 33 34 ViewContext newViewContext = new ViewContext(ViewContext, ViewContext.View, newViewData, ViewContext.TempData, writer); 35 IView view = FindPartialView(newViewContext, partialViewName, viewEngineCollection); 36 view.Render(newViewContext, writer); 37 }
也即使說RenderPartialInternal 只能在程序集內部被調用,外部沒法調用。這樣的情況讓我郁悶了好久。
然后我就把 RenderPartialInternal及其相關的源碼復制了出來,組成了一個自己的類,幸運的是,復制出來的代碼在略微修改后可以運行,實現如下:

1 public static class HtmlHelperExtension 2 { 3 public static void RenderPartial2(this HtmlHelper htmlHelper, string partialViewName) 4 { 5 RenderPartialInternal(htmlHelper.ViewContext,partialViewName, htmlHelper.ViewData, null /* model */, htmlHelper.ViewContext.Writer, ((Controller)htmlHelper.ViewContext.Controller).ViewEngineCollection); 6 } 7 8 internal static IView FindPartialView(ViewContext viewContext, string partialViewName, ViewEngineCollection viewEngineCollection) 9 { 10 ViewEngineResult result = viewEngineCollection.FindPartialView(viewContext, partialViewName); 11 if (result.View != null) 12 { 13 return result.View; 14 } 15 16 StringBuilder locationsText = new StringBuilder(); 17 foreach (string location in result.SearchedLocations) 18 { 19 locationsText.AppendLine(); 20 locationsText.Append(location); 21 } 22 23 throw new InvalidOperationException(partialViewName+locationsText); 24 } 25 26 internal static void RenderPartialInternal(ViewContext ViewContext, string partialViewName, ViewDataDictionary viewData, object model, TextWriter writer, ViewEngineCollection viewEngineCollection) 27 { 28 if (String.IsNullOrEmpty(partialViewName)) 29 { 30 throw new ArgumentException("partialViewName"); 31 } 32 33 ViewDataDictionary newViewData = null; 34 35 if (model == null) 36 { 37 newViewData = new ViewDataDictionary(viewData); 38 } 39 else 40 { 41 if (viewData == null) 42 { 43 newViewData = new ViewDataDictionary(model); 44 } 45 else 46 { 47 newViewData = new ViewDataDictionary(viewData) { Model = model }; 48 } 49 } 50 51 ViewContext newViewContext = new ViewContext(ViewContext, ViewContext.View, newViewData, ViewContext.TempData, writer); 52 IView view = FindPartialView(newViewContext, partialViewName, viewEngineCollection); 53 view.Render(newViewContext, writer); 54 } 55 }
這樣用戶控件的調用便變成了下面的形式:

1 <%Html.RenderPartial2("Menu");%>
經過測試,一切正確。
對於上面mvc中bug,不知道有朋友發覺沒,應該有吧。這是我首次遇到,就發帖出來與大家共享。
這篇文章,其實也總結了mvc中自定義主題的實現方法。
這是我在做我的網站車聘網的時候遇到的。
上面的源碼本來都折疊的,但發出來后竟然打不開,然后又重新貼了邊,郁悶壞了,難道是博客園的編輯器有問題?
我用的cuteeditor,有知道的朋友說下。