[MVC] 剖析 NopCommerce 的 Theme 機制


前言

目前開源的CMS、Blog或者電子商務站點,他們都有一個共同的亮點,無疑就是可任意切換皮膚,並且定制和擴展能力都非常強。在這方面PHP可以說做的是最好的。那么我們如何能夠在我們的ASP.NET MVC站點下面實現任意切換皮膚呢?我立馬想到最近流行的NopCommerce—開源的 ASP.NET MVC 電子商務站點。它提供了強大的換膚功能,可通過一鍵切換皮膚。那接下來,我們就一起去尋找換膚的秘訣,讓我們的ASP.NET MVC站點也具有一鍵換膚的功能吧。讓我們的ASP.NET MVC 站點可以隨意 變 變 變!

 

換膚試用

先試用下Nop站點的換膚效果吧,打開Nop的源碼,下載地址:http://nopcommerce.codeplex.com, 按照官方的Theme制作方法:http://www.nopcommerce.com/docs/72/designers-guide.aspx,拷貝默認的皮膚DarkOrange,並做相應處理。此處略去500字…

  

運行站點,首先呈現的是默認皮膚:

切換成我們剛才制作的皮膚:

 

換膚后的思考?

我們剛才制作皮膚的時候,將默認的皮膚文件夾下所有的文件拷貝到新的皮膚文件夾下面,並做了樣式和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。該接口定義如下:

/// <summary>
/// Defines the methods that are required for a view engine.
/// </summary>
public interface IViewEngine
{
    /// <summary>
    /// Finds the specified partial view by using the specified controller context.
    /// </summary>
    ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
    /// <summary>
    /// Finds the specified view by using the specified controller context.
    /// </summary>
    ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
    /// <summary>
    /// Releases the specified view by using the specified controller context.
    /// </summary>
    /// <param name="controllerContext">The controller context.</param><param name="view">The view.</param>
    void ReleaseView(ControllerContext controllerContext, IView view);
}

 

深入Nop,找到幕后黑手

那我們就到Nop的源代碼中去尋找 IViewEngine 的實現類,看看運氣如何? 運氣不錯,找到了3個Themeable****ViewEngine. 從名字就可以斷定該類是用來實現Theme的。

 

Tips: 借助Reshareper可輕松的查找某個接口的實現類,此外Reshareper還有其它的高級功能,誰用誰知道…)

先看看離接口IViewEngine最近的類—ThemeableVirtualPathProviderViewEngine,該類重寫了FindViewFindPartialView 2個方法。我們以FindView為例進行研究吧,實際上FindPartialViewFindView都差不多。

 

public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
    var mobileDeviceHelper = EngineContext.Current.Resolve<IMobileDeviceHelper>();
    bool useMobileDevice = mobileDeviceHelper.IsMobileDevice(controllerContext.HttpContext)
        && mobileDeviceHelper.MobileDevicesSupported()
        && !mobileDeviceHelper.CustomerDontUseMobileVersion();

    string overrideViewName = useMobileDevice ?
        string.Format("{0}.{1}", viewName, _mobileViewModifier)
        : viewName;

    ViewEngineResult result = FindThemeableView(controllerContext, overrideViewName, masterName, useCache, useMobileDevice);
    // If we're looking for a Mobile view and couldn't find it try again without modifying the viewname
    if (useMobileDevice && (result == null || result.View == null))
        result = FindThemeableView(controllerContext, viewName, masterName, useCache, false);
    return result;
}

查找View的重擔放到了內部方法FindThemeableView中完成的,看看該方法的實現吧:

protected virtual ViewEngineResult FindThemeableView(ControllerContext controllerContext, string viewName, string masterName, bool useCache, bool mobile)
{
    string[] strArray;
    string[] strArray2;
    if (controllerContext == null)
    {
        throw new ArgumentNullException("controllerContext");
    }
    if (string.IsNullOrEmpty(viewName))
    {
        throw new ArgumentException("View name cannot be null or empty.", "viewName");
    }
    var theme = GetCurrentTheme(mobile);
    string requiredString = controllerContext.RouteData.GetRequiredString("controller");
    string str2 = this.GetPath(controllerContext, this.ViewLocationFormats, this.AreaViewLocationFormats, "ViewLocationFormats", viewName, requiredString, theme, "View", useCache, mobile, out strArray);
    string str3 = this.GetPath(controllerContext, this.MasterLocationFormats, this.AreaMasterLocationFormats, "MasterLocationFormats", masterName, requiredString, theme, "Master", useCache, mobile, out strArray2);
    if (!string.IsNullOrEmpty(str2) && (!string.IsNullOrEmpty(str3) || string.IsNullOrEmpty(masterName)))
    {
        return new ViewEngineResult(this.CreateView(controllerContext, str2, str3), this);
    }
    if (strArray2 == null)
    {
        strArray2 = new string[0];
    }
    return new ViewEngineResult(strArray.Union<string>(strArray2));
}

