以编程方式指定母版页 (C#)

作者 :Scott Mitchell

查看通过 PreInit 事件处理程序以编程方式设置内容页的母版页。

简介

使用母版页创建 Site-Wide 布局中的首例以来,所有内容页都通过 MasterPageFile 指令中的 @Page 属性以声明方式引用了母版页。 例如,以下 @Page 指令将内容页链接到母版页 Site.master

<%@ Page Language="C#" MasterPageFile="~/Site.master" ... %>

Page命名空间中的 System.Web.UI 类包含一个MasterPageFile属性,该属性返回内容页母版页的路径;该属性由 @Page 指令设置。 此属性还可用于以编程方式指定内容页的母版页。 如果要根据外部因素(例如访问页面的用户)动态分配母版页,此方法非常有用。

在本教程中,我们将第二个母版页添加到网站,并动态决定在运行时使用哪个母版页。

步骤 1:查看页面生命周期

每当针对内容页 ASP.NET 页的请求到达 Web 服务器时,ASP.NET 引擎必须将页面的内容控件融合到母版页的相应 ContentPlaceHolder 控件中。 这种融合会创建一个控件层次结构,然后可以继续执行典型的页面生命周期。

图 1 说明了这种融合。 图 1 中的步骤 1 显示了初始内容和母版页控件层次结构。 在 PreInit 阶段的尾端,页面中的内容控件将添加到母版页中的相应 ContentPlaceHolders (步骤 2) 。 在此融合之后,母版页将用作融合控件层次结构的根。 然后将此融合控件层次结构添加到页面,以在步骤 3) (生成最终的控件层次结构。 最终结果是页面的控件层次结构包括融合的控件层次结构。

在 PreInit 阶段,母版页和内容页的控件层次结构融合在一起

图 01:在 PreInit 阶段期间,母版页和内容页的控件层次结构融合在一起 (单击以查看全尺寸图像)

步骤 2:通过代码设置MasterPageFile属性

此融合中的母版页部分取决于对象的 MasterPageFile 属性的值PageMasterPageFile在 指令中@Page设置 属性具有在初始化阶段分配 PageMasterPageFile 属性的净效果,这是页面生命周期的第一个阶段。 或者,我们可以以编程方式设置此属性。 但是,在图 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设置母版页。 回想一下,我们创建了一个自定义基页类, BasePage () 回到 在母版页中指定标题、元标记和其他 HTML 标头 教程中。 当前 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 属性,因此可以从 指令中删除 MasterPageFile 属性 @Page ,而不会影响最终用户的体验。 若要说服自己了解这一点,请继续从 @Page 中的 Default.aspx 指令中删除 MasterPageFile 属性,然后通过浏览器访问页面。 正如预期的那样,输出与删除属性之前相同。

属性是通过 指令设置的 MasterPageFile 还是以编程方式设置 @Page 的,对最终用户的体验无关紧要。 但是,MasterPageFileVisual Studio 在设计时使用 指令中的 @Page 属性在Designer中生成 WYSIWYG 视图。 如果在 Visual Studio 中返回到 Default.aspx 并导航到Designer你将看到消息“母版页错误:该页具有需要母版页引用的控件,但没有指定任何控件” (请参阅图 2) 。

简而言之,需要在 指令中@Page保留 MasterPageFile 属性,才能在 Visual Studio 中享受丰富的设计时体验。

Visual Studio 使用 <span class=@Page 指令的 MasterPageFile 属性呈现设计视图“ />

图 02:Visual Studio 使用 @Page 指令的 MasterPageFile 属性呈现设计视图 (单击以查看全尺寸图像)

步骤 3:创建备用母版页

由于可以在运行时以编程方式设置内容页的母版页,因此可以根据某些外部条件动态加载特定的母版页。 当网站布局需要因用户而异时,此功能非常有用。 例如,博客引擎 Web 应用程序可能允许其用户为其博客选择布局,其中每个布局都与不同的母版页相关联。 在运行时,当访问者正在查看用户的博客时,Web 应用程序需要确定博客的布局,并将相应的母版页与内容页动态关联。

让我们看看如何在运行时基于一些外部条件动态加载母版页。 我们网站目前仅包含一个母版页 (Site.master) 。 我们需要另一个母版页来说明如何在运行时选择母版页。 此步骤重点介绍如何创建和配置新的母版页。 步骤 4 介绍如何确定在运行时要使用的母版页。

在名为 Alternate.master的根文件夹中创建新的母版页。 此外,将新的样式表添加到名为 AlternateStyles.css的网站。

将另一个母版页和 CSS 文件添加到网站

图 03:将另一个母版页和 CSS 文件添加到网站 (单击以查看全尺寸图像)

我设计 Alternate.master 了母版页,使标题显示在页面顶部、居中和海军背景上。 我已分配左列,并将该内容移到 ContentPlaceHolder 控件下 MainContent ,该控件现在跨越页面的整个宽度。 此外,我取消了无序课程列表,并将其替换为上面的 MainContent水平列表。 我还更新了母版页 (使用的字体和颜色,并扩展了其内容页) 。 图 4 显示了 Default.aspx 使用 Alternate.master 母版页时。

注意

ASP.NET 包括定义 主题的功能。 主题是图像、CSS 文件和样式相关的 Web 控件属性设置的集合,可在运行时应用于页面。 如果网站的布局仅在显示的图像及其 CSS 规则上有所不同,则主题是可采用的方法。 如果布局差异较大,例如使用不同的 Web 控件或具有完全不同的布局,则需要使用单独的母版页。 有关主题的详细信息,请参阅本教程末尾的“进一步阅读”部分。

我们的内容页面现在可以使用新的外观和感觉

图 04:我们的内容页面现在可以使用新的外观 (单击以查看全尺寸图像)

当母版页和内容页的标记融合时, MasterPage 类会进行检查,以确保内容页中的每个 Content 控件引用母版页中的 ContentPlaceHolder。 如果找到引用不存在的 ContentPlaceHolder 的 Content 控件,则会引发异常。 换句话说,分配给内容页的母版页必须具有内容页中每个 Content 控件的 ContentPlaceHolder。

Site.master 版页包括四个 ContentPlaceHolder 控件:

  • head
  • MainContent
  • QuickLoginUI
  • LeftColumnContent

我们网站中的某些内容页面仅包含一个或两个内容控件;其他包括每个可用 ContentPlaceHolders 的 Content 控件。 如果我们的新母版页 (Alternate.master) 可能曾经分配给具有所有 ContentPlaceHolders Site.master 的 Content 控件的内容页,那么还必须 Alternate.master 包含与 Site.master相同的 ContentPlaceHolder 控件。

若要使 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 控件,其值与 中的 Site.masterContentPlaceHolder 控件相同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。 将产品添加到 中的 ~/Admin/AddProduct.aspx DetailsView 会导致 NullReferenceException 从尝试设置母版页 GridMessageText 的 属性的代码行生成 。 访问 ~/Admin/Products.aspxInvalidCastException 时,会在页面加载时引发消息:“无法将类型为'ASP.alternate_master'的对象强制转换为类型'ASP.site_master'。”

发生这些错误的原因是 Site.master 代码隐藏类包括未在 中 Alternate.master定义的公共事件、属性和方法。 这两个页面的标记部分具有 @MasterType 引用母版页的 Site.master 指令。

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

此外,中 DetailsView 的ItemInserted事件处理程序包括将松散类型Page.Master属性转换为 类型的Site对象的代码。~/Admin/AddProduct.aspx 指令 @MasterType (以这种方式使用) ,事件处理程序中的 ItemInserted 强制转换将 和 ~/Admin/Products.aspx 页紧密耦合~/Admin/AddProduct.aspxSite.master母版页。

为了打破这种紧密耦合,我们可以从 Site.master 包含公共成员定义的公共基类获得和 Alternate.master 派生。 之后,我们可以更新 @MasterType 指令以引用此常见基类型。

创建自定义基础母版页类

将一个新的类文件添加到名为 的文件夹,App_Code并使其派生自 System.Web.UI.MasterPageBaseMasterPage.cs 我们需要在 中定义 RefreshRecentProductsGrid 方法和 GridMessageText 属性,但不能简单地将它们从Site.master中移动,因为这些成员使用特定于Site.master母版页的 Web 控件, (RecentProducts GridView 和 GridMessageBaseMasterPageLabel) 。

我们需要做的是 BaseMasterPage 配置这些成员,以便这些成员在该处定义,但实际上由 BaseMasterPage的派生类 (Site.masterAlternate.master) 实现。 通过将 类及其成员 abstract标记为 ,可以进行这种类型的继承。 简言之,将 abstract 关键字 (keyword) 添加到这两个BaseMasterPage成员将宣布尚未实现 RefreshRecentProductsGridGridMessageText,但其派生类将实现 。

我们还需要在 中BaseMasterPage定义 PricesDoubled 事件,并通过派生类提供一种方法来引发事件。 .NET Framework中用于促进此行为的模式是在基类中创建一个公共事件,virtual并添加一个名为 的OnEventName受保护方法。 然后,派生类可以调用此方法来引发事件,也可以重写它以在引发事件之前或之后立即执行代码。

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。 因为 BaseMasterPage 我们需要abstract在 中Site.master重写这些abstract成员。 将override关键字 (keyword) 添加到方法和属性定义。 此外,通过调用基类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,也不包含在将新产品添加到数据库后显示消息的 Label,因此这些方法不需要执行任何操作。

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" %>

属性现在引用基类型 () BaseMasterPage@MasterType而不是引用文件路径。 因此,这两个页面的代码隐藏类中使用的强类型 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 阶段中的硬编码值。 我们可以更新此代码,使母版页基于某些外部因素。 也许要加载的母版页取决于当前登录用户的首选项。 在这种情况下,我们需要在 方法中OnPreInitBasePage编写代码,用于查找当前访问的用户的母版页首选项。

让我们创建一个网页,允许用户选择要使用的母版页 - Site.masterAlternate.master - 并将此选项保存在 Session 变量中。 首先在名为 ChooseMasterPage.aspx的根目录中创建新的网页。 创建此页面 (或任何其他内容页时) 不需要将其绑定到母版页,因为母版页是在 中以编程方式设置的 BasePage。 但是,如果不将新页面绑定到母版页,则新页面的默认声明性标记包含 Web 窗体和母版页提供的其他内容。 需要将此标记手动替换为相应的内容控件。 因此,我发现将新的 ASP.NET 页绑定到母版页更容易。

注意

由于 Site.masterAlternate.master 具有相同的一组 ContentPlaceHolder 控件,因此创建新内容页时选择哪个母版页并不重要。 为了保持一致性,我建议使用 Site.master

向网站添加新内容页面

图 05:向网站添加新内容页面 (单击以查看全尺寸图像)

更新 文件以 Web.sitemap 包含本课程的条目。 在 母版页的 下面 <siteMapNode> 添加以下标记,ASP.NET AJAX 课程:

<siteMapNode url="~/ChooseMasterPage.aspx" title="Choose a Master Page" />

在将任何内容添加到 ChooseMasterPage.aspx 页面之前,请花些时间更新页面的代码隐藏类,使其派生自 BasePage (而不是 System.Web.UI.Page) 。 接下来,将 DropDownList 控件添加到页面,将其 ID 属性设置为 MasterPageChoice,并添加两个值为“~/Site.master”和“~/Alternate.master”的 ListItems Text

将按钮 Web 控件添加到页面,并将其 IDText 属性分别设置为 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

我们还需要将用户选择保存到 Session 变量的代码 MyMasterPage 。 为 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 Session 变量的值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(); 
}

