第14章 ASP.NET母版页
在标准化的Web程序页面布局设计中,我们面临的首要问题就是对这些通用的、重复的页面元素的处理,如网站的标题、网站的导航菜单与网站的版权声明等。通常,不仅需要在设计中避免这些通用元素的重复设计,而且还需要保证这些通用元素在每个页面里都出现在相同的位置。
解决这一问题的关键在于是否能够创建一个可以重复应用到整个网站的、简单而灵活的布局。在这里,有三个方法可以选择:
1)用户控件:前面已经讲过,用户控件允许你创建一些可重复应用的小页面。但它却解决不了页面的标准布局问题,因为它没办法保证用户控件在网站的所有页面都被放到相同的位置。
2)HTML框架:框架是在一个浏览器窗口中允许同时显示多个页面的HTML的基本工具。但它也有一个致命的缺点,它里面的每个页面都必须单独地请求服务器资源,而这些页面不得不完全相互独立。因此,一个框架里的页面很难和其他框架里的页面进行交互。
3)母版页:其实,早在ASP.NET 2.0中就提供了母版页技术,它的出现专门用于标准化Web页面布局。母版页是一个页面模板,它定义了固定的内容并声明了Web页面里将要用自定义的内容插入的部分。如果在整个网站中使用同一个母版页,就可以确保获得同样的布局效果。最妙的是,应用母版页后,如果修改了它的定义,所有使用它的页面会自动跟着变化。
14.1 母版页基础
为了能够满足标准化Web页面布局,ASP.NET中定义了两种新的页面类型:母版页和内容页。母版页是一个页面模板,它保存网站结构的一个页面。该文件通过.master文件扩展名指定,并且通过内容页面的@Page指令的MasterPageFile属性导入到内容页面。它们可以提供站点中所有页面都能使用的模板。实际上,它们并不保存单个页面的内容甚至页面的样式定义,而只提供站点外观的蓝图,然后将该模板与分离的CSS文件(如果合适)中的样式规则集连接起来。
和普通的ASP.NET Web页面一样,母版页中可以包含任何HTML、Web控件甚至代码的组合。此外,母版页还可以包含内容占位符—定义的可修改区域。每个内容页引用一个母版页并获得它的布局和内容。此外,内容页可以在任意的占位符里加入页面特定的内容。换句话说,内容页将母版页没有定义的、缺失了的内容填入母版页。
14.1.1 创建简单的母版页
要在项目中添加一个母版页的操作方法很简单,与添加Web窗体一样,选中项目右击鼠标,执行“Add”|“New Items”命令,会弹出一个“Add New Item”对话框,如图14-1所示。
图 14-1 选择母版页模板
在“Add New Item”对话框中选择“Master Page”模板,并在“Name”文本框中输入母版页文件名称。注意,母版页文件名称是以“.master”后缀名结尾的。然后单击“Add”按钮,就可以在项目中看见添加的母版页。默认的母版页会自动生成部分代码,如代码清单14-1所示。
代码清单14-1默认的Test.Master文件
<%@Master Language="C#"AutoEventWireup="true"
CodeBehind="Test.master.cs"Inherits="_14_1.Test"%>
<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<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>
<asp:ContentPlaceHolder ID="ContentPlaceHolder1"
runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
在代码清单14-1中,母版页文件的第一行就是使用@Master指令。@Master指令非常类似于@Page指令,但@Master指令用于master页面(.master)。如下所示:
<%@Master Language="C#"AutoEventWireup="true"
CodeBehind="Test.master.cs"Inherits="_14_1.Test"%>
同时,会发现默认的母版页有两个ContentPlaceHolder控件。第一个ContentPlaceHolder控件定义在<head>区域,它让内容页面能够增加页面元数据库,如搜索关键字和样式表链接等;第二个ContentPlaceHolder控件定义在<body>区域,它代表页面显示的内容。它以一个轮廓不明显的方框的形式出现在页面上。如果单击它的内部或把鼠标停留在它上方,ContentPlaceHolder的名字就会出现在提示里。要创建更加复杂的页面布局,可以添加其他标记以及ContentPlaceHolder控件。
其实,通过代码清单14-1所示的结果,可以发现母版页与普通的Web窗体大致存在着两处区别:
1)Web窗体都是以@Page指令开始,页面后缀名为“.aspx”;而母版页则是以@Master指令开始的,页面后缀名为“.master”。
2)母版页可以使用ContentPlaceHolder控件,ContentPlaceHolder控件是内容页可以插入内容的页面部分;Web窗体则不能够使用ContentPlaceHolder控件。
到现在为止,已经大致地介绍了母版页的基础内容。为了能够加深读者的理解,下面就来在上面创建的默认母版页中添加一些简单的代码,如代码清单14-2所示。
代码清单14-2 Test.Master
<%@Master Language="C#"AutoEventWireup="true"
CodeBehind="Test.master.cs"Inherits="_14_1.Test"%>
<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1"runat="server">
<div style="background-color:#cccccc;
height:30px;text-align:left;">
<asp:ContentPlaceHolder ID="Top"runat="server">
</asp:ContentPlaceHolder>
</div>
<div style="text-align:left;">
<asp:ContentPlaceHolder ID="Main"runat="server">
</asp:ContentPlaceHolder>
</div>
<div style="height:30px;text-align:center;">
Copyright(c)2010
</div>
</form>
</body>
</html>
在代码清单14-2中,添加了两个ContentPlaceHolder控件。其中,控件Top用于内容页插入导航菜单,控件Main则应用于内容页插入页面主内容。设计效果如图14-2所示。
值得注意的是,母版页不能够作为单独的页面直接运行。因此,只能够在设计器里查看它的设计效果,而不能够像普通的Web窗体那样运行起来进行浏览。要使用母版页,必须创建一个关联的内容页。
图 14-2 母版页设计效果
14.1.2 使用简单的内容页
创建母版页的内容页的方法很简单,与创建普通的Web页面一样,即选中项目右击鼠标,执行“Add”|“New Items”命令,在弹出的“Add New Item”对话框中选择“Web Form using Master Page”模板,如图14-3所示。
图 14-3 “Add New Item”对话框
在Name文本框中填写内容页名称,然后单击“Add”按钮,会看见一个如图14-4所示的“SelectaMaster Page”窗体。
在该窗体里面,可以选择相应的母版页,然后单击“OK”按钮便为项目创建了一个内容页。默认情况下,内容页代码如下所示:
<%@Page Title=""Language="C#"MasterPageFile="~/Test.Master"
AutoEventWireup="true"CodeBehind="Test.aspx.cs"
Inherits="_14_1.Test1"%>
<asp:Content ID="Content1"ContentPlaceHolderID="Top"
runat="server">
</asp:Content>
<asp:Content ID="Content2"ContentPlaceHolderID="Main"
runat="server">
</asp:Content>
图 14-4 “SelectaMaster Page”窗体
在上面的代码中,@Page指令的MasterPageFile是一个非常重要的属性,它指定了要使用的母版页的文件名称。这里的MasterPageFile属性以路径“~/”开头,即它指定网站的根文件夹。如果只指定文件名,ASP.NET会为母版页检查预定义的子文件夹(叫做MasterPages)。如果还没有创建这个文件夹或者母版页不在那里,它接下来会检查Web的根文件夹。
当然,只设置@Page指令的MasterPageFile属性还不足以把普通的页面转变成为内容页。除了定义内容页@Page指令的MasterPageFile属性之外,还必须为内容页定义要插入的一个或多个ContentPlaceHolder控件的内容,并编写所有这些控件需要的功能代码。要为ContentPlaceHolder控件提供内容,就要在内容页里面用到Content控件,如语句“<asp:Content ID="Content1"ContentPlaceHolderID="Top"runat="server"></asp:Content>”。母版页的ContentPlaceHolder控件和内容页Content控件具有一对一的关系,这种对应关系如图14-5所示。
如图14-5所示,对于母版页里的每个ContentPlaceHolder控件,内容页会提供一个对应的Content控件,除非不准备为那个区域提供任何内容。ASP.NET通过匹配母版页的ContentPlaceHolder控件的ID以及对应内容页的Content控件的ContentPlaceHolderID属性来把内容页的Content控件关联到适当的母版页的ContentPlaceHolder控件上。如果在内容页的Content控件里引用了一个不存在的母版页的ContentPlaceHolder控件的ID,那么在运行时就会得到一个错误的报告。
注意 内容页没有定义页面,因为母版页已经提供了外壳。因此,如果试图在内容页面加入<html>、<head>和<body>之类的元素,则会产生一个错误,因为它们已经由母版页定义了。
图 14-5 母版页和内容页的对应关系
其实,通过上面的“Web Form using Master Page”模板来添加内容页,系统会自动根据母版页的ContentPlaceHolder控件生成相应的Content控件,而无须手动在内容页里添加Content控件。创建好内容页之后,就可以直接在内容页Content控件里面为母版页的ContentPlaceHolder控件添加相关的页面内容,如代码清单14-3所示。
代码清单14-3 Test.aspx
<%@Page Title="Content Page"Language="C#"
MasterPageFile="~/Test.Master"AutoEventWireup="true"
CodeBehind="Test.aspx.cs"Inherits="_14_1.Test1"%>
<asp:Content ID="Content1"ContentPlaceHolderID="Top"
runat="server">
<a href="Test.aspx">首页</a>|
    
