一個網站的主題包括布局,色調,內容展示等,每種主題在某些方面應該或多或少不一樣的,否則就不能稱之為不同的主題了。每一個網站至少都有一個主題,我這里稱之為默認主題,也就是我們平常開發設計網站時的一個固定布局,固定色調,固定內容展示等構成一個默認的固定主題。單一主題針對一些小網站或網站用戶群體相對單一固定還是比較適用的,但如果是大型網站或是網站的用戶群體比較多而且復雜,如:京東,博客園里的每個博客空間、文俊IT社區網(我的網站,雖不是大型網站,但也實現了主題切換功能的哦!~)等,是需要多個網站主題的,當然我舉的這兩個網站他們都實現了多主題,比如:
京東默認主題:適合國內人群
英文主題:適合國外人群
博客園就不用在此舉例了吧,大家看每個人的博客風格不同就知道了。
上面的表述其作用是為了說明主題對于一個大中型或多種不同用戶群體的網站的重要性,而如何實現多種主題的實現與切換也是本文主要說明的。主題的實現與切換方法有很多,常見的方法有:動態替換CSS+JS、ASPX頁面可以采取制作多種控件主題進行切換、切換頁面路徑,不同的路徑同頁面文件設計成不同的主題等,而我這里要講解的是MVC下如何實現通過切換路徑來實現主題的切換功能。
MVC頁面展示的內容來自視圖頁面(view.cshtml,view.vbhtml),如果說要實現在相同的后臺處理代碼下,實現不同的主題展示,我們只需要改變視圖頁面內容就可以了,但默認情況下,一個Action對應一個視圖,如何實現一個Action在某種主題下對應不同的的視圖文件呢?我這里給出如下兩種方案:
第一種方案,很簡單,可以根據請求的主題名稱來返回不同的視圖,代碼實現如下:
Action:
public ActionResult TestCustomViewPath(string theme) { string themeName = string.IsNullOrEmpty(theme) ? "Default" : theme; return View(themeName + "/Test"); //注意這里指定了一相對路徑 }
View:
//Default: <!DOCTYPE html> <html> <head> <title></title> </head> <body> <div> 我是自定義的視圖路徑,風格:@ViewBag.CurrentThemeName </div> @using(Html.BeginForm()){ <span>選擇主題:</span> @Html.DropDownList("theme", new[] { new SelectListItem() { Text = "默認", Value = "Default" }, new SelectListItem() { Text = "綠色", Value = "Green" }}) <p> <input type="submit" value="應用" /> </p> } </body> </html> //Green: <!DOCTYPE html> <html> <head> <title></title> </head> <body style="background-color:Green;color:Orange;"> <div> 我是自定義的視圖路徑,風格:@ViewBag.CurrentThemeName </div> @using(Html.BeginForm()){ <span>選擇主題:</span> @Html.DropDownList("theme", new[] { new SelectListItem() { Text = "默認", Value = "Default" }, new SelectListItem() { Text = "綠色", Value = "Green" }}) <p> <input type="submit" value="應用" /> </p> }</body> </html>
注意這里面的視圖文件,不再是之前的默認情況,而是如下結構:即:在同一個控制器對應的視圖目錄下存在多個以主題為名的子目錄,子目錄中的文件名相同。
實現效果如下圖示:
從上面的效果來看,確實已經實現了所謂的主題切換,但這種方法我并不是很推薦,因為視圖文件目錄過于混亂,每個控制器視圖目錄下均包含多個主題子目錄,且每個子目錄下的文件名相同,這讓我很難區別,容易出現更錯的情況,有人可能會說,我也可以將視圖文件名改成不同的呀,比如:Green_Test.cshtml,這樣就能區分了,但我仍然說不建議這樣做,至少找視圖文件都找得麻煩,推薦我下面介紹的第2種方法,具體如何實現,請君往下看。
我們知道,視圖如何呈現,或者說如何被解析,均是需要視圖引擎ViewEngine來支持的,微軟給我們提供了兩種視圖引擎,即:WebFormViewEngine,對應的視圖文件擴展名為:aspx;RazorViewEngine,對應的視圖文件擴展名為:cshtml,vbhtml;從如下源代碼也可以看出來:
namespace System.Web.Mvc { public static class ViewEngines { private static readonly ViewEngineCollection _engines = new ViewEngineCollection { new WebFormViewEngine(), new RazorViewEngine() }; public static ViewEngineCollection Engines { get { return ViewEngines._engines; } } } }
看到這個代碼,我們應該可以看出,可以通過ViewEngines.Engines來添加我們自定義或是第三方的視圖引擎(如:Nvelocity),來實現支持不同的視圖文件格式,我這里因為目的是需要實現主題切換,所以就來自定義視圖引擎,因為視圖引擎實現細節比較復雜,若直接繼承自IViewEngine接口,需要考慮與實現的代碼會比較多且存在風險,所以我是直接繼承自RazorViewEngine,既然要實現該類,我們先看一下該類的源代碼:
using System; using System.Collections.Generic; namespace System.Web.Mvc { public class RazorViewEngine : BuildManagerViewEngine { internal static readonly string ViewStartFileName = "_ViewStart"; public RazorViewEngine() : this(null) { } public RazorViewEngine(IViewPageActivator viewPageActivator) : base(viewPageActivator) { base.AreaViewLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.vbhtml", "~/Areas/{2}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.vbhtml" }; base.AreaMasterLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.vbhtml", "~/Areas/{2}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.vbhtml" }; base.AreaPartialViewLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.vbhtml", "~/Areas/{2}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.vbhtml" }; base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml", "~/Views/{1}/{0}.vbhtml", "~/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.vbhtml" }; base.MasterLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml", "~/Views/{1}/{0}.vbhtml", "~/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.vbhtml" }; base.PartialViewLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml", "~/Views/{1}/{0}.vbhtml", "~/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.vbhtml" }; base.FileExtensions = new string[] { "cshtml", "vbhtml" }; } protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) { string layoutPath = null; bool runViewStartPages = false; IEnumerable<string> fileExtensions = base.FileExtensions; IViewPageActivator viewPageActivator = base.ViewPageActivator; return new RazorView(controllerContext, partialPath, layoutPath, runViewStartPages, fileExtensions, viewPageActivator); } protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) { bool runViewStartPages = true; IEnumerable<string> fileExtensions = base.FileExtensions; IViewPageActivator viewPageActivator = base.ViewPageActivator; return new RazorView(controllerContext, viewPath, masterPath, runViewStartPages, fileExtensions, viewPageActivator); } } }
有沒有發現有什么值得我們關注的地方呢?對了,就是構造函數,構造函數中直接初始化了7個數組,除了FileExtensions外,其余6個均是用來存放視圖查找的路徑的,中間的{0}\{1}\{2}分別為view,controller,area的占位符,在執行查找視圖時會被替換成實際的值。知道了這6個數組,等于就掌握了視圖查找路徑,我們只需要重新賦值這個6個數組,就能實現視圖的隨意切換了,以下面我實現的自定義視圖引擎代碼:
public class CustomViewEngine : RazorViewEngine { public CustomViewEngine(string theme) { FormatLocations(base.AreaViewLocationFormats, theme); FormatLocations(base.AreaMasterLocationFormats, theme); FormatLocations(base.AreaPartialViewLocationFormats, theme); FormatLocations(base.ViewLocationFormats, theme); FormatLocations(base.MasterLocationFormats, theme); FormatLocations(base.PartialViewLocationFormats, theme); FormatLocations(base.MasterLocationFormats, theme); } private void FormatLocations(string[] locationFormats,string theme) { for (int i = 0; i < locationFormats.Length; i++) { locationFormats[i] = locationFormats[i].Replace("/Views/",string.Format("/Views/{0}/",theme)); } } }
這個類很簡單,只有一個構造函數及一個私有方法FormatLocations,目的就是改變默認的視圖查找路徑。
有了這個視圖引擎,要實現主題切換就很簡單了,實現方式如下:
1.全局實現:
public class HomeController : Controller { private string themeName = null; protected override void OnResultExecuting(ResultExecutingContext filterContext) { var viewResult = filterContext.Result as ViewResult; if (viewResult != null) { string themeCookieName="ThemeName"; if (!string.IsNullOrEmpty(themeName)) { HttpContext.Response.Cookies.Set(new HttpCookie(themeCookieName, themeName) { Expires=DateTime.Now.AddYears(1)}); } else { themeName = (HttpContext.Request.Cookies[themeCookieName].Value??"").Trim(); } ViewEngineCollection viewEngines = new ViewEngineCollection() { new CustomViewEngine(themeName) }; foreach (var item in ViewEngines.Engines) { viewEngines.Add(item); } viewResult.ViewEngineCollection = viewEngines; //viewResult.ViewEngineCollection = new ViewEngineCollection(new List<IViewEngine> { new CustomViewEngine(themeName) });//這里是只用自定義視圖引擎 } base.OnResultExecuting(filterContext); } public ActionResult TestCustomViewEngine(string theme) { themeName = string.IsNullOrEmpty(theme) ? "Default" : theme; ViewBag.CurrentThemeName = themeName; var viewResult = View("Test"); return viewResult; } }
注意上面代碼中定義的私有字段themeName,這個主要是用作第一次設置主題時會用到,因為如果單純的靠Cookie是不行的,Cookie在沒有輸出到客戶端時,Request.Cookies是空的,那樣的話就會存在延遲,不信的話,大家可以試試,另一個地方是viewResult.ViewEngineCollection,這里我是采用將自定義與默認的引擎都放到引擎集合中,這樣可以保證兼容多種路徑下視圖,當然如果只希望應用自定義的引擎,則可以初始化一個引擎集合且里面只有自定義的視圖引擎。
這里也可以將這些方法定義到一個ThemeController中,然后其它的控制器繼承自它就可以了,還可以實現過濾器,然后再應用到ACTION上。
2.單個視圖實現:僅作演示,一般不會這么用
public ActionResult TestCustomViewEngine(string theme) { string themeName = string.IsNullOrEmpty(theme) ? "Default" : theme; var viewResult = View("Test"); viewResult.ViewEngineCollection = new ViewEngineCollection(new List<IViewEngine> { new CustomViewEngine(themeName) }); ViewBag.CurrentThemeName = themeName; return viewResult; }
視圖內容:
Views/Default/Home/Test.cshtml
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <div> 我是自定義的視圖路徑,風格:@ViewBag.CurrentThemeName </div> @using(Html.BeginForm()){ <span>選擇主題:</span> @Html.DropDownList("theme", new[] { new SelectListItem() { Text = "默認", Value = "Default" }, new SelectListItem() { Text = "綠色", Value = "Green" }}) <p> <input type="submit" value="應用" /> </p> } </body> </html>
Views/SkyBlue/Home/Test.cshtml
<!DOCTYPE html> <html> <head> <title></title> </head> <body style="background-color:skyblue;color:Green;"> <div> 我是自定義的視圖路徑,風格:@ViewBag.CurrentThemeName </div> @using(Html.BeginForm()){ <span>選擇主題:</span> @Html.DropDownList("theme", new[] { new SelectListItem() { Text = "默認", Value = "Default" }, new SelectListItem() { Text = "天空藍", Value = "SkyBlue" }}) <p> <input type="submit" value="應用" /> </p> } </body> </html>
視圖文件目錄結構如下圖示:
最終的實現主題切換效果如下圖所示:
當切到其它視圖,如:View/Home/Index
主題切換實現成功,到此結束,歡迎大家討論,寫代碼很快,但寫文章卻實有點耗時間,花了我幾個小時,主要是需要將設計思路講出來,好讓大家明白,我不想寫那種只講結果不講原理及設計思路的文章,想讓大家看了我的文章,不僅會用,還能明白實現原理,最后大家都能自己設計出更好的方案。獨樂樂不如眾樂樂,如果覺得這篇文章我(夢在旅途)用心了,還請支持一下哦,謝謝!
(今天是我的生日,在此祝我自己生日快樂的同時,也感嘆時間過得很快啊!)
文章列表