创建定制的数据库驱动的站点地图提供程序61

简介

ASP.NET 2.0 的站点地图功能允许页面开发者在一些持久介质中,如XML 文件,定义 web 应用的站点地图。一旦进行了定义,通过 System.Web 命名空间 的 SiteMap 类 或诸如 SiteMapPath 、 Menu 和 TreeView 控件的多种导航 Web 控件就可通过编码访问站点地图数据。站点地图系统使用 提供程序模型 以便能够创建不同的站点地图序列化实现并插入到 web 应用。与 ASP.NET 2.0 一起提供的默认站点地图提供程序将站点地图结构保留在 XML 文件中。我们曾在 母版页与网站导航 教程中创建了一个包含该结构的名为 Web.sitemap 的文件,并随新的教程内容更新其 XML 。

如果站点地图的结构相对静态 , 比如本系列教程中的站点地图 , 则默认的基于XML 的站点地图提供程序可正常工作。然而在多数场景下,需要更动态的站点地图。见图 1 中给出的站点地图,每个类别和产品都作为网站结构的一部分而出现。有了该站点地图,访问与根节点对应的 web 页面可能会列出所有的类别。相反,访问特定类别的 web 页面会列出该类别的产品,访问特定产品的 web 页面会列出该产品的详细信息。

图1 : 类别和产品构成的站点地图结构

这种基于类别和产品的结构可以通过硬编码的方式添加到Web.sitemap 文件 , 每次添加、删除或重命名类别或产品时都需要更新该文件。因此,如果从数据库或更理想地从应用架构的业务逻辑层获取其结构,则站点地图的维护将大大简化。这样的话,添加、重命名或删除产品和类别时,站点地图将自动更新以反映这些变化。

由于ASP.NET 2.0 的站点地图序列化建立在提供程序模型上面 , 我们可创建一个定制站点地图提供程序 , 从诸如数据库或架构的可选数据存储中获取数据。在本教程中,我们将建立从 BLL 获取数据的定制提供程序。让我们开始吧 !

注意 : 本教程中创建的定制网站提供程序与应用的架构和数据模型是紧耦合的。 Jeff Prosise 的 在 SQL Server 中存储站点地图和 应运而生的 SQL 站点地图提供程序 文章详细介绍了在 SQL Server 中存储站点地图数据的一般方法。

步骤1 : 创建定制站点地图提供程序Web 页面

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

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

同样 , 在App_Code 文件夹中添加 CustomProviders 子文件夹。

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

由于这部分只有一个教程 ,所以我们 不必让 Default.aspx 列出本部分的教程 。相反, Default.aspx 将显示 GridView 控件中的类别。我们在步骤 2 中对此进行探讨。

接下来 , 更新Web.sitemap 以包括对 Default.aspx 页面的引用。具体而言 , 在 “Caching” <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 ,它将列出指定 categoryID 的所有产品。最后,每个产品站点地图节点将指向 ~/SiteMapProvider/ProductDetails.aspx?ProductID=productID ,它将显示指定产品的详细信息。

首先 , 我们需要创建Default.aspx 、ProductsByCategory.aspx 和ProductDetails.aspx 页面。分别在步骤 2 、3 和 4 中完成这些页面。由于本教程的重点是站点地图提供程序 ,况 且前面的教程都已讨论过如何创建这些多页面主/ 明细报表 ,所以 我们将在步骤 2 到 4 一笔带过 。如果您需要复习跨多个页面创建主 / 明细报表的话,就参见前面的 跨两页面的主/明细筛选 教程。

步骤2 : 显示类别列表

打开SiteMapProvider 文件夹中的 Default.aspx 页面 , 从Toolbox 中将一个GridView 拖 到设计器中 , 将其 ID 赋值为 Categories 。从 GridView 的智能标记将它绑定到一个名为 CategoriesDataSource 的新 ObjectDataSource ,并配置其使用 CategoriesBLL 类的 GetCategories 方法提取数据。由于该 GridView 仅显示类别且不提供数据修改功能,将 UPDATE 、 INSERT 和 DELETE 选项卡中的下拉列表赋值为 “(None)” 。

图4 : 配置 ObjectDataSource 以使其 使用 GetCategories 方法返回类别

图5 : 将 UPDATE 、INSERT 和 DELETE 选项卡中的下拉列表设置为 “(None)”

