生成自定义数据库驱动站点地图提供程序 (C#)

作者 :Scott Mitchell

下载 PDF

ASP.NET 2.0 中的默认站点地图提供程序从静态 XML 文件检索其数据。 虽然基于 XML 的提供程序适用于许多中小型网站,但较大的 Web 应用程序需要更动态的网站地图。 在本教程中,我们将生成一个自定义站点地图提供程序,用于从业务逻辑层检索其数据,而业务逻辑层又从数据库检索数据。

简介

ASP.NET 2.0 s 站点地图功能使页面开发人员能够在某些持久性媒体(如 XML 文件中)中定义 Web 应用程序的站点地图。 定义后,可以通过 命名空间中的 System.Web 类或各种导航 Web 控件(如 SiteMapPath、Menu 和 TreeView 控件)以编程方式SiteMap访问站点地图数据。 站点地图系统使用提供程序模型,以便可以创建不同的站点地图序列化实现并将其插入 Web 应用程序。 ASP.NET 2.0 附带的默认站点地图提供程序将站点地图结构保存在 XML 文件中。 回到 母版页和网站导航 教程,我们创建了一个名为 Web.sitemap 的文件,其中包含此结构,并一直在使用每个新教程部分更新其 XML。

如果站点地图的结构相当静态(如这些教程),则默认的基于 XML 的站点地图提供程序可正常工作。 但是,在许多情况下,需要更动态的站点地图。 请考虑图 1 中显示的网站地图,其中每个类别和产品都显示为网站结构中的部分。 使用此网站地图,访问对应于根节点的网页可能会列出所有类别,而访问特定类别的网页将列出该类别的产品,并查看特定产品的网页将显示该产品的详细信息。

构成网站地图结构的类别和产品

图 1:构成网站地图结构 (类别和产品 单击以查看全尺寸图像)

虽然这种基于类别和产品的结构可以硬编码到 Web.sitemap 文件中,但每次添加、删除或重命名类别或产品时,都需要更新该文件。 因此,如果站点地图的结构是从数据库或理想情况下从应用程序体系结构的业务逻辑层检索的,则会大大简化站点地图维护。 这样,在添加、重命名或删除产品和类别时,站点地图将自动更新以反映这些更改。

由于 ASP.NET 2.0 s 站点地图序列化是在提供程序模型之上构建的,因此我们可以创建自己的自定义站点地图提供程序,从备用数据存储(如数据库或体系结构)获取其数据。 在本教程中,我们将生成一个自定义提供程序,用于从 BLL 检索其数据。 让我们开始吧!

注意

本教程中创建的自定义站点地图提供程序与应用程序的体系结构和数据模型紧密耦合。 Jeff Prosise 在 SQL Server 中存储站点地图SQL 站点地图提供程序你一直在等待的文章探讨了在 SQL Server 中存储站点地图数据的通用方法。

步骤 1:创建自定义网站地图提供程序网页

在开始创建自定义站点地图提供程序之前,让我们先添加本教程所需的 ASP.NET 页面。 首先添加名为 SiteMapProvider的新文件夹。 接下来,将以下 ASP.NET 页添加到该文件夹,确保将每个页面与 Site.master 母版页相关联:

  • Default.aspx
  • ProductsByCategory.aspx
  • ProductDetails.aspx

此外,将 CustomProviders 子文件夹添加到 App_Code 文件夹。

为网站地图 Provider-Related 教程添加 ASP.NET 页面

图 2:为网站地图添加 ASP.NET 页面 Provider-Related 教程

由于此部分只有一个教程,因此无需 Default.aspx 列出该部分的教程。 Default.aspx而是在 GridView 控件中显示类别。 我们将在步骤 2 中解决此问题。

接下来,更新 Web.sitemap 以包含对页面的 Default.aspx 引用。 具体而言,在 缓存 <siteMapNode>后面添加以下标记:

<siteMapNode 
    title="Customizing the Site Map" url="~/SiteMapProvider/Default.aspx" 
    description="Learn how to create a custom provider that retrieves the site map 
                 from the Northwind database." />

更新 Web.sitemap后,请花点时间通过浏览器查看教程网站。 左侧的菜单现在包含唯一站点地图提供程序教程的项。

站点地图现在包括站点地图提供程序教程的条目

图 3:站点地图现在包含站点地图提供程序教程的条目

本教程main重点介绍如何创建自定义站点地图提供程序以及配置 Web 应用程序以使用该提供程序。 具体而言,我们将生成一个提供程序,该提供程序返回包含根节点以及每个类别和产品的节点的站点地图,如图 1 所示。 通常,站点地图中的每个节点都可以指定一个 URL。 对于站点地图,根节点的 URL 将为 ~/SiteMapProvider/Default.aspx,这将列出数据库中的所有类别。 站点地图中的每个类别节点都有一个指向 ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID的 URL,该 URL 将列出指定 categoryID 中的所有产品。 最后,每个产品站点地图节点都将指向 ~/SiteMapProvider/ProductDetails.aspx?ProductID=productID,这将显示特定产品的详细信息。

若要开始,需要创建 Default.aspxProductsByCategory.aspxProductDetails.aspx 页面。 这些页面分别在步骤 2、3 和 4 中完成。 由于本教程的主要内容是站点地图提供程序,并且由于过去的教程已涵盖创建此类多页母版/详细信息报表,因此我们将快速完成步骤 2 到 4。 如果需要复习创建跨多个页面的主控/详细信息报表,请参阅 母版/详细信息跨两页筛选 教程。

步骤 2:显示类别列表

Default.aspx打开 文件夹中的页面SiteMapProvider,将“工具箱”中的 GridView 拖到Designer,并将其ID设置为 Categories。 在 GridView 智能标记中,将其绑定到名为 CategoriesDataSource 的新 ObjectDataSource,并对其进行配置,以便它使用 CategoriesBLL 类 s GetCategories 方法检索其数据。 由于此 GridView 仅显示类别且不提供数据修改功能,因此请将“更新”、“插入”和“删除”选项卡中的下拉列表设置为 (“无”) 。

配置 ObjectDataSource 以使用 GetCategories 方法返回类别

图 4:使用 GetCategories 方法配置 ObjectDataSource 以返回类别 (单击以查看全尺寸图像)

将“更新”、“插入”和“删除”选项卡中的 Drop-Down Lists 设置为“无 (”)

图 5:将“更新”、“插入”和“删除”选项卡中的 Drop-Down Lists 设置为“无 () (单击以查看全尺寸图像)

完成“配置数据源”向导后,Visual Studio 将为 、、CategoryNameDescriptionNumberOfProductsBrochurePath添加 BoundFieldCategoryID。 编辑 GridView,使其仅包含 CategoryNameDescription BoundFields,并将 CategoryName BoundField 的 HeaderText 属性更新为 Category 。

接下来,添加 HyperLinkField 并将其定位为最左侧的字段。 将 DataNavigateUrlFields 属性设置为 CategoryID,将 DataNavigateUrlFormatString 属性设置为 ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0}。 将 Text 属性设置为“查看产品”。