<a href="http://www.comesns.com/aspnet/">ASP.NET4.0程序设计</a>|
    
<a href="http://www.comesns.com/csharp/">易学C#</a>
</asp:Content>
<asp:Content ID="Content2"ContentPlaceHolderID="Main"
runat="server">
ASP.NET 4.0相对于ASP.NET 3.5来说,算是一个功能性增强版本,它主要在下列四方面增强了许多
新的功能,这些新功能我们将会在后面的章节详细地阐述:
<br/>
在ASP.NET AJAX方面:
<br/>
推出了ASP.NET AJAX4.0版本,增加了许多新控件和新功能,同时支持来自服务器端的JSON数据非常灵
活地显示为HTML,而且框架本身也经过了重构。
<br/>
在Web窗体的开发方面:
<br/>
(1)将主要的配置元素都移到了machine.config配置文件里,因而大大地简化了Web.config文件。同
时,Web.config文件使用了多文件配置方案。
<br/>
(2)增强了对ViewState的控制,View State默认为false。
<br/>
(3)对空间增加了ClientIDMode属性的支持,它可以控制客户端ID,大大减少了客户端工作量。
<br/>
……
</asp:Content>
在代码清单14-3中,首先在@Page指令中设置MasterPageFile属性和Title属性。Title属性允许为内容页指定标题,从而覆盖母版页中的标题;其次,在Content1控件里,也就是母版页的Top控件里面设置页面的连接导航菜单;最后,在Content2控件里,也就是母版页的Main控件里面设置一些文字内容。运行结果如图14-6所示。
图 14-6 运行结果
从上面的例子中可以看出,与普通的Web页面相比,内容页非常整洁,因为它不包含任何母版页定义的细节。而且,它使网站更新变得简单—你所要做的只是修改母版页,只要保持相同的ContentPlaceHolder控件,现有的内容页就会很好地工作,并且会自行适应任何指定的新布局。
为了更好地理解母版页是如何工作的,可以通过跟踪的方式来查看内容页的运行情况,即在Page指令里加入Trace属性((Tace="true")。借助这种方式,可以检查控件的层次。你将会发现,ASP.NET首先为母版页创建控件对象(包括ContentPlaceHolder控件),它充当一个容器;然后把内容页的控件加入ContentPlaceHolder控件。
如果需要动态配置母版页或内容页,可以响应任意一个类中的Page.Load事件。有时可以同时在母版页和内容页中使用初始化代码。这种情况下,理解每个事件发生的顺序就很重要:
1)ASP. NET创建母版页控件。
2)添加内容页的子控件。
3)触发母版页的Page.Init事件。
4)内容页的Page.Init事件。
对于Page.Load事件,也可以执行相同的步骤。如果有冲突,在内容页进行的自定义(如修改页面标题)会覆盖在母版页相同阶段所做的变化。
14.1.3 ContentPlaceHolder控件里默认内容
在母版页里面定义ContentPlaceHolder控件时,还可以定义相关的默认的内容。这些默认的内容在内容页里面没有提供相应的Content控件时才会使用。定义默认内容非常简单,只需要在母版页的ContentPlaceHolder控件中放入所需要的页面代码即可,这些页面代码可以是HTML标签或者Web服务器控件等。如下面的代码所示:
<asp:ContentPlaceHolder ID="Top"runat="server">
<a href="Test.aspx">首页</a>|
    
