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

 更新时间:2016年05月18日 09:05:56   作者:heker2007   我要评论
ASP.NET 2.0的site map是建立在provider模式的基础上的,因此我们可以创建一个自定义的site map provider,从金沙国际官网或某个层来获取数据。本文就详解介绍如何自定义的site map provider动态的获取数据,替代先前通过"硬编码"的方式添加到Web.sitemap文件的方法。

导言:

  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页面时,将列出所有的种类;再访问某个具体的种类的根目录时,将列出属于该种类的所有产品;再访问某个具体的产品时将列出该产品的详细信息。

//files.jb51.net/file_images/article/201605/201605180912081.gif
图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

//files.jb51.net/file_images/article/201605/201605180912092.gif
图2:添加相关的ASP.NET页面.

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

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

//files.jb51.net/file_images/article/201605/201605180912093.gif
图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)”.

//files.jb51.net/file_images/article/201605/201605180912094.gif
图4:设置ObjectDataSource使用GetCategories方法返回Categories

//files.jb51.net/file_images/article/201605/201605180912095.gif
图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”.

//files.jb51.net/file_images/article/201605/201605180912096.gif
图6:为GridView添加一个HyperLinkField

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

<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页面,该页面我们将在第三步新建。

//files.jb51.net/file_images/article/201605/201605180912097.gif
图7:每个类都有一个“View Products”链接

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

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

//files.jb51.net/file_images/article/201605/201605180912108.gif
图8:使用ProductsBLL类的GetProductsByCategoryID(categoryID)方法

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

//files.jb51.net/file_images/article/201605/201605180912109.gif
图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}.

//files.jb51.net/file_images/article/201605/2016051809121010.gif
图10:添加一个“View Details” 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页面,点Beverages(饮料)的“View Products”链接,这将转到ProductsByCategory.aspx?CategoryID=1页面,显示饮料类的所有产品的names, prices, 和 suppliers信息(见图11)。尽管改进该页面吧,添加一个链接以方便用户返回上一页(Default.aspx) .还可以添加一个DetailsView 或 FormView控件来显示该种类的名称和描述。

//files.jb51.net/file_images/article/201605/2016051809121011.gif
图11:显示Beverages类的Names, Prices, Suppliers信息

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

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

//files.jb51.net/file_images/article/201605/2016051809121012.gif
图12:设置该ObjectDataSource控件调用GetProductByProductID(productID)方法

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

//files.jb51.net/file_images/article/201605/2016051809121013.gif
图13:设置参数productID来源于查询字符串

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

<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所示).

//files.jb51.net/file_images/article/201605/2016051809121114.gif
图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等等.

//files.jb51.net/file_images/article/201605/2016051809121115.gif
图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文件里添加如下的代码:

<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的新类,在类里添加如下的代码:

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>部分:

<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控件到页面。该控件位于工具箱的导航区域.

//files.jb51.net/file_images/article/201605/2016051809121116.gif
图16:为Default.aspx页面添加一个SiteMapPath控件

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

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

//files.jb51.net/file_images/article/201605/2016051809121117.gif
图17:Breadcrumb使用的是默认的Site Map Provider

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

//files.jb51.net/file_images/article/201605/2016051809121118.gif
图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”.

//files.jb51.net/file_images/article/201605/2016051809121119.gif
图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》部分.

//files.jb51.net/file_images/article/201605/2016051809121120.gif
图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属性,使用如下的代码:

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技术。大家可以点击查看全部教程《[翻译]Scott Mitchell 的ASP.NET 2.0数据教程》,希望对大家的学习ASP.NET有所帮助。

相关文章

最新评论