通过


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

作者 :斯科特·米切尔

下载 PDF

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

简介

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

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

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

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

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

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

注意

本教程中创建的自定义网站地图提供程序与应用程序的体系结构和数据模型紧密耦合。 Jeff Prosise 在Storing Site Maps in SQL ServerThe SQL Site Map Provider You’ve Been Waiting For的文章中探讨了一种通用方法,用于在 SQL Server 中存储站点地图数据。

步骤 1:创建自定义网站地图供应者网页

在开始创建自定义网站地图提供程序之前,让我们先添加本教程所需的 ASP.NET 页面。 首先新增名为SiteMapProvider的新文件夹。 接下来,将以下 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 将为CategoryIDCategoryNameDescriptionNumberOfProductsBrochurePath添加一个BoundField。 编辑 GridView,使其仅包含CategoryNameDescription这两个 BoundField,并将CategoryName BoundField的HeaderText属性更新为 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 中,以显示产品数据字段。 删除除ProductNameUnitPriceSupplierName BoundFields 外的所有内容。 自定义设置这三个 BoundFields HeaderText 属性,以便分别读取 Product、Price 和 Supplier。 将 UnitPrice BoundField 格式化为货币。

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

添加一个指向ProductDetails.aspx的查看详情超链接字段

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

进行这些自定义后,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。 删除ProductIDSupplierIDCategoryID的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具有TitleUrlDescription等属性,这些属性为SiteMapNode所代表的部分提供信息。 还有一个 Key 属性,可以唯一标识层次结构中的每个 SiteMapNode,以及 ChildNodesParentNodeNextSiblingPreviousSibling 等用于建立此层次结构的属性。

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

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

图 15:每个 SiteMapNode 都具有类似 TitleUrlKey 等属性(点击查看全尺寸图像)

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

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

所有网站地图提供程序都必须派生自 SiteMapProvider 该类,其中包括站点地图提供程序所需的基本方法和属性,但省略许多实现详细信息。 第二类 StaticSiteMapProvider扩展了 SiteMapProvider 该类,并包含所需功能的更可靠的实现。 在内部,StaticSiteMapProvider的站点地图实例存储在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,并在<add>元素中的Web.config中传递任何分配给该提供程序的自定义属性,例如:<add name="name" type="type" customAttribute="value" />。 如果希望允许页面开发人员指定与网站地图提供程序相关的各种设置,而无需修改提供程序代码,则非常有用。 例如,如果我们是直接从数据库中读取类别和产品数据,而不是通过体系结构读取,那么我们可能会希望页面开发人员通过Web.config指定数据库连接字符串,而不是在提供程序的代码中使用硬编码值。 我们将在步骤 6 中生成的自定义网站地图提供程序不会重写此方法 Initialize 。 有关使用 Initialize 该方法的示例,请参阅 Jeff ProsiseSQL Server 文章中的 Storing Site Maps。

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

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