将 HyperLinkField 添加到类别网格视图

图 6:将 HyperLinkField 添加到 Categories GridView

创建 ObjectDataSource 并自定义 GridView 的字段后,两个控件声明性标记将如下所示:

<asp:GridView ID="Categories" runat="server" AutoGenerateColumns="False" 
    DataKeyNames="CategoryID" DataSourceID="CategoriesDataSource" 
    EnableViewState="False">
    <Columns>
        <asp:HyperLinkField DataNavigateUrlFields="CategoryID" 
            DataNavigateUrlFormatString=
                "~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0}"
            Text="View Products" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            SortExpression="CategoryName" />
        <asp:BoundField DataField="Description" HeaderText="Description" 
            SortExpression="Description" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetCategories" 
    TypeName="CategoriesBLL"></asp:ObjectDataSource>

图 7 显示了 Default.aspx 通过浏览器查看时的情况。 单击类别的“查看产品”链接会将你转到 ProductsByCategory.aspx?CategoryID=categoryID,我们将在步骤 3 中生成。

每个类别随视图产品链接一起列出

图 7:列出每个类别以及视图产品链接 (单击以查看全尺寸图像)

步骤 3:列出所选类别的产品

ProductsByCategory.aspx打开页面并添加 GridView,将其ProductsByCategory命名为 。 在其智能标记中,将 GridView 绑定到名为 ProductsByCategoryDataSource的新 ObjectDataSource。 将 ObjectDataSource 配置为使用 ProductsBLL 类 s GetProductsByCategoryID(categoryID) 方法,并将下拉列表设置为在“UPDATE”、“INSERT”和“DELETE”选项卡中 (None) 。

使用 ProductsBLL 类 getProductsByCategoryID (categoryID) 方法

图 8:使用 ProductsBLL 类方法 GetProductsByCategoryID(categoryID) (单击以查看全尺寸图像)

配置数据源向导中的最后一步会提示输入 categoryID 的参数源。 由于此信息是通过 querystring 字段 CategoryID传递的,因此从下拉列表中选择“QueryString”,然后在“QueryStringField”文本框中输入 CategoryID,如图 9 所示。 单击“完成”以完成向导。

将 CategoryID 查询字符串字段用于 categoryID 参数

图 9:使用 CategoryIDcategoryID 参数的查询字符串字段 (单击以查看全尺寸图像)

完成向导后,Visual Studio 会将相应的 BoundFields 和 CheckBoxField 添加到 GridView 的产品数据字段。 删除除 、 UnitPriceSupplierName BoundFields 外ProductName的所有字段。 自定义这三个 BoundFields HeaderText 属性,以分别读取 Product、Price 和 Supplier。 将 UnitPrice BoundField 格式设置为货币。

接下来,添加 HyperLinkField 并将其移动到最左侧的位置。 将其 Text 属性设置为“查看详细信息”,将其 DataNavigateUrlFields 属性设置为 ProductID,将其 DataNavigateUrlFormatString 属性设置为 ~/SiteMapProvider/ProductDetails.aspx?ProductID={0}

添加指向ProductDetails.aspx的视图详细信息 HyperLinkField

图 10:添加指向的视图详细信息 HyperLinkField ProductDetails.aspx

进行这些自定义后,GridView 和 ObjectDataSource 的声明性标记应如下所示:

<asp:GridView ID="ProductsByCategory" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ProductsByCategoryDataSource" 
    EnableViewState="False">
    <Columns>
        <asp:HyperLinkField DataNavigateUrlFields="ProductID" 
            DataNavigateUrlFormatString=
                "~/SiteMapProvider/ProductDetails.aspx?ProductID={0}"
            Text="View Details" />
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}" 
            HeaderText="Price" HtmlEncode="False" 
            SortExpression="UnitPrice" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier" 
            ReadOnly="True" SortExpression="SupplierName" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsByCategoryDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProductsByCategoryID" TypeName="ProductsBLL">
    <SelectParameters>
        <asp:QueryStringParameter Name="categoryID" 
            QueryStringField="CategoryID" Type="Int32" />
    </SelectParameters>
</asp:ObjectDataSource>

返回到通过浏览器查看 Default.aspx ,然后单击饮料的“查看产品”链接。 这会带你到 ProductsByCategory.aspx?CategoryID=1,显示 Northwind 数据库中属于饮料类别的产品的名称、价格和供应商 (请参阅图 11) 。 请随时进一步增强此页面,以包含一个链接,用于将用户返回到类别列表页 (Default.aspx) ,以及显示所选类别名称和说明的 DetailsView 或 FormView 控件。

显示饮料名称、价格和供应商

图 11:单击查看 全尺寸图像 (显示饮料名称、价格和供应商)

步骤 4:显示产品详细信息

最后一页 ProductDetails.aspx显示所选产品的详细信息。 打开ProductDetails.aspx“详细信息视图”并将其从“工具箱”拖到Designer。 将 DetailsView 的 ID 属性设置为 ProductInfo 并清除其 HeightWidth 属性值。 在其智能标记中,将 DetailsView 绑定到名为 ProductDataSource的新 ObjectDataSource,将 ObjectDataSource 配置为从 ProductsBLL 类 s GetProductByProductID(productID) 方法拉取其数据。 与步骤 2 和 3 中创建的先前网页一样,将“更新”、“插入”和“删除”选项卡中的下拉列表设置为 (“无”) 。

配置 ObjectDataSource 以使用 GetProductByProductID (productID) 方法

图 12:将 ObjectDataSource 配置为使用 GetProductByProductID(productID) 方法 (单击以查看全尺寸图像)

配置数据源向导的最后一步会提示输入 productID 参数的源。 由于此数据来自查询字符串字段 ProductID,因此将下拉列表设置为 QueryString,并将 QueryStringField 文本框设置为 ProductID。 最后,单击“完成”按钮以完成向导。

配置 productID 参数以从 ProductID 查询字符串字段拉取其值

图 13:配置 productID 参数以从 ProductID 查询字符串字段拉取其值 (单击以查看全尺寸图像)

完成“配置数据源”向导后,Visual Studio 将在产品数据字段的 DetailsView 中创建相应的 BoundFields 和 CheckBoxField。 ProductID删除 、 SupplierIDCategoryID BoundFields,并根据需要配置剩余字段。 经过一些美观配置后,我的 DetailsView 和 ObjectDataSource 声明性标记如下所示:

<asp:DetailsView ID="ProductInfo" runat="server" AutoGenerateRows="False" 
    DataKeyNames="ProductID" DataSourceID="ProductDataSource" 
    EnableViewState="False">
    <Fields>
        <asp:BoundField DataField="ProductName" HeaderText="Product" 
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier" 
            ReadOnly="True" SortExpression="SupplierName" />
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit" 
            SortExpression="QuantityPerUnit" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}" 
            HeaderText="Price" HtmlEncode="False" 
            SortExpression="UnitPrice" />
        <asp:BoundField DataField="UnitsInStock" HeaderText="Units In Stock" 
            SortExpression="UnitsInStock" />
        <asp:BoundField DataField="UnitsOnOrder" HeaderText="Units On Order" 
            SortExpression="UnitsOnOrder" />
        <asp:BoundField DataField="ReorderLevel" HeaderText="Reorder Level" 
            SortExpression="ReorderLevel" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" 
            SortExpression="Discontinued" />
    </Fields>
</asp:DetailsView>
<asp:ObjectDataSource ID="ProductDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProductByProductID" TypeName="ProductsBLL">
    <SelectParameters>
        <asp:QueryStringParameter Name="productID" 
            QueryStringField="ProductID" Type="Int32" />
    </SelectParameters>
</asp:ObjectDataSource>

若要测试此页面,请返回 , Default.aspx 然后单击“查看饮料”类别的产品。 在饮料产品列表中,单击柴茶的“查看详细信息”链接。 这将带你到 ProductDetails.aspx?ProductID=1,其中显示了柴茶的详细信息 (请参阅图 14) 。

显示柴茶供应商、类别、价格和其他信息

图 14:显示柴茶供应商、类别、价格和其他信息 (单击以查看全尺寸图像)

步骤 5:了解站点地图提供程序的内部工作

站点地图在 Web 服务器的内存中表示为构成层次结构的实例的 SiteMapNode 集合。 必须恰好有一个根,所有非根节点必须恰好有一个父节点,并且所有节点可能具有任意数量的子节点。 每个对象表示 SiteMapNode 网站结构中的一个部分;这些部分通常具有相应的网页。 因此, SiteMapNode具有 、 和 DescriptionTitleUrl属性,这些属性为 表示的 节SiteMapNode提供信息。 还有一个唯一Key标识SiteMapNode层次结构中的每个属性,以及用于建立此层次结构 、ParentNodeNextSiblingPreviousSibling等的属性ChildNodes

图 15 显示了图 1 中的常规站点地图结构,但更详细地绘制了实现细节。

每个 SiteMapNode 都有标题、URL、密钥等属性

图 15:每个属性 SiteMapNode (如 TitleUrlKey等) (单击以查看全尺寸图像)

可通过 命名空间中的 System.Web类访问SiteMap站点地图。 此类的 RootNode 属性返回站点映射的根 SiteMapNode 实例; CurrentNode 返回 SiteMapNodeUrl 属性与当前请求页面的 URL 匹配的 。 此类由 ASP.NET 2.0 秒导航 Web 控件在内部使用。

SiteMap访问类属性时,它必须将站点地图结构从某些永久性介质序列化到内存中。 但是,站点地图序列化逻辑不是硬编码到 类中 SiteMap 。 相反,在运行时, SiteMap 类确定用于序列化的站点地图 提供程序 。 默认情况下, XmlSiteMapProvider 使用 类 ,它从格式正确的 XML 文件中读取站点地图结构。 但是,通过一些工作,我们可以创建自己的自定义站点地图提供程序。

所有站点地图提供程序都必须派生自 SiteMapProvider,该类包括站点地图提供程序所需的基本方法和属性,但省略了许多实现详细信息。 第二个类 StaticSiteMapProvider扩展类 SiteMapProvider ,并包含所需功能的更可靠的实现。 在内部, StaticSiteMapProvider 将站点地图的实例存储在 SiteMapNode 中,Hashtable并提供方法(如 AddNode(child, parent)),RemoveNode(siteMapNode),以及Clear()向内部 Hashtable添加和删除 SiteMapNode 的方法。 XmlSiteMapProvider 派生自 StaticSiteMapProvider

创建扩展 的 StaticSiteMapProvider自定义站点地图提供程序时,必须重写两个抽象方法: BuildSiteMapGetRootNodeCoreBuildSiteMap顾名思义,负责从永久性存储加载站点地图结构并在内存中构造它。 GetRootNodeCore 返回站点映射中的根节点。

Web 应用程序必须先在应用程序的配置中注册站点地图提供程序,然后才能使用站点地图提供程序。 默认情况下,类 XmlSiteMapProvider 是使用名称 AspNetXmlSiteMapProvider注册的。 若要注册其他站点地图提供程序,请将以下标记添加到 Web.config

<configuration>
    <system.web>
        ...
        <siteMap defaultProvider="defaultProviderName">
          <providers>
            <add name="name" type="type" />
          </providers>
        </siteMap>
    </system.web>
</configuration>

名称值将人类可读的名称分配给提供程序,而 type 指定站点地图提供程序的完全限定类型名称。 在创建自定义站点地图提供程序后,我们将在步骤 7 中探索名称和类型值的具体值。

站点地图提供程序类在首次从 SiteMap 类访问时实例化,并在 Web 应用程序的生存期内保留在内存中。 由于只能从多个并发网站访问者调用站点地图提供程序的一个实例,因此提供程序的方法必须 线程安全

出于性能和可伸缩性原因,请务必缓存内存中站点映射结构并返回此缓存结构,而不是在每次调用方法时 BuildSiteMap 重新创建它。 BuildSiteMap 根据页面上使用的导航控件和站点地图结构的深度,每个用户的每个页面请求可能会调用多次。 在任何情况下,如果我们不缓存 BuildSiteMap 站点地图结构,那么每次调用它时,都需要从体系结构 (重新检索产品和类别信息,这将导致对数据库) 查询。 如前面的缓存教程中所述,缓存的数据可能会过时。 为了解决此问题,可以使用基于时间或 SQL 缓存依赖项的过期。

注意

站点地图提供程序可以选择替代 Initialize 方法Initialize在首次实例化站点地图提供程序时调用,并在 元素中Web.config<add>传递分配给提供程序的任何自定义属性,例如:<add name="name" type="type" customAttribute="value" />。 如果希望允许页面开发人员指定与站点地图提供程序相关的各种设置,而无需修改提供程序的代码,这非常有用。 例如,如果我们直接从数据库而不是通过体系结构读取类别和产品数据,我们很可能希望让页面开发人员指定数据库连接字符串Web.config,而不是在提供程序的代码中使用硬编码值。 我们将在步骤 6 中生成的自定义站点地图提供程序不会覆盖此方法 Initialize 。 有关使用 Initialize 方法的示例,请参阅 Jeff Prosise s Storing Site Maps in SQL Server一文。

步骤 6:创建自定义站点地图提供程序

若要创建从 Northwind 数据库中的类别和产品生成站点地图的自定义站点地图提供程序,我们需要创建一个扩展 的 StaticSiteMapProvider类。 在步骤 1 中,我要求你在 文件夹中App_Code添加一个CustomProviders文件夹 - 将一个新类添加到名为 的NorthwindSiteMapProvider此文件夹中。 将以下代码添加到 NorthwindSiteMapProvider 类:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Web.Caching;
public class NorthwindSiteMapProvider : StaticSiteMapProvider
{
    private readonly object siteMapLock = new object();
    private SiteMapNode root = null;
    public const string CacheDependencyKey = 
        "NorthwindSiteMapProviderCacheDependency";
    public override SiteMapNode BuildSiteMap()
    {
        // Use a lock to make this method thread-safe
        lock (siteMapLock)
        {
            // First, see if we already have constructed the
            // rootNode. If so, return it...
            if (root != null)
                return root;
            // We need to build the site map!
            
            // Clear out the current site map structure
            base.Clear();
            // Get the categories and products information from the database
            ProductsBLL productsAPI = new ProductsBLL();
            Northwind.ProductsDataTable products = productsAPI.GetProducts();
            // Create the root SiteMapNode
            root = new SiteMapNode(
                this, "root", "~/SiteMapProvider/Default.aspx", "All Categories");
            AddNode(root);
            // Create SiteMapNodes for the categories and products
            foreach (Northwind.ProductsRow product in products)
            {
                // Add a new category SiteMapNode, if needed
                string categoryKey, categoryName;
                bool createUrlForCategoryNode = true;
                if (product.IsCategoryIDNull())
                {
                    categoryKey = "Category:None";
                    categoryName = "None";
                    createUrlForCategoryNode = false;
                }
                else
                {
                    categoryKey = string.Concat("Category:", product.CategoryID);
                    categoryName = product.CategoryName;
                }
                SiteMapNode categoryNode = FindSiteMapNodeFromKey(categoryKey);
                // Add the category SiteMapNode if it does not exist
                if (categoryNode == null)
                {
                    string productsByCategoryUrl = string.Empty;
                    if (createUrlForCategoryNode)
                        productsByCategoryUrl = 
                            "~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=" 
                            + product.CategoryID;
                    categoryNode = new SiteMapNode(
                        this, categoryKey, productsByCategoryUrl, categoryName);
                    AddNode(categoryNode, root);
                }
                // Add the product SiteMapNode
                string productUrl = 
                    "~/SiteMapProvider/ProductDetails.aspx?ProductID=" 
                    + product.ProductID;
                SiteMapNode productNode = new SiteMapNode(
                    this, string.Concat("Product:", product.ProductID), 
                    productUrl, product.ProductName);
                AddNode(productNode, categoryNode);
            }
            
            // Add a "dummy" item to the cache using a SqlCacheDependency
            // on the Products and Categories tables
            System.Web.Caching.SqlCacheDependency productsTableDependency = 
                new System.Web.Caching.SqlCacheDependency("NorthwindDB", "Products");
            System.Web.Caching.SqlCacheDependency categoriesTableDependency = 
                new System.Web.Caching.SqlCacheDependency("NorthwindDB", "Categories");
            // Create an AggregateCacheDependency
            System.Web.Caching.AggregateCacheDependency aggregateDependencies = 
                new System.Web.Caching.AggregateCacheDependency();
            aggregateDependencies.Add(productsTableDependency, categoriesTableDependency);
            // Add the item to the cache specifying a callback function
            HttpRuntime.Cache.Insert(
                CacheDependencyKey, DateTime.Now, aggregateDependencies, 
                Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, 
                CacheItemPriority.Normal, 
                new CacheItemRemovedCallback(OnSiteMapChanged));
            // Finally, return the root node
            return root;
        }
    }
    protected override SiteMapNode GetRootNodeCore()
    {
        return BuildSiteMap();
    }
    protected void OnSiteMapChanged(string key, object value, CacheItemRemovedReason reason)
    {
        lock (siteMapLock)
        {
            if (string.Compare(key, CacheDependencyKey) == 0)
            {
                // Refresh the site map
                root = null;
            }
        }
    }
    public DateTime? CachedDate
    {
        get
        {
            return HttpRuntime.Cache[CacheDependencyKey] as DateTime?;
        }
    }
}

让我们从探索此类方法 BuildSiteMap 开始,该方法以 lock 语句开头。 语句 lock 一次只允许一个线程进入,从而序列化对其代码的访问,并防止两个并发线程单步执行另一个脚趾。

类级 SiteMapNode 变量 root 用于缓存站点地图结构。 首次构造站点地图时,或在修改基础数据后首次构造站点地图时, root 将为 null ,并且将构造站点地图结构。 在构造过程中将站点地图的根节点分配给 root ,以便下次调用此方法时, root 不会是 null。 因此,只要 root 不是 null ,站点地图结构将返回到调用方,而无需重新创建它。

如果 root 为 null,则从产品和类别信息创建站点地图结构。 站点地图是通过创建 SiteMapNode 实例,然后通过调用 StaticSiteMapProvider 类 s 方法形成层次结构来构建的 AddNodeAddNode 执行内部记账,将各种 SiteMapNode 实例存储在 中 Hashtable。 在开始构造层次结构之前,首先调用 Clear 方法,该方法从内部 Hashtable中清除元素。 接下来,类 ProductsBLLGetProducts 方法和生成的 ProductsDataTable 存储在局部变量中。

站点地图的构造首先创建根节点并将其分配给 root。 此处使用的 构造函数的SiteMapNode重载以及在整个过程中BuildSiteMap传递以下信息:

  • 对站点地图提供程序的引用 (this) 。
  • s SiteMapNodeKey。 对于每个 SiteMapNode,此必需值必须是唯一的。
  • s SiteMapNodeUrlUrl 是可选的,但如果提供,则每个 SiteMapNode 值必须是唯一的 Url
  • 需要SiteMapNode的 。Title

方法 AddNode(root) 调用将 作为根添加到 SiteMapNoderoot 站点地图。 接下来, ProductRow 枚举 中的每个 ProductsDataTable 。 如果已存在 SiteMapNode 当前产品类别的 ,则会引用它。 否则,将创建类别的新SiteMapNode,并通过 AddNode(categoryNode, root) 方法调用将其添加为 的SiteMapNode``root子级。 找到或创建相应的类别SiteMapNode节点后,会为当前产品创建 ,SiteMapNode并通过 添加为类别的子级SiteMapNodeAddNode(productNode, categoryNode)。 请注意,类别SiteMapNode的 属性值为 ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID ,而产品 SiteMapNode 属性Url被分配。~/SiteMapNode/ProductDetails.aspx?ProductID=productIDUrl

注意

具有其CategoryID数据库NULL值的产品将分组到属性设置为 None 且属性设置为空字符串的Url类别SiteMapNodeTitle下。 我决定将 设置为 Url 空字符串, ProductBLL 因为 类方法 GetProductsByCategory(categoryID) 当前缺少仅返回具有 NULLCategoryID 值的产品的功能。 此外,我想演示导航控件如何呈现 SiteMapNode 缺少其 Url 属性值的 。 建议扩展本教程,以便 None SiteMapNode 的 属性指向 ProductsByCategory.aspx,但仅显示具有CategoryIDNULLUrl的产品。

构造站点地图后,通过 AggregateCacheDependency 对象使用 和 Products 表上的 Categories SQL 缓存依赖项将任意对象添加到数据缓存中。 我们在前面的教程“使用 SQL 缓存依赖项”中探讨了 如何使用 SQL 缓存依赖项。 但是,自定义站点地图提供程序使用我们尚未探索的数据缓存 Insert 方法的重载。 此重载接受从缓存中删除对象时调用的委托作为其最终输入参数。 具体而言,我们传入一个新的 CacheItemRemovedCallback 委托 ,该委托指向 OnSiteMapChanged 类中 NorthwindSiteMapProvider 进一步定义的 方法。

注意

站点地图的内存中表示形式通过类级变量 root进行缓存。 由于自定义站点地图提供程序类只有一个实例,并且该实例在 Web 应用程序中的所有线程之间共享,因此此类变量用作缓存。 方法BuildSiteMap还使用数据缓存,但仅作为在 或 Products 表中的基础数据库数据Categories发生更改时接收通知的方法。 请注意,放入数据缓存中的值只是当前日期和时间。 实际站点地图数据 不会 放入数据缓存中。

方法 BuildSiteMap 通过返回站点地图的根节点完成。

其余方法相当简单。 GetRootNodeCore 负责返回根节点。 由于 BuildSiteMap 返回根, GetRootNodeCore 只需返回 BuildSiteMap 返回值。 删除 OnSiteMapChanged 缓存项时, 方法将设置 rootnull 。 将根设置回 null,下次 BuildSiteMap 调用 时,将重新生成站点地图结构。 最后, CachedDate 属性返回存储在数据缓存中的日期和时间值(如果存在此类值)。 页面开发人员可以使用此属性来确定上次缓存站点地图数据时。

步骤 7:注册NorthwindSiteMapProvider

为了使 Web 应用程序能够使用步骤 6 中创建的NorthwindSiteMapProvider站点地图提供程序,我们需要在 的 Web.config节中<siteMap>注册它。 具体而言,在 中的 Web.config元素中添加以下标记<system.web>

<siteMap defaultProvider="AspNetXmlSiteMapProvider">
  <providers>
    <add name="Northwind" type="NorthwindSiteMapProvider" />
  </providers>
</siteMap>

此标记执行两项操作:第一,它指示内置 AspNetXmlSiteMapProvider 是默认站点地图提供程序;第二,它使用人类友好名称 Northwind 注册在步骤 6 中创建的自定义站点地图提供程序。

注意

对于位于应用程序 文件夹的 App_Code 站点地图提供程序,属性的值 type 只是类名。 或者,可以在单独的类库项目中创建自定义站点地图提供程序,并将已编译的程序集放置在 Web 应用程序的 /Bin 目录中。 在这种情况下,type属性值将为 NamespaceClassName,AssemblyName

更新 Web.config后,请花点时间在浏览器中查看教程中的任何页面。 请注意,左侧的导航界面仍显示 中 Web.sitemap定义的部分和教程。 这是因为我们保留 AspNetXmlSiteMapProvider 为默认提供程序。 为了创建使用 NorthwindSiteMapProvider的导航用户界面元素,我们需要显式指定应使用 Northwind 站点地图提供程序。 我们将在步骤 8 中了解如何完成此操作。

步骤 8:使用自定义站点地图提供程序显示站点地图信息

在 中创建 Web.config并注册自定义站点地图提供程序后,我们准备将导航控件添加到 文件夹中的 Default.aspxProductsByCategory.aspxProductDetails.aspx 页面 SiteMapProvider 。 首先打开页面,Default.aspx并将 从工具箱拖动SiteMapPath到Designer。 SiteMapPath 控件位于“工具箱”的“导航”部分中。

将 SiteMapPath 添加到 Default.aspx

图 16:将 SiteMapPath 添加到 Default.aspx (单击以查看全尺寸图像)

SiteMapPath 控件显示痕迹导航,指示当前页在网站地图中的位置。 我们在母版页 和网站导航 教程中将 SiteMapPath 添加到母版页顶部。

花点时间通过浏览器查看此页面。 图 16 中添加的 SiteMapPath 使用默认站点地图提供程序,从 Web.sitemap拉取其数据。 因此,痕迹导航显示主页 > 自定义站点地图,就像右上角的痕迹导航一样。

痕迹导航使用默认站点地图提供程序

图 17:痕迹导航使用默认站点地图提供程序 (单击以查看全尺寸图像)

若要使图 16 中添加的 SiteMapPath 使用我们在步骤 6 中创建的自定义站点地图提供程序,请将其 SiteMapProvider 属性设置为 Northwind,即我们在 中Web.config分配给 的名称NorthwindSiteMapProvider。 遗憾的是,Designer继续使用默认站点地图提供程序,但如果在更改此属性后通过浏览器访问页面,你将看到痕迹导航现在使用自定义站点地图提供程序。