注意

我从事件处理程序中移出OnPreInit分配Page对象的 MasterPageFile 属性的代码,移到了两个单独的方法中。 第一个方法 SetMasterPageFile将 属性分配给 MasterPageFile 第二个方法 GetMasterPageFileFromSession返回的值。 我制作了 SetMasterPageFile 方法 virtual ,以便将来扩展 BasePage 的类可以根据需要选择性地重写它以实现自定义逻辑。 我们将在下一教程中看到重写 BasePageSetMasterPageFile 属性的示例。

完成此代码后,请访问页面 ChooseMasterPage.aspx 。 最初, Site.master 选择母版页 (见图 6) ,但用户可以从下拉列表中选择不同的母版页。

使用 Site.master 母版页显示内容页

图 06:使用 Site.master 母版页显示内容页 (单击以查看全尺寸图像)

现在使用 Alternate.master 母版页显示内容页

图 07:现在使用 Alternate.master 母版页显示内容页 (单击以查看全尺寸图像)

总结

访问内容页时,其 Content 控件将与其母版页的 ContentPlaceHolder 控件融合在一起。 内容页的母版页由 Page 类的 MasterPageFile 属性表示,该属性在初始化阶段分配给 @Page 指令的 MasterPageFile 属性。 如本教程所示,只要在 PreInit 阶段结束之前为 属性赋值 MasterPageFile 即可。 能够以编程方式指定母版页为更高级的方案打开大门,例如基于外部因素将内容页动态绑定到母版页。

编程愉快!

深入阅读

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

关于作者

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

特别感谢

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