|
Web站點風(fēng)格切換的實現(xiàn)
引言
Web站點的風(fēng)格切換是很常見、也很受大家歡迎的功能,比如大家熟知的博客園就提供了幾十款風(fēng)格模板供大家選擇。在ASP.NET中,我們可以通過模板頁master page和主題theme來實現(xiàn)網(wǎng)站的風(fēng)格切換,但是.NET提供的默認設(shè)置不夠強大和靈活。本文將向大家介紹如何在.NET提供的方法上進行改進和擴展,以提供更加強大的網(wǎng)站風(fēng)格切換功能。
效果預(yù)覽:http://www.tracefact.NET/Demo/StyleSetting/default.ASPx
NOTE:本文將master page稱為模板(有的書上叫母版),將theme稱為主題。
網(wǎng)頁的結(jié)構(gòu) 和 模板、主題配置的局限
靜態(tài)網(wǎng)頁的結(jié)構(gòu)
網(wǎng)站的風(fēng)格的切換,說白了,實際上就是對頁面進行分解和重組。所以在進行之前,我們先簡單回顧看一下頁面究竟有哪些組成部分可以供我們分解,分清楚哪些是可變的、哪些是不變的,進行后繼的工作才會容易得多。現(xiàn)在我們先暫時脫離服務(wù)器端,看一下一個靜態(tài)的.htm頁面由哪幾部分組成:
結(jié)構(gòu)(有語義的XHTML):這部分由XHTML標記組成,應(yīng)該注意,這里使用“有語義”三個字作為修飾。XHTML的職責(zé)是告訴“這里是什么”,而不是告訴“這里應(yīng)該如何顯示”。盡管瀏覽器對于幾乎每個XHTML的標記都賦予了某種內(nèi)置的樣式控制,但是XHTML的本意只是規(guī)范文檔的結(jié)構(gòu)。比如h1表示為標題,p表示為段落。而不是為了這個字顯示的更大一些才去使用h1。
表現(xiàn)和布局(CSS):CSS用來控制頁面的顯示及布局。在Web標準的概念普及以前,我想大多人都是表格套表格來布局的,現(xiàn)在基本都在使用CSS了,這里沒有太多好說的。
行為(Javascript):靜態(tài)網(wǎng)頁也可以添加一些交互的行為,這些行為由Javascript來完成。我們時常會把onclick="alert('hello')"這樣的代碼嵌入到XHTML標記中,比如一個Input標記上;一些結(jié)構(gòu)、行為分離的狂熱人士則主張將行為與結(jié)構(gòu)(XHTML)分離,他們不會將Javascript代碼寫到<body>之間,而全部寫在了head中,或者是body下面,使用 window.onload=function(){ // …} 這種形式。有個極力主張這種做法的人(Peter-Paul Koch)寫了本書叫《ppk on Javascript》。
好了,大概了解了這些,我們看下.NET中如何將這三者分離,以及它的一些限制:
.NET對頁面結(jié)構(gòu)的分離
我到現(xiàn)在都覺得 行為與結(jié)構(gòu) 完全分離的概念太前衛(wèi)了,另外它也不影響對網(wǎng)站的風(fēng)格設(shè)置,所以我們這里不討論它。
我們先看一下結(jié)構(gòu):現(xiàn)在我們將思維向服務(wù)器端靠攏一下,很快會發(fā)現(xiàn)上面的結(jié)構(gòu)部分仍需要再細分一次,就是XHTML標記和標記中的內(nèi)容(網(wǎng)頁內(nèi)容)分離。XHTML標記屬于變化的部分,不同的風(fēng)格可能會需要不同的XHTML結(jié)構(gòu),而對于各個風(fēng)格,它所顯示的內(nèi)容顯然是一樣的。想要得到這樣的效果,我們可以使用Master Page模板頁。由Master Page模板頁對應(yīng)XHTML結(jié)構(gòu)(變化部分),由Page頁面對應(yīng)于XHTML頁面的內(nèi)容(不變部分),即是一個Page可以設(shè)置為不同的Master Page以達到不同的樣式(look and feel)。
NOTE:這里在說一下CSS,如果你的CSS水平足夠強,XHTML的代碼寫得足夠好,那么你無需搞得這么復(fù)雜,僅僅使用CSS就可以實現(xiàn)換膚了。www.csszengarden.com有這樣的一個項目提供給全世界的人作為實踐,它提供一套統(tǒng)一的XHTML代碼,其他人則自己編寫CSS來對這個XHTML代碼進行樣式化,結(jié)果是百花齊放,一模一樣的XHTML實現(xiàn)了風(fēng)格迥異的頁面設(shè)計。
現(xiàn)在在看一下表現(xiàn)部分,表現(xiàn)層分為全局式的CSS以及基于控件的皮膚Skin,這些都可以交由主題Theme來完成。
.NET 設(shè)置上的局限
看到這里,你可能覺得不用往下看了,使用Master Page和Theme誰不會啊。現(xiàn)在我們就討論下.NET 中Master Page 和 Theme 的局限:
- 大家知道,我們可以在 Web.config中的System.Web結(jié)點下的pages結(jié)點中添加Theme和masterPageFile屬性來對網(wǎng)站進行設(shè)置。但是它提供了一個全局性的配置,就是對于整個站點的所有頁面,都將使用這個Master Page。而有時候我們希望能夠每個頁面不相同,這里就無法實現(xiàn)了。雖然我們可以通過使用location結(jié)點來為各個頁面進行單獨設(shè)置,但是顯然太麻煩了。
- 我們希望每個風(fēng)格都有個名字,比如說“默認風(fēng)格”、“春意盎然”。
- 我們希望用戶可以選擇風(fēng)格,而不是像博客園這樣由博主設(shè)置風(fēng)格。
實現(xiàn)網(wǎng)站的風(fēng)格切換
自定義風(fēng)格配置
有了思路后我們就來一步步地實現(xiàn)它,我們希望可以對風(fēng)格進行簡單的設(shè)置,我們應(yīng)該先明確需要設(shè)置的內(nèi)容:我們都有哪些風(fēng)格、當前使用的風(fēng)格是什么、每個風(fēng)格使用了什么主題、哪個頁面對應(yīng)哪個模板。了解了這些之后,我們可以寫下這樣的結(jié)點配置來:
<!-- MasterPage的 Path為 masterRoot + theme + master -->
<!-- 不為Default設(shè)置masterPage,
使用Default風(fēng)格時會使用創(chuàng)建頁面時所選擇的Master Page -->
<styleTemplates default="默認風(fēng)格" masterRoot="~/MasterPage">
<style name="默認風(fēng)格" theme="Default" ></style>
<style name="春意盎然" theme="Spring" >
<masterPages>
<page path="/website/default.ASPx" master="Default.master" />
<!--<page path="/other.ASPx" master="Default.master" />-->
<page path="/default.ASPx" master="Default.master" />
</masterPages>
</style>
</styleTemplates>
styleTemplates是風(fēng)格設(shè)置的根節(jié)點,default是默認的風(fēng)格名稱;masterRoot是指模板頁的根目錄的地址;style結(jié)點是各個風(fēng)格的名稱,以及其所使用的主題的名稱;style結(jié)點下的masterPages結(jié)點組包含此風(fēng)格所使用的模板的信息;其中Page子結(jié)點對應(yīng)每個獨立的頁面,path屬性記錄的是頁面的路徑,master屬性是每個頁面對應(yīng)的模板的路徑。
page結(jié)點的master屬性沒有給出模板的全路徑,這么做一個是為了使目錄結(jié)構(gòu)更一目了然,還有就是不想master屬性的內(nèi)容變得很長,所以需要在程序中指定頁面的模板的路徑為 masterRoot + Theme + masterPage名稱。這樣在模板的根目錄下,必須建立與主題同名的目錄,然后將模板位于該目錄下。
在創(chuàng)建頁面時,我們需要要選擇一個模板。注意到這一點非常重要,因為這個模板實際上是該頁面的默認模板。當我們在Web.config中將相應(yīng)風(fēng)格的page結(jié)點留空時(沒有對此頁面設(shè)置模板),那么就會應(yīng)用這個模板。所以在Web.Config中定義風(fēng)格的模板,只是覆蓋了這個我們在創(chuàng)建頁面時選擇的模板。由此,我們只用定義一套完整的Master Page模板頁,供每個頁面在創(chuàng)建時選擇。而Web.config風(fēng)格結(jié)點下設(shè)置的master page,實際上僅僅是動態(tài)地覆蓋這些模板。
以上面的配置為例:當我們將 “春意盎然” 下的第二個 page 結(jié)點注釋掉時,它會應(yīng)用創(chuàng)建頁面時所選擇的模板。“默認風(fēng)格”下面沒有設(shè)置任何的 page 結(jié)點,所以對于該風(fēng)格的每個頁面,都會應(yīng)用創(chuàng)建頁面時所選擇的模板。
如果你希望僅使用Css來換膚,你可以使用也可以不使用Master Page,那么在Web.Config中可以像下面這樣設(shè)置:
<styleTemplates default="默認風(fēng)格" masterRoot="">
<style name="默認風(fēng)格" theme="Default" ></style>
<style name="春意盎然" theme="Spring" ></style>
<style name="秋高氣爽" theme="Autumn" ></style>
</styleTemplates>
對于上面使用Master Page時的設(shè)置,站點的目錄結(jié)構(gòu)如下所示:
可以看到模板頁的根目錄下包含了目錄Default、Spring與主題名稱相同(必須)。之后我們要編寫結(jié)點處理程序,如何編寫自定義結(jié)點處理程序,我在《.NET 自定義應(yīng)用程序配置》一文中已經(jīng)詳細的討論了,所以這里我們直接看實現(xiàn),在AppCode目錄中新建一個文件StyleTemplateConfigHandler.cs:
public class StyleTemplateConfigHandler : IConfigurationSectionHandler
{
public object Create(object parent, object configContext, XmlNode section) {
return new StyleTemplateConfiguration(section);
}
}
// 映射 styleTemplates 結(jié)點的實體類
public class StyleTemplateConfiguration {
private XmlNode node; // styleTemplates 結(jié)點
private string defaultTheme; // 默認的主題名稱
private string defaultStyle; // 站點默認風(fēng)格名稱
private HttpContext context;
private Dictionary<string,string> styleDic; // Key: 風(fēng)格名稱;Value: 主題名稱
public StyleTemplateConfiguration(XmlNode node) {
this.node = node;
context = HttpContext.Current;
styleDic = new Dictionary<string, string>();
// 獲取所有style結(jié)點的 name屬性 和 theme屬性
XmlNodeList styleList = node.SelectNodes("style");
foreach (XmlNode style in styleList) {
styleDic[style.Attributes["name"].Value] = style.Attributes["theme"].Value;
}
// 獲取 站點默認風(fēng)格 名稱
defaultStyle = node.Attributes["default"].Value;
// 根據(jù) 風(fēng)格名稱 獲取主題
defaultTheme = styleDic[defaultStyle];
}
// 獲取所有風(fēng)格名稱
public ICollection<String> StyleNames {
get {
return styleDic.Keys;
}
}
// 根據(jù)風(fēng)格名稱獲取主題名稱
public string GetTheme(string styleName) {
return styleDic[styleName];
}
// 設(shè)置、獲取 站點默認風(fēng)格
public string DefaultStyle{
get {
return defaultStyle;
}
set { // 更改Web.Config中的默認風(fēng)格,一般為站長才可以使用
XmlDocument doc = new XmlDocument();
doc.Load(context.Server.MapPath(@"~/web.config"));
XmlNode root = doc.DocumentElement;
XmlNode styleTemp = root.SelectSingleNode("styleTemplates");
styleTemp.Attributes["default"].Value = value;
doc.Save(context.Server.MapPath(@"~/web.config"));
}
}
// 獲取默認主題名稱
public string DefaultTheme {
get { return defaultTheme; }
}
// 根據(jù)頁面路徑獲取其對應(yīng)的 masterPage 的路徑
public string GetMasterPage(string userTheme){
// 獲取當前頁面路徑
string pagePath = context.Request.Path;
// 用于定位page結(jié)點的 XPath
string xpath = "style[@theme='" + userTheme + "']" + "/masterPages/page[@path='" + pagePath.ToLower() + "']";
// 獲取與path屬性相匹配的page結(jié)點
XmlNode pageNode = node.SelectSingleNode(xpath);
string master;
if (pageNode != null) {
// 獲取page結(jié)點的 master屬性的值
master = pageNode.Attributes["master"].Value;
return prepareMasterPath(master, userTheme);
} else
return null;
}
// 獲取 Master Page 的路徑
// MasterPagePath = 跟路徑 + Theme路徑 + 模板路徑
private string prepareMasterPath(string masterPath, string userTheme) {
string path;
if (node.Attributes["masterRoot"] != null)
path = node.Attributes["masterRoot"].Value + "/" + userTheme + "/" + masterPath;
else {
if (userTheme != null) {
path = "~/" + userTheme + "/" + masterPath;
} else {
path = "~/" + masterPath;
}
}
return path;
}
}
這個類提供了一些簡單的對XmlNode的操作,對styleTemplates結(jié)點進行了映射,這里需要明確兩個概念:默認風(fēng)格 和 用戶風(fēng)格:
- 默認風(fēng)格,指的是站點管理員 或者 博主設(shè)置的風(fēng)格,也就是Web.Config 中styleTemplates結(jié)點的Default屬性。
- 用戶風(fēng)格,用戶設(shè)置的風(fēng)格,頁面的實際顯示是根據(jù)用戶風(fēng)格而 不是默認風(fēng)格。當用戶沒有設(shè)置風(fēng)格時,才顯示默認風(fēng)格。
很顯然,這個類處理的所有的均是默認風(fēng)格,我們來看一下它的幾個主要方法和屬性:
// 獲取所有風(fēng)格名稱
public ICollection<String> StyleNames { get {;} }
// 根據(jù)風(fēng)格名稱獲取主題名稱
public string GetTheme(string styleName) {}
// 設(shè)置、獲取 站點默認風(fēng)格
public string DefaultStyle{}
// 獲取默認主題名稱
public string DefaultTheme { }
// 根據(jù)頁面路徑獲取其對應(yīng)的 masterPage 的路徑
public string GetMasterPage(string userTheme){}
IUserStyleStrategy,獲取、設(shè)置用戶風(fēng)格
在繼續(xù)進行之前,我們來考慮這樣一個問題:因為我們要根據(jù)用戶選擇的風(fēng)格來動態(tài)地為頁面加載主題和模板,那么用戶信息(用戶選擇了什么風(fēng)格)應(yīng)該保存在哪里,從哪里獲得呢?我們有很多的選擇:可以使用Session、可以使用Cookie,還可以保存到數(shù)據(jù)庫中。此時最好將這部分抽象出來,以便日后為不同的方法提供實現(xiàn)。我們定義一個接口 IUserStyleStrategy,它用來定義如何獲取、設(shè)置用戶風(fēng)格,在AppCode中新建文件IUserStyleStragety.cs:
public interface IUserStyleStrategy{
void ResetUserStyle(string styleName); // 重新設(shè)置用戶風(fēng)格
string GetUserStyle(); // 獲取用戶風(fēng)格
}
然后我在這里提供了一個基于Cookie的默認實現(xiàn):
// 默認風(fēng)格設(shè)置方法:使用Cookie記錄
public class DefaultStyleStrategy : IUserStyleStrategy {
private string cookieName; // cookie名稱
private HttpContext context;
public DefaultStyleStrategy(string cookieName){
this.cookieName = cookieName;
context = HttpContext.Current;
}
// 重新設(shè)置用戶風(fēng)格名稱
public void ResetUserStyle(string styleName) {
HttpCookie cookie;
if(context.Request.Cookies[cookieName]!=null)
cookie = context.Request.Cookies[cookieName];
else
cookie = new HttpCookie(cookieName);
cookie.Value = context.Server.UrlEncode(styleName);
cookie.Expires = DateTime.Now.AddHours(2); // 設(shè)置Cookie過期時間
context.Response.Cookies.Add(cookie);
// 因為風(fēng)格(master page和theme)的動態(tài)設(shè)置只能在 PreInit 事件中
// 而Button的Click事件在PreInit事件之后,所以需要Redirect才可以生效
context.Response.Redirect(context.Request.Url.PathAndQuery);
}
// 獲取用戶自己設(shè)置的風(fēng)格名稱
public string GetUserStyle() {
if (context.Request.Cookies[cookieName] != null) {
string value = context.Request.Cookies[cookieName].Value;
value = context.Server.UrlDecode(value); // 避免出現(xiàn)中文亂碼
return value;
} else
return null;
}
}
如果日后你需要將信息保存在數(shù)據(jù)庫中,那么你只要重新實現(xiàn)一下這個接口就可以了:
// 如果將用戶風(fēng)格設(shè)置存儲到了數(shù)據(jù)庫中,可以實現(xiàn)這個接口
public class SqlStyleStrategy : IUserStyleStrategy {
private int userId; // 用戶Id
public SqlStyleStrategy(int userId){
this.userId = userId;
// ...
}
public void ResetUserStyle(string styleName) {
throw new NotImplementedException();
}
public string GetUserStyle() {
throw new NotImplementedException();
}
}
PageBase 類:繼承自Page的基類
因為所有的頁面都要運行這樣的一個邏輯:判斷用戶是否有設(shè)置風(fēng)格,如果沒有,使用Web.Config中設(shè)置的默認風(fēng)格;如果設(shè)置了,使用用戶風(fēng)格。最后動態(tài)地給Page類的Theme屬性和MasterPageFile屬性賦值。
那么我們可以定一個基類,在這個基類中去做這件事,然后讓所有的頁面繼承這個基類;又因為頁面是一定要繼承自System.Web.UI.Page類的,所以這個基類也必須繼承自System.Web.UI.Page。現(xiàn)在在AppCode中再建一個文件 PageBase.cs:
// 供所有基類繼承
public class PageBase : Page
{
protected StyleTemplateConfiguration styleConfig;
protected string userStyle; // 用戶設(shè)置的風(fēng)格
protected string userTheme; // 用戶設(shè)置的主題
protected IUserStyleStrategy userStrategy; // 使用何種算法來獲得用戶自定義的信息
protected override void OnPreInit(EventArgs e) {
base.OnPreInit(e);
// 這里會被緩存只在第一次時調(diào)用有用
this.styleConfig = (StyleTemplateConfiguration)ConfigurationManager.GetSection("styleTemplates");
// 當你實現(xiàn)了自己的 Strategy時只需要更改這里就可以了
// 更好的辦法是將Stragey的類型保存在Web.config中,
// 然后使用反射來動態(tài)創(chuàng)建
userStrategy = new DefaultStyleStrategy("userStyle");
// 獲取用戶風(fēng)格
userStyle = userStrategy.GetUserStyle();
// 如果用戶沒有設(shè)置風(fēng)格,使用默認風(fēng)格
if (String.IsNullOrEmpty(userStyle)) {
userStyle = styleConfig.DefaultStyle;
userTheme = styleConfig.DefaultTheme;
} else {
// 根據(jù)用戶設(shè)置的風(fēng)格 獲取 主題名稱
userTheme = styleConfig.GetTheme(userStyle);
}
// 根據(jù)當前頁獲取MasterPage的路徑
string masterPagePath = styleConfig.GetMasterPage(userTheme);
// 設(shè)置當前頁的MasterPage
if (masterPagePath != null)
this.MasterPageFile = masterPagePath;
this.Theme = userTheme; // 設(shè)置當前頁的主題
}
}
這里需要注意:
- ConfigurationManager.GetSection()方法只會調(diào)用一次,然后會將結(jié)果進行緩存;只要Web.Config不做修改,以后不管是Request還是PostBack都不會再重新生成StyleTemplateConfig的類型實例,而會從緩存中讀取。我在《.NET 自定義應(yīng)用程序配置》中忘記說了。
- userStrategy = new DefaultStyleStrategy("userStyle");這里可以將IUserStyleStrategy的類型信息保存在Web.config中,然后使用反射動態(tài)創(chuàng)建。具體方法依然參見《.NET 自定義應(yīng)用程序配置》。
- 如果想動態(tài)地為頁面設(shè)置主題和模板,代碼必須寫在PreInit事件中。參見《ASP.NET Page Life Cycle Overview》。
效果預(yù)覽
因為這只是一個范例程序,我主要是表達實現(xiàn)的思路,而不是代碼的編寫,所以省略了很多諸如結(jié)點屬性是否為空之類的判斷。下面的測試僅僅在Web.Config中的配置正確時。在站點下新建一個頁面,比如Default.ASPx,注意創(chuàng)建一個模板頁,因為這里設(shè)置的是會被覆蓋的,所以無所謂選擇哪個模板。
添加App_Theme下創(chuàng)建目錄Default、Spring,新建一個目錄MasterPage,在下面創(chuàng)建目錄Default、Spring,然后添加一些文件(這就不用我說了吧)。在頁面上添加一個DropDonwList,一個Button,當我們選擇要顯示的模板時,會進行相應(yīng)的切換,編寫后置代碼:
protected void Page_Load(object sender, EventArgs e) {
if (!IsPostBack) {
ltrStyleName.Text = userStyle;
foreach (string styleName in styleConfig.StyleNames) {
ListItem item = new ListItem(styleName);
if (string.Compare(styleName, userStyle) == 0)
item.Selected = true;
ddlStyles.Items.Add(item);
}
}
}
// 更換風(fēng)格
protected void Button1_Click(object sender, EventArgs e) {
string styleName = ddlStyles.SelectedValue;
userStrategy.ResetUserStyle(styleName); // 委托給userStragety去處理
}
之后你可以看到下面的畫面:
可以通過下面這個連接來看實際的效果,注意到:在這里我讓 Default.ASPx 和 Other.ASPx 使用了同一個模板,你也可以設(shè)置它們使用不同的模板。
http://www.tracefact.NET/Demo/StyleSetting/default.ASPx
總結(jié)
在這篇文章中,我簡單地向大家介紹了實現(xiàn)網(wǎng)站風(fēng)格切換的一個思路。
我們首先復(fù)習(xí)了網(wǎng)頁的結(jié)構(gòu),了解了.NET默認配置的不足。接著分三個步驟實現(xiàn)了網(wǎng)站的風(fēng)格切換:處理配置結(jié)點的程序、獲取用戶風(fēng)格的方法、以及通過基類繼承來為各個頁面設(shè)置風(fēng)格。最后我們通過簡單的兩個頁面進行了下測試和預(yù)覽。
感謝閱讀,希望這篇文章能給你帶來幫助!
AspNet技術(shù):asp.net Web站點風(fēng)格切換的實現(xiàn),轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯(lián)系我們修改或刪除,多謝。