這段代碼讀起來有點費力,又一次告訴大家命名的重要性,當然你不想讓別人看懂你的代碼那就是另外一回事哈。str2實際上是ViewPath,str3是MasterPagePath。其中內部方法GetPath是用來獲取View的實際路徑。我們來研究下GetPath的參數吧,其中最關鍵的是屬性ViewLocationFormatsAreaViewLocationFormats。由於ThemeableVirtualPathProviderViewEngine是抽象類,我們看看派生自該類的ThemeableRazorViewEngine吧:

View Code
public ThemeableRazorViewEngine()
{
    AreaViewLocationFormats = new[]
                                  {
                                      //themes
                                      "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.cshtml", 
                                      "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.vbhtml", 
                                      "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.cshtml", 
                                      "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.vbhtml",
                                      
                                      //default
                                      "~/Areas/{2}/Views/{1}/{0}.cshtml", 
                                      "~/Areas/{2}/Views/{1}/{0}.vbhtml", 
                                      "~/Areas/{2}/Views/Shared/{0}.cshtml", 
                                      "~/Areas/{2}/Views/Shared/{0}.vbhtml"
                                  };

    AreaMasterLocationFormats = new[]
                                    {
                                        //themes
                                        "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.cshtml", 
                                        "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.vbhtml", 
                                        "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.cshtml", 
                                        "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.vbhtml",


                                        //default
                                        "~/Areas/{2}/Views/{1}/{0}.cshtml", 
                                        "~/Areas/{2}/Views/{1}/{0}.vbhtml", 
                                        "~/Areas/{2}/Views/Shared/{0}.cshtml", 
                                        "~/Areas/{2}/Views/Shared/{0}.vbhtml"
                                    };

    AreaPartialViewLocationFormats = new[]
                                         {
                                             //themes
                                            "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.cshtml", 
                                            "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.vbhtml", 
                                            "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.cshtml", 
                                            "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.vbhtml",
                                            
                                            //default
                                            "~/Areas/{2}/Views/{1}/{0}.cshtml", 
                                            "~/Areas/{2}/Views/{1}/{0}.vbhtml", 
                                            "~/Areas/{2}/Views/Shared/{0}.cshtml", 
                                            "~/Areas/{2}/Views/Shared/{0}.vbhtml"
                                         };

    ViewLocationFormats = new[]
                              {
                                    //themes
                                    "~/Themes/{2}/Views/{1}/{0}.cshtml", 
                                    "~/Themes/{2}/Views/{1}/{0}.vbhtml", 
                                    "~/Themes/{2}/Views/Shared/{0}.cshtml",
                                    "~/Themes/{2}/Views/Shared/{0}.vbhtml",

                                    //default
                                    "~/Views/{1}/{0}.cshtml", 
                                    "~/Views/{1}/{0}.vbhtml", 
                                    "~/Views/Shared/{0}.cshtml",
                                    "~/Views/Shared/{0}.vbhtml",


                                    //Admin
                                    "~/Administration/Views/{1}/{0}.cshtml",
                                    "~/Administration/Views/{1}/{0}.vbhtml",
                                    "~/Administration/Views/Shared/{0}.cshtml",
                                    "~/Administration/Views/Shared/{0}.vbhtml",
                              };

    MasterLocationFormats = new[]
                                {
                                    //themes
                                    "~/Themes/{2}/Views/{1}/{0}.cshtml", 
                                    "~/Themes/{2}/Views/{1}/{0}.vbhtml", 
                                    "~/Themes/{2}/Views/Shared/{0}.cshtml", 
                                    "~/Themes/{2}/Views/Shared/{0}.vbhtml",

                                    //default
                                    "~/Views/{1}/{0}.cshtml", 
                                    "~/Views/{1}/{0}.vbhtml", 
                                    "~/Views/Shared/{0}.cshtml", 
                                    "~/Views/Shared/{0}.vbhtml"
                                };

    PartialViewLocationFormats = new[]
                                     {
                                         //themes
                                        "~/Themes/{2}/Views/{1}/{0}.cshtml", 
                                        "~/Themes/{2}/Views/{1}/{0}.vbhtml", 
                                        "~/Themes/{2}/Views/Shared/{0}.cshtml", 
                                        "~/Themes/{2}/Views/Shared/{0}.vbhtml",

                                        //default
                                        "~/Views/{1}/{0}.cshtml", 
                                        "~/Views/{1}/{0}.vbhtml", 
                                        "~/Views/Shared/{0}.cshtml", 
                                        "~/Views/Shared/{0}.vbhtml",

                                        //Admin
                                        "~/Administration/Views/{1}/{0}.cshtml",
                                        "~/Administration/Views/{1}/{0}.vbhtml",
                                        "~/Administration/Views/Shared/{0}.cshtml",
                                        "~/Administration/Views/Shared/{0}.vbhtml",
                                     };

    FileExtensions = new[] { "cshtml", "vbhtml" };
}

