作者:Fritz Onion
相关技术:模板、ASP.NET,Master Pages
难度:★★★☆☆
读者类型:ASP.NET开发人员
[导读]本文首先介绍了在传统的Web开发下“模板”的实现,包括了ASP和ASP.NET 1.x的实现,然后介绍了ASP.NET 2.0下模板的实现方式——Master Pages,最后阐述了母版页的实现原理。
多年以来,Web开发人员一直在努力通过多种技术来“模板化”他们的站点,但没有任何一种经证明技术,能够在整个站点中维护标准化外观的真正通用,且可重用的方法。母版页作为在ASP.NET 2.0中问世的最令人期待的功能之一,最终提供了一种在应用程序中基于模板设计页面公认、一流的方法。
模板设计 在Web站点设计中为所有页面定义一种标准的外观是令人赏心悦目的。这可能包括公用页眉、页脚和菜单,他能够在整个站点中提供一组核心功能和外观。对于通过如ASP或ASP.NET这样的技术生成的动态站点而言,如果将所有页面上的这些公用功能整合为某种类型的页面模板将十分有用,这可以使各个页面只包含它自己的独特内容,并且提供一个在外观和行为方面进行站点范围更改的中心位置。有关可以从某种类型的页面模板技术中受益的站点的简单而具体的示例,请参见图1。
图1 简单的模板化站点
该特定页面在顶部有一个页眉,在底部有一个页脚,在左侧有一个导航栏,并且有一个填满了其余空间的用于页面特有内容的区域。理想情况下,页眉、页脚和导航栏应该只需要定义一次,并且以某种方式应用到该站点中的所有页面。
这正好是ASP.NET 2.0中的母版页能够简单而巧妙地解决的问题。通过定义一个母版页,并且随后根据它来创建许多内容页,您可以非常容易地用单个模板(母版页)所驱动的通用外观来创建站点。但是,在我探讨母版页的细节之前,了解一下目前开发人员如何在ASP和ASP.NET 1.x中生成模板化站点将是有用的。
在传统ASP中生成模板 利用ASP 3.0生成的许多站点都使用了模板的概念——一般是使用服务器端include (SSI)指令。SSI指令提供了在ASP页面内特定位置插入文件内容的能力。一种通用技术是使用SSI指令来引入常用的元素,如页脚、页眉和导航栏。图2演示了这种做法。
图2 ASP中的服务器端包含指令
尽管该技术在正确方向上迈出了一步,但它仍然意味着每个页面除了生成所有特定于页面的内容以外,还必须生成周围的布局元素和总体结构。许多站点采用的另一种更加具有模板特征的方法是:指定两个通用的包含文件(如pageStart.asp和pageEnd.asp),并且使这两个文件不仅包含通用的导航栏、页眉和页脚,还包含周围的布局元素和总体页面结构。留待各个页面添加的所有元素是它特有的内容。
该方法在整个站点的集中式模板中提供了许多开发人员需要的控制;但它也有几个缺点。首先,不支持用于编辑含有SSI指令的文件的设计器,因此设计人员必须实现合并的呈现效果以查看完整的页面外观,只是稍后需要将其重新分开。其次,包含机制过分简单,因为它只是在ASP分析程序进行服务器端计算之前将一个文件的内容插入到另一个文件中。这意味着在匹配HTML元素结束标记以及正确选择包含位置以避免错误呈现时,很容易出错。同时,很难自定义被包含文件的某些部分。在可以从MSDN Magazine Web站点下载的asp“technique2”示例中,您立即就可以注意到的一个缺陷是每个页面的标题都被有意保留为空。发生这种情况的原因在于:标题仅在顶级page_start包含文件中指定一次,并且没有什么巧妙的方法来让包含模板的页面指定应该在标题元素中出现的内容。最后,使用包含文件需要系统开销,因为它们增加了ASP脚本引擎缓存的总大小。
基于上述使用服务器端包含指令的简单方法,真正通用的模板化机制在理想情况下应该具有下列五个功能:
具有某种功能,使开发人员能够创建可以独立定义、并且可以在多个页面中重用的页面部分 能够为页面定义可供其他页面用作模板的“外壳” 页面能够改变继承的模板页面内部各种元素,例如更改标题元素 能够在页面内部以声明方式指定备用页面模板 能够使用与页面关联的模板实际查看该页面的呈现版本 将服务器端包含文件用于ASP 3.0可以满足上述第一个条件,并且可以在某种程度上满足第二个条件,但无法提供最后三个功能。
在ASP.NET 1.x中生成模板化站点 ASP.NET引入了一种全新的、基于对象的编程模型,从而有可能提供一种更新颖、更优雅的模板化机制。ASP.NET通过要呈现到客户端元素的完整层次化对象模型,向您提供了每个页面的服务器端表示形式,而不必依赖于ASP 3.0中比较低级的SSI机制。最恰当地反映SSI文件功能的功能是用户控件。用户控件提供了一种声明性方式,以便创建可以将其内容和行为插入其他页面任意位置的自定义控件,因此使用这些控件作为定义页面的可重用部分的通用方法是非常简单的,如图3所示。
图3 带有用户控件的可重用页面部分
除了充当简单内容占位符以外,用户控件还是能够公开属性、方法和事件功能的完整控件。与服务器端包含指令不同,您可以赋予用户控件能够影响其在页面上呈现的功能。例如,您可以在名为ShowBreadcrumbs的页眉控件上创建一个属性,以便能够根据需要选择性地打开或关闭面包屑功能。
但是,用户控件重用模型有一些局限和缺点。首先,许多开发人员发现,必须在将使用用户控件的每个页面中针对每个用户控件添加一条Register指令,因此非常麻烦。具有讽刺意味的是,许多ASP.NET开发人员已经转而使用SSI指令将Register指令集合导入他们的所有页面中,以便只用一条SSI指令替换多个代码行。然而,更为重要的是,没有任何一种内置方式可以使用用户控件来满足我在前面列出的有关开发模板化机制的五个条件中的任何一个(第一个条件除外,因为您可以创建可重用的页面部分)。
ASP.NET确实提供了一种更为丰富的对象模型,并且许多人已经建立了他们自己的模板化机制,以克服ASP.NET 1.x中缺少原生模板化机制的缺点。借助于一点儿创造性和一些聪明的编码,您可以生成通用的模板化机制。大多数实现都依赖于以下事实:您有机会在Page类为您自动构建的控件层次结构被发送回客户端之前操纵该控件层次结构。例如,一种技术是创建一个通用的、派生于Page的基类(该基类能够提取所生成的页面层次结构中的控件),动态加载充当页面模板的用户控件,然后将提取到的控件插入用户控件层次结构中的已知位置。该技术的完整实现包含在本文的代码下载资料中。
使用与此类似的技术,可以非常接近于达到真正有用的模板化机制所需要的全部五个条件。具有用于定义可重用页面部分的用户控件。具有一种页面模板机制,通过该机制,可以定义单个模板并将其应用于系统中任意数量的页面。使用该技术可以定义模板中的“可替换”元素,并且可以为每个页面指定备用模板。所缺少的一个功能就是设计器集成—如果没有Visual Studio .NET在识别这一模板化技术方面提供帮助,则要实现该功能确实是不可能的。
与此类似的自定义模板技术的另一个缺点是:没有一种完成这一工作的受到认可的方法,并且.NET Framework中没有支持这一技术的内置组件。这意味着每个站点所使用的模板化机制都可能完全不同。
尽管ASP.NET为您提供了极其灵活且更为强大的编程模型,但它仍然不具有用于模板化站点的内置机制。特别是,尽管某些开发人员已经通过巧妙地使用控件层次结构替换和用户控件生成了模板化机制,但他们仍需要完成额外的配置步骤,并且更为重要的是,他们不具有设计器支持。这时候,让我们走入ASP.NET 2.0中的母版页
母版页 ASP.NET 2.0中母版页的问世代表着Microsoft提供了第一个能够满足我在前面概述的全部条件的模板化机制。他们提供了站点级别的页面模板、一种用于进行细粒度内容替换的机制、对页面应该使用哪个模板的编程控制和声明性控制,或许最引人注目的是,他们提供了集成的设计器支持。从技术角度来说,母版页的实现方式与我所描述的ASP.NET 1.1中自定义模板机制的实现方式非常类似,但增加了来自Visual Studio 2005的设计器支持,并且它现在是受到认可和支持的通过可视化继承来构建模板化站点的方式,这一切使您最终拥有了一种完整的模板解决方案。
ASP.NET 2.0中母版页的实现包含两个概念元素:母版页和内容页。母版页充当内容页的模板,而内容页则提供内容以填充母版页中要求“填满”的部分。母版页在本质上是一个标准的ASP.NET页面,不同之处在于它使用扩展名.master以及指令(而不是使用)。该母版页文件充当其他页面的模板,因此通常它将包含顶级HTML元素、主窗体、页眉、页脚等等。在母版页内,您可以在希望内容页提供特定于页面的内容的位置上添加ContentPlaceHolder控件的实例,如图4 所示。
<!-- file: sitetemplate.master -->
<%@ master language="C#" %>
<html>
<head>
<title>
<asp:contentplaceholder runat="server" id="_titleContent">
Standard title
</asp:contentplaceholder>
</title>
</head>
<body>
<form runat="server">
<h2>Common header</h2>
<asp:contentplaceholder runat="server" id="_mainContent" />
<h2>Common footer</h2>
</form>
</body>
</html>
图4 添加占位符实例
相反,内容页只是使用masterpagefile属性在其页面指令中指定关联母版页的普通.aspx文件。这些页面必须仅包含Content控件的实例,因为它们的唯一用途就是为所继承的母版页模板提供内容。每个Content控件都必须映射到在所引用的母版页中定义的特定ContentPlaceHolder控件(该控件的内容将在呈现时插入到母版页的占位符中)。下面的内容页为图4 中所示的sitetemplate.master母版页提供内容:
<!-- file: default.aspx -->
<%@ page language="C#" masterpagefile="sitetemplate.master" %>
<asp:content contentplaceholderid="_titleContent" runat="server">
Main page
</asp:content>
<asp:content contentplaceholderid="_mainContent" runat="server">
This is the content for the default page.
</asp:content>
请注意,通过该机制,您可以指定要放置在母版页模板中非常具体的位置上的内容。刚刚显示的示例说明了如何通过在母版页的标题元素内提供一个ContentPlaceHolder控件,以及在内容页中提供一个具有匹配ContentPlaceHolderId的相应Content控件,来轻松地解决用模板生成唯一页面标题的微妙问题。该示例还阐明了母版页如何为占位符提供默认内容,因此如果内容页决定不为特定的占位符提供Content控件,则它将具有默认的呈现效果。
图5 在 ASP.NET 中使用母版页来呈现页
在母版页的基本结构就绪之后,现在我可以重新考察我在前面使用其他技术生成的模板化示例。请回想一下,该示例使用可重用的页面内容元素(ASP.NET中的用户控件)来定义页眉、导航栏和页脚。模板页使用这些元素来布置页面,而内容页则为页面提供内部内容。图5 使用母版页和内容页显示了该示例的呈现效果。
图6 针对母版页的设计支持
更为引人注目的事实是母版页可以被Visual Studio 2005中的设计器所支持,因此当您以可视方式编辑内容页时,它将以灰色显示所继承的母版页的内容,从而使您对页面的最终呈现效果一目了然。图6显示了使用母版页的延续示例在编辑隶属于我的母版页的内容页时所具有的外观。
实现细节 正如前面所提到的那样,母版页和内容页的实现方式与许多开发人员在ASP.NET 1.x中生成他们自己的自定义模板化机制时所采用的方法非常类似。特别是,MasterPage类派生于UserControl,因此继承了用户控件所提供的相同的通用容器功能。与我在前面所讨论的自定义实现极为类似,母版页所定义的模板被插入到为请求的页面所生成的控件层次结构中。这一插入操作刚好发生在Page类的Init事件之前,以便所有控件都可以在Init(此时通常会对控件执行编程操纵)之前准备就绪。
对母版页的控件层次结构和页面的控件层次结构执行实际合并的方式与我在前面概述的方法类似。母版页的顶级控件(该控件将与包含该母版页的文件具有相同的名称)将作为根控件插入到新的页面层次结构中。然后,将页面中每个Content控件的内容作为子控件集合插入到相应的ContentPlaceHolder控件的下方。图7 显示了一个具有关联母版页的示例内容页,以及所得到的合并控件层次结构(它是在页面处理过程中于Init事件之前创建的)。请注意用不同颜色标记的从属关系,它们表明了所得到的层次结构中每个控件的来历。
图7 母版页和内容页的混合层次
该实现的含意之一是:母版页本身只是页面类层次结构中的另一个控件,并且您可以直接对母版页执行您习惯于对控件执行的任何任务。与任何给定页面相关联的当前母版页始终可以通过Master属性访问器使用。作为与母版页交互的示例,您可以在图7 中所示的default.aspx页面内部添加代码,以便用编程方式访问由母版页隐式添加的HtmlForm,如下面的代码片段所示:
void Page_Load(object sender, EventArgs e)
{
HtmlForm f = (HtmlForm)Master.FindControl("_theForm");
if (f != null)
{
// use f here...
}
}
除了访问母版页以外,您还可以在运行时更改内容页的母版页从属关系。MasterPageFile属性作为Page类上的公共属性公开,并且可以在任何页面的代码内部进行修改。对该属性的任何修改都必须在Page类的新PreInit事件的处理程序中进行才能生效,因为母版页的创建以及与母版页的合并都会在激发Init事件之前发生。可以将下面的OnPreInit方法重写添加到任何使用母版页的Page类,以便用编程方式更改母版页从属关系:
protected override void OnPreInit(EventArgs e)
{
this.MasterPageFile = "othertemplate.master";
base.OnPreInit(e);
}
详细用法 当您开始在站点设计中使用母版页时,如果您从未使用过站点级别模板化机制,则可能会遇到一些以前没有发生过的问题。第一个问题与所引用资源(如图像或样式表)中的相对路径有关。当您创建母版页时,请务必记住:在计算相对路径时作为起始位置的目录很可能会基于所访问的页面而改变。请考虑一下图8中所示的站点的目录结构。
图8 站点目录结构
如果您要从masterpages目录的Site.master中添加对images目录中Check.gif图像的引用,则可能会倾向于添加一个简单的图像元素,如下所示:
<PRE class="clsCode">
<img src="../images/check.gif" />
遗憾的是,只有当页面所在的相对目录位置类似于作为母版页的图像所在的位置时(如page1.aspx),这种方法才有效。任何其他页面(如default.aspx)都无法正确解析相对路径。该问题的一种解决方案是使用ASP.NET中的根路径引用语法,并且确保从服务器端控件(这是该语法能够生效的唯一位置)进行所有相对引用。我在前面提到的图像引用将变为:
<img src="../images/check.gif" />
另一种选择是依赖以下事实:服务器端控件中的相对路径引用是相对于它们被放置到的母版页进行计算的。这意味着可以将图像引用更改为以下形式:
<img src="~/images/check.gif" runat="server" />
引用母版页的页面中的服务器端路径引用仍然相对于页面本身,因此您不必更改可能已经就绪的任何技术来处理页面中的相对引用。
ASP.NET开发人员在第一次遇到母版页时提出的另一个常见请求是:他们希望能够要求应用程序中的所有页面都是引用特定母版页的内容页。尽管不存在这种“必用”属性,但您可以通过向web.config文件中添加一个pages元素以指定一个通用的母版页,来指定一个在默认情况下用于应用程序所有页面的母版页,如下面的代码片段所示:
<!-- file: web.config -->
<configuration>
<pages masterPageFile="~/sitetemplate.master" />
</configuration>
像在应用程序级别指定的任何设置一样,单个页面可以选择重写默认的masterPageFile属性,但如果将该语句添加到您的配置文件中,则可以保证不会意外地将任何不具有关联母版页的页面添加到您的应用程序中。
最后,您可能会发现拥有“元”母版页(即一组母版页的母版页)会很有用。母版页支持任意深度的嵌套,因此您可以创建您认为对应用程序有意义的任何级别的母版页。就像具有母版页的页面一样,具有母版页的母版页在顶级必须仅包含ContentPlaceHolder控件。在这些ContentPlaceHolder控件内部,母版页可以添加其他ContentPlaceHolder控件以供实际页面使用。请注意,引用本身带有母版页的母版页的页面只能为直接父级母版页上的ContentPlaceHolder控件提供内容元素。没有办法在特定页面向上两级或更多级的母版页上直接填充占位符。作为一个示例,请看一下图9 中的代码所示的母版页定义(metatemplate.master)。
<%@ master %>
<html>
<head>
<title>
<asp:contentplaceholder
runat="server"
id="_titleContent"
>
Standard title
</asp:contentplaceholder>
</title>
</head>
<body>
<form id="_theForm" runat="server">
<h2>Header</h2>
<asp:contentplaceholder runat="server" id="_mainContent" />
<h2>Footer</h2>
</form>
</body>
</html>
图9 母版页定义
现在,您可以定义另一个母版页,让它指定上述母版页作为它的母版页,并且为父级母版页(sitetemplate.master)中的每个ContentPlaceHolder控件提供内容元素,如图10 所示。
<%@ master masterpagefile="~/metatemplate.master" %>
<asp:content
runat="server"
contentplaceholderid="_titleContent"
>
<asp:contentplaceholder runat="server" id="_title">
Default title
</asp:contentplaceholder>
</asp:content>
<asp:content
runat="server"
contentplaceholderid="_mainContent"
>
<table>
<tr>
<td>
<asp:contentplaceholder
id="_leftContent"
runat="server"
/>
</td>
<td>
<asp:contentplaceholder
id="_rightContent"
runat="server"
/>
</td>
</tr>
</table>
</asp:content>
图10 定义一个母版页
请注意,为了授予页面对父级母版页中_titleContent占位符的访问权限,我必须声明一个Content控件,并在其中嵌套一个新的ContentPlaceHolder控件,以授予页面对父级母版页中该位置的访问权限。
小结 在ASP.NET 2.0中增加母版页(人们期盼已久的站点级别模板功能)代表了一个新时代的到来,即Web开发人员将能够更加轻松地创建Web站点。开发人员将不必再求助于ASP中笨拙的服务器端包含指令或ASP.NET中复杂的控件层次结构操纵技术。现在,创建可供您的所有其他Web页作为基础的页面模板就像设计任何其他普通类型的Web页一样容易。我敢打赌您已经迫不及待地想要立刻行动了。
作者简介:Fritz Onion是一位专门研究ASP.NET的独立顾问、作者和培训讲师。他著有Essential ASP.NET (Addison Wesley, 2003)一书,并且正在着手撰写有关ASP.NET 2.0的第二版。他为DevelopMentor编写和讲授课程,同时还经常在行业会议上发表演讲。