以编程方式指定母版页 (C#)
作者 :斯科特·米切尔
查看通过 PreInit 事件处理程序以编程方式设置内容页的母版页。
简介
由于“使用母版页创建网站范围布局”中的首例示例,所有内容页都通过MasterPageFile
指令中的@Page
属性以声明方式引用了母版页。 例如,以下 @Page
指令将内容页链接到母版页 Site.master
:
<%@ Page Language="C#" MasterPageFile="~/Site.master" ... %>
命名空间中的System.Web.UI
类包括一个MasterPageFile
属性,该属性返回内容页母版页的路径;它是由指令设置的属性@Page
。Page
此属性还可用于以编程方式指定内容页的母版页。 如果要根据外部因素(例如访问页面的用户)动态分配母版页,此方法非常有用。
在本教程中,我们将第二个母版页添加到网站,并动态确定在运行时要使用的母版页。
步骤 1:查看页面生命周期
每当请求到达 Web 服务器以获取内容页的 ASP.NET 页面时,ASP.NET 引擎必须将页面的内容控件融合到母版页的相应 ContentPlaceHolder 控件中。 此融合创建一个控件层次结构,然后可以继续执行典型的页面生命周期。
图 1 说明了这种融合。 图 1 中的步骤 1 显示了初始内容和母版页控件层次结构。 在 PreInit 阶段的尾端,页面中的内容控件将添加到母版页中的相应 ContentPlaceHolders(步骤 2)。 在此融合之后,母版页充当融合控件层次结构的根。 然后将此融合控件层次结构添加到页面中,以生成最终的控制层次结构(步骤 3)。 净结果是页面的控件层次结构包括融合控件层次结构。
图 01:母版页和内容页的控件层次结构在 PreInit 阶段合并在一起(单击可查看全尺寸图像)
步骤 2:从代码设置MasterPageFile
属性
此融合中的母版页部件取决于对象的MasterPageFile
属性的值Page
。 MasterPageFile
在@Page
指令中设置属性具有在初始化阶段分配 Page
's MasterPageFile
属性的净效果,这是页面生命周期的第一个阶段。 或者,我们可以以编程方式设置此属性。 但是,必须在图 1 中的融合之前设置此属性。
在 PreInit 阶段开始时, Page
对象将引发其 PreInit
事件 并调用其 OnPreInit
方法。 若要以编程方式设置母版页,我们可以为 PreInit
事件创建事件处理程序或重写 OnPreInit
方法。 让我们看看这两种方法。
首先 Default.aspx.cs
,打开网站主页的代码隐藏类文件。 通过键入以下代码为页面 PreInit
的事件添加事件处理程序:
protected void Page_PreInit(object sender, EventArgs e)
{
}
在这里,我们可以设置属性 MasterPageFile
。 更新代码,以便将值“~/Site.master”分配给 MasterPageFile
属性。
protected void Page_PreInit(object sender, EventArgs e)
{
this.MasterPageFile = "~/Site.master";
}
如果设置断点并开始调试,你将看到每当 Default.aspx
访问页面或每当有回发到此页面时, Page_PreInit
事件处理程序将执行 MasterPageFile
该属性,并将该属性分配给“~/Site.master”。
或者,可以重写 Page
类 OnPreInit
的方法,并在其中设置 MasterPageFile
属性。 在此示例中,我们不要在特定页面中设置母版页,而是从 BasePage
中设置母版页。 回想一下,我们在母版页教程的“指定标题”、“元标记”和其他 HTML 标头中重新创建了一个自定义基页类(BasePage
)。 当前 BasePage
重写 Page
类 OnLoadComplete
的方法,在该方法中,它将基于网站地图数据设置页面 Title
的属性。 让我们进行更新 BasePage
以替代 OnPreInit
方法以编程方式指定母版页。
protected override void OnPreInit(EventArgs e)
{
this.MasterPageFile = "~/Site.master";
base.OnPreInit(e);
}
由于所有内容页都派生自 BasePage
,因此所有这些页面现在都以编程方式分配了母版页。 此时, PreInit
事件处理程序 Default.aspx.cs
是多余的;可以随意将其删除。
指令@Page
是什么?
可能有点令人困惑的是,内容页 MasterPageFile
的属性现在在两个位置指定:以编程方式在 BasePage
类 OnPreInit
的方法中,以及通过 MasterPageFile
每个内容页 @Page
指令中的属性。
页面生命周期中的第一个阶段是初始化阶段。 在此阶段,Page
为对象的MasterPageFile
属性分配指令中的@Page
属性值MasterPageFile
(如果提供)。 PreInit 阶段遵循初始化阶段,在这里,我们以编程方式设置 Page
对象的 MasterPageFile
属性,从而覆盖从 @Page
指令分配的值。 由于我们正在以编程方式设置Page
对象的MasterPageFile
属性,因此我们可以从@Page
指令中删除MasterPageFile
该属性,而不会影响最终用户的体验。 若要说服自己,请继续从指令Default.aspx
中删除MasterPageFile
该属性@Page
,然后通过浏览器访问页面。 正如预期的那样,输出与删除属性之前相同。
属性是通过 MasterPageFile
指令设置 @Page
的,还是以编程方式设置的,与最终用户的体验无关。 但是, MasterPageFile
在设计时,Visual Studio 使用指令中的 @Page
属性在设计器中生成 WYSIWYG 视图。 如果在 Visual Studio 中返回 Default.aspx
并导航到设计器,则会看到消息“母版页错误:页面具有需要母版页引用但未指定任何控件”(请参阅图 2)。
简言之,需要在指令中@Page
保留该MasterPageFile
属性才能在 Visual Studio 中享受丰富的设计时体验。
@Page指令的 MasterPageFile 属性来呈现设计视图“/>
图 02:Visual Studio 使用 @Page
指令的属性 MasterPageFile
呈现设计视图(单击以查看全尺寸图像)
步骤 3:创建备用母版页
由于内容页的母版页可以在运行时以编程方式设置,因此可以根据某些外部条件动态加载特定母版页。 在网站布局需要因用户而异的情况下,此功能非常有用。 例如,博客引擎 Web 应用程序可能允许用户为其博客选择布局,其中每个布局都与不同的母版页相关联。 在运行时,当访问者查看用户的博客时,Web 应用程序需要确定博客的布局,并将相应的母版页与内容页动态关联。
让我们看看如何在运行时根据一些外部条件动态加载母版页。 我们的网站目前只包含一个母版页(Site.master
)。 我们需要另一个母版页来说明在运行时选择母版页。 此步骤重点介绍如何创建和配置新的母版页。 步骤 4 查看确定在运行时要使用的母版页。
在名为 <AlternateStyles.css
“.
图 03:向网站添加另一个母版页和 CSS 文件(单击可查看全尺寸图像)
我设计了 Alternate.master
母版页,使标题显示在页面顶部、居中和海军背景上。 我分配了左列,并在 ContentPlaceHolder 控件下 MainContent
移动了该内容,该控件现在跨越页面的整个宽度。 此外,我取消了无序课程列表,并将其替换为上面的 MainContent
水平列表。 我还更新了母版页使用的字体和颜色(以及扩展后的内容页)。 图 4 显示 Default.aspx
使用 Alternate.master
母版页时。
注意
ASP.NET 包括定义 主题的功能。 主题是图像、CSS 文件和样式相关的 Web 控件属性设置的集合,可在运行时应用于页面。 如果网站的布局仅在显示的图像及其 CSS 规则中不同,则主题是一种使用主题的方式。 如果布局具有更大的差异,例如使用不同的 Web 控件或具有完全不同的布局,则需要使用单独的母版页。 有关主题的详细信息,请参阅本教程末尾的“进一步阅读”部分。
图 04:我们的内容页面现在可以使用新的外观(单击以查看全尺寸图像)
当母版页和内容页的标记融合时, MasterPage
类会检查以确保内容页中的每个内容控件引用母版页中的 ContentPlaceHolder。 如果找到引用不存在的 ContentPlaceHolder 的内容控件,则会引发异常。 换句话说,必须向内容页分配母版页,该母版页具有内容页中每个内容控件的 ContentPlaceHolder。
母 Site.master
版页包括四个 ContentPlaceHolder 控件:
head
MainContent
QuickLoginUI
LeftColumnContent
我们网站中的一些内容页面仅包含一两个内容控件;其他项包括每个可用 ContentPlaceHolders 的内容控件。 如果我们的新母版页 (Alternate.master
) 可能曾经分配给所有 ContentPlaceHolders Site.master
具有 Content Controls 的内容页,那么它必须 Alternate.master
同时包含与 ContentPlaceHolder 控件相同的内容页 Site.master
。
若要使 Alternate.master
母版页看起来类似于我的(请参阅图 4),请先在样式表中定义母版页的样式 AlternateStyles.css
。 将以下规则添加到 AlternateStyles.css
:
body
{
font-family: Comic Sans MS, Arial;
font-size: medium;
margin: 0px;
}
#topContent
{
text-align: center;
background-color: Navy;
color: White;
font-size: x-large;
text-decoration: none;
font-weight: bold;
padding: 10px;
height: 50px;
}
#topContent a
{
text-decoration: none;
color: White;
}
#navContent
{
font-size: small;
text-align: center;
}
#footerContent
{
padding: 10px;
font-size: 90%;
text-align: center;
border-top: solid 1px black;
}
#mainContent
{
text-align: left;
padding: 10px;
}
接下来,将以下声明性标记添加到 Alternate.master
。 如所见,Alternate.master
包含四个 ContentPlaceHolder 控件,其值与 ContentPlaceHolder Site.master
控件的值相同ID
。 此外,它还包括 ScriptManager 控件,对于使用 ASP.NET AJAX 框架的网站中的页面来说是必需的。
<!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>Untitled Page</title>
<asp:ContentPlaceHolder id="head" runat="server">
</asp:ContentPlaceHolder>
<link href="AlternateStyles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="MyManager" runat="server">
</asp:ScriptManager>
<div id="topContent">
<asp:HyperLink ID="lnkHome" runat="server" NavigateUrl="~/Default.aspx"
Text="Master Pages Tutorials" />
</div>
<div id="navContent">
<asp:ListView ID="LessonsList" runat="server"
DataSourceID="LessonsDataSource">
<LayoutTemplate>
<asp:PlaceHolder runat="server" ID="itemPlaceholder" />
</LayoutTemplate>
<ItemTemplate>
<asp:HyperLink runat="server" ID="lnkLesson"
NavigateUrl='<%# Eval("Url") %>'
Text='<%# Eval("Title") %>' />
</ItemTemplate>
<ItemSeparatorTemplate> | </ItemSeparatorTemplate>
</asp:ListView>
<asp:SiteMapDataSource ID="LessonsDataSource" runat="server"
ShowStartingNode="false" />
</div>
<div id="mainContent">
<asp:ContentPlaceHolder id="MainContent" runat="server">
</asp:ContentPlaceHolder>
</div>
<div id="footerContent">
<p>
<asp:Label ID="DateDisplay" runat="server"></asp:Label>
</p>
<asp:ContentPlaceHolder ID="QuickLoginUI" runat="server">
</asp:ContentPlaceHolder>
<asp:ContentPlaceHolder ID="LeftColumnContent" runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
测试新母版页
若要测试此新的母版页, BasePage
请更新类 OnPreInit
的方法,以便为 MasterPageFile
属性分配值“~/Alternate.master”,然后访问网站。 每一页都应正常运行,但两个页面除外: ~/Admin/AddProduct.aspx
和 ~/Admin/Products.aspx
。 将产品添加到 DetailsView 将导致~/Admin/AddProduct.aspx
NullReferenceException
尝试设置母版页属性的代码GridMessageText
行。 访问~/Admin/Products.aspx
InvalidCastException
页面加载时,将引发消息:“无法将类型为”ASP.alternate_master“的对象强制转换为类型”ASP.site_master”。
发生这些错误的原因是 Site.master
代码隐藏类包括未在 中 Alternate.master
定义的公共事件、属性和方法。 这两页 @MasterType
的标记部分具有引用母版页的 Site.master
指令。
<%@ MasterType VirtualPath="~/Site.master" %>
此外,DetailsView 的 ItemInserted
事件处理程序包括 ~/Admin/AddProduct.aspx
将松散类型 Page.Master
属性强制转换为类型的 Site
对象的代码。 指令 @MasterType
(以这种方式使用)和事件处理程序中的 ItemInserted
强制转换紧密耦合 ~/Admin/AddProduct.aspx
母版页和 ~/Admin/Products.aspx
页面 Site.master
。
为了打破这种紧密耦合,我们可以拥有 Site.master
并 Alternate.master
派生自包含公共成员定义的公用基类。 接下来,我们可以更新 @MasterType
指令以引用此通用基类型。
创建自定义基母版页类
将一个新类文件添加到 App_Code
命名 BaseMasterPage.cs
的文件夹,并将其派生自 System.Web.UI.MasterPage
。 我们需要定义 RefreshRecentProductsGrid
方法和 GridMessageText
属性 BaseMasterPage
,但我们不能简单地从那里 Site.master
移动它们,因为这些成员使用特定于 Site.master
母版页的 Web 控件( RecentProducts
GridView 和 GridMessage
Label)。
我们需要执行的操作是以这样的方式配置 BaseMasterPage
这些成员,以便定义这些成员,但实际上由 BaseMasterPage
派生类(Site.master
和 Alternate.master
)实现。 可以通过将类及其成员 abstract
标记为此类来进行这种类型的继承。 简言之,将 abstract
关键字添加到这两个成员后,将 BaseMasterPage
宣布尚未实现 RefreshRecentProductsGrid
, GridMessageText
但会声明其派生类。
我们还需要定义事件 PricesDoubled
, BaseMasterPage
并提供派生类引发事件的方法。 .NET Framework 中用于促进此行为的模式是在基类中创建公共事件,并添加一个名为OnEventName
受保护的virtual
方法。 然后,派生类可以调用此方法来引发事件,也可以重写它以在引发事件之前或之后立即执行代码。
更新类 BaseMasterPage
,使其包含以下代码:
using System; public abstract class BaseMasterPage : System.Web.UI.MasterPage
{
public event EventHandler PricesDoubled;
protected virtual void OnPricesDoubled(EventArgs e)
{
if (PricesDoubled != null)
PricesDoubled(this, e);
}
public abstract void RefreshRecentProductsGrid();
public abstract string GridMessageText
{
get;
set;
}
}
接下来,转到 Site.master
代码隐藏类并使其派生自 BaseMasterPage
。 abstract
因为BaseMasterPage
我们需要重写此处Site.master
的这些abstract
成员。 将 override
关键字添加到方法和属性定义。 此外,使用对基类OnPricesDoubled
方法的调用更新 Button Click
事件处理程序中DoublePrice
引发PricesDoubled
事件的代码。
这些修改后, Site.master
代码隐藏类应包含以下代码:
public partial class Site : BaseMasterPage {
protected void Page_Load(object sender, EventArgs e)
{
DateDisplay.Text = DateTime.Now.ToString("dddd, MMMM dd");
}
public override void RefreshRecentProductsGrid()
{
RecentProducts.DataBind();
}
public override string GridMessageText
{
get
{
return GridMessage.Text;
}
set
{
GridMessage.Text = value;
}
}
protected void DoublePrice_Click(object sender, EventArgs e)
{
// Double the prices
DoublePricesDataSource.Update();
// Refresh RecentProducts
RecentProducts.DataBind();
// Raise the PricesDoubled event
base.OnPricesDoubled(EventArgs.Empty);
}
}
我们还需要更新 Alternate.master
代码隐藏类来派 BaseMasterPage
生和重写这两 abstract
个成员。 但是,由于 Alternate.master
不包含列出最新产品的 GridView,也不包含在将新产品添加到数据库后显示消息的标签,因此这些方法无需执行任何操作。
public partial class Alternate : BaseMasterPage
{
public override void RefreshRecentProductsGrid()
{
// Do nothing
}
public override string GridMessageText
{
get
{
return string.Empty;
}
set
{
// Do nothing
}
}
}
引用基母版页类
完成该 BaseMasterPage
类并扩展了两个母版页后,最后一步是更新 ~/Admin/AddProduct.aspx
和 ~/Admin/Products.aspx
页面以引用此常见类型。 首先从以下两个页面中更改 @MasterType
指令:
<%@ MasterType VirtualPath="~/Site.master" %>
更改为:
<%@ MasterType TypeName="BaseMasterPage" %>
属性 @MasterType
现在引用基类型(BaseMasterPage
)而不是引用文件路径。 因此,这两个页面的代码隐藏类中使用的强类型 Master
属性现在的类型 BaseMasterPage
(而不是类型 Site
)。 随着此更改的重新访问 ~/Admin/Products.aspx
。 以前,这会导致强制转换错误,因为页面配置为使用 Alternate.master
母版页,但 @MasterType
指令引用了 Site.master
该文件。 但现在页面呈现时没有错误。 这是因为 Alternate.master
母版页可以强制转换为类型的 BaseMasterPage
对象(因为它扩展了它)。
需要进行 ~/Admin/AddProduct.aspx
一个小的改变。 DetailsView 控件的 ItemInserted
事件处理程序同时使用强类型 Master
属性和松散类型 Page.Master
属性。 我们在更新 @MasterType
指令时修复了强类型引用,但仍需要更新松散类型的引用。 替换以下代码行:
Site myMasterPage = Page.Master as Site;
使用以下命令,该类型转换为 Page.Master
基类型:
BaseMasterPage myMasterPage = Page.Master as BaseMasterPage;
步骤 4:确定要绑定到内容页的母版页
我们的 BasePage
类当前将所有内容页 MasterPageFile
的属性设置为页面生命周期的 PreInit 阶段中的硬编码值。 我们可以更新此代码以基于某些外部因素的母版页。 也许要加载的母版页取决于当前登录用户的首选项。 在这种情况下,我们需要在方法BasePage
中OnPreInit
编写代码,以便查找当前访问用户的母版页首选项。
让我们创建一个网页,允许用户选择要使用的母版页, Site.master
或者 Alternate.master
- 并在会话变量中保存此选项。 首先,在名为 <BasePage
。 但是,如果不将新页面绑定到母版页,则新页面的默认声明性标记包含 Web 窗体和母版页提供的其他内容。 需要手动将此标记替换为相应的内容控件。 因此,我发现将新的 ASP.NET 页绑定到母版页更容易。
注意
因为 Site.master
并 Alternate.master
具有相同的 ContentPlaceHolder 控件集,因此创建新内容页时选择的母版页并不重要。 为了保持一致性,我建议使用 Site.master
。
图 05:向网站添加新内容页(单击可查看全尺寸图像)
更新 Web.sitemap
文件以包含本课程的条目。 在母版页和 ASP.NET AJAX 课程的下面 <siteMapNode>
添加以下标记:
<siteMapNode url="~/ChooseMasterPage.aspx" title="Choose a Master Page" />
在将任何内容添加到 ChooseMasterPage.aspx
页面之前,需要花点时间更新页面的代码隐藏类,使其派生自 BasePage
(而不是 System.Web.UI.Page
)。 接下来,将 DropDownList 控件添加到页面,将其 ID
属性设置为 MasterPageChoice
该页,并添加两个 ListItems, Text
其值为“~/Site.master”和“~/Alternate.master”。
向页面添加按钮 Web 控件,并将按钮 Web 控件及其 ID
属性 Text
分别设置为 SaveLayout
“保存布局选择”。 此时,页面的声明性标记应如下所示:
<p>
Your layout choice:
<asp:DropDownList ID="MasterPageChoice" runat="server">
<asp:ListItem>~/Site.master</asp:ListItem>
<asp:ListItem>~/Alternate.master</asp:ListItem>
</asp:DropDownList>
</p>
<p>
<asp:Button ID="SaveLayout" runat="server" Text="Save Layout Choice" />
</p>
首次访问页面时,我们需要显示用户当前选定的母版页选项。 创建 Page_Load
事件处理程序并添加以下代码:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
if (Session["MyMasterPage"] != null)
{
ListItem li = MasterPageChoice.Items.FindByText(Session["MyMasterPage"].ToString());
if (li != null)
li.Selected = true;
}
}
}
上述代码仅在第一页访问(而不是后续回发时)执行。 它首先检查会话变量 MyMasterPage
是否存在。 如果这样做,它会尝试在 DropDownList 中找到匹配的 MasterPageChoice
ListItem。 如果找到匹配的 ListItem,则其 Selected
属性设置为 true
。
我们还需要将用户选择的代码保存到 MyMasterPage
Session 变量中。 为 SaveLayout
Button Click
的事件创建事件处理程序并添加以下代码:
protected void SaveLayout_Click(object sender, EventArgs e)
{
Session["MyMasterPage"] = MasterPageChoice.SelectedValue;
Response.Redirect("ChooseMasterPage.aspx");
}
注意
在 Click
回发时事件处理程序执行时,已选择母版页。 因此,在下一页访问之前,用户的下拉列表选择不会生效。 强制 Response.Redirect
浏览器重新请求 ChooseMasterPage.aspx
。
完成页面后ChooseMasterPage.aspx
,最终任务是BasePage
基于会话变量的值MyMasterPage
分配MasterPageFile
属性。 如果未设置会话变量,则默认为 BasePage
Site.master
。
protected override void OnPreInit(EventArgs e)
{
SetMasterPageFile();
base.OnPreInit(e);
}
protected virtual void SetMasterPageFile()
{
this.MasterPageFile = GetMasterPageFileFromSession();
}
protected string GetMasterPageFileFromSession()
{
if (Session["MyMasterPage"] == null)
return "~/Site.master";
else
return Session["MyMasterPage"].ToString();
}
注意
我将对象属性从事件处理程序中移Page
MasterPageFile
出OnPreInit
的代码,并移到了两个单独的方法中。 第一个方法 SetMasterPageFile
将属性分配给 MasterPageFile
第二个方法 GetMasterPageFileFromSession
返回的值。 我做了 SetMasterPageFile
该方法 virtual
,以便扩展 BasePage
的未来类可以选择重写它以实现自定义逻辑(如果需要)。 在下一教程中,我们将看到重写 BasePage
's SetMasterPageFile
属性的示例。
使用此代码就位后,请访问 ChooseMasterPage.aspx
页面。 最初, Site.master
母版页处于选中状态(见图 6),但用户可以从下拉列表中选择不同的母版页。
图 06:使用 Site.master
母版页显示内容页(单击以查看全尺寸图像)
图 07:现在使用母版页显示 Alternate.master
内容页(单击以查看全尺寸图像)
总结
访问内容页面时,其内容控件与其母版页的 ContentPlaceHolder 控件融合在一起。 内容页的母版页由 Page
类 MasterPageFile
的属性表示,该属性在初始化阶段分配给 @Page
指令 MasterPageFile
的属性。 如本教程所示,只要在 PreInit 阶段结束之前,就可以向属性分配值 MasterPageFile
。 能够以编程方式指定母版页为更高级的方案打开大门,例如基于外部因素动态将内容页绑定到母版页。
快乐编程!
深入阅读
有关本教程中讨论的主题的详细信息,请参阅以下资源:
关于作者
斯科特·米切尔是多个 ASP/ASP.NET 书籍的作者,4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 3.5。 斯科特可以在他的博客上 mitchell@4GuysFromRolla.com 或通过他的博客联系 http://ScottOnWriting.NET。
特别感谢
本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Suchi Banerjee。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请在以下位置放置我一行 mitchell@4GuysFromRolla.com