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

作者 :斯科特·米切尔

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

介绍

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

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

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

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

当需要逐页配置页面的特定区域时,我们使用 ContentPlaceHolder 控件。 但是,大多数页面需要发出特定输出的情况如何,但少量页面需要对其进行自定义以显示其他内容? 我们在多个 ContentPlaceHolders 和默认内容教程中检查的一个此类示例涉及在每个页面上显示登录界面。 虽然大多数页面都应包括登录界面,但对于少数页面(例如:主登录页(Login.aspx)、“创建帐户”页和其他仅可供经过身份验证的用户访问的页面,应将其取消。 多个 ContentPlaceHolders 和默认内容教程演示了如何在母版页中定义 ContentPlaceHolder 的默认内容,以及如何在不需要默认内容的页面中重写它。

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

在内容页中出现某些操作后,可能需要刷新母版页中显示的数据时,内容和母版页交互的最常见示例。 请考虑包含 GridView 的母版页,该母版页显示特定数据库表中最近添加的五条记录,并且其中一个内容页包含用于向同一表添加新记录的接口。

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

注意

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

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

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

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

注意

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

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

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

关键字 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>

正如你所看到的,标记包含:标签 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> 元素的添加反映在 Lessons 列表中。

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

SELECT ProductName, UnitPrice FROM Products

然后,在 INSERT 选项卡中添加以下 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 控件中填充用户输入的值。 遗憾的是,没有发生插入的视觉反馈。 最好显示一条消息,确认已添加新记录。 我把这作为读者的练习。 此外,在从 DetailsView 添加新记录后,母版页中的 GridView 仍显示与之前相同的五条记录:它不包括刚刚添加的记录。 我们将在后续步骤中研究如何解决此问题。

注意

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

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

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

由于 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的页面。

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

使用松散类型属性Page.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 我们可以引用特定于站点的属性和方法。 如图 7 所示,公共属性 GridMessageText 显示在 IntelliSense 下拉列表中。

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

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

注意

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

使用@MasterType指令创建强类型引用

如果仔细查看,可以看到 ASP.NET 页面的代码隐藏类是分部类(请注意 partial 类定义中的关键字)。 分部类是在 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有了指令,我们可以直接通过Master属性调用Site.master母版页的公共属性和方法,而无需任何强制转换。

注意

如果省略该 @MasterType 指令,则语法 Page.MasterMaster 返回相同的内容:页面母版页的松散类型对象。 如果包含该 @MasterType 指令,则 Master 返回对指定母版页的强类型引用。 Page.Master但是,仍返回松散类型的引用。 若要更全面地了解为何如此,以及如何Master在包含指令时@MasterType构造属性,请参阅 K. Scott Allen ASP.NET 2.0 中的博客文章@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集合访问;如你所看到的那样,通过 <a0/> 访问刚刚添加ProductName的值。

图 8 显示 AddProduct.aspx 新产品斯科特的 Soda 后立即添加到数据库中的页面。 请注意,刚刚添加的产品名称在母版页的标签中记录,并已刷新 GridView 以包含产品及其价格。

母版页的标签和 GridView 显示刚刚添加的产品

图 08:母版页的标签和 GridView 显示刚刚添加的产品(单击以查看全尺寸图像

总结

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

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

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

快乐编程!

深入阅读

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

关于作者

斯科特·米切尔是多个 ASP/ASP.NET 书籍的作者,4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 3.5。 斯科特可以在他的博客上 mitchell@4GuysFromRolla.com 或通过他的博客联系 http://ScottOnWriting.NET

特别感谢

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