完成Configure Data Source 向导后 ,Visual Studio 将为 CategoryID 、CategoryName 、Description 、NumberOfProducts 和 BrochurePath 添加 BoundField 。编辑 GridView ,使其只包含 CategoryName 和 Description BoundFields ,并更新 CategoryName BoundField 的 HeaderText 属性为 “Category” 。

接下来 , 添加一个HyperLinkField , 并将它放在最左边。将 DataNavigateUrlFields 属性赋值为 CategoryID ,DataNavigateUrlFormatString 属性赋值为 ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0} 。将 Text 属性赋值为 “View Products” 。

图6 : 向 Categories GridView 添加一个 HyperLinkField

创建完 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 。单击类别的 “View Products” 链接 , 将会转到 ProductsByCategory.aspx?CategoryID=categoryID , 我们将在步骤3 中创建它。

图7 : 随 “View Products” 链接列出的各类别

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

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

图8 : 使用 ProductsBLL 类的 GetProductsByCategoryID(categoryID) 方法

Configure Data Source 向导的最后一个步骤提示输入 categoryID 的参数源。由于该信息通过查询字符串CategoryID 传递 ,所以 从下拉列表中选择 QueryString , 在 QueryStringField 文本框中输入“CategoryID” , 如图 9 所示。然后单击 Finish 完成向导。

图9 : 使用 CategoryID Querystring 字段作为 categoryID 参数

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

接下来 , 添加一个HyperLinkField 并将它移到最左边。将其 Text 属性赋值为 “View Details” , DataNavigateUrlFields 属性赋值为 ProductID , DataNavigateUrlFormatString 属性赋值为 ~/SiteMapProvider/ProductDetails.aspx?ProductID={0} 。

图10 : 添加指向 ProductDetails.aspx 的 “View Details” 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 , 并单击Beverages 的 “View Products” 链接。这将转到 ProductsByCategory.aspx?CategoryID=1 , 显示出属于Beverages 类别的 Northwind 数据库中产品的名称、价格和厂商 ( 见图 11 ) 。可随意增强该页面,使其包含一个链接以方便用户返回到类别显示页面 (Default.aspx) ,并包含一个 DetailsView 或 FormView 控件来显示所选类别的名称和描述。

图 11 : 所显示的Beverages 的名称、价格和厂商 

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

最后一个页面 , 即ProductDetails.aspx , 显示所选产品的详细信息。打开 ProductDetails.aspx , 从Toolbox 将一个 DetailsView 拖到Designer 。将该 DetailsView 的 ID 属性赋值为 ProductInfo ,并清除其 Height 和 Width 属性值。从其智能标记将该 DetailsView 与一个名为 ProductDataSource 的新 ObjectDataSource 绑定,并配置该 ObjectDataSource 使用 ProductsBLL 类的 GetProductByProductID(productID) 方法获取数据。与步骤 2 和 3 中创建的 web 页面一样,将 UPDATE 、 INSERT 和 DELETE 选项卡的下拉列表赋值为 “(None)” 。

图12 : 配置 ObjectDataSource 使其 使用 GetProductByProductID(productID) 方法

Configure Data Source 向导的最后一个步骤提示输入 productID 参数源。由于该数据源于查询字符串 ProductID ,所以将下拉列表赋值为 QueryString ,将 QueryStringField 文本框赋值为 “ProductID” 。最后,单击 Finish 按钮完成向导。

图13 : 配置 productID 参数从ProductID Querystring 字段 提取值

完成Configure Data Source 向导后 ,Visual Studio 将在产品数据字段的 DetailsView 中创建相应的 BoundFields 和 CheckBoxField 。删除 ProductID 、 SupplierID 和 CategoryID 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 ,并 单击 Beverages 类别的 “View Products” 。在饮料产品列表中 , 单击Chai Tea 的 “View Details” 链接。 这将转到 ProductDetails.aspx?ProductID=1 ,显示 Chai Tea 的详细信息(见图 14 )。

图14 : 显示出的 Chai Tea 的厂商、类别、价格及其它信息

步骤5 : 了解站点地图提供程序的内部运行机制

