zoukankan      html  css  js  c++  java
  • 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,有知道的朋友说下。 

      

  • 相关阅读:
    [转]进程的用户栈和内核栈
    什么是URL,URL格式
    设计灵感
    Spring源码学习相关记录
    HTML图片标签路径解析
    一次Spring Bean初始化顺序问题排查记录
    是要面向对象,还是简单粗暴?
    2018/07/26学习节点记录
    数据结构-堆 Java实现
    2018 ICPC 徐州邀请赛 总结
  • 原文地址:https://www.cnblogs.com/cxd4321/p/3522469.html
Copyright © 2011-2022 走看看