Imports System.Web
Imports System.Web.Caching
Public Class NorthwindSiteMapProvider
    Inherits StaticSiteMapProvider
    Private ReadOnly siteMapLock As New Object()
    Private root As SiteMapNode = Nothing
    Public Const CacheDependencyKey As String = "NorthwindSiteMapProviderCacheDependency"
    Public Overrides Function BuildSiteMap() As System.Web.SiteMapNode
        ' Use a lock to make this method thread-safe
        SyncLock siteMapLock
            ' First, see if we already have constructed the
            ' rootNode. If so, return it...
            If root IsNot Nothing Then
                Return root
            End If
            ' We need to build the site map!
            ' Clear out the current site map structure
            MyBase.Clear()
            ' Get the categories and products information from the database
            Dim productsAPI As New ProductsBLL()
            Dim products As Northwind.ProductsDataTable = productsAPI.GetProducts()
            ' Create the root SiteMapNode
            root = New SiteMapNode( _
                Me, "root", "~/SiteMapProvider/Default.aspx", "All Categories")
            AddNode(root)
            ' Create SiteMapNodes for the categories and products
            For Each product As Northwind.ProductsRow In products
                ' Add a new category SiteMapNode, if needed
                Dim categoryKey, categoryName As String
                Dim createUrlForCategoryNode As Boolean = True
                If product.IsCategoryIDNull() Then
                    categoryKey = "Category:None"
                    categoryName = "None"
                    createUrlForCategoryNode = False
                Else
                    categoryKey = String.Concat("Category:", product.CategoryID)
                    categoryName = product.CategoryName
                End If
                Dim categoryNode As SiteMapNode = FindSiteMapNodeFromKey(categoryKey)
                ' Add the category SiteMapNode if it does not exist
                If categoryNode Is Nothing Then
                    Dim productsByCategoryUrl As String = String.Empty
                    If createUrlForCategoryNode Then
                        productsByCategoryUrl = _
                            "~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=" & _
                            product.CategoryID
                    End If
                    categoryNode = New SiteMapNode _
                        (Me, categoryKey, productsByCategoryUrl, categoryName)
                    AddNode(categoryNode, root)
                End If
                ' Add the product SiteMapNode
                Dim productUrl As String = _
                    "~/SiteMapProvider/ProductDetails.aspx?ProductID=" & _
                    product.ProductID
                Dim productNode As New SiteMapNode _
                    (Me, String.Concat("Product:", product.ProductID), _
                    productUrl, product.ProductName)
                AddNode(productNode, categoryNode)
            Next
            ' Add a "dummy" item to the cache using a SqlCacheDependency
            ' on the Products and Categories tables
            Dim productsTableDependency As New _
                System.Web.Caching.SqlCacheDependency("NorthwindDB", "Products")
            Dim categoriesTableDependency As New _
                System.Web.Caching.SqlCacheDependency("NorthwindDB", "Categories")
            ' Create an AggregateCacheDependency
            Dim aggregateDependencies As 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, AddressOf OnSiteMapChanged)
            ' Finally, return the root node
            Return root
        End SyncLock
    End Function
    Protected Overrides Function GetRootNodeCore() As System.Web.SiteMapNode
        Return BuildSiteMap()
    End Function
    Protected Sub OnSiteMapChanged _
    (key As String, value As Object, reason As CacheItemRemovedReason)
        SyncLock siteMapLock
            If String.Compare(key, CacheDependencyKey) = 0 Then
                ' Refresh the site map
                root = Nothing
            End If
        End SyncLock
    End Sub
    Public ReadOnly Property CachedDate() As Nullable(Of DateTime)
        Get
            Dim value As Object = HttpRuntime.Cache(CacheDependencyKey)
            If value Is Nothing OrElse Not TypeOf value Is Nullable(Of DateTime) Then
                Return Nothing
            Else
                Return CType(value, Nullable(Of DateTime))
            End If
        End Get
    End Property
End Class

让我们先探索这个类的方法 BuildSiteMap,它以一条lock语句开始。 该lock语句一次只允许一个线程进入,从而序列化对其代码的访问,并防止两个并发线程相互干扰。

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

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

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

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

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

注意

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

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

注意

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

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

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

步骤 7:注册NorthwindSiteMapProvider

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

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

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

注意

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

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

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

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

将 SiteMapPath 添加到 Default.aspx

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

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.aspxProductsByCategory.aspxProductDetails.aspx 页面,例如,均包含菜单控件(见图 20)。 请参阅 ASP.NET 2.0 的复杂网站导航功能和ASP.NET 2.0 快速入门“使用网站导航控件”部分,深入了解 ASP.NET 2.0 中的导航控件和网站地图系统。

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

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

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

Dim root As SiteMapNode = SiteMap.RootNode

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

Dim root As SiteMapNode = SiteMap.Providers("name").RootNode

其中,name 是自定义网站地图提供程序的名称(例如,在我们的 Web 应用程序中为 Northwind)。

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

Dim customProvider As NorthwindSiteMapProvider = _
    TryCast(SiteMap.Providers("Northwind"), NorthwindSiteMapProvider)
If customProvider IsNot Nothing Then
    Dim lastCachedDate As Nullable(Of DateTime) = customProvider.CachedDate
    If lastCachedDate.HasValue Then
        SiteMapLastCachedDate.Text = _
            "Site map cached on: " & lastCachedDate.Value.ToString()
    Else
        SiteMapLastCachedDate.Text = "The site map is being reconstructed!"
    End If
End If

注意

请务必测试 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 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams Teach Yourself ASP.NET 2.0 in 24 Hours》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。

特别感谢

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