在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider

时间:2022-09-01 20:47:46

导言:

  ASP.NET 2.0的网站地图(site map)功能允许页面开发者在一些持久介质(persistent medium),比如一个XML文件里,自己定义一个web程序的site map.一旦定义了之后,我们可以通过System.Web命名空间的SiteMap class类或某个Web导航控件,比如SiteMapPath, Menu, 或TreeView来对其进行访问。site map系统使用的是provider model模式,所以可以创建不同的site map,并将其应用到一个web应用程序。ASP.NET 2.0默认的site map provider,其结构为一个XML文件。在教程《Master Pages and Site Navigation》里我们创建了一个Web.sitemap文件,它就包含了这种结构,并且在教程的每一个新部分里我们都要更新其XML.

  当site map的结构是静态的时候,默认的这种基于XML(XML-based)的site map provider工作正常,就像本系列教程一样。但是在很多时候我们需要动态的site map.如图1的site map,每个种类以及属于该种类的产品在网站的结构里做层次状体系分布。在该site map里,当访问根目录的web页面时,将列出所有的种类;再访问某个具体的种类的根目录时,将列出属于该种类的所有产品;再访问某个具体的产品时将列出该产品的详细信息。

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图1:Categories 和 Products构成了Site Map的层次结构

  这种基于category 和product的结构可以通过"硬编码"的方式添加到Web.sitemap文件.每当对category 或 product进行添加、删除、重命名等操作时,都需要对该文件进行更新。很自然的,如果其结构是通过数据库,或更理想地,是从业务逻辑层来获取的,那么对site map的维护是很简单的。那样的话,只要对products 和 categories进行添加、删除、重命名时,site map会自动的更新以反应这些变化。

  由于ASP.NET 2.0的site map是建立在provider模式的基础上的,因此我们可以创建一个自定义的site map provider,从数据库或某个层来获取数据.在本文,我们创建的provider将从业务逻辑层获取数据。让我们开始吧!

  注意:本文创建的用户定制site map provider仅仅依赖于系统的层及其数据模式(data model)。Jeff Prosise的文章《Storing Site Maps in SQL Server》(http://msdn.microsoft.com/msdnmag/issues/05/06/WickedCode/)
和《The SQL Site Map Provider You've Been Waiting For》
(http://msdn.microsoft.com/msdnmag/issues/06/02/wickedcode/default.aspx)
考察了将site map数据存储在SQL Server的方法。

第一步:创建用户定制Site Map Provider页面

在创建用户定制Site Map Provider之前,先添加本章将用到的ASP.NET页面。首先添加一个名为SiteMapProvider的文件夹;然后在文件夹里添加如下所示的页面确保采用母版Site.master:

Default.aspx
ProductsByCategory.aspx
ProductDetails.aspx

同样,在App_Code文件夹里添加CustomProviders

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图2:添加相关的ASP.NET页面.

由于这部分只有一篇文章,没有必要使Default.aspx页面列出本部分的文章;我们将在Default.aspx里用一个GridView控件来列出categories,在第二步里探讨.

然后,更新Web.sitemap使其包含对Default.aspx页面的引用。特别的,在“Caching” <siteMapNode>后面添加以下代码:

?
1
2
3
<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的更新后,花点时间在浏览器里登录页面,在左面的菜单里包含了本教程的条目。

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图3:Site Map现在包含了本章的条目

  本教程主要考察如何创建一个用户自定义的site map provider,及对设置web应用程序进行包含该site map provider.具体来讲,它返回的网站地图(site map)不仅包含了根节点,而且包含每个种类节点和产品节点,就像图1显示的那样。总的来说,网站地图里的每一个节点都对应一个具体的URL.就我们的网站地图而言,根节点的URL为~/SiteMapProvider/Default.aspx,它列出了所有产品和种类;每个种类节点对应的URL 为~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID,它根据指定的categoryID列出该种类的所有产品;最后,每个产品对应的URL为~/SiteMapProvider/ProductDetails.aspx?ProductID=productID, 它根据指定的productID值,列出该产品的详细信息。

  首先,让我们创建Default.aspx, ProductsByCategory.aspx和ProductDetails.aspx页面。我们将分别在第二、三、四步创建这些页面.因为本文的重点是site map providers,并且这种主/从页面在前面的教程里已经讨论过了,我们在第2到第4步将一笔带过,如果你对这种主/从页面页面不是很了解的话,请参考前面的教程之9《Master/Detail Filtering Across Two Pages》.

第二步:将Categories显示出来

  打开文件夹SiteMapProvider里的Default.aspx页面,在设计模式里从工具箱拖一个 GridView控件到页面,设置其ID为Categories.从其智能标签里,将其绑定到一个名为CategoriesDataSource的ObjectDataSource,设置其使用CategoriesBLL类的 GetCategories方法。因为该GridView控件只是显示categories而不修改数据,因此在UPDATE, INSERT和 DELETE标签里选“(None)”.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图4:设置ObjectDataSource使用GetCategories方法返回Categories

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图5:在UPDATE, INSERT和DELETE标签里选“(None)”

  设置完成后,Visual Studio会自动的添加CategoryID, CategoryName, Description, NumberOfProducts 和 BrochurePath这些绑定列(BoundField),修改GridView,使其只包含CategoryName 和 Description两列,且将CategoryName绑定列的HeaderText属性改为“Category”.

  然后,添加一个HyperLinkField,将其放在最左边,设其DataNavigateUrlFields属性为 CategoryID;DataNavigateUrlFormatString 属性为 ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0};再将Text属性设置为“View Products”.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图6:为GridView添加一个HyperLinkField

创建完ObjectDataSource并定制GridView控件的列后,这2个控件的声明代码看起来应该和下面的差不多:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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页面,该页面我们将在第三步新建。

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图7:每个类都有一个“View Products”链接

第三步:显示指定类的所有产品

  打开ProductsByCategory.aspx页面并添加一个GridView控件,设其ID为ProductsByCategory.从其智能标签,将其绑定到一个名为ProductsByCategoryDataSource的ObjectDataSource;设置它使用ProductsBLL类的 GetProductsByCategoryID(categoryID)方法;在UPDATE, INSERT,和 DELETE标签里选择“(None)”.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图8:使用ProductsBLL类的GetProductsByCategoryID(categoryID)方法

  设置向导的最后一步是指定categoryID的参数来源,因为此信息是通过查询字符串(querystring field)CategoryID来传递的,因此在参数来源里选QueryString,在QueryStringField里输入“CategoryID”;如图9所示,点Finish完成设置.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图9:为参数categoryID指定CategoryID Querystring Field

  完成设置后,Visual Studio将为GridView添加相应的绑定列以及CheckBo列;将除ProductName, UnitPrice, SupplierName外的列删除掉。将这3个列的HeaderText属性分别设置为“Product”, “Price”, and “Supplier”, 将UnitPrice列格式化为货币形式.

  然后,添加一个HyperLinkField列,并将其放在最左边;设其Text属性为“View Details”,设其DataNavigateUrlFields属性为ProductID;其DataNavigateUrlFormatString属性为 ~/SiteMapProvider/ProductDetails.aspx?ProductID={0}.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图10:添加一个“View Details” HyperLinkField,以链接到ProductDetails.aspx

完成后,GridView和 ObjectDataSource的声明代码为:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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页面,显示饮料类的所有产品的names, prices, 和 suppliers信息(见图11)。尽管改进该页面吧,添加一个链接以方便用户返回上一页(Default.aspx) .还可以添加一个DetailsView 或 FormView控件来显示该种类的名称和描述。

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图11:显示Beverages类的Names, Prices, Suppliers信息

第四步:显示产品的详细信息

  最后要创建的页面—ProductDetails.aspx,是用来显示指定产品的详细信息的。打开ProductDetails.aspx页面,从工具箱拖一个DetailsView控件到页面,设置其ID为ProductInfo,并清除其Height 和 Width属性值。在其智能标签里,绑定到一个名为ProductDataSource的ObjectDataSource,设置该ObjectDataSource使用ProductsBLL类的GetProductByProductID(productID)方法。在UPDATE, INSERT,和DELETE标签里选“(None)”.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图12:设置该ObjectDataSource控件调用GetProductByProductID(productID)方法

  最后,需要设置参数productID的来源,由于数据通过查询字符串ProductID来传递,在参数源下拉列表里选QueryString,在QueryStringField对话框里输入“ProductID”. 最后,点Finish按钮完成设置。

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图13:设置参数productID来源于查询字符串

  完成设置后,Visual Studio会为DetailsView控件添加相应的绑定列和CheckBox列,移除ProductID, SupplierID, 和CategoryID列,剩下的列想怎样设就怎样设置吧。我对界面做了些优化,这样的话,声明代码看起来像下面这样:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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所示).

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图14:Chai Tea的Supplier, Category, Price等信息显示出来了

