標准化網站布局
標准化網站布局的格式只是整個過程的一部分,你還需要保證通用的元素,如網站的標題、網站的導航控件等在每個頁面里都出現在相同的位置。解決這一問題的關鍵在於創建一個可以重復應用到整個網站的簡單而靈活的布局。有 3 個基本辦法可以選擇:
- 用戶控件。用戶控件時標准化通用頁面元素的優秀方式,但是,它們自身並不能解決頁面布局的問題,因為沒有辦法保證用戶控件在所有頁面中都被放到同樣的位置。
- HTML 框架。框架是在一個瀏覽器窗口中同時顯示多個頁面的 HTML 基本工具。它的主要缺點是它里面的每個頁面必須通過單獨請求服務器進行檢索,這些頁面的代碼不得不完全獨立。這同時意味着一個框架里的頁面不能和其他框架中的頁面交互,也不能影響其他框架中的頁面。(至少不能通過服務器端代碼)
- 母版頁。母版頁是 ASP.NET 的一個特性,它專門設計用於標准化 Web 頁面布局。它可定義固定的內容並聲明 Web 頁面里可插入自定義內容的部分。如果在整個網站中使用同一個母版頁,就可以確保獲得同樣的布局。最妙的是,如果修改了母版頁的定義,應用它的所有頁面都會自動變化。
母版頁基礎
要為頁面模版提供一個可操作且靈活的解決方案,必須滿足以下幾個條件。
- 要能夠單獨定義頁面的某個部分並在多個頁面里重用它。
- 要能夠創建一個定義了可編輯區域的封閉布局。重用這個模版的頁面只能夠在許可的區域內添加或修改內容。
- 頁面能夠對重用的元素做一些自定義。
- 可以聲明性的綁定頁面倒頁面模版(不使用代碼)或者能夠在運行時動態綁定到頁面。
- 可以用工具(如 VS)設計一個使用頁面模版的頁面。
為了實現這一切,ASP.NET 定義了兩種新的頁面類型:母版頁和內容頁。
母版頁和普通的 Web 頁面一樣,它可以包含任何 HTML、Web 控件甚至代碼的組合。母版頁還可以包含內容占位符(定義的可修改區域)。
內容頁引用一個母版頁並獲得它的布局和內容。此外,內容頁可以在任意的占位符里加入頁面特定的內容。換句話說,內容頁將母版頁沒有定義的缺失了的內容填入母版頁。
簡單的母版頁
母版頁和一般Web窗體的區別是:
- 母版頁由 Master 指令開始,並提供和 Page 指令相同的信息。而所有 Web 窗體都由 Page 指令開始。
- 只有母版頁才可以使用 ContentPlaceHolder 控件,這個控件是內容頁可以插入內容的部分。
創建一個母版頁后,會得到一個只包含 2 個 ContentPlaceHolder 控件的空白頁。第一個是在 <head> 區域定義的,它讓內容頁面能夠增加頁面元數據,比如搜索關鍵字和樣式表鏈接。第二個也是更重要的 ContentPlaceHolder 被定義在 <body> 區域,它代表頁面顯示的內容。
另外,母版頁不能被直接請求,要使用母版頁,必須創建一個關聯的內容頁。
下面是個簡單的母版頁示例,它有一個靜態的橫幅,其后跟着一個 ContentPlaceHolder 控件,然后是一個頁腳:
<%@ Master Language="C#" AutoEventWireup="true" CodeFile="SiteTemplate.master.cs"
Inherits="Chapter16_SiteTemplate" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<asp:ContentPlaceHolder ID="head" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<form id="form1" runat="server">
<div>
<div style="background: black; height: 87px; font-weight: bold; font-size: 20px;
color: white; font-family: Verdana">
<img align="left" src="headerleft.jpg" />
<img align="right" src="headerright.jpg" />
<br />
<asp:ContentPlaceHolder ID="TitleContent" runat="server">
My Site
</asp:ContentPlaceHolder>
</div>
<br />
<br />
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
<br />
<em>Copyright © 2008.</em>
</div>
</form>
</body>
</html>
簡單的內容頁
要在其他頁面里使用母版頁,必須在 Page 指令里加入 MasterPageFile 特性:
<%@ Page Title="" Language="C#" MasterPageFile="~/Chapter16/SiteTemplate.master" ... %>
只設置 MasterPageFile 特性還不足以把普通的頁面轉變成內容頁。內容頁必須定義要插入一個或多個 ContentPlaceHolder 控件的內容(並編寫這些控件需要的代碼)。由於母版頁已經提供了外殼,因此,試圖在內容頁中加入 <html>、<head>、<body> 之類的元素,則會產生一個錯誤。
要為 ContentPlaceHolder 提供內容,要用到另一個叫 Content 的特殊控件。ContentPlaceHolder 和 Content 控件具有一對一的關系。對於母版頁里的每個 ContentPlaceHolder ,內容頁會提供一個對應的 Content 控件(除非不准備為那個區域提供任何內容)。ASP.NET 通過匹配 ContentPlaceHolder 的 ID 和對應的 Content控件的 Content.ContentPlaceHolderID 屬性將它們對應起來。
<%@ Page Title="" Language="C#" MasterPageFile="~/Chapter16/SiteTemplate.master"
AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="Chapter16_Default" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
<p class="Code" style="margin: 0in 0.1in 0pt 0in">
<span style="font-size: 10pt; font-family: TheSansMonoConNormal">Far out in the uncharted
backwaters of the unfashionable end of the western spiral arm of the Galaxy lies
a small unregarded yellow sun.</span></p>
</asp:Content>
<asp:Content ContentPlaceHolderID="TitleContent" ID="Content2" runat="server">
Custom Title</asp:Content>
為了更好的理解母版頁是如何工作的,值得通過跟蹤(在 Page 指令里加入 Trace=true)來看看內容頁。借助這種方式可以了解控件的層次。你會發現 ASP.NET 首先為母版頁創建控件對象,包括 ContentPlaceHolder(它充當一個容器),接着它會把內容頁的控件加入 ContentPlaceHolder 。
如果需要動態配置母版頁或內容頁,可以響應任意一個類中的 Page.Load 事件。有時你可能會同時在母版頁和內容頁中使用初始化代碼。這種情況下,理解每個事件發生的順序就很重要。ASP.NET 首先創建母版頁控件,然后添加內容頁的子控件。然后它觸發母版頁的 Page.Init 事件,隨后是內容頁的 Page.Init 事件。對於 Page.Load 事件,也是相同的步驟。(如果有沖突,那么內容頁的自定義會覆蓋在母版頁相同階段所做的修改)
默認內容
母版頁定義 ContentPlaceHolder 時可以包含默認的內容(內容頁沒有提供相應的 Content 控件時才會使用的內容)。
內容頁不能只使用母版頁默認內容的一部分或只編輯這一部分。這是不可能的,因為默認內容是保存在母版頁里面而不是內容頁中。所有,要么完全使用,要么就全部替換它。
具有表格和 CSS 布局的母版頁
HTML 使用基於流的布局。這意味着隨着內容的增加,頁面會被重新組織,其他一些內容會被擠到一邊。這樣的布局會使得難以獲得母版頁預期的結果。如果你不小心,就會破壞原本完美的布局,插入到 <Content> 標簽的大量信息會把頁面結構弄得亂七八糟。
為了控制這些問題,大部分母版頁使用 HTML 表格或者 CSS 定位來控制布局。
使用表格時,基本原則是把整個頁面或頁面的部分分解到行和列里。然后你就可以把 ContentPlaceHolder 加入到某個單元格里,從而保證其他內容多少會按照預期的那樣對齊。
使用 CSS 定位時,基本思想是把內容放入 <div> 標簽,然后使用絕對坐標控制 <div> 的位置或者讓它們浮動在頁面的某一邊,最后你可以把 ContentPlaceHolder 放入 <div> 標簽。(http://www.csszengarden.com 和 http://www.bluerobot.com/web/layouts 中有很多基於 CSS 布局的優秀示例)
下面示例演示了如何用母版頁創建包含一個頁頭、頁尾、導航欄的傳統 Web 應用程序,這些元素都通過表格進行定義:
<%@ Master Language="C#" AutoEventWireup="true" CodeFile="TableMaster.master.cs"
Inherits="TableMaster" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<table width="100%">
<tr>
<td colspan="2" style="border: thin #008000 dotted; background: #FFFFEA; padding: 10px;">
My Header
</td>
</tr>
<tr>
<td style="border: thin #008000 dotted; background: #FFFFEA; padding: 10px;">
<asp:TreeView ID="Treeview1" runat="server" Width="150px">
<Nodes>
<asp:TreeNode Text="New Node" Value="New Node"></asp:TreeNode>
</Nodes>
</asp:TreeView>
</td>
<td>
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</td>
</tr>
<tr>
<td colspan="2" style="border: thin #008000 dotted; background: #FFFFEA; padding: 10px;">
My Footer
</td>
</tr>
</table>
</div>
</form>
</body>
</html>
<%@ Page Language="C#" MasterPageFile="~/Chapter16/TableMaster.master" AutoEventWireup="true"
CodeFile="TableContentPage.aspx.cs" Inherits="TableContentPage_aspx" Title="Untitled Page" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
Your content goes in this cell.<br />
<asp:Button ID="cmdShow" runat="server" Text="Show" OnClick="cmdShow_Click" />
<asp:Button ID="cmdHide" runat="server" Text="Hide" OnClick="cmdHide_Click" />
</asp:Content>
public partial class TableContentPage_aspx : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void cmdHide_Click(object sender, EventArgs e)
{
TableMaster master = (TableMaster)this.Master;
master.ShowNavigationControls = false;
}
protected void cmdShow_Click(object sender, EventArgs e)
{
TableMaster master = (TableMaster)this.Master;
master.ShowNavigationControls = true;
}
}
很多專業的 Web 開發人員傾向於使用基於 CSS 的布局技術(基於 CSS 的布局允許編寫便於閱讀、之后也更容易修改的標記,它易於減少長期令人頭痛的問題)。
在基於 CSS 的布局中使用 ContentPlaceHolder 和在基於表格的布局中使用它同樣簡單。只要把 ContentPlaceHolder 放入到不同的 <div> 元素里,然后樣式表使用 position、left、right、top 和 bottom 等特性對每個 <div> 元素進行定位。
例如,一個常見的頁面設計是把頁面分為 3 欄。頁面的每個邊欄被設置為固定的大小,而中間的部分占有其余的全部空間:
.leftPanel
{
position: absolute;
top: 70px;
left: 10px;
width: 150px;
background-color:Yellow;
}
.rightPanel
{
position: absolute;
top: 70px;
right: 10px;
width: 150px;
background-color:Yellow
}
.centerPanel
{
margin-left: 151px;
margin-right: 151px;
padding-left: 12px;
padding-right: 12px;
background-color:Green;
}
現在可以把頁面分成列,並在適當的區域加入 ContentPlaceHolder 控件:
<div class="leftPanel">Menu</div>
<div class="centerPanel">
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server"></asp:ContentPlaceHolder>
</div>
<div class="rightPanel">Advertisement</div>
母版頁和相對路徑
一個經常困擾開發人員的問題是母版頁如何處理相對路徑的。把母版頁和內容頁分放到不同的目錄里是大型網站推薦使用的最佳實踐,但同時,關於相對路徑的問題就發生了。
假設把母版頁放在了一個叫做 MasterPages 的子文件夾里,並在母版頁里加入了如下這條標簽:
<img src="banner.jpg" />
假設 \MasterPages\banner.jpg 存在,這看起來是行得通的,甚至 VS 開發環境中也會出現這張圖片。但是如果在另一個子文件夾里創建了內容頁,路徑就會被解釋成相對於那個文件夾的路徑。如果文件不在那里,就會得到一個破損的鏈接而看不到圖片。
這一問題之所以會發生,是因為 <img> 標簽只是普通的 HTML,ASP.NET 不會接觸到它。要解決這一問題,可以預先把 URL 寫成相對於內容頁的地址。不過這會帶來混淆,這限制了母版頁的使用范圍,並且會產生在設計環境里錯誤顯示母版頁的負面效應。
另一個快捷的解決方案是把圖片標簽變為服務器端控件,這樣 ASP.NET 就會自動修復這個錯誤:
<img src="banner.jpg" runat="server" />
這個方案會起作用是因為 ASP.NET 會根據這一信息創建一個 HtmlImage 服務器控件。這個對象在母版頁的 Page 對象被實例化后創建,此時,ASP.NET 把所有路徑解釋為相對於母版頁的位置(同樣,這個技術也可以修復 <a> 標簽等)。
還可以使用根路徑語法,並用“.”字符作為 URL 的開頭:
<img src="./MasterPagesbanner.jpg" runat="server" />
上面的這個路徑毫無歧義。但遺憾的是,這種語法只對服務器控件有效。如果要對普通的 HTML 控件有效,需要在鏈接里包含域名的完整相對路徑,這樣的 HTML 代碼難看且不可移植,不推薦使用。
通過配置文件應用母版頁
值得注意的是,還可以借助 web.config 文件一次對整個網站的所有頁面應用母版頁。你所要做的只是像下面這樣加入 <pages> 特性並設置它的 masterPageFile 特性:
<system.web>
<pages masterPageFile="SiteTemplate.master"></pages>
</system.web>
這種方式的一個問題是,它不太靈活。任何違背了規則(例如,包含根 <html> 標簽或者定義了一個不對應 ContentPlaceHolder 的內容區域)的 Web 頁面都會自動損壞。如果一定要使用這一功能,就不要對整個網站應用該功能,而應在內容頁面這創建子文件夾,在子文件夾里面再創建 web.config 文件。
高級模板頁
你還可以借助其他技巧和技術優化母版頁的工作方式,你將看到內容頁與母版頁如何交互、如何動態設置母版頁、如何在母版頁里嵌套母版頁。
1. 和母版頁類交互
母版頁要解決的一個問題是,它的模型如何認定你是希望在所有頁面間精確復制一些東西(這種情況下,可以把這些內容包含在母版頁里)還是在每個頁面改變這些東西(這種情況下,需要加入一個 ContentPlaceHolder,並包含每個內容頁的信息)。這種區分對很多頁面都很見效,但是當你在母版頁和內容頁之間允許更多的交互的時候,就會遇到一些麻煩。
例如,你希望母版頁能夠提供 3 種顯示模式,然后由內容頁選擇正確的顯示模式,這會改變母版頁的外觀。不過,內容頁不能任意修改母版頁,除了這 3 個預設的變更外,其他都會被拒絕。顯然,需要通過編程才能實現它們的交互(可以通過 Page.Master 屬性訪問母版頁的當前實例)。
首先,母版頁可以通過公有的屬性或方法開放可以讓內容頁修改的頁面內容,比如這樣:
public string BannerText
{
get { return lblTitleContent.Text; }
set { lblTitleContent.Text = value; }
}
內容頁現在可以修改母版頁的標題文字了(需要注意的是 Master 屬性返回的是一般的 MasterPage 類,需要轉型):
protected void Page_Load(object sender, EventArgs e)
{
Chapter16_SiteTemplate master = Master as Chapter16_SiteTemplate;
master.BannerText = "Content Page #1";
}
另一個能夠訪問強類型母版頁的辦法是在內容頁加入 MasterType 指令,你只需要在指令中指定相應 .Master 文件的虛擬路徑即可:
<%@ MasterType VirtualPath="~/Chapter16/SiteTemplate.master" %>
這樣你就可以使用簡單一些的強類型代碼來訪問母版頁:
protected void Page_Load(object sender, EventArgs e)
{
Master.BannerText = "Content Page #1.";
}
對於所有這些示例,有一點需要注意:當從一個頁面導航到另一個頁面時,所有的 Web 頁面對象都會重新創建。也就是說,即使你跳轉到另一個使用相同母版頁的內容頁,ASP.NET 也會創建一個不同的母版頁對象實例。因此,用戶每跳轉到一個新的頁面時,標題里的 Label 控件的 Text 屬性會恢復它的默認值,要改變這一行為,必須在其他位置(如 cookie)保存信息並在母版頁檢查這些值的初始化代碼!
你還可以強行訪問母版頁上的某個控件。使用的技巧是根據對象的唯一名稱:
Label lbl = Master.FindControl("lblTitleContent") as Label;
if (lbl != null)
{
lbl.Text = "Content Page #1.";
}
當然,這種交互方式打破了基於設計和封裝的原則。如果確實需要訪問母版頁的控件,最好在母版頁里添加屬性或方法來開放需要開放的內容。
2. 動態設置母版頁
有時候,你可能希望動態改變母版頁。這可能在以下兩種情形下發生:
- 存在幾種類型的用戶,而你希望根據不同用戶調整版面設計的復雜性或功能的可見性。(例如根據用戶的帶寬、可訪問性、喜好…)
- 在和其他公司合作時,你希望網站能夠相應地調整自身的外觀和布局。
通過編程,只需設置 Master 屬性即可,不過需要記住的是,這一過程必須在 Page.Init 事件階段完成!這一技術的實現,和本章先前介紹的動態主題非常相似。但有一個潛在的危險:內容頁可能不能和任意的母版頁兼容。如果內容頁包含母版頁里沒有對應的 ContentPlaceHolder 的 Content 標簽,就會產生錯誤(要避免這一問題,必須保證所有動態設置的母版頁包含相同的占位符)。
3. 嵌套母版頁
例如,某網站可能有多個不同位置的導航,但它們擁有共同的標題。母版頁的嵌套其實並不多見,這里就不詳細敘述了。不過需要知道的是,可以使用任意級的嵌套母版頁,但實現時要小心一點。雖然它聽起來是模塊化的好辦法,但是它帶給你的束縛比你想象的要多。