站点地图在web 服务器的内存中表现为形成层次结构的SiteMapNode 实例集。必须只有一个根节点,所有的非根节点必须只有一个父节点,但所有的节点可有任意数量的子节点。每个 SiteMapNode 对象表示网站结构的一部分;这些部分通常有相应的 web 页面。因此, SiteMapNode 类 有 Title 、 Url 和 Description 等属性,用以描述 SiteMapNode 所对应部分的信息。还有一个 Key 属性,用以唯一标识层次结构中的各 SiteMapNode ,以及用于建立该层次结构的属性: ChildNodes 、 ParentNode 、 NextSibling 、 PreviousSibling 等。

图15 显示的是图 1 中的总体站点地图结构 ,只是其实现细节提供得更细化 了。

图15 : 每个 SiteMapNode 都提供 Title 、Url 、Key 等属性

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

访问SiteMap 类的属性时 , 必须从某种永久介质中将站点地图结构序列化地传入内存。不过,站点地图序列化逻辑不是通过硬编码进入 SiteMap 类的。而是在运行时, SiteMap 类确定用于序列化的站点地图提供程序。默认状态下,使用的是XmlSiteMapProvider 类 ,它从格式正确的 XML 文件中读取站点地图的结构。不过,我们稍微做点工作就可创建自己的定制站点地图提供程序。

所有的站点地图提供程序必须从 SiteMapProvider 类 派生,该类 包括站点地图提供程序所需的基本方法和属性 , 但忽略了许多实现细节。另一个类, StaticSiteMapProvider ,扩展了 SiteMapProvider 类并包含所需函数的更强实现。在其内部, StaticSiteMapProvider 将站点地图的 SiteMapNode 实例存储在一个哈希表中,并提供 AddNode(childparent) 、 RemoveNode(siteMapNode) 和 Clear() 等方法,以对内部哈希表的 SiteMapNodes 进行添加和删除。 XmlSiteMapProvider 是由 StaticSiteMapProvider 派生的。

创建扩展StaticSiteMapProvider 的定制站点地图提供程序时 , 必须覆盖两个抽象方法 : BuildSiteMap 和 GetRootNodeCore。 BuildSiteMap ,正如其名称所示,负责从持久的存储中装载站点地图结构并在内存中构造它。 GetRootNodeCore 则返回站点地图的根节点。

在web 应用程序可使用站点地图提供程序之前 , 必须在应用程序的配置中进行注册。默认状态下,使用 AspNetXmlSiteMapProvider 名称注册 XmlSiteMapProvider 类。要注册到其它站点地图提供程序,在 Web.config 中添加如下标记:

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

name 值为提供程序指派一个易读的名称 , 而 type 则 指定完全合格的站点地图提供程序的类型名。创建了定制站点地图提供程序后,我们将在步骤 7 中探讨 name 和 type 值的具体值。

首次从SiteMap 类中访问和例示站点地图提供程序类 , 并在整个 web 应用程序生命周期都保持在内存中。由于多个并发的网站访问者只能调用一个站点地图提供程序的实例,所以必须保证提供程序的方法是 线程安全的

基于性能和伸缩性原因 , 重要的是缓存内存站点地图结构 ,并在 每次调用BuildSiteMap 方法时返回缓存的结构而不用重新创建它。每用户对每页面的请求可多次调用 BuildSiteMap ,这取决于页面上使用的导航控件以及站点地图结构的深度。任何情况下,如果我们没有缓存 BuildSiteMap 中的站点地图结构,则每次调用时我们都必须从架构中重新提取产品和类别信息(这将导致对数据库的查询)。就如我们在前面的缓存教程中所讨论的,缓存的数据可能过时。为避免此情况,我们可使用基于时间或基于 SQL 缓存依赖项的期限。

注意 : 站点地图提供程序可选择覆盖 Initialize 方法 。Initialize 是在首次例示站点地图提供程序 , 并将如下 <add> 元素中 Web.config 的定制属性传递给提供程序时被调用的。<add name="name" type="typecustomAttribute="value" />.这在允许页面开发者在不修改提供程序代码的前提下指定各种与站点地图有关的设置时有用。例如,如果我们直接从数据库中而不是架构中读取类别和产品数据,我们可能希望页面开发者使用 Web.config 中的数据库连接字符串,而不使用提供程序代码中的硬编码值。我们将在步骤 6 中建立的定制站点地图提供程序不会覆盖该 Initialize 方法。有关使用 Initialize 方法的示例,可参考 Jeff Prosise 的 在 SQL Server 中存储站点地图 文章。

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

