从内容页与母版页交互 (C#)

作者 :Scott Mitchell

检查如何从内容页中的代码调用母版页的方法、设置属性等。

简介

在过去的五个教程中,我们介绍了如何创建母版页、定义内容区域、将 ASP.NET 页绑定到母版页,以及定义特定于页面的内容。 当访问者请求特定内容页时,内容和母版页的标记会在运行时融合,从而呈现统一的控件层次结构。 因此,我们已经了解了母版页与其其中一个内容页进行交互的一种方式:内容页拼出标记以转换为母版页的 ContentPlaceHolder 控件。

我们尚未了解母版页和内容页如何以编程方式进行交互。 除了为母版页的 ContentPlaceHolder 控件定义标记外,内容页还可以为其母版页的公共属性赋值并调用其公共方法。 同样,母版页可以与其内容页交互。 虽然母版页和内容页之间的编程交互不如声明性标记之间的交互更常见,但在许多情况下需要这种编程交互。

在本教程中,我们将探讨内容页如何以编程方式与其母版页交互;在下一教程中,我们将了解母版页如何以类似方式与其内容页交互。

内容页与其母版页之间的编程交互示例

当需要逐页配置页面的特定区域时,我们使用 ContentPlaceHolder 控件。 但是,如果大多数页面需要发出特定输出,但少数页面需要自定义它以显示其他内容,情况呢? 我们在 多个 ContentPlaceHolders 和默认内容 教程中研究的一个此类示例涉及在每个页面上显示登录界面。 虽然大多数页面应包含登录界面,但对于少数页面,应禁止显示登录界面,例如:main登录页面 (Login.aspx) ;“创建帐户”页;以及其他只有经过身份验证的用户才能访问的页面。 多个 ContentPlaceHolders 和默认内容教程演示了如何在母版页中定义 ContentPlaceHolder 的默认内容,以及如何在不需要默认内容的那些页面中重写它。

另一个选项是在母版页中创建一个公共属性或方法,用于指示是显示还是隐藏登录接口。 例如,母版页可能包含名为 ShowLoginUI 的公共属性,其值用于在母版页中设置 Visible Login 控件的属性。 应禁止登录用户界面的那些内容页随后可以通过编程方式将 ShowLoginUI 属性设置为 false

当母版页中显示的数据需要在内容页中执行某些操作后刷新时,可能会发生最常见的内容和母版页交互示例。 假设一个母版页包含一个 GridView,该网格视图显示来自特定数据库表最近添加的五条记录,并且其中一个内容页包含用于向同一表添加新记录的接口。

当用户访问页面以添加新记录时,她会看到母版页中显示的五条最近添加的记录。 填写新记录的列的值后,她提交表单。 假设母版页中的 GridView 属性 EnableViewState 设置为 true (默认) ,则从视图状态重新加载其内容,因此,即使刚将较新的记录添加到数据库中,也会显示五条相同的记录。 这可能会使用户感到困惑。

注意

即使禁用 GridView 的视图状态,以便它在每次回发时重新绑定到其基础数据源,它仍然不会显示刚添加的记录,因为数据在页面生命周期中绑定到 GridView 的时间早于将新记录添加到数据库时。

若要解决此问题,以便刚添加的记录在母版页的 GridView 中显示在回发时,我们需要指示 GridView 在新记录添加到数据库 重新绑定到其数据源。 这需要内容和母版页之间的交互,因为用于添加新记录 (及其事件处理程序) 的接口位于内容页中,但需要刷新的 GridView 位于母版页中。

由于从内容页中的事件处理程序刷新母版页的显示是内容和母版页交互的最常见需求之一,因此让我们更详细地探讨本主题。 本教程的下载内容包括网站文件夹中名为 NORTHWIND.MDFApp_Data Microsoft SQL Server 2005 Express Edition 数据库。 Northwind 数据库存储虚构公司 Northwind Traders 的产品、员工和销售信息。

步骤 1 逐步讲解在母版页的 GridView 中显示五个最近添加的产品。 步骤 2 创建用于添加新产品的内容页面。 步骤 3 介绍如何在母版页中创建公共属性和方法,步骤 4 演示了如何以编程方式与内容页中的这些属性和方法进行交互。

注意

本教程不深入探讨在 ASP.NET 中使用数据的详细信息。 设置母版页以显示数据和插入数据的内容页的步骤已完成,但很轻快。 若要更深入地了解如何显示和插入数据以及如何使用 SqlDataSource 和 GridView 控件,请参阅本教程末尾的“进一步读取”部分中的资源。

步骤 1:在母版页中显示五个最近添加的产品

Site.master打开母版页,并将 Label 和 GridView 控件添加到 。leftContent<div> 清除 Label 的 Text 属性,将其 EnableViewState 属性设置为 false,将其 ID 属性设置为 GridMessage;将 GridView 的 ID 属性设置为 RecentProducts。 接下来,从Designer展开 GridView 的智能标记,然后选择将其绑定到新的数据源。 这会启动数据源配置向导。 由于 文件夹中的 Northwind 数据库App_Data是 Microsoft SQL Server 数据库,因此请选择创建 SqlDataSource,方法是选择 (请参阅图 1) ;将 SqlDataSource RecentProductsDataSource命名为 。

将 GridView 绑定到名为 RecentProductsDataSource 的 SqlDataSource 控件

图 01:将 GridView 绑定到名为 RecentProductsDataSource 的 SqlDataSource 控件 (单击以查看全尺寸图像)

下一步要求我们指定要连接到的数据库。 NORTHWIND.MDF从下拉列表中选择数据库文件,然后单击“下一步”。 由于这是我们第一次使用此数据库,因此向导将提供在 中Web.config存储连接字符串。 使用名称 NorthwindConnectionString存储连接字符串。

连接到 Northwind 数据库

图 02:连接到 Northwind 数据库 (单击以查看全尺寸图像)

“配置数据源”向导提供了两种方法,用于指定用于检索数据的查询:

  • 通过指定自定义 SQL 语句或存储过程,或者
  • 选择表或视图,然后指定要返回的列

由于我们只想返回最近添加的五个产品,因此需要指定一个自定义 SQL 语句。 使用以下 SELECT 查询:

SELECT TOP 5 ProductName, UnitPrice FROM Products ORDER BY ProductID DESC

关键字 (keyword) TOP 5 仅返回查询中的前五条记录。 表 Products 的主键 ProductID是一个 IDENTITY 列,它确保添加到表中的每个新产品的值都比上一个条目大。 因此,按降序对结果进行 ProductID 排序会返回从最近创建的产品开始的产品。

返回五个最近添加的产品

图 03:返回五个最近添加的产品 (单击以查看全尺寸图像)

完成向导后,Visual Studio 将为 GridView 生成两个 BoundField,以显示 ProductName 从数据库返回的 和 UnitPrice 字段。 此时,母版页的声明性标记应包含类似于以下内容的标记:

<asp:Label ID="GridMessage" runat="server" EnableViewState="false"></asp:Label>
<asp:GridView ID="RecentProducts" runat="server" AutoGenerateColumns="False"
 DataSourceID="RecentProductsDataSource">
 <Columns> 
 <asp:BoundField DataField="ProductName" HeaderText="ProductName" 
 SortExpression="ProductName"/> 
 <asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice"
 SortExpression="UnitPrice"/> 
 </Columns> 
</asp:GridView> 

<asp:SqlDataSource ID="RecentProductsDataSource" runat="server" 
 ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>" 
 SelectCommand="SELECT TOP 5 ProductName, UnitPrice FROM Products ORDER BY ProductID DESC"> 
</asp:SqlDataSource>

如你所看到的,标记包含:Label Web 控件 (GridMessage) ;GridView RecentProducts,包含两个 BoundFields;以及一个返回最近添加的五个产品的 SqlDataSource 控件。

创建此 GridView 并配置其 SqlDataSource 控件后,通过浏览器访问网站。 如图 4 所示,左下角会显示一个网格,其中列出了最近添加的五种产品。

GridView 显示五个最近添加的产品

图 04:GridView 显示五个最近添加的产品 (单击以查看全尺寸图像)

注意

随意清理 GridView 的外观。 一些建议包括将显示 UnitPrice 值的格式设置为货币,以及使用背景色和字体来改善网格的外观。

步骤 2:创建内容页以添加新产品

我们的下一个任务是创建一个内容页,用户可以从该 Products 页面向表添加新产品。 将新内容页添加到 Admin 名为 AddProduct.aspx的文件夹,确保将其 Site.master 绑定到母版页。 图 5 显示了将此页面添加到网站后解决方案资源管理器。

向管理员文件夹添加新 ASP.NET 页

图 05:向文件夹添加新 ASP.NET 页 Admin (单击以查看全尺寸图像)

回想一下 ,在母版页中指定标题、元标记和其他 HTML 标头 教程中,我们创建了一个名为 BasePage 的自定义基页类,如果未显式设置,该类将生成页面的标题。 转到 AddProduct.aspx 页面的代码隐藏类,使其派生自 BasePage (而不是 System.Web.UI.Page) 。

最后,更新 文件以 Web.sitemap 包含本课程的条目。 在 下面为控件 ID 命名问题课程添加以下标记 <siteMapNode>

<siteMapNode url="~/Admin/AddProduct.aspx" title="Content to Master Page Interaction" />

如图 6 所示,此 <siteMapNode> 元素的添加反映在“课程”列表中。

返回到 AddProduct.aspx。 在 ContentPlaceHolder 的 Content 控件中 MainContent ,添加 DetailsView 控件并将其命名为 NewProduct。 将 DetailsView 绑定到名为 NewProductDataSource的新 SqlDataSource 控件。 与步骤 1 中的 SqlDataSource 一样,配置向导,使其使用 Northwind 数据库并选择指定自定义 SQL 语句。 由于 DetailsView 将用于向数据库添加项,因此我们需要同时 SELECT 指定 语句和 INSERT 语句。 SELECT使用以下查询:

SELECT ProductName, UnitPrice FROM Products

然后,从“插入”选项卡添加以下 INSERT 语句:

INSERT INTO Products(ProductName, UnitPrice) VALUES(@ProductName, @UnitPrice)

完成向导后,转到 DetailsView 的智能标记,检查“启用插入”复选框。 这会将 CommandField 添加到 DetailsView,其 ShowInsertButton 属性设置为 true。 由于此 DetailsView 将仅用于插入数据,因此请将 DetailsView 的 DefaultMode 属性设置为 Insert

就是这么简单! 让我们测试此页面。 通过浏览器访问 AddProduct.aspx ,输入名称和价格 (请参阅图 6) 。

将新产品添加到数据库

图 06:将新产品添加到数据库 (单击以查看全尺寸图像)

键入新产品的名称和价格后,单击“插入”按钮。 这会导致窗体回发。 回发时,执行 SqlDataSource 控件的 INSERT 语句;其两个参数使用 DetailsView 的两个 TextBox 控件中用户输入的值填充。 遗憾的是,没有出现插入的视觉反馈。 最好显示一条消息,确认已添加新记录。 我留给读者做练习。 此外,从“详细信息”“查看”添加新记录后,母版页中的 GridView 仍显示与之前相同的五条记录;它不包括刚刚添加的记录。 我们将在后续步骤中研究如何解决此问题。

注意

除了添加插入成功的某种形式的视觉反馈外,我建议你还更新 DetailsView 的插入界面以包含验证。 目前没有验证。 如果用户为 UnitPrice 字段输入了无效值,例如“太昂贵”,则当系统尝试将该字符串转换为小数时,将在回发时引发异常。 有关自定义插入接口的详细信息,请参阅使用数据教程系列中的自定义数据修改接口教程

步骤 3:在母版页中创建公共属性和方法

在步骤 1 中,我们在母版页的 GridView 上方添加了一个名为 GridMessage 的标签 Web 控件。 此标签用于选择性地显示消息。 例如,在向 Products 表添加新记录后,我们可能需要显示一条消息:“ProductName 已添加到数据库。”我们可能希望内容页可自定义邮件,而不是在母版页中对此标签的文本进行硬编码。

由于 Label 控件是作为母版页中的受保护成员变量实现的,因此无法直接从内容页访问它。 若要从内容页 (使用母版页中的 Label,或者为此,母版页中的任何 Web 控件) 我们需要在母版页中创建一个公共属性,该属性公开 Web 控件或用作代理,通过其可以访问其属性之一。 将以下语法添加到母版页的代码隐藏类,以公开 Label 的 Text 属性:

public string GridMessageText 
{ 
    get
    { 
        return GridMessage.Text; 
    } 
    set 
    {
        GridMessage.Text = value; 
    }
}

从内容页将新记录添加到 Products 表中时, RecentProducts 母版页中的 GridView 需要重新绑定到其基础数据源。 若要重新绑定 GridView,请调用其 DataBind 方法。 由于内容页无法以编程方式访问母版页中的 GridView,因此我们需要在母版页中创建一个公共方法,在调用时,该方法将数据重新绑定到 GridView。 将以下方法添加到母版页的代码隐藏类:

public void RefreshRecentProductsGrid() 
{ 
    RecentProducts.DataBind();
}

GridMessageText使用 属性和 RefreshRecentProductsGrid 方法后,任何内容页都可以以编程方式设置或读取 Label Text 属性的值GridMessage,或将数据RecentProducts重新绑定到 GridView。 步骤 4 检查如何从内容页访问母版页的公共属性和方法。

注意

不要忘记将母版页的属性和方法标记为 public。 如果不将这些属性和方法显式表示为 public,则无法从内容页访问它们。

步骤 4:从内容页调用母版页的公共成员

现在母版页具有必要的公共属性和方法,我们可以从 AddProduct.aspx 内容页调用这些属性和方法。 具体而言,我们需要设置母版页的 GridMessageText 属性,并在将新产品添加到数据库后调用其 RefreshRecentProductsGrid 方法。 所有 ASP.NET 数据 Web 控件在完成各种任务之前和之后立即触发事件,这使得页面开发人员可以轻松地在任务之前或之后执行一些编程操作。 例如,当最终用户单击 DetailsView 的“插入”按钮时,在回发时,DetailsView 将引发其 ItemInserting 事件,然后再开始插入工作流。 然后,它将记录插入数据库。 之后,DetailsView 将引发其 ItemInserted 事件。 因此,若要在添加新产品后使用母版页,请为 DetailsView 的 ItemInserted 事件创建事件处理程序。

内容页可通过两种方式以编程方式与其母版页进行交互:

  • Page.Master使用 属性,该属性返回对母版页的松散类型引用,或
  • 通过 @MasterType 指令指定页面的母版页类型或文件路径;这会自动将强类型属性添加到名为 Master的页面。

让我们来看看这两种方法。

使用 Loosely-TypedPage.Master属性

所有 ASP.NET 网页都必须派生自 Page 命名空间中的 System.Web.UI 类。 类 Page 包含一个 Master 属性 ,该属性返回对页面母版页的引用。 如果页面没有母版页 Master ,则 null返回 。

属性 Master 返回类型 MasterPage (对象也位于 System.Web.UI 命名空间) 是所有母版页派生自的基类型。 因此,若要使用网站母版页中定义的公共属性或方法,必须将从 Master 属性返回的对象转换为MasterPage适当的类型。 由于我们将母版页文件 Site.master命名为 ,因此代码隐藏类名为 Site。 因此,以下代码将 Page.Master 属性强制转换为 Site 类的实例。

// Cast the loosely-typed Page.Master property and then set the GridMessageText property 
Site myMasterPage = Page.Master as Site;

现在,我们已将松散类型 Page.Master 属性强制转换为 类型, Site 我们可以引用特定于 Site 的属性和方法。 如图 7 所示,公共属性 GridMessageText 显示在 IntelliSense 下拉列表中。

IntelliSense 显示母版页的公共属性和方法

图 07:IntelliSense 显示母版页的公共属性和方法 (单击以查看全尺寸图像)

注意

如果为母版页文件 MasterPage.master 命名,则母版页的代码隐藏类名称为 MasterPage。 从 类型System.Web.UI.MasterPageMasterPage转换为类时,这可能会导致代码不明确。 简而言之,需要完全限定要强制转换为的类型,使用网站项目模型时可能有点棘手。 我的建议是确保在创建母版页时将其命名为其他 MasterPage.master 名称,或者更好的是创建对母版页的强类型引用。

使用@MasterType指令创建 Strongly-Typed 引用

如果仔细查看,可以看到 ASP.NET 页的代码隐藏类是分部类 (请注意partial类定义) 中的关键字 (keyword) 。 分部类是在 C# 和 Visual Basic with.NET Framework 2.0 中引入的,简而言之,允许跨多个文件定义类的成员。 代码隐藏类文件 ( AddProduct.aspx.cs例如)包含我们(页面开发人员)创建的代码。 除了我们的代码,ASP.NET 引擎还自动创建一个单独的类文件,其中包含 中的属性和事件处理程序,用于将声明性标记转换为页面的类层次结构。

每当访问 ASP.NET 页时发生的自动代码生成为一些相当有趣且有用的可能性铺平了道路。 对于母版页,如果我们告诉 ASP.NET 引擎内容页正在使用哪个母版页,它将为我们生成强类型 Master 属性。

@MasterType使用 指令通知 ASP.NET 引擎内容页的母版页类型。 指令 @MasterType 可以接受母版页的类型名称或其文件路径。 若要指定页面 AddProduct.aspx 使用 Site.master 作为其母版页,请将以下 指令添加到 的 AddProduct.aspx顶部:

<%@ MasterType VirtualPath="~/Site.master" %>

此指令指示 ASP.NET 引擎通过名为 Master的属性添加对母版页的强类型引用。 @MasterType指令到位后,我们可以直接通过 属性调用Site.master母版页的公共属性和方法,Master而无需任何强制转换。

注意

如果省略 @MasterType 指令,则语法 Page.MasterMaster 返回相同的内容:一个松散类型的对象到页面的母版页。 如果包含 指令, @MasterTypeMaster 返回对指定母版页的强类型引用。 Page.Master但是,仍返回松散类型的引用。 若要更全面地了解为何会出现这种情况,以及如何Master在包含 指令时@MasterType构造 属性,请参阅 ASP.NET 2.0 中的 K. Scott Allen 博客文章@MasterType

添加新产品后更新母版页

了解如何从内容页调用母版页的公共属性和方法后,我们已准备好更新 AddProduct.aspx 页面,以便在添加新产品后刷新母版页。 在步骤 4 的开头,我们为 DetailsView 控件的 ItemInserting 事件创建了一个事件处理程序,该事件处理程序在将新产品添加到数据库后立即执行。 将以下代码添加到该事件处理程序:

protected void NewProduct_ItemInserted(object sender, DetailsViewInsertedEventArgs e) 
{ 
    // Cast the loosely-typed Page.Master property and then set the GridMessageText property 
    Site myMasterPage = Page.Master as Site; 
    myMasterPage.GridMessageText = string.Format("{0} added to grid...", e.Values["ProductName"]); 
    // Use the strongly-typed Master property 
    Master.RefreshRecentProductsGrid();
}

上述代码同时使用松散类型 Page.Master 属性和强类型 Master 属性。 请注意, GridMessageText 属性设置为“添加到网格的 ProductName ...”刚添加的产品的值可通过 e.Values 集合访问;如你所看到的,刚添加 ProductName 的值是通过 e.Values["ProductName"]访问的。

图 8 显示了 AddProduct.aspx 将新产品(Scott's Soda)添加到数据库后立即的页面。 请注意,刚添加的产品名称在母版页的标签中注明,并且 GridView 已刷新以包含产品及其价格。

母版页的标签和 GridView 显示 Just-Added 产品

图 08:母版页的标签和网格视图 显示 Just-Added 产品 (单击以查看全尺寸图像)

总结

理想情况下,母版页及其内容页彼此完全独立,不需要任何级别的交互。 虽然在设计母版页和内容页时应考虑到这一目标,但在很多常见方案中,内容页必须与其母版页进行交互。 最常见的原因之一是基于内容页中发生的某些操作更新母版页显示的特定部分。

好消息是,让内容页以编程方式与其母版页交互相对简单。 首先在母版页中创建公共属性或方法,这些属性或方法封装需要由内容页调用的功能。 然后,在内容页中,通过松散类型的 Page.Master 属性访问母版页的属性和方法,或使用 @MasterType 指令创建对母版页的强类型引用。

在下一教程中,我们将探讨如何让母版页以编程方式与其内容页之一交互。

编程愉快!

深入阅读

有关本教程中讨论的主题的详细信息,请参阅以下资源:

关于作者

Scott Mitchell 是多本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 3.5。 可以在 上联系 mitchell@4GuysFromRolla.com 斯科特,也可以通过他的博客在 联系 http://ScottOnWriting.NET

特别感谢

本教程系列由许多有用的审阅者审阅。 本教程的首席审阅者是 Zack Jones。 有兴趣查看我即将发布的 MSDN 文章? 如果是这样,请在 mitchell@4GuysFromRolla.com