第五步:理解Site Map Provider的内部处理机制

  site map呈现的是源于某种层次结构的SiteMapNode实例集(a  collection of SiteMapNode instances)。其必须有一个根节点,所有的非根节点都有一个父节点,且每个节点都可以有任意数量的子节点.每个SiteMapNode对象对应的是website体系结构的某个部分。这些部分通常都有对应的web页面,因此,SiteMapNode class类有像Title, Url, 和 Description这样的属性,它们用来描述SiteMapNode所对应部分的相关信息。
还有一个Key属性用来专门唯一的标识这些SiteMapNode;除此以外,还有ChildNodes, ParentNode, NextSibling, PreviousSibling等等.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图15显示的是对应于图1的site map的总体结构,只是更细化了而已.

   可以通过命名空间System.Web的SiteMap class类来访问site map;该类的RootNode属性返回网站地图的根目录的SiteMapNode实例;CurrentNode属性返回的是这种SiteMapNode,其Url属性刚好与当前请求页面的URL匹配.ASP.NET 2.0的Web导航控件的内部就会用到SiteMap class类.

  当访问SiteMap class类的属性时,必须将网站地图的层次结构从某个介质传入内存(memory).SiteMap class类并不是通过“硬编码”的方式来处理网站地图的逻辑关系,而是通过某种site map provider来工作.在默认情况下,使用的是XmlSiteMapProvider class类,它从一个标准的XML文件读取网站地图的结构.不过,我们稍微做点工作就可以创建自己的site map provider.

  所有的site map providers都继承自SiteMapProvider class类,该类包含了site map providers要用到的最基本的方法和属性,不过略去了很多执行细节.site map providers要用到的第二个类是StaticSiteMapProvider class类,它对SiteMapProvider class类进行了扩充,包含了更多的必要的函数.在其内部,StaticSiteMapProvider将网站地图的SiteMapNode实例存储在一个哈希表(Hashtable)里,并包含了AddNode(child, parent), RemoveNode(siteMapNode), Clear()等方法,以对哈希表里的SiteMapNodes执行添加、删除等操作.另外,XmlSiteMapProvider也继承自StaticSiteMapProvider.

  当创建自定义的site map provider时,要对StaticSiteMapProvider进行扩充,重写(overrid)2个抽象方法——BuildSiteMap 和 GetRootNodeCore. 对BuildSiteMap而言,就像它的名字暗示的那样,将网站地图的结构从某种介质里按层次结构装载进内存;而GetRootNodeCore返回的是网站地图的根目录.

  在使用某个site map provider时,需要在应用程序的配置文件里进行注册(registered)
