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 }
企业中心代码如下:
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,有知道的朋友说下。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。