WebFormViewEngine及用戶控件尋址bug


 在做我的網站的時候遇到了主題切換的問題,特總結與大家共享。

     熟悉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      }
View Code

 

 

企業中心代碼如下:

 

 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      }
View Code

 

 

 

對於前台和后台的模板引擎,可以自己通過簡單的修改去實現 。

視圖引擎能被調用的地方有三處,第一處是在globals中,也就是在application_start中注冊視圖引擎;第二個地方時在父類的構造函數中初始化;第三個是在view函數被調用前,也就是利用過濾器。

 application_start中代碼如下:

 protected void Application_Start()

         {
             ViewEngines.Engines.Clear();
             ViewEngines.Engines.Add(new UserViewEngine());
             AreaRegistration.RegisterAllAreas();
             RegisterRoutes(RouteTable.Routes);
             
         } 
View Code

 

構造函數中如下:

 public ctor()

         {
             ViewEngines.Engines.Clear();
             ViewEngines.Engines.Add(new UserViewEngine());
             
         } 
View Code

 

過濾器中如下:

1  public override void OnActionExecuting(ActionExecutingContext filterContext)
2 
3      {
4          //其他操作
5          ViewEngines.Engines.Clear();
6          ViewEngines.Engines.Add(new UserViewEngine());
7      } 
View Code

 

對於第一種調用方法,顯然滿足不了我們的需求。這樣只有求助於第二種和第三種調用方法。 也就是在view函數被調用之前進行對當前視圖引擎的替換。view函數一般如下:

1  public ActionResult About()
2 
3          {
4              return View();
5          } 
View Code

 

這樣便實現了我們的需求。

 

但是,目前實現的只能在單線程下使用,或者在同時都訪問前台或者都放我用戶中心的情況下是對的。假若同時出現並發現象。也就是一個用戶訪問企業中心,一個用戶訪問用戶中心。為了說明問題(簡單代碼實現),貼代碼如下,用戶中心類似代碼:

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          } 
View Code

 

 企業中心類似代碼

1  public ActionResult List()
2 
3          {
4          ViewEngineCollection.Clear();
5          ViewEngineCollection.Add(new MemberViewEngine());
6  
7          Thread.Sleep(10000)
8              return View();
9          } 
View Code

 

 為了說明問題,我故意在上面加了線程睡眠時間。

現在,個人中心先被訪問,然后企業中心再被訪問。這樣運行之后是會報錯的,說找不到視圖。

究其原因,是由於個人中心運行慢,企業中心運行快。那么用戶中心的視圖引擎就變成了企業中心的視圖引擎。但我們明明都在函數里加了如下代碼,為啥還報錯呢?

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

 

 

1  ViewEngines.Clear();
2 
3          ViewEngines.Add(new MemberViewEngine()); 
View Code

 

我抱着究根問底的態度去研究程序的源碼,發覺微軟的實現如下:

 

 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  } 
View Code

 

 

 

 看到上面的代碼,大家應該都明白了問題出現的根源,是靜態類造成的。

那么,我們有沒解決問題的辦法呢?幸運的是,微軟給出了另外一個調用辦法,這個方法是在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          } 
View Code

 

看到這里,大家是不是感覺到微軟考慮問題是很周全了呢?

別急,我們看看 get { return _viewEngineCollection ?? ViewEngines.Engines; }

也就是說 _viewEngineCollection 為空的時候取的是ViewEngines.Engines。那么,我們在每個view函數被調用前都把ViewEngineCollection賦值不就行了嗎?類似於下面的代碼:

1  ViewEngineCollection.Clear();
2 
3          ViewEngineCollection.Add(new MemberViewEngine()); 
View Code

 

     有的朋友可能發覺,這樣實現是不對的,他們會提出下面的實現辦法:

1 ViewEngineCollection = new ViewEngineCollection { new MemberViewEngine() }; 
View Code

 

 這樣的代碼一運行,確實正確,我們ok!

但這是在我們沒用到用戶控件的情況下,假若一個母版頁中加載了用戶控件,類似下面的代碼

1 <%Html.RenderPartial("Menu");%> 
View Code

 

 上面的代碼在運行中還是會報錯的。 

這個問題困擾了我一段時間,我想微軟不會錯吧。是不是我的程序出錯了,郁悶了好久。

然后,我就想到了看看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  } 
View Code

 

 

 

  以上是微軟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          } 
View Code

 

 也即使說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      }
View Code

 

 

 這樣用戶控件的調用便變成了下面的形式:

1  <%Html.RenderPartial2("Menu");%> 
View Code

 

經過測試,一切正確。

對於上面mvc中bug,不知道有朋友發覺沒,應該有吧。這是我首次遇到,就發帖出來與大家共享。

這篇文章,其實也總結了mvc中自定義主題的實現方法。

這是我在做我的網站車聘網的時候遇到的。 

 

上面的源碼本來都折疊的,但發出來后竟然打不開,然后又重新貼了邊,郁悶壞了,難道是博客園的編輯器有問題?

我用的cuteeditor,有知道的朋友說下。 

  

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM