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

作者 :斯科特·米切尔

下载 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 s Storing Site Maps in SQL Server and The SQL Site Map Provider you一直在等待 的文章中检查一种通用方法,用于在 SQL Server 中存储站点地图数据。

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

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

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

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

为网站地图提供程序相关的教程添加 ASP.NET 页面

图 2:为网站地图提供程序相关的教程添加 ASP.NET 页

由于本部分只有一个教程,因此无需 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:网站地图现在包括网站地图提供程序教程的条目

本教程的主要重点是说明创建自定义网站地图提供程序,以及配置 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 从工具箱拖到设计器上,将其ID设置为 Categories。 从 GridView 智能标记中,将其绑定到名为 CategoriesDataSource 并配置的新 ObjectDataSource,以便它使用 CategoriesBLL 类的方法 GetCategories 检索其数据。 由于此 GridView 只显示类别,并且不提供数据修改功能,因此请将 UPDATE、INSERT 和 DELETE 选项卡中的下拉列表设置为 “无”。

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

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

将 UPDATE、INSERT 和 DELETE 选项卡中的下拉列表设置为 (无)

图 5:将 UPDATE、INSERT 和 DELETE 选项卡中的下拉列表设置为(无)(单击以查看全尺寸图像

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

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

将 HyperLinkField 添加到类别 GridView

图 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 选项卡中的“无”。

使用 ProductsBLL 类 s GetProductsByCategoryID(categoryID) 方法

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

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

将 CategoryID Querystring 字段用于 categoryID 参数

图 9:对 categoryID 参数使用 CategoryID Querystring 字段单击以查看全尺寸图像

完成向导后,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 并将 DetailsView 从工具箱拖到设计器上。 将 DetailsView 属性 ID 设置为 ProductInfo 并清除其 HeightWidth 属性值。 从智能标记中,将 DetailsView 绑定到名为 ProductDataSource的新 ObjectDataSource,将 ObjectDataSource 配置为从 ProductsBLL 类方法 GetProductByProductID(productID) 拉取其数据。 与在步骤 2 和 3 中创建的上一个网页一样,请将 UPDATE、INSERT 和 DELETE 选项卡中的下拉列表设置为 (None) 。

将 ObjectDataSource 配置为使用 GetProductByProductID(productID) 方法

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

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

将 productID 参数配置为从 ProductID Querystring 字段拉取其值

图 13:将 productID 参数配置为从 ProductID Querystring 字段拉取其值(单击以查看全尺寸图像

完成“配置数据源”向导后,Visual Studio 将在 DetailsView 中为产品数据字段创建相应的 BoundFields 和 CheckBoxField。 ProductID删除和 SupplierIDBoundFields,CategoryID并在看到合适的情况下配置剩余字段。 经过一些审美配置,我的 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具有属性,TitleUrl并且Description提供表示部分SiteMapNode的信息。 还有一个唯一 Key 标识 SiteMapNode 层次结构中的每个属性,以及用于建立此层次结构 ChildNodesParentNodeNextSibling等等 PreviousSibling的属性。

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

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

图 15:每个SiteMapNode属性都具有“类似Title”、“UrlKey等”属性(单击可查看全尺寸图像

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

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

所有网站地图提供程序都必须派生自 SiteMapProvider 该类,其中包括站点地图提供程序所需的基本方法和属性,但省略许多实现详细信息。 第二类 StaticSiteMapProvider扩展了 SiteMapProvider 该类,并包含所需功能的更可靠的实现。 在内部,站点StaticSiteMapProvider地图的实例存储在SiteMapNode一个Hashtable站点地图中,并提供如下AddNode(child, parent)方法:RemoveNode(siteMapNode),Clear()内部Hashtable添加和移除SiteMapNodeXmlSiteMapProvider 派生自 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>

名称值为提供程序分配可读名称,而类型指定站点地图提供程序的完全限定类型名称。 创建自定义网站地图提供程序后,我们将探索步骤 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 中,我要求你在文件夹中添加一个 CustomProviders 文件夹 App_Code - 向名为 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 用于缓存站点地图结构。 首次构造站点地图时,或在修改基础数据后首次构造站点地图时, rootnull 构造站点地图结构。 站点映射的根节点在 root 施工过程中被分配到,以便下次调用 root 此方法时,不会 null。 因此,只要 root 不是 null 站点地图结构,就不会返回给调用方,而无需重新创建它。

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

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

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

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

注意

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

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

注意

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

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

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

步骤 7:注册NorthwindSiteMapProvider

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

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

此标记执行两项操作:首先,它指示内置 AspNetXmlSiteMapProvider 是默认站点地图提供程序;其次,它注册在步骤 6 中创建的自定义网站地图提供程序,其名称为 Northwind。

注意

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

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

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

创建并注册 Web.config自定义网站地图提供程序后,我们便可以将导航控件添加到文件夹中的 Default.aspx导航 ProductsByCategory.aspx控件和 ProductDetails.aspx 页面 SiteMapProvider 。 首先打开页面, Default.aspx 然后将一个 SiteMapPath 从工具箱拖动到设计器上。 SiteMapPath 控件位于工具箱的导航部分中。

将 SiteMapPath 添加到 Default.aspx

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

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

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

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

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

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

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

图 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 中的导航控件和网站地图系统。

菜单控件列出每个类别和产品

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

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

SiteMapNode root = SiteMap.RootNode;

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

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

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

若要访问特定于站点地图提供程序的成员,请使用 SiteMap.Providers["name"] 检索提供程序实例,然后将其强制转换为适当的类型。 例如,若要在 ASP.NET 页中显示 NorthwindSiteMapProvider s 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 缓存依赖项使缓存结构失效。

快乐编程!

深入阅读

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

关于作者

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

特别感谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是戴夫·加德纳、扎克·琼斯、特蕾莎·墨菲和伯纳黛特·利。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请把我扔一条线。mitchell@4GuysFromRolla.com