• 剖析 NopCommerce 的 Theme 机制


    原文链接http://www.cnblogs.com/coolite/archive/2012/12/28/NopTheme.html

    前言

    目前开源的CMS、Blog或者电子商务站点,他们都有一个共同的亮点,无疑就是可任意切换皮肤,并且定制和扩展能力都非常强。在这方面PHP可以说做的是最好的。那么我们如何能够在我们的ASP.NET MVC站点下面实现任意切换皮肤呢?NopCommerce—开源的 ASP.NET MVC 电子商务站点,它提供了强大的换肤功能,可通过一键切换皮肤。那接下来,我们就一起去寻找换肤的秘诀,让我们的ASP.NET MVC站点也具有一键换肤的功能吧。

    换肤试用

    先试用下Nop站点的换肤效果吧,打开Nop的源码,下载地址:http://nopcommerce.codeplex.com, 按照官方的Theme制作方法:http://www.nopcommerce.com/docs/72/designers-guide.aspx

    具体步骤戳这儿

    换肤后的思考?

    我们刚才制作皮肤的时候,将默认的皮肤文件夹下所有的文件拷贝到新的皮肤文件夹下面,并做了样式和HTML结构的修改。Nop应该是根据客户选择的皮肤定位到相应的皮肤文件夹下面,去找到View并加载出来。那实现换肤功能的关键就是: 根据用户选择的皮肤,ASP.NET MVC动态定位到皮肤文件夹下的View,并呈现出来。 

    做过ASP.NET MVC开发的朋友都知道,如果在Controller里面新建一个Action,但View不存在,页面肯定会报如下错误:

     

    从异常信息可以看出,ASP.NET MVC内部有一种加载View的机制。如果我们能够扩展这种内部的加载View的机制,去按照我们的自定义逻辑根据不同的皮肤加载不同的View,那我们的站点就能够实现换肤功能了。实现这个功能的核心就是IViewEngine,资料介绍:http://www.cnblogs.com/answercard/archive/2011/05/07/2039809.html。该接口定义如下(此段代码不在NopCommerce里):

     1 /// <summary>
     2 /// Defines the methods that are required for a view engine.
     3 /// </summary>
     4 public interface IViewEngine
     5 {
     6     /// <summary>
     7     /// Finds the specified partial view by using the specified controller context.
     8     /// </summary>
     9     ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
    10     /// <summary>
    11     /// Finds the specified view by using the specified controller context.
    12     /// </summary>
    13     ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
    14     /// <summary>
    15     /// Releases the specified view by using the specified controller context.
    16     /// </summary>
    17     /// <param name="controllerContext">The controller context.</param><param name="view">The view.</param>
    18     void ReleaseView(ControllerContext controllerContext, IView view);
    19 }
    View Code

      

    深入Nop,找到幕后黑手

    那我们就到Nop的源代码中去寻找 IViewEngine 的实现类,看看运气如何? 运气不错,找到了3个Themeable****ViewEngine. 从名字就可以断定该类是用来实现Theme的。

    Tips: 借助Reshareper可轻松的查找某个接口的实现类,此外Reshareper还有其它的高级功能,谁用谁知道…)

    先看看离接口IViewEngine最近的类—ThemeableVirtualPathProviderViewEngine,该类重写了FindViewFindPartialView 2个方法。我们以FindView为例进行研究吧,实际上FindPartialViewFindView都差不多。

     1 public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
     2         {
     3             var mobileDeviceHelper = EngineContext.Current.Resolve<IMobileDeviceHelper>();
     4             bool useMobileDevice = mobileDeviceHelper.IsMobileDevice(controllerContext.HttpContext)
     5                 && mobileDeviceHelper.MobileDevicesSupported()
     6                 && !mobileDeviceHelper.CustomerDontUseMobileVersion();
     7  
     8             string overrideViewName = useMobileDevice ?
     9                 string.Format("{0}.{1}", viewName, _mobileViewModifier)
    10                 : viewName;
    11  
    12             ViewEngineResult result = FindThemeableView(controllerContext, overrideViewName, masterName, useCache, useMobileDevice);
    13             // If we're looking for a Mobile view and couldn't find it try again without modifying the viewname
    14             if (useMobileDevice && (result == null || result.View == null))
    15                 result = FindThemeableView(controllerContext, viewName, masterName, useCache, false);
    16             return result;
    17  
    18         }
    View Code

    查找View的重担放到了内部方法FindThemeableView中完成的,看看该方法的实现吧:

     1 protected virtual ViewEngineResult FindThemeableView(ControllerContext controllerContext, string viewName, string masterName, bool useCache, bool mobile)
     2         {
     3             string[] strArray;
     4             string[] strArray2;
     5             if (controllerContext == null)
     6             {
     7                 throw new ArgumentNullException("controllerContext");
     8             }
     9             if (string.IsNullOrEmpty(viewName))
    10             {
    11                 throw new ArgumentException("View name cannot be null or empty.", "viewName");
    12             }
    13             var theme = GetCurrentTheme(mobile);
    14             string requiredString = controllerContext.RouteData.GetRequiredString("controller");
    15             string str2 = this.GetPath(controllerContext, this.ViewLocationFormats, this.AreaViewLocationFormats, "ViewLocationFormats", viewName, requiredString, theme, "View", useCache, mobile, out strArray);
    16             string str3 = this.GetPath(controllerContext, this.MasterLocationFormats, this.AreaMasterLocationFormats, "MasterLocationFormats", masterName, requiredString, theme, "Master", useCache, mobile, out strArray2);
    17             if (!string.IsNullOrEmpty(str2) && (!string.IsNullOrEmpty(str3) || string.IsNullOrEmpty(masterName)))
    18             {
    19                 return new ViewEngineResult(this.CreateView(controllerContext, str2, str3), this);
    20             }
    21             if (strArray2 == null)
    22             {
    23                 strArray2 = new string[0];
    24             }
    25             return new ViewEngineResult(strArray.Union<string>(strArray2));
    26  
    27         }
    View Code

    这段代码读起来有点费力,又一次告诉大家命名的重要性,当然你不想让别人看懂你的代码那就是另外一回事哈。

    str2实际上是ViewPath,str3是MasterPagePath

    其中内部方法GetPath是用来获取View的实际路径。

    我们来研究下GetPath的参数吧,其中最关键的是属性ViewLocationFormatsAreaViewLocationFormats

    由于ThemeableVirtualPathProviderViewEngine是抽象类,我们看看派生自该类的ThemeableRazorViewEngine吧(ThemeableRazorViewEngine继承自

    ThemeableBuildManagerViewEngine ,ThemeableBuildManagerViewEngine又继承ThemeableVirtualPathProviderViewEngine,所以,你懂的):

      1 public ThemeableRazorViewEngine()
      2         {
      3             AreaViewLocationFormats = new[]
      4                                           {
      5                                               //themes
      6                                               "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.cshtml", 
      7                                               "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.vbhtml", 
      8                                               "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.cshtml", 
      9                                               "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.vbhtml",
     10                                               
     11                                               //default
     12                                               "~/Areas/{2}/Views/{1}/{0}.cshtml", 
     13                                               "~/Areas/{2}/Views/{1}/{0}.vbhtml", 
     14                                               "~/Areas/{2}/Views/Shared/{0}.cshtml", 
     15                                               "~/Areas/{2}/Views/Shared/{0}.vbhtml"
     16                                           };
     17  
     18             AreaMasterLocationFormats = new[]
     19                                             {
     20                                                 //themes
     21                                                 "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.cshtml", 
     22                                                 "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.vbhtml", 
     23                                                 "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.cshtml", 
     24                                                 "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.vbhtml",
     25  
     26  
     27                                                 //default
     28                                                 "~/Areas/{2}/Views/{1}/{0}.cshtml", 
     29                                                 "~/Areas/{2}/Views/{1}/{0}.vbhtml", 
     30                                                 "~/Areas/{2}/Views/Shared/{0}.cshtml", 
     31                                                 "~/Areas/{2}/Views/Shared/{0}.vbhtml"
     32                                             };
     33  
     34             AreaPartialViewLocationFormats = new[]
     35                                                  {
     36                                                      //themes
     37                                                     "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.cshtml", 
     38                                                     "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.vbhtml", 
     39                                                     "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.cshtml", 
     40                                                     "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.vbhtml",
     41                                                     
     42                                                     //default
     43                                                     "~/Areas/{2}/Views/{1}/{0}.cshtml", 
     44                                                     "~/Areas/{2}/Views/{1}/{0}.vbhtml", 
     45                                                     "~/Areas/{2}/Views/Shared/{0}.cshtml", 
     46                                                     "~/Areas/{2}/Views/Shared/{0}.vbhtml"
     47                                                  };
     48  
     49             ViewLocationFormats = new[]
     50                                       {
     51                                             //themes
     52                                             "~/Themes/{2}/Views/{1}/{0}.cshtml", 
     53                                             "~/Themes/{2}/Views/{1}/{0}.vbhtml", 
     54                                             "~/Themes/{2}/Views/Shared/{0}.cshtml",
     55                                             "~/Themes/{2}/Views/Shared/{0}.vbhtml",
     56  
     57                                             //default
     58                                             "~/Views/{1}/{0}.cshtml", 
     59                                             "~/Views/{1}/{0}.vbhtml", 
     60                                             "~/Views/Shared/{0}.cshtml",
     61                                             "~/Views/Shared/{0}.vbhtml",
     62  
     63  
     64                                             //Admin
     65                                             "~/Administration/Views/{1}/{0}.cshtml",
     66                                             "~/Administration/Views/{1}/{0}.vbhtml",
     67                                             "~/Administration/Views/Shared/{0}.cshtml",
     68                                             "~/Administration/Views/Shared/{0}.vbhtml",
     69                                       };
     70  
     71             MasterLocationFormats = new[]
     72                                         {
     73                                             //themes
     74                                             "~/Themes/{2}/Views/{1}/{0}.cshtml", 
     75                                             "~/Themes/{2}/Views/{1}/{0}.vbhtml", 
     76                                             "~/Themes/{2}/Views/Shared/{0}.cshtml", 
     77                                             "~/Themes/{2}/Views/Shared/{0}.vbhtml",
     78  
     79                                             //default
     80                                             "~/Views/{1}/{0}.cshtml", 
     81                                             "~/Views/{1}/{0}.vbhtml", 
     82                                             "~/Views/Shared/{0}.cshtml", 
     83                                             "~/Views/Shared/{0}.vbhtml"
     84                                         };
     85  
     86             PartialViewLocationFormats = new[]
     87                                              {
     88                                                  //themes
     89                                                 "~/Themes/{2}/Views/{1}/{0}.cshtml", 
     90                                                 "~/Themes/{2}/Views/{1}/{0}.vbhtml", 
     91                                                 "~/Themes/{2}/Views/Shared/{0}.cshtml", 
     92                                                 "~/Themes/{2}/Views/Shared/{0}.vbhtml",
     93  
     94                                                 //default
     95                                                 "~/Views/{1}/{0}.cshtml", 
     96                                                 "~/Views/{1}/{0}.vbhtml", 
     97                                                 "~/Views/Shared/{0}.cshtml", 
     98                                                 "~/Views/Shared/{0}.vbhtml",
     99  
    100                                                 //Admin
    101                                                 "~/Administration/Views/{1}/{0}.cshtml",
    102                                                 "~/Administration/Views/{1}/{0}.vbhtml",
    103                                                 "~/Administration/Views/Shared/{0}.cshtml",
    104                                                 "~/Administration/Views/Shared/{0}.vbhtml",
    105                                              };
    106  
    107             FileExtensions = new[] { "cshtml", "vbhtml" };
    108         }
    View Code

     看到这里你是否了解有些明白了?这里就是定义的查找View的路径的模版,程序(Nop和MVC默认实现都是相同的策略)会按照顺序依次查找View是否存在。

    实际上就是Theme的路由机制。

    再来看看GetPath的实现吧:

     1 protected virtual string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string theme, string cacheKeyPrefix, bool useCache, bool mobile, out string[] searchedLocations)
     2         {
     3             searchedLocations = _emptyLocations;
     4             if (string.IsNullOrEmpty(name))
     5             {
     6                 return string.Empty;
     7             }
     8             string areaName = GetAreaName(controllerContext.RouteData);
     9 
    10             //little hack to get nop's admin area to be in /Administration/ instead of /Nop/Admin/ or Areas/Admin/
    11             if (!string.IsNullOrEmpty(areaName) && areaName.Equals("admin", StringComparison.InvariantCultureIgnoreCase))
    12             {
    13                 //admin area does not support mobile devices
    14                 if (mobile)
    15                 {
    16                     searchedLocations = new string[0];
    17                     return string.Empty;
    18                 }
    19                 var newLocations = areaLocations.ToList();
    20                 newLocations.Insert(0, "~/Administration/Views/{1}/{0}.cshtml");
    21                 newLocations.Insert(0, "~/Administration/Views/{1}/{0}.vbhtml");
    22                 newLocations.Insert(0, "~/Administration/Views/Shared/{0}.cshtml");
    23                 newLocations.Insert(0, "~/Administration/Views/Shared/{0}.vbhtml");
    24                 areaLocations = newLocations.ToArray();
    25             }
    26 
    27             bool flag = !string.IsNullOrEmpty(areaName);
    28             List<ViewLocation> viewLocations = GetViewLocations(locations, flag ? areaLocations : null);
    29             if (viewLocations.Count == 0)
    30             {
    31                 throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Properties cannot be null or empty.", new object[] { locationsPropertyName }));
    32             }
    33             bool flag2 = IsSpecificPath(name);
    34             string key = this.CreateCacheKey(cacheKeyPrefix, name, flag2 ? string.Empty : controllerName, areaName, theme);
    35             if (useCache)
    36             {
    37                 var cached = this.ViewLocationCache.GetViewLocation(controllerContext.HttpContext, key);
    38                 if (cached != null)
    39                 {
    40                     return cached;
    41                 }
    42             }
    43             if (!flag2)
    44             {
    45                 return this.GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, theme, key, ref searchedLocations);
    46             }
    47             return this.GetPathFromSpecificName(controllerContext, name, key, ref searchedLocations);
    48         }
    View Code

     这里明显使用了缓存,所以大家不用担心每次读取View都要依次去进行IO操作去查找View引起的性能问题。 

    最后我们再来看看GetPathFromGeneralName的具体实现吧:

     1 protected virtual string GetPathFromGeneralName(ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string theme, string cacheKey, ref string[] searchedLocations)
     2         {
     3             string virtualPath = string.Empty;
     4             searchedLocations = new string[locations.Count];
     5             for (int i = 0; i < locations.Count; i++)
     6             {
     7                 string str2 = locations[i].Format(name, controllerName, areaName, theme);
     8                 if (this.FileExists(controllerContext, str2))
     9                 {
    10                     searchedLocations = _emptyLocations;
    11                     virtualPath = str2;
    12                     this.ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
    13                     return virtualPath;
    14                 }
    15                 searchedLocations[i] = str2;
    16             }
    17             return virtualPath;
    18         }
    View Code

     该方法会将参数Theme、Controller和Action传入上文提到的View路径模版,生成实际的路径,如果文件不存在,继续尝试下一个View路径模版。直到找到View存在的实际路径。

    偷梁换柱,让MVC使用自定义的ViewEngine

    Nop是通过在Global文件的事件Application_Start中注入以下代码:

      if (databaseInstalled)
                {
                    //remove all view engines
                    ViewEngines.Engines.Clear();
                    //except the themeable razor view engine we use
                    ViewEngines.Engines.Add(new ThemeableRazorViewEngine());
                }
    

    总结

    到这里,你是否已经知道了Nop实现Theme的奥秘?但又觉得过于复杂?其实Nop实现Theme的同时,还为Mobile和Admin管理后台做了很多特殊处理,所以代码看起来有点乱。那我们就来自己动手打造一个轻量级的ThemeViewEngine吧。预知后事如何,请戳这儿

  • 相关阅读:
    利用列表的知识写一个购物小程序
    基本数据类型(While循环,For循环,列表以及相关用法)
    爬虫学习--Day3(小猿圈爬虫开发_1)
    爬虫学习--常用的正则表达式 Day3
    win10系统任务栏点击没有反应
    python 内建类型
    MWeb
    jmeter创建测试计划
    jmeter建立FTP测试计划
    jmeter配置元件
  • 原文地址:https://www.cnblogs.com/jingshihaisu/p/3741580.html
Copyright © 2020-2023  润新知