.默认情况下,XmlSiteMapProvider class类被注册为AspNetXmlSiteMapProvider.为对额外添加的site map providers进行注册,可以在Web.config文件里添加如下的代码:

?
1
2
3
4
5
6
7
8
9
10
11
<configuration>
 <system.web>
 ...
 
 <siteMap defaultProvider="defaultProviderName">
  <providers>
  <add name="name" type="type" />
  </providers>
 </siteMap>
 </system.web>
</configuration>

  name属性可以为site map provider指派一个易读的名称;type属性决定了该site map provider的类型.当创建完我们定制的site map provider后,我们将在第七步为name 和type属性赋值.

  当第一次从SiteMap class类访问site map provider时,site map provider class类都应该被实例化,并在web应用程序的整个生命周期里都驻留在内存.

  基于性能等方面的考虑,我们应该将对驻留在内存里的网站地图结构进行数据缓存,每次调用BuildSiteMap的方法时,直接返回缓存的数据而不用重新检索数据.在任何情况下,如果我们不对BuildSiteMap对应的网站结构进行缓存的话,每次调用时,我们都需要通过“层”来重新检索产品和种类的信息(这将最终导致对数据库的查询).我们在前面的缓存章节探讨过缓存数据“过时”的问题,为此,我们要么使用基于时间,要么使用基于SQL cache dependency的缓存技术.

  注意:一个site map provider可以任意地重写(override)Initialize method方法.Initialize 方法是当site map provider第一次实例化的时候被调用的,并可以将我们在Web.config 文件的<add>元素里赋值的用户自定义属性值传递给它,比如:<add name="name" type="type" customAttribute="value" />.当一个页面开发者希望指定各种与site map provider相关的设置,而又不希望修改site map provider的代码的时候,这样做很有用.比如,假如我们希望不通过“层”而直接从数据库读取category 和 products的数据时,我们当然希望页面开发者调用Web.config文件里的数据库连接字符串,而不使用site map provider代码里的“硬编码”值.我们不打算在第六步创建的自定义site map provider里重写Initialize方法.见Jeff Prosise的文章《Storing Site Maps in SQL Server》(http://msdn.microsoft.com/msdnmag/issues/05/06/WickedCode/)

第六步:创建自定义的Site Map Provider

  要想创建一个自定义的site map provider来构建源于Northwind数据库里的categories 和 products信息的网站地图(site map),我们需要创建一个类来扩展StaticSiteMapProvider.在前面我们在App_Code文件夹里添加了一个CustomProviders文件夹,在该文件夹里添加名为NorthwindSiteMapProvider的新类,在类里添加如下的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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 statement声明。lock statement每次只允许“单线程操作”(one thread at a time to enter),以避免“多线程操作”之间的冲突.

  属于“类级别”(class-level)的SiteMapNode变量—root,用来缓存网站地图结构.当网站地图第一次被“结构化”,或“源数据”发生变动后的第一次“结构化”时,root为null值,在“结构化”的过程中,root被赋值为网站地图的根节点;所以,当第二次调用BuildSiteMap方法时,root就不为null值了.自然,只要root不为null,直接将网站地图结构返回,而用不着重新创建.

  如果root为null,那么将根据product 和 category信息创建网站地图结构.为此,先要创建一个SiteMapNode实例,再调用StaticSiteMapProvider class类的AddNode method方法来构建网站地图的层次体系,再将SiteMapNode实例存储进一个哈希表.在我们构建层次体系之前,我们首先调用Clear method方法,将内部的哈希表清空;然后,调用ProductsBLL class类的GetProducts()方法,把返回的ProductsDataTable存储进局部变量.

  创建网站地图结构从创建根节点并赋值给root开始,本章要用到的SiteMapNode's constructor重载,接受如下的信息:

对一个site map provider (this)的引用.

SiteMapNode的Key值:对每个SiteMapNode而言,这个待定值必须是唯一的.

SiteMapNode的Url值:Url为可选项,但一旦指定的话,每个SiteMapNode的Url值必须是唯一的.

SiteMapNode的Title值:此为必选项.

  AddNode(root) method方法将SiteMapNode root添加给网站地图作为根节点。然后,遍历ProductsDataTable里的所有ProductRow,如果当前product的category所对应的SiteMapNode已经存在的话,那么引用该SiteMapNode;如果不存在的话,则为该category创建一个新的SiteMapNode,并且调用AddNode(categoryNode, root) method方法,将其作为SiteMapNode root的子节点进行添加.当找到或创建category对应的SiteMapNode后,创建一个当前product对应的SiteMapNode,并通过AddNode(productNode, categoryNode)方法将其作为category SiteMapNode的子节点进行添加.注意,category SiteMapNode的Url属性为~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID;而product SiteMapNode的Url属性为~/SiteMapNode/ProductDetails.aspx?ProductID=productID.

  注意:对那种CategoryID为NULL值的产品,统统将其归为一个category,其对应的category SiteMapNode的Title属性可设置为“None”;Url属性设置为空字符串。我将其Url设置为空字符串是因为ProductBLL class类的GetProductsByCategory(categoryID)方法无法返回那些CategoryID值为NULL的产品.不过我鼓励你对本教程进行扩展,使该category SiteMapNode的Url属性对应一个ProductsByCategory.aspx页面,该页面专门用来展示那些CategoryID为NULL的产品.

  当完成site map的构建后,将一个AggregateCacheDependency object对象添加到data cache,该对象使用基于Categories 和 Products表的SQL cache dependency技术。我们在前面的教程里探讨过SQL cache dependencies,不过我们自定义的site map provider使用的是重载(overload)的data cache的Insert方法,该重载方法接受一个delegate作为输入参数.具体而言,我们将传入一个CacheItemRemovedCallback delegate,其指向OnSiteMapChanged method方法,该方法定义在NorthwindSiteMapProvider class类里
注意:内存里的site map表述是缓存在一个“类级”(class-level)变量root里的.由于只有一个site map provider的实例(instance),并且对web应用程序的线程来说都是共享的,这个类级变量当作缓存服务。BuildSiteMap method方法也会用到data cache,但仅仅做作为一种探测Categories 或 Products表里的数据发生改变的方法。注意添加到data cache里的仅仅是当前的date和time,实际的site map数据并没有添加到data cache.

BuildSiteMap method方法最后返回网站地图的根节点.

  剩下的方法就比较简单易懂了.GetRootNodeCore方法用来返回根节点,由于BuildSiteMap返回根节点root, GetRootNodeCore方法仅仅返回BuildSiteMap方法的返回值.当缓存条码被清除掉时,OnSiteMapChanged方法将root设置为null;当root为null的时候,当下一次调用BuildSiteMap时,将重新创建地图网站结构.最后,如果data cache里存储有date 和 time值的话,CachedDate属性将返回这些值.页面开发员可以用该属性来探测site map数据最近被缓存的时间.

第七步:对NorthwindSiteMapProvider进行登记

  为了使用我们在第六步创建的NorthwindSiteMapProvider site map provider,我们需要在Web.config文件的<siteMap>部分进行注册.具体来说,将下面的代码添加到Web.config文件的<system.web>部分:

?
1
2
3
4
5
<siteMap defaultProvider="AspNetXmlSiteMapProvider">
 <providers>
 <add name="Northwind" type="NorthwindSiteMapProvider" />
 </providers>
</siteMap>

  上述代码阐明了如下2个事实:第一,它指明了“内置”的AspNetXmlSiteMapProvider为默认的site map provider;第二,它将我们在第六步创建的用户自定义site map provider进行了注册,取名为“Northwind”.
注意:对那些位于在App_Code文件夹的site map providers而言,type属性的值就是类的名称.还一种方法,我们可以用一个单独的类库工程来创建自定义的site map provider,将其编译文件放置在/Bin目录;如果是那样的话,type属性就变成了“Namespace.ClassName, AssemblyName”.

  更新Web.config文件后,花点时间在浏览器里登录本教程的任何一个页面,我们注意到左边的导航界面跟以前一样,那是因为我们把AspNetXmlSiteMapProvider作为默认的provider,为了使导航用户界面使用我们定制的NorthwindSiteMapProvider,我们应明确的指定使用“Northwind” site map provider,我们将在第八步完成.

第八步:使用定制的Site Map Provider来显示网站地图信息

  把我们定制的site map provider注册到Web.config文件后,我们可以将导航控件添加到SiteMapProvider文件夹里的Default.aspx, ProductsByCategory.aspx, 和ProductDetails.aspx页面.首先,打开Default.aspx页面进入设计模式,从工具箱拖一个SiteMapPath控件到页面。该控件位于工具箱的导航区域.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图16:为Default.aspx页面添加一个SiteMapPath控件

  SiteMapPath控件包含一个breadcrumb,用来显示当前页面在网站地图里的位置。我们在第三章《模板页和站点导航》里在模板页的顶部添加了一个SiteMapPath控件.

  花点时间在浏览器里登录页面,我们在图16里添加的SiteMapPath控件使用的是默认的site map provider,它从Web.sitemap文件获取数据,因此breadcrumb显示为“Home > Customizing the Site Map”.如下图:

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图17:Breadcrumb使用的是默认的Site Map Provider

  要使在图16里添加的SiteMapPath使用我们定制的site map provider的话,设其SiteMapProvider property属性为“Northwind”, 这个名字是我们在Web.config文件里分配给NorthwindSiteMapProvider的.不过,在设计器里依然使用的是默认的site map provider,但是如你在浏览器里登录该页面的话,你将看到breadcrumb使用的是我们定制的site map provider了.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图18:Breadcrumb现在使用的是我们定制的NorthwindSiteMapProvider

  SiteMapPath控件将在ProductsByCategory.aspx 和 ProductDetails.aspx页面展示更具功能性的用户界面.在这2个页面里添加SiteMapPath控件,设置其SiteMapProvider属性为“Northwind”. 在Default.aspx页面里点击Beverages类的“View Products”链接,然后再点Chai Tea的“View Details”链接,如图19所示,breadcrumb显示的是当前的网站地图节点(“Chai Tea”),及其上级节点:“Beverages” 和“All Categories”.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图19:Breadcrumb现在使用的是我们定制的NorthwindSiteMapProvider

  除了SiteMapPath外,还可以使用其它的导航控件,比如Menu 和 TreeView控件.本章的下载代码里,Default.aspx, ProductsByCategory.aspx,和ProductDetails.aspx页面都包含Menu控件(见图20).要想更深入的了解ASP.NET 2.0里的导航控件和site map体系的话,可参阅《ASP.NET 2.0 QuickStarts》系列(http://quickstarts.asp.net/QuickStartv20/aspnet/)的《Examining ASP.NET 2.0's Site Navigation Features》和《Using Site Navigation Controls》部分.

在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
图20:Menu控件列出了所有的Categories 和 Products

就像在本教程前面提到的那样,网站地图结构可以通过SiteMap class类来进行访问,下面的代码返回默认的provider的root SiteMapNode:

SiteMapNode root = SiteMap.RootNode;

由于AspNetXmlSiteMapProvider是默认的provider,上述代码返回的是定义在Web.sitemap文件里的根节点,要引用其它的site map provider的话,使用SiteMap class类的Providers property属性,如:

SiteMapNode root = SiteMap.Providers["name"].RootNode;
这里的name是用户定制的site map provider的名称(就本文而言,为“Northwind”)

要访问某个具体的site map provider,使用SiteMap.Providers["name"]来获取该provider的实例,再将其转换成恰当的类型。比如,要展示NorthwindSiteMapProvider的CachedDate property属性,使用如下的代码:

?
1
2
3
4
5
6
7
8
9
10
11
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 cache dependency属性,访问完Default.aspx, ProductsByCategory.aspx, 和 ProductDetails.aspx页面后,转到本系列教程的《编辑插入和删除数据》部分的任一个页面,编辑某个category 或 product的名称;然后再转到SiteMapProvider文件夹里的某个页面,假设时间足够长,长到检测机制(polling mechanism)发现“源数据库”已经发生了改动,那么site map应该被更新以显示新的product 或 category名字.

结语:

  ASP.NET 2.0的site map属性包含一个SiteMap class类,一系列内置的的导航Web控件,以及一个默认的site map provider.为了使用来自某些数据源的site map信息——比如数据库、系统的“层”、或者某些Web服务,我们需要创建一个用户定制的 site map provider.这就要创建一个类,该类直接或间接的源自SiteMapProvider class类.

  本章我们探讨了如何创建一个用户定制的site map provider,它以一个由product 和 category信息构成的site map为基础.我们的provider对StaticSiteMapProvider class类进行了扩充,并创建了一个BuildSiteMap method方法来获取数据、构建site map的层次体系,并且将最终的网站地图结构缓存在一个“类级”的变量里.我们使用一个SQL cache dependency来确保当Categories 或 Products的“源数据”发生改动时使缓存的数据失效.

  祝编程快乐!

作者简介

  本系列教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。希望对大家的学习ASP.NET有所帮助。