ASP.NET MVC 下打造輕量級的 Theme 機制


上一篇文章[剖析 NopCommerce 的 Theme 機制]介紹了Nop實現Theme的實現原理。但由於Nop要為Admin和Mobile做特殊處理,因此寫了太多的其它東西。因此我們決定自己寫一個Theme的ViewEngine,僅僅用來實現皮膚功能。

 

需求分析

考慮到Demo程序,為簡單起見,我們將Theme放到Url中,格式:

{Controller}/{Action}?Theme={Theme},當然你完全可以從Cookie或者數據庫中去讀取用戶設置的Theme信息。

 

其次,Theme文件夾的組織結構,就采用NopCommerce的這種文件夾結構吧。

Themes/{Theme}/Views/…

 

 

新建站點

首先新建一個MVC 4 的默認站點 ThemeDemo,用默認的Internet 模版吧。

 

實現ThemeViewEngine

自定義Theme的ViewEngine,並從RazorViewEngine繼承。在構造函數中設置View的路徑模版:

View Code
public ThemeViewEngine(IViewPageActivator viewPageActivator)
            : base(viewPageActivator)
{
    this.AreaViewLocationFormats = new[]
      {
        "~/Themes/{3}/Areas/{2}/Views/{1}/{0}.cshtml",
        "~/Themes/{3}/Areas/{2}/Views/{1}/{0}.vbhtml",
        "~/Themes/{3}/Areas/{2}/Views/Shared/{0}.cshtml",
        "~/Themes/{3}/Areas/{2}/Views/Shared/{0}.vbhtml",

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

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

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

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

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

        "~/Views/{1}/{0}.cshtml",
        "~/Views/{1}/{0}.vbhtml",
        "~/Views/Shared/{0}.cshtml",
        "~/Views/Shared/{0}.vbhtml"
      };
    this.FileExtensions = new[]
      {
        "cshtml",
        "vbhtml"
      };

    GetExtensionThunk = new Func<string, string>(VirtualPathUtility.GetExtension);
}

PS:我是從RazorViewEngine源碼里面拷貝出默認的View路徑模版,然后加上Themes的View路徑模版。

實現接口:IViewEngine的方法 FindView 和 FindPartialView

通過反編譯VirtualPathProviderViewEngine,發現FindView和FindPartialView的2個方法均會訪問 GetPath方法,而GetPath方法的作用是根據Controller和Action,返回View的實際路徑。理論上講,只要重寫GetPath,並根據Theme生成新的View路徑。 但由於GetPath是內部方法,我們無法重寫,於是我們不得不重寫FindView和FindPartialView 2個方法。這些工作其實也不難,就是Ctrl+Cà Ctrl+V,我相信大家都很熟練這們技術了,也就略去不講。下面重點介紹下自定義GetPath的具體實現:

View Code
protected virtual string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations)
{
    string theme = GetCurrentTheme(controllerContext);
    searchedLocations = _emptyLocations;
    if (string.IsNullOrEmpty(name))
        return string.Empty;
    string areaName = GetAreaName(controllerContext.RouteData);

    bool flag = !string.IsNullOrEmpty(areaName);
    List<ThemeViewLocation> 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 isSpecificPath = IsSpecificPath(name);
    string key = this.CreateCacheKey(cacheKeyPrefix, name, isSpecificPath ? string.Empty : controllerName, areaName, theme);
    if (useCache)
    {
        var cached = this.ViewLocationCache.GetViewLocation(controllerContext.HttpContext, key);
        if (cached != null)
        {
            return cached;
        }
    }
    if (!isSpecificPath)
    {
        return this.GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, theme, key, ref searchedLocations);
    }
    return this.GetPathFromSpecificName(controllerContext, name, key, ref searchedLocations);
}
  • 讀取當前的Theme,可根據實際需求自定義,這里是從QueryString或者Form中讀取的
protected virtual string GetCurrentTheme(ControllerContext controllerContext)
{
    var theme = controllerContext.RequestContext.HttpContext.Request["Theme"];
    return theme;
}
  • 讀取Area,調用方法GetAreaName,拷貝自VirtualPathProviderViewEngine
View Code
protected virtual string GetAreaName(RouteBase route)
{
    var area = route as IRouteWithArea;
    if (area != null)
    {
        return area.Area;
    }
    var route2 = route as Route;
    if ((route2 != null) && (route2.DataTokens != null))
    {
        return (route2.DataTokens["area"] as string);
    }
    return null;
}
  • 獲取ViewLocation的相關信息,該類是用來根據參數,從View路徑模版生成實際的View路徑。自定義如下2個類,參考VirtualPathProviderViewEngine,Format方法增加Theme參數
View Code
public class ThemeAreaAwareViewLocation : ThemeViewLocation
{
    public ThemeAreaAwareViewLocation(string virtualPathFormatString)
        : base(virtualPathFormatString)
    {
    }

    public override string Format(string viewName, string controllerName, string areaName, string theme)
    {
        return string.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName, theme);
    }
}

public class ThemeViewLocation
{
    protected readonly string _virtualPathFormatString;

    public ThemeViewLocation(string virtualPathFormatString)
    {
        _virtualPathFormatString = virtualPathFormatString;
    }

    public virtual string Format(string viewName, string controllerName, string areaName, string theme)
    {
        return string.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, theme);
    }
}
  • 先從緩存中讀取View的實際路徑,如果不存在,則通過方法GetPathFromGeneralName 獲取View的實際路徑信息 
View Code
protected virtual string GetPathFromGeneralName(ControllerContext controllerContext, 
    List<ThemeViewLocation> 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存在的實際路徑。然后緩存起來,提高效率。 

  • 最后將ThemeViewEngine注入到MVC中
//remove all view engines
ViewEngines.Engines.Clear();
//except the themeable razor view engine we use
ViewEngines.Engines.Add(new ThemeViewEngine());

 

創建Theme

首先新建一個Theme取名叫Black吧。將背景色設置為黑色。

Web.Config 是從Views文件夾下面拷貝過來的。將默認的_Layout.cshtml按照文件夾的結構拷貝過來,並修改如下樣式:

<body style="background-color: black; color: white;">

PS:如果不拷貝Web.Config,View里面沒有智能提示。

 

測試

到這里,我已經有點激動了,Theme功能馬上就要實現了。顯然大部分代碼都是從微軟的代碼哪里拷貝的,自定義的幾行代碼絕對有信心,但做集成測試還是必不可少。至少要看到Theme的效果吧。 

運行項目,Ctrl+F5

鍵入Theme參數:

 

悲劇發生了,沒有預期的效果。各種Debug,自定義代碼均能夠正常工作,說明自定義ViewEngine正常。通過Debug NopCommerce的ViewEngine對比發現:Nop每次請求,自定義的ViewEngine都會調用FindView去查找MasterPage的路徑。而我們自定義的ThemeViewEngine不會通過FindView去查找MasterPage的路徑。故讀取MasterPage還是從原來的路徑讀取。我們自定義的ThemeViewEngine只對View和Partial View有效,對於MasterPage無效。問題終於找到,接下來該如何解決呢?難道要我們放棄對於MasterPage的支持嗎?顯然對於挑剔的我是無法接受的。

 

既然NopCommerce實現了整個功能,肯定還有什么機關我們沒有觸碰到。通過查找Nop的源碼發現了抽象類:WebViewPage,通過Reshareper發現該類“未被”任何類繼承。但發現所有的View都繼承自該類。該類重寫了基類的Layout屬性,而這里是通過ViewEngine重新獲取MasterPage文件路徑的。

View Code
public abstract class WebViewPage<TModel> : System.Web.Mvc.WebViewPage<TModel>
{
    public override string Layout
    {
        get
        {
            var layout = base.Layout;

            if (!string.IsNullOrEmpty(layout))
            {
                var filename = System.IO.Path.GetFileNameWithoutExtension(layout);
                ViewEngineResult viewResult = System.Web.Mvc.ViewEngines.Engines.FindView(ViewContext.Controller.ControllerContext, filename, "");

                if (viewResult.View != null && viewResult.View is RazorView)
                {
                    layout = (viewResult.View as RazorView).ViewPath;
                }
            }

            return layout;
        }
        set
        {
            base.Layout = value;
        }
    }
}

我們依葫蘆畫瓢也寫一個WebViewPage。然后在Web.Config做如下配置:

<system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="ThemeDemo.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Optimization"/>
        <add namespace="System.Web.Routing" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>

(PS:記得在Theme下的Web.Config中也要做同樣的修改)

 

再次調試:

 

黑色的背景白色的字,終於出現了。什么這也太丑了吧….

 

后記

要實現一個好的皮膚機制,除了解決皮膚文件的定位之外,還有很多工作要做。比如要對頁面布局有一個精細地設計,對兼容性、擴展性、復用性都有一個全面的考慮。

文中示例源碼下載地址:http://files.cnblogs.com/coolite/ThemeDemo.zip

 


免責聲明!

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



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