看到這里你是否了解有些明白了?這里就是定義的查找View的路徑的模版,程序(Nop和MVC默認實現都是相同的策略)會按照順序依次查找View是否存在。

再來看看GetPath的實現吧:

View Code
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)
{
    searchedLocations = _emptyLocations;
    if (string.IsNullOrEmpty(name))
    {
        return string.Empty;
    }
    string areaName = GetAreaName(controllerContext.RouteData);

    //little hack to get nop's admin area to be in /Administration/ instead of /Nop/Admin/ or Areas/Admin/
    if (!string.IsNullOrEmpty(areaName) && areaName.Equals("admin", StringComparison.InvariantCultureIgnoreCase))
    {
        //admin area does not support mobile devices
        if (mobile)
        {
            searchedLocations = new string[0];
            return string.Empty;
        }
        var newLocations = areaLocations.ToList();
        newLocations.Insert(0, "~/Administration/Views/{1}/{0}.cshtml");
        newLocations.Insert(0, "~/Administration/Views/{1}/{0}.vbhtml");
        newLocations.Insert(0, "~/Administration/Views/Shared/{0}.cshtml");
        newLocations.Insert(0, "~/Administration/Views/Shared/{0}.vbhtml");
        areaLocations = newLocations.ToArray();
    }

    bool flag = !string.IsNullOrEmpty(areaName);
    List<ViewLocation> viewLocations = GetViewLocations(locations, flag ? areaLocations : null);
    if (viewLocations.Count == 0)
    {
        throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Properties cannot be null or empty.", new object[] { locationsPropertyName }));
    }
    bool flag2 = IsSpecificPath(name);
    string key = this.CreateCacheKey(cacheKeyPrefix, name, flag2 ? string.Empty : controllerName, areaName, theme);
    if (useCache)
    {
        var cached = this.ViewLocationCache.GetViewLocation(controllerContext.HttpContext, key);
        if (cached != null)
        {
            return cached;
        }
    }
    if (!flag2)
    {
        return this.GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, theme, key, ref searchedLocations);
    }
    return this.GetPathFromSpecificName(controllerContext, name, key, ref searchedLocations);
}

這里明顯使用了緩存,所以大家不用擔心每次讀取View都要依次去進行IO操作去查找View引起的性能問題。 

最后我們再來看看GetPathFromGeneralName的具體實現吧:

View Code
protected virtual string GetPathFromGeneralName(ControllerContext controllerContext, 
    List<ViewLocation> locations, string name, string controllerName, string areaName, 
    string theme, string cacheKey, ref string[] searchedLocations)
{
    string virtualPath = string.Empty;
    searchedLocations = new string[locations.Count];
    for (int i = 0; i < locations.Count; i++)
    {
        string str2 = locations[i].Format(name, controllerName, areaName, theme);
        if (this.FileExists(controllerContext, str2))
        {
            searchedLocations = _emptyLocations;
            virtualPath = str2;
            this.ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
            return virtualPath;
        }
        searchedLocations[i] = str2;
    }
    return virtualPath;
}

該方法會將參數Theme、Controller和Action傳入上文提到的View路徑模版,生成實際的路徑,如果文件不存在,繼續嘗試下一個View路徑模版。直到找到View存在的實際路徑。 

 

偷梁換柱,讓MVC使用自定義的ViewEngine

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

//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吧。預知后事,且聽下回分解。

下一篇:[ASP.NET MVC 下打造輕量級的 Theme 機制]

 

 


免責聲明!

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



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