要创建从Northwind 数据库的 categories 和 products 构建站点地图的定制站点地图提供程序 , 我们需要创建一个扩展 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 将为空,并将构建站点地图结构。构建过程中, root 被赋值为站点地图的根节点;这样,下次调用该方法时, root 就不会为空。因此,只要 root 不为空,站点地图结构就会返回,而无需重建。

如果root 为空 , 将从产品和类别信息中创建站点地图结构。站点地图的创建方法:先创建 SiteMapNode 实例,然后调用 StaticSiteMapProvider 类的 AddNode 方法来形成层次结构。 AddNode 执行内部记账、在哈希表中存储各种 SiteMapNode 实例。在开始构建层次体系之前,我们要调用 Clear 方法,以清除内部哈希表的元素。接下来,将 ProductsBLL 类的 GetProducts 方法以及返回的 ProductsDataTable 存储在局部变量中。

从创建根节点并将它赋值给 root 开始来构建站点地图。这里要用到 SiteMapNode 的构造函数 过载并通过该 BuildSiteMap 传递如下信息:

  • 对站点地图提供程序的引用 (this) 。
  • SiteMapNode 的 Key 。该必需值对每个 SiteMapNode 必须是惟一的。
  • SiteMapNode 的 Url 。Url 是可选的 , 但如果提供的话 , 每个 SiteMapNode 的 Url 值就必须是惟一的。
  • SiteMapNode 的 Title , 也是必需的。

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

注意 : 对于CategoryID 为数据库 NULL 值的那些产品 , 统统被归类在 Title 属性赋值为 “None” 且 Url 属性赋值为空字符串的类别SiteMapNode 下。由于 ProductBLL 类的 GetProductsByCategory(categoryID) 方法目前不能返回那些具有 NULL CategoryID 值的产品,所以我决定将 Url 赋值为空字符串。同时,我想说明导航控件如何呈现缺乏 Url 属性值的 SiteMapNode 。鼓励您扩展该教程,使 “None” SiteMapNode 的 Url 属性指向 ProductsByCategory.aspx ,并只显示有 NULL CategoryID 值的产品。

构建了站点地图后 , 通过AggregateCacheDependency 对象 , 使用 Categories 和Products 表上的 SQL 缓存依赖项添加任意对象到数据缓存中。在前面的 使用 SQL 缓存依赖项 教程中,我们探讨了相关内容。然而,定制站点地图提供程序使用的是我们还未探讨的数据缓存的 Insert 方法过载。该过载接受一个 delegate 作为输入参数,从缓存中删除对象时调用该 delegate 。特别地,我们传入一个新的 CacheItemRemovedCallback delegate ,它指向在 NorthwindSiteMapProvider 类中详细定义的 OnSiteMapChanged 方法。

注意 : 站点地图的内存表示通过一个类级别的变量 root 缓存。由于只有一个定制站点地图提供程序类的实例,并且该实例在 web 应用程序的所有线程间共享,这个类变量充当缓存。 BuildSiteMap 方法也会用到数据缓存,但仅作为一种接收 Categories 或 Products 表的底层数据库数据变化通知的方法。注意,加入到数据缓存的值仅仅是当前的日期和时间。实际的站点地图 并不在数据缓存中。

BuildSiteMap 方法最后返回站点地图的根节点。

剩下的方法就比较简单易懂了。GetRootNodeCore 负责返回根节点。由于 BuildSiteMap 返回 root , GetRootNodeCore 仅返回 BuildSiteMap 的返回值。清除缓存条目时, OnSiteMapChanged 方法又将 root 赋值为 null 。当 root 又被赋值为空时,下一次调用 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 是默认的站点地图提供程序 ; 第二 , 它注册了步骤 6 中创建的定制站点地图提供程序 , 并取了一个易读的名字 :“Northwind” 。

注意 : 对于那些位于 App_Code 文件夹的站点地图提供程序, type 属性的值就是类名。或者,在单独的 Class Library 项目中创建的定制站点地图提供程序,其编译文件放在 web 应用程序的 /Bin 目录下。此时, type 属性值将为 “Namespace.ClassName,AssemblyName” 。