显示痕迹导航如何显示自定义站点地图提供程序的屏幕截图。

图 18:痕迹导航现在使用自定义站点地图提供程序 NorthwindSiteMapProvider (单击以查看全尺寸图像)

SiteMapPath 控件在 ProductsByCategory.aspxProductDetails.aspx 页面中显示功能更强大的用户界面。 向这些页面添加 SiteMapPath,同时将 SiteMapProvider 两者中的 属性设置为 Northwind。 单击 Default.aspx 饮料的“查看产品”链接,然后单击柴茶的“查看详细信息”链接。 如图 19 所示,痕迹导航包括当前网站地图部分 ( 柴茶 ) 及其祖先:饮料和所有类别。

显示痕迹导航如何显示当前站点地图部分 (柴茶) 及其祖先 (饮料和所有类别) 的屏幕截图。

图 19:痕迹导航现在使用自定义站点地图提供程序 NorthwindSiteMapProvider (单击以查看全尺寸图像)

除了 SiteMapPath 之外,还可以使用其他导航用户界面元素,例如 Menu 和 TreeView 控件。 Default.aspx本教程下载中的 、 ProductsByCategory.aspxProductDetails.aspx 页面,例如,所有包括菜单控件 (见图 20) 。 若要更深入地了解 ASP.NET 2.0 中的导航控件和网站地图系统,请参阅 ASP.NET 2.0 快速入门 ASP.NET 2.0 的复杂网站导航功能和使用网站导航控件部分。

菜单控件Lists每个类别和产品

图 20:菜单控件Lists每个类别和产品 (单击以查看全尺寸图像)

如本教程前面所述,可以通过 类以编程方式 SiteMap 访问站点地图结构。 以下代码返回默认提供程序的根 SiteMapNode

SiteMapNode root = SiteMap.RootNode;

AspNetXmlSiteMapProvider由于 是应用程序的默认提供程序,因此上述代码将返回 中Web.sitemap定义的根节点。 若要引用非默认站点地图提供程序,请使用SiteMap属性Providers,如下所示:

SiteMapNode root = SiteMap.Providers["name"].RootNode;

其中 ,name 是自定义站点地图提供程序 ( Northwind 的名称,用于 Web 应用程序) 。

若要访问特定于站点地图提供程序的成员,请使用 SiteMap.Providers["name"] 检索提供程序实例,然后将其强制转换为适当的类型。 例如,若要在 ASP.NET 页中显示 NorthwindSiteMapProvider 属性 CachedDate ,请使用以下代码:

NorthwindSiteMapProvider customProvider = 
    SiteMap.Providers["Northwind"] as NorthwindSiteMapProvider;
if (customProvider != null)
{
    DateTime? lastCachedDate = customProvider.CachedDate;
    if (lastCachedDate != null)
        LabelID.Text = "Site map cached on: " + lastCachedDate.Value.ToString();
    else
        LabelID.Text = "The site map is being reconstructed!";
}

注意

请务必测试 SQL 缓存依赖项功能。 访问 Default.aspxProductsByCategory.aspxProductDetails.aspx 页面后,转到“编辑”、“插入”和“删除”部分中的其中一个教程,然后编辑类别或产品名称。 然后返回到 文件夹中的其中一个页面 SiteMapProvider 。 假设轮询机制已过足够的时间来记录对基础数据库的更改,则应更新站点地图以显示新的产品或类别名称。

总结

ASP.NET 2.0 s 站点地图功能包括类 SiteMap 、许多内置导航 Web 控件和默认站点地图提供程序,该提供程序需要将站点地图信息保存到 XML 文件中。 为了使用来自其他源(例如数据库、应用程序体系结构或远程 Web 服务)的站点地图信息,我们需要创建自定义站点地图提供程序。 这涉及到创建直接或间接派生自 类的 SiteMapProvider 类。

在本教程中,我们了解了如何创建自定义站点地图提供程序,该提供程序基于从应用程序体系结构中剔除的产品和类别信息的网站地图。 我们的提供程序扩展了 StaticSiteMapProvider 类,并需要创建一个 BuildSiteMap 方法来检索数据、构造站点地图层次结构,并将生成的结构缓存在类级变量中。 我们在修改基础 CategoriesProducts 数据时,将 SQL 缓存依赖项与回调函数结合使用,使缓存的结构失效。

编程愉快!

深入阅读

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

关于作者

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

特别感谢

本教程系列由许多有用的审阅者查看。 本教程的主要审阅者是 Dave Gardner、Zack Jones、Teresa Murphy 和 Bernadette Leigh。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处放置一行 mitchell@4GuysFromRolla.com。