<a href="http://www.comesns.com/aspnet/">
ASP.NET4.0程序设计</a>|
    
<a href="http://www.comesns.com/csharp/">易学C#</a>
</asp:ContentPlaceHolder>
在这里需要提醒大家的是,如果要在内容里面使用这个ContentPlaceHolder控件的默认内容,那么就必须得在内容页里面删除ContentPlaceHolder控件所对应的<Content>标签,否则内容页里<Content>标签会自动覆盖默认内容。
注意 内容页不能只使用默认内容的一部分或者只编辑某一部分,这样做也是不可能的,因为默认内容保存在母版页里而不是内容页中。所以,必须决定是按原样使用默认内容,还是在内容页中使用新的内容完全替换这些默认内容。
14.1.4 相对路径的处理
在对母版页的设计中,相对路径的处理经常是一件让人头痛的事情。如果使用的是静态文字,这一问题不会困扰你。不过,如果需要在母版页中添加图片和链接,根据所使用的HTML标签或者ASP.NET服务器控件的不同,相对路径就会有不同的解析方式。这时,相对路径的问题就可能发生。
为了能够更好地了解这种相对路径的不同的解析方式,下面将通过一个示例来描述它。示例项目结构如图14-7所示。
图 14-7 相对路径处理的示例项目
如图14-7所示,为了表现良好的项目层次性,我们将母版页和内容页分别放在不同的目录中,即将母版页和图片资源放入MasterPages文件夹中,将内容页放入Pages文件夹中。其实,把母版页和内容页分放到不同的目录,这是大型网站推荐使用的最佳实践。实际上,微软也建议在专门的文件夹里保存所有的母版页。
创建好项目之后,接下来分别以三种方式向母版页Test.Master中添加相关的图片资源,即image1采用了Web服务器控件、img2采用了HTML服务器控件、img3采用了HTML标签。如代码清单14-4所示。
代码清单14-4 Test.Master
<%@Master Language="C#"AutoEventWireup="true"
CodeBehind="Test.master.cs"
Inherits="_14_2.MasterPages.Test"%>
<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1"runat="server">
<title></title>
</head>
<body>
<form id="form1"runat="server">
<div>
<asp:ContentPlaceHolder ID="Images"runat="server">
<asp:Image ID="image1"ImageUrl="1.gif"runat="Server"/>
<img id="img2"src="2.gif"alt="img2"runat="server"/>
<img id="img3"src="3.gif"alt="img3"/>
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
为了查看母版页的三种方式的图片资源显示结果,需要继续在Pages文件下创建一个内容页Test.aspx。Test.aspx页面很简单,如下面的代码所示:
<%@Page Title=""Language="C#"
MasterPageFile="~/MasterPages/Test.Master"
AutoEventWireup="true"CodeBehind="Test.aspx.cs"
Inherits="_14_2.Pages.Test"%>
运行Test.aspx页面,结果如图14-8所示。
图 14-8 Test.aspx页面运行结果
在图14-8中,我们发现image1和img2控件都能够显示相应的图片,img3标签却显示错误。而在母版页中,给image1、img2、img3所赋给的图片地址完全相同,为什么img3标签却显示错误呢?
带着这个问题,继续查看页面结果源代码。它们所生成的页面代码如下所示:
<div>
<img id="Images_image1"
src="../MasterPages/1.gif"/>
<img src="../MasterPages/2.gif"
id="Images_img2"alt="img2"/>
<img id="img3"src="3.gif"alt="img3"/>
</div>
在上面的页面代码中,我们发现image1和img2所生成的地址都是“../MasterPages/1.gif”,即路径被解释成相对于Pages文件的路径,所以它们能够正确显示;而img3所生成的地址为“3.gif”。这样的问题之所以会发生,是因为<img>标签是普通的HTML标签。所以,ASP.NET不会接触到它。遗憾的是,当ASP.NET创建内容页的时候,这个标签就不合适了。相同的问题在以下两种情况下还会出现:
1)向其他页面提供相对链接的<a>标签。
2)用来把母版页链接到样式表的<link>元素。
要解决如img3这样的路径问题,可以通过如下三种方式来进行:
1)可以预先在母版页里把图片路径写成相对于内容页的地址。不过这会带来混淆,限制母版页使用的范围,并且产生在设计环境里不正确显示母版页的负面效应。
2)把HTML标签变成服务器端控件,这样ASP.NET就会修复这个错误。如下面的代码所示:
<img id="img3"src="3.gif"
alt="img3"runat="server"/>
这样,ASP.NET就会根据这一信息创建一个HtmlImage服务器控件。这个对象在母版页的Page对象被实例化后创建,此时,ASP.NET把所有路径解释为相对于母版页的位置。可以使用同样的技术来修复<a>标签,它提供其他页面的相对链接。
当然,还可以使用根路径语法,并用“~/”字符作为路径的开头。如下面的代码所示:
<img id="img3"src="~/MasterPages/3.gif"
alt="img3"runat="server"/>
但值得注意的是,这种根路径语法只对服务器端控件有效。
3)在母版页中使用方法来重新解析相对路径,如下面的代码所示:
<img id="img3"
src="<%=Page.ResolveUrl("~/MasterPages/3.gif")%>"
alt="img3"/>
14.1.5 div+css方式布局母版页
前面已经说过,div+css的布局方式是Web站点布局的标准,尤其是在XHTML网站设计标准中,将不再使用表格定位技术,而是采用div+css的方式实现各种定位。与普通Web页面一样,使用div+css的布局方式来设计母版页同样简单。
为了能够更好地了解这种布局方式,下面仍然使用前面的经典三行两列布局的示例来阐述如何以div+css方式来布局母版页。示例项目结构如图14-9所示。
图 14-9 div+css布局的母版页示例
在图14-9中,分别创建了三个文件夹来管理这三种不同的文件,即MasterPages文件夹用于管理母版页文件,Pages文件夹用于管理内容页文件,Style文件夹用于管理CSS文件。
下面首先来定义一个CSS文件CommonStyle.css,该CSS文件用于控制母版页中的div定位,如代码清单14-5所示。
代码清单14-5 CommonStyle.css
*
{
margin:0;
padding:0;
}
header
{
width:776px;
height:50px;
margin:0 auto;
background:#06f;
}
main
{
width:776px;
margin:0 auto;
}
main#sidebar
{
width:200px;
float:left;
background:#f93;
}
main#containe
{
width:576px;
float:right;
background:#dceafc;
}
footer
{
width:776px;
height:40px;
margin:0 auto;
background:#666;
}
clearfloat
{
clear:both;
height:1px;
overflow:hidden;
margin-top:-1px;
}
定义好CSS文件之后,接下来就是定义母版页。同普通的Web页面一样,在母版页中也必须先使用语句“<link href="/Style/CommonStyle.css"rel="Stylesheet"type="text/css"/>”将CSS文件CommonStyle.css引入母版页。然后再在母版页里面布局好相关的div标签,在div标签里面引用相关的样式。
布局好div标签之后,就需要将ContentPlaceHolder控件放到不同的div标签元素里,以便在内容页中设置具体内容。详细的母版页定义如代码清单14-6所示。
代码清单14-6 Test.Master
<%@Master Language="C#"AutoEventWireup="true"
CodeBehind="Test.master.cs"
Inherits="_14_3.MasterPages.Test"%>
<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<link href="/Style/CommonStyle.css"rel="Stylesheet"
type="text/css"/>
</head>
<body>
<form id="form1"runat="server">
<div id="header">
<asp:ContentPlaceHolder ID="Header"runat="server">
</asp:ContentPlaceHolder>
</div>
<div id="main">
<div id="sidebar">
<asp:ContentPlaceHolder ID="Sidebar"runat="server">
</asp:ContentPlaceHolder>
</div>
<div id="containe">
<asp:ContentPlaceHolder ID="Containe"runat="server">
</asp:ContentPlaceHolder>
</div>
<div id="clearfloat">
</div>
</div>
<div id="footer">
<asp:ContentPlaceHolder ID="Footer"runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
在内容页里,只需要给相关ContentPlaceHolder控件添加上需要的内容就可以了,如代码清单14-7所示。
代码清单14-7 Test.aspx
<%@Page Title=""Language="C#"
MasterPageFile="~/MasterPages/Test.Master"
AutoEventWireup="true"CodeBehind="Test.aspx.cs"
Inherits="_14_3.Pages.Test"%>
<asp:Content ID="Content1"
ContentPlaceHolderID="Header"runat="server">
Header
</asp:Content>
<asp:Content ID="Content2"
ContentPlaceHolderID="Sidebar"runat="server">
Sidebar
</asp:Content>
<asp:Content ID="Content3"
ContentPlaceHolderID="Containe"runat="server">
Containe
</asp:Content>
<asp:Content ID="Content4"
ContentPlaceHolderID="Footer"runat="server">
Footer
</asp:Content>
运行代码清单14-7的内容页,结果如图14-10所示。
14.1.6 通过Web.config文件全局设置母版页
如果创建一个Web应用程序,它只有一个母版页,那么为站点内的每个页面都设置母版页似乎有点过分。因此,还可以借助Web.config文件一次性对整个网站的所有页面应用母版页。所要做的只是像下面的代码这样,在Web.config文件里面加入一个<pages>节点,并设置<pages>节点的masterPageFile属性:
<system.web>
<pages masterPageFile="~/MasterPages/Test.Master"/>
</system.web>
图 14-10 div+css布局的母版页示例运行结果
值得注意的是,这种通过Web.config文件全局设置母版页的方式不太灵活,任何违背了规则(例如,包含根<html>标签或者定义了一个不对应ContentPlaceHolder的内容区域)的Web页面都会自动中断。即使通过Web.config文件应用了母版页,还是不能保证页面不会通过设置Page指令的MasterPageFile特性覆盖设置。如果MasterPageFile特性被设置为一个空字符串,无论web.config文件里定义了什么,页面根本不会有任何母版页。