ASP.NET 2.0 Site Map Provider 在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
heker2007 人气:0导言:
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页面时,将列出所有的种类;再访问某个具体的种类的根目录时,将列出属于该种类的所有产品;再访问某个具体的产品时将列出该产品的详细信息。
图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
图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的更新后,花点时间在浏览器里登录页面,在左面的菜单里包含了本教程的条目。
图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)”.
图4:设置ObjectDataSource使用GetCategories方法返回Categories
图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”.
图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页面,该页面我们将在第三步新建。
图7:每个类都有一个“View Products”链接
第三步:显示指定类的所有产品
打开ProductsByCategory.aspx页面并添加一个GridView控件,设其ID为ProductsByCategory.从其智能标签,将其绑定到一个名为ProductsByCategoryDataSource的ObjectDataSource;设置它使用ProductsBLL类的 GetProductsByCategoryID(categoryID)方法;在UPDATE, INSERT,和 DELETE标签里选择“(None)”.
图8:使用ProductsBLL类的GetProductsByCategoryID(categoryID)方法
设置向导的最后一步是指定categoryID的参数来源,因为此信息是通过查询字符串(querystring field)CategoryID来传递的,因此在参数来源里选QueryString,在QueryStringField里输入“CategoryID”;如图9所示,点Finish完成设置.
图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}.
图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控件来显示该种类的名称和描述。
图11:显示Beverages类的Names, Prices, Suppliers信息
第四步:显示产品的详细信息
最后要创建的页面—ProductDetails.aspx,是用来显示指定产品的详细信息的。打开ProductDetails.aspx页面,从工具箱拖一个DetailsView控件到页面,设置其ID为ProductInfo,并清除其Height 和 Width属性值。在其智能标签里,绑定到一个名为ProductDataSource的ObjectDataSource,设置该ObjectDataSource使用ProductsBLL类的GetProductByProductID(productID)方法。在UPDATE, INSERT,和DELETE标签里选“(None)”.
图12:设置该ObjectDataSource控件调用GetProductByProductID(productID)方法
最后,需要设置参数productID的来源,由于数据通过查询字符串ProductID来传递,在参数源下拉列表里选QueryString,在QueryStringField对话框里输入“ProductID”. 最后,点Finish按钮完成设置。
图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所示).
图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等等.
图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控件到页面。该控件位于工具箱的导航区域.
图16:为Default.aspx页面添加一个SiteMapPath控件
SiteMapPath控件包含一个breadcrumb,用来显示当前页面在网站地图里的位置。我们在第三章《模板页和站点导航》里在模板页的顶部添加了一个SiteMapPath控件.
花点时间在浏览器里登录页面,我们在图16里添加的SiteMapPath控件使用的是默认的site map provider,它从Web.sitemap文件获取数据,因此breadcrumb显示为“Home > Customizing the Site Map”.如下图:
图17:Breadcrumb使用的是默认的Site Map Provider
要使在图16里添加的SiteMapPath使用我们定制的site map provider的话,设其SiteMapProvider property属性为“Northwind”, 这个名字是我们在Web.config文件里分配给NorthwindSiteMapProvider的.不过,在设计器里依然使用的是默认的site map provider,但是如你在浏览器里登录该页面的话,你将看到breadcrumb使用的是我们定制的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”.
图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》部分.
图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有所帮助。
加载全部内容