更新完 Web.config 后,花点时间在浏览器中查看本教程的任何页面。注意,左边的导航界面仍然显示 Web.sitemap 中定义的区域和教程。这是因为我们把 AspNetXmlSiteMapProvider 作为默认的提供程序了。为创建使用该 NorthwindSiteMapProvider 的导航用户界面元素,我们需要明确地指出要使用 “Northwind” 站点地图提供程序。这一点我们将在步骤 8 中完成。

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

创建了定制站点地图提供程序并在 Web.config 中注册后 , 我们可以向 SiteMapProvider 文件夹中的 Default.aspx 、ProductsByCategory.aspx 和 ProductDetails.aspx 页面添加导航控件了。从打开 Default.aspx 页面开始,从 Toolbox 拖一个 SiteMapPath 到设计器中。 SiteMapPath 控件位于 Toolbox 的 Navigation 区域。

图16 : 向 Default.aspx 添加一个 SiteMapPath

SiteMapPath 控件显示一个 breadcrumb , 表明当前页面在站点地图中的位置。在母版页和网站导航教程中,我们在母版页的顶部添加了一个 SiteMapPath 。

花点时间在浏览器中查看本页。在图 16 中添加的 SiteMapPath 使用的是默认的站点地图提供程序,从 Web.sitemap 获取其数据。因此, breadcrumb 显示为 “Home > Customizing the Site Map” ,同右上角的 breadcrumb 。

图17 :Breadcrumb 使用默认的站点地图提供程序

为了让在图16 中添加的 SiteMapPath 使用我们在步骤 6 中创建的定制站点地图提供程序 , 需要将其 SiteMapProvider 属性 赋值为 “Northwind” , 这个名字在 Web.config 中被我们分配给了 NorthwindSiteMapProvider 。不过, Designer 依然使用默认的站点地图提供程序,但是如果您在属性更改后通过浏览器访问该页面,您将看到 breadcrumb 现在使用的是定制站点地图提供程序。

图18 :Breadcrumb 现在使用定制站点地图提供程序NorthwindSiteMapProvider

SiteMapPath 控件将在 ProductsByCategory.aspx 和 ProductDetails.aspx 页面上展示更具功能性的用户界面。在这两个页面里添加一个 SiteMapPath ,并将其 SiteMapProvider 属性赋值为 “Northwind” 。在 Default.aspx 页面,单击 Beverages 的 “View Products” 链接,然后单击 Chai Tea 的 “View Details” 链接。如图 19 所示, breadcrumb 包括当前的站点地图部分 (“Chai Tea”) 及其上一级: “Beverages” 和 “All Categories” 。

图19 :Breadcrumb 现在使用定制站点地图提供程序NorthwindSiteMapProvider

除SiteMapPath 外 , 还可以使用其它导航用户界面元素 , 如Menu 和 TreeView 控件。例如,本教程下载的 Default.aspx 、 ProductsByCategory.aspx 和 ProductDetails.aspx 页面都有 Menu 控件(见图 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 的 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.aspx 、ProductsByCategory.aspx 和 ProductDetails.aspx 页面后 , 转到编辑、插入和删除一节中的任一教程 , 编辑某个类别或产品的名称。然后转到 SiteMapProvider 文件夹中的某个页面。假定为轮询机制提供了足够的时间去留意底层数据库所发生的变化,应对站点地图进行更新,以显示新的产品或类别名称。

小结

ASP.NET 2.0 的站点地图特性包括一个 SiteMap 类、许多内置的导航 Web 控件,以及期望站点地图信息保存在 XML 文件的默认站点地图提供程序。为使用来自其它数据源(数据库、应用程序的架构、或远程 Web 服务等)的站点地图信息,我们需要创建定制站点地图提供程序。这涉及到要创建一个类,该类由 SiteMapProvider 类直接或间接派生。

在本教程中,我们介绍了如何创建一个定制站点地图提供程序,其站点地图基于从应用程序架构中获取的产品和类别信息。我们的提供程序扩充了 StaticSiteMapProvider 类,创建了一个 BuildSiteMap 方法来获取数据、构建了站点地图层次结构,并将最终的结构缓存在一个类级别的变量中。我们使用一个具有回传功能的 SQL 缓存依赖项,在底层 Categories 或 Products 数据发生改动时使缓存的结构无效。

快乐编程!

posted @ 2016-05-02 00:27  迅捷之风  阅读(80)  评论(0编辑  收藏  举报