[SqlClientPermission(SecurityAction.Demand, Unrestricted=true)]
public class SqlSiteMapProvider : StaticSiteMapProvider
{
     
// static error messages omitted

    
const string _cacheDependencyName = "__SiteMapCacheDependency";

    
private string _connect;
    
private string _database, _table;
    
private bool _2005dependency = false;
    
private int _indexID, _indexTitle, _indexUrl, _indexDesc,
        indexRoles, _indexParent;
    
private Dictionary<int, SiteMapNode> _nodes =
        
new Dictionary<int, SiteMapNode>(16);
    
private SiteMapNode _root;
    
private readonly object _lock = new object();

    
public override void Initialize (
        
string name, NameValueCollection config)
    
{
        
// Verify parameters
        if (config == nullthrow new ArgumentNullException("config");
        
if (String.IsNullOrEmpty(name)) name = "SqlSiteMapProvider";

        
// Add a default "description" attribute to config if the
        
// attribute doesn't exist or is empty
        if (string.IsNullOrEmpty(config["description"]))
        
{
            config.Remove(
"description");
            config.Add(
"description""SQL site map provider");
        }


        
// Call the base class's Initialize method
        base.Initialize(name, config);

        
// Initialize _connect
        string connect = config["connectionStringName"];
        
if (String.IsNullOrEmpty(connect))
            
throw new ProviderException(_errmsg5);
        config.Remove(
"connectionStringName");

        
if (WebConfigurationManager.ConnectionStrings[connect] == null)
            
throw new ProviderException(_errmsg6);
        _connect 
= WebConfigurationManager.ConnectionStrings[
            connect].ConnectionString;
        
if (String.IsNullOrEmpty(_connect))
            
throw new ProviderException(_errmsg7);
        
        
// Initialize SQL cache dependency info
        string dependency = config["sqlCacheDependency"];

        
if (!String.IsNullOrEmpty(dependency))
        
{
            
if (String.Equals(dependency, "CommandNotification",
                StringComparison.InvariantCultureIgnoreCase))
            
{
                SqlDependency.Start(_connect);
                _2005dependency 
= true;
            }

            
else
            
{
                
// If not "CommandNotification", then extract
                
// database and table names
                string[] info = dependency.Split(new char[] ':' });
                
if (info.Length != 2)
                    
throw new ProviderException(_errmsg8);
                _database 
= info[0];
                _table 
= info[1];
            }


            config.Remove(
"sqlCacheDependency");
        }

        
        
// Throw an exception if unrecognized attributes remain
        if (config.Count > 0)
        
{
            
string attr = config.GetKey(0);
            
if (!String.IsNullOrEmpty(attr))
                
throw new ProviderException(
                    
"Unrecognized attribute: " + attr);
        }

    }


    
public override SiteMapNode BuildSiteMap()
    
{
        
lock (_lock)
        
{
            
// Return immediately if this method has been called before
            if (_root != nullreturn _root;

            
// Query the database for site map nodes
            using(SqlConnection connection = new SqlConnection(_connect))
            
{
                SqlCommand command 
= new SqlCommand(
                    
"proc_GetSiteMap", connection);
                command.CommandType 
= CommandType.StoredProcedure;

                
// Create a SQL cache dependency if requested
                SqlCacheDependency dependency = null;
                
if (_2005dependency)
                    dependency 
= new SqlCacheDependency(command);
                
else if (!String.IsNullOrEmpty(_database) &&
                         
!String.IsNullOrEmpty(_table))
                    dependency 
= new SqlCacheDependency(_database,
                        _table);

                connection.Open();
                SqlDataReader reader 
= command.ExecuteReader();
                _indexID 
= reader.GetOrdinal("ID");
                _indexUrl 
= reader.GetOrdinal("Url");
                _indexTitle 
= reader.GetOrdinal("Title");
                _indexDesc 
= reader.GetOrdinal("Description");
                _indexRoles 
= reader.GetOrdinal("Roles");
                _indexParent 
= reader.GetOrdinal("Parent");

                
if (reader.Read())
                
{
                    
// Create the root SiteMapNode and add it to site map
                    _root = CreateSiteMapNodeFromDataReader(reader);
                    AddNode(_root, 
null);

                    
// Build a tree of SiteMapNodes under the root node
                    while (reader.Read())
                    
{
                        
// Create another site map node and add it 
                        AddNode(CreateSiteMapNodeFromDataReader(reader),
                            GetParentNodeFromDataReader(reader));
                    }


                    
// Use the SQL cache dependency
                    if (dependency != null)
                    
{
                        HttpRuntime.Cache.Insert(_cacheDependencyName,
                            
new object(), dependency,
                            Cache.NoAbsoluteExpiration,
                            Cache.NoSlidingExpiration,
                            CacheItemPriority.NotRemovable,
                            
new CacheItemRemovedCallback(
                              OnSiteMapChanged));
                    }

                }

            }


            
// Return the root SiteMapNode
            return _root;
        }

    }


    
protected override SiteMapNode GetRootNodeCore ()
    
{
        
return BuildSiteMap();
    }


     
// Helper methods CreateSiteMapNodeFromDataReader and 
        
// GetParentNodeFromDataReader
}


Cache Dependencies (SQL Server 7.0 and 2000)
<configuration>
  
<connectionStrings>
    
<add name="SiteMapConnectionString"
      connectionString
=""
      providerName
="System.Data.SqlClient" />
  
</connectionStrings>
  
<system.web>
    
<siteMap enabled="true" defaultProvider="AspNetSqlSiteMapProvider">
      
<providers>
        
<add name="AspNetSqlSiteMapProvider"
          type
="SqlSiteMapProvider"
          securityTrimmingEnabled
="true"
          connectionStringName
="SiteMapConnectionString"
          sqlCacheDependency
="SiteMapDatabase:SiteMap" />
      
</providers>
    
</siteMap>
    
<caching>
      
<sqlCacheDependency enabled="true" pollTime="5000">
        
<databases>
          
<add name="SiteMapDatabase"
            connectionStringName
="SiteMapConnectionString" />
        
</databases>
      
</sqlCacheDependency>
    
</caching>
  
</system.web>
</configuration>

Cache Dependencies (SQL Server 2005)
<configuration>
  
<connectionStrings>
    
<add name="SiteMapConnectionString"
      connectionString
=""
      providerName
="System.Data.SqlClient" />
  
</connectionStrings>
  
<system.web>
    
<siteMap enabled="true" defaultProvider="AspNetSqlSiteMapProvider">
      
<providers>
        
<add name="AspNetSqlSiteMapProvider"
          type
="SqlSiteMapProvider"
          securityTrimmingEnabled
="true"
          connectionStringName
="SiteMapConnectionString"
          sqlCacheDependency
="CommandNotification" />
      
</providers>
    
</siteMap>
    
<caching>
      
<sqlCacheDependency enabled="true" />
    
</caching>
  
</system.web>
</configuration>
SQL Script for Creating a SiteMap Table
-- Create the site map node table

CREATE TABLE [dbo].[SiteMap] (
    
[ID]          [int] NOT NULL,
    
[Title]       [varchar] (32),
    
[Description] [varchar] (512),
    
[Url]         [varchar] (512),
    
[Roles]       [varchar] (512),
    
[Parent]      [int]
ON [PRIMARY]
GO

ALTER TABLE [dbo].[SiteMap] ADD 
    
CONSTRAINT [PK_SiteMap] PRIMARY KEY CLUSTERED 
    (
        
[ID]
    )  
ON [PRIMARY] 
GO

-- Add site map nodes

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)
VALUES (1'Home'NULL'~/Default.aspx'NULLNULL)

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)
VALUES (10'News'NULLNULL'*'1)

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)
VALUES (11'Local''News from greater Seattle''~/Summary.aspx?CategoryID=0'NULL10)

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)
VALUES (12'World''News from around the world''~/Summary.aspx?CategoryID=2'NULL10)

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)
VALUES (20'Sports'NULLNULL'*'1)

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)
VALUES (21'Baseball''What''s happening in baseball''~/Summary.aspx?CategoryID=3'NULL20)



-- Create the stored proc used to query site map nodes

CREATE PROCEDURE proc_GetSiteMap AS
    
SELECT [ID][Title][Description][Url][Roles][Parent]
    
FROM [SiteMap] ORDER BY [ID]
GO

既然 ASP.NET 2.0 是一个交付的产品,那么重新探究触及这个位列许多开发人员的新功能期待列表顶部的这个功能问题看起来很合适: 一个 SQL Server_ 站点映射提供程序。

正如您可能知道的,ASP.NET 2.0 极大地简化了生成数据驱动的站点导航界面的过程。 您生成一个站点映射,将一个 SiteMapDataSource 控件托放到该页面上,并将一个 Menu 或 TreeView 控件绑定到 SiteMapDataSource。 SiteMapDataSource 使用默认站点映射提供程序(通常是 XMLSiteMapProvider)读取该站点映射,然后将站点映射节点传递到 Menu 或 TreeView,它们将这些节点呈现为 HTML。 作为额外增添,您也可能以将一个 SiteMapPath 控件添加到该页面。SiteMapPath 显示常见的 breadcrumb 元素,该元素显示到当前页的路径。 图 1 显示了该站点导航子系统的组件,并阐释了它们如何协作。


图 1 导航系统


站点导航的一个缺陷点是,XMLSiteMapProvider 是 ASP.NET 2.0 包含的唯一一个站点映射提供程序,这意味着站点映射必须存储在 XML 文件中。 甚至在 ASP.NET 2.0 交付之前,开发人员已经在期待一种将站点映射存储在数据库中的方法。

Wicked Code 的 2005 年 7 6 月号以一个名为 SQLSiteMapProvider 的自定义站点映射提供程序的方式提供了一个解决方案。 与 XMLSiteMapProvider 不同,SQLSiteMapProvider 从 SQL Server 数据库读取站点映射。 而且它利用 ASP.NET 2.0 提供程序体系结构与站点导航子系统无缝继承集成。 只需创建站点映射数据库并将 SQLSiteMapProvider 注册为默认提供程序,然后一切都就立刻可以正常工作。

那么,我为什么想要重新探讨以前就已解决的问题呢? 有三个原因。 首先,通过在夏天的大部分时间里深入探究该提供程序体系结构,我认识到我的原始 SQLSiteMapProvider 实现需要进行一些改进,以便与内置 XMLSiteMapProvider 更为一致。 其次,ASP.NET 2.0 看到了 Beta 2 和 RTM 之间的一些重要更改动,而且我想针对即将交付的平台更新 SQLSiteMapProvider。 最后也是最重要的一点,我想添加几个读者通过电子邮件发给我的一个功能: 通过更改站点映射数据库自动重新加载站点映射。 许多读者似乎认为,如果没有该功能,那么存储在 SQL Server 中的站点映射就像没有机翼的飞机一样 — 今年夏天,当我最喜爱的遥控飞机夹在栅栏上时,我深深体会到了这一点。 但那只是昨天的故事。

新改进的 SQL 站点映射提供程序


新改进的 SQLSiteMapProvider 版本的结果是,它能满足我所有的目标,等等。 它的体系结构与内置提供程序的体系结构一致;它在 ASP.NET 2.0 的零售版本上编译并运行;它使用 ASP.NET 2.0 SQLCacheDependency 类监视站点映射数据库并在出现更改时刷新站点映射。XMLSiteMapProvider 具有在基础 XML 站点映射文件发生更改时重新加载站点映射的类似功能。

图 2 列出了用于新的 SQLSiteMapProvider 的源代码。 Initialize 方法(所有提供程序都具有该方法)是 加载提供程序之后 ASP.NET 在加载提供程序之后调用的一个特殊方法。ASP.NET 为 Initialize 传递一个名为 NameValueCollection 指定的配置,该配置包含注册该提供程序的配置元素中的所有配置属性 (Attribute)( 及其值)。 Initialize 方法的任务是应用配置设置,并进执行初始化提供程序所需的任何操作。 SQLSiteMapProvider 的 Initialize 方法执行以下任务:

1.

它要求 SQLClientPermission 确保它具有访问数据库的权限。 没有该权限,SQLSiteMapProvider 就无法操作。

2.

它调用该基类的 Initialize 方法,该方法处理 securityTrimmingEnabled 配置属性 (Attribute)(如果存在有),等等。

3.

它处理 connectionStringName 和 SQLCacheDependency 配置属性 (Attribute)(如果存在有)。

4.

如果注册提供程序的元素包含未识别的配置属性 (Attribute),它将引发一个异常。

如果基础数据库发生更改变,SQLCacheDependency 属性 (Attribute) 使允许您能够利用 SQLSiteMapProvider 的功能力刷新站点映射。 将 SQLCacheDependency 设置为“SiteMapDatabase:SiteMap”指示提供程序,如果 SQL Server 7.0 中名为 SiteMap 的表或 SQL Server 2000 数据库发生更改,则刷新该站点映射。(“SiteMapDatabase”通过引用<SQLCacheDependency> 配置部分的<databases> 部分中的项,间接指定数据库名称。)} 如果该站点映射位于 SQL Server 2005 数据库中,您需要将 SQLCacheDependency 设置为“CommandNotification”。 这是高度概括;稍后将介绍细节。

SQLSiteMapProvider 的核心是它的 BuildSiteMap 方法。 有时,当在加载该提供程序以生成站点映射(只是链接在一起形成树的 SiteMapNode 集合)之后,ASP.NET 调用该方法。 每个 SiteMapNode 代表示该站点映射中的一个节点,并通过以下属性 (Property) 进行区分: Title,指定导航控件为节点显示的文本;Url,指定当单击该节点时用户所发送到至的 URL;Description,指定当光标悬停在节点上时所显示的描述性文本;Roles,指定如果在启用了安全调整时所允许查看的一个或多个节点("*" 如果任何人可以查看它)。 使用逗号或分号作为分隔符,可以指定多个角色。

BuildSiteMap 的 SQLSiteMapProvider 的实现需要查询站点映射数据库。 然后,它对这些记录逐一进行迭代通过这些记录,将它们转换为 SiteMapNodes。 最后,通过返回针对根站点映射节点的引用,它将该站点映射移交给 ASP.NET。 而且由于 Initialize 方法外的所有提供程序代码都必须是线程安全的,因此 SQLSiteMapProvider 用一个 lock 锁语句将所有内容包装在 BuildSiteMap 中,以便序列化并发线程访问。

除了查询数据库并生成站点映射之外,BuildSiteMap 也还创建基本的基础结构,如果该站点映射数据库发生更改,该基础结构会启用 SQLSiteMapProvider 来刷新该站点映射。 如果注册该提供程序的配置元素包含一个 SQLCacheDependency="CommandNotification" 属性 (Attribute),则 BuildSiteMap 会创建一个与 SQL Server 2005 兼容的 SQLCacheDependency 对象,该对象包装用户查询该站点映射数据库的所使用的 SQLCommand:

// In Initialize
SQLDependency.Start(_connect);
// In BuildSiteMap
dependency = new SQLCacheDependency(command);

在另一个方面,如果该配置元素包含在 SQL Server 7.0 或 SQL Server 2000 中使用的那种 SQLCacheDependency 配置字符串(例如,“SiteMapDatabase:SiteMap”),则 BuildSiteMap 创建一个包装提供的数据库名和表名的 SQLCacheDependency 对象:

// Initialize
_database = info[0];
_table = info[1];
// BuildSiteMap
dependency = new SQLCacheDependency(_database, _table);

不管不论它创建哪种类型的 SQLCacheDependency,BuildSiteMap 稍后都会将一个小对象插入到 ASP.NET 应用程序缓存中,并通过在针对 Cache.Insert 的调用中包括 SQLCacheDependency,从而在该对象和数据库之间创建一个依赖项:

if (dependency != null)
{
HttpRuntime.Cache.Insert(_cacheDependencyName,
new object(), dependency,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.NotRemovable,
new CacheItemRemovedCallback(OnSiteMapChanged));
}

如果 SQLCacheDependency 触发一个缓存移除(即,如果该站点映射数据库发生更改),则 Cache.Insert 的 的最后一个参数指示 ASP.NET 调用该提供程序的 OnSiteMapChanged。OnSiteMapChanged 清除以前的站点映射并调用 BuildSiteMap 生成一个新站点映射。

当 SQLSiteMapProvider 没什么内容要进行缓存却使用 ASP.NET 应用程序缓存时,您可能会觉得很奇怪(毕竟,它插入到该缓存中的对象只是一个不包含任何有意义数据的标记),但是这样做可以使 SQLSiteMapProvider 利用 ASP.NET 2.0 的一个关键功能。

ADO.NET 2.0 具有一个 SQLDependency 类,该类使应用程序代码能够查询 SQL Server 2005 数据库并在基础数据发生更改时接收回调,但是它没不具有像 SQL Server 7.0 或 SQL Server 2000 那样的功能。 相反,ASP.NET 2.0 SQLCacheDependency 类使用 SQL Server 7.0、SQL Server 2000 和 SQL Server 2005。 通过将 SQLCacheDependency 附带的将标记对象放在缓存中并注册缓存移除回调是利用 SQLCacheDependency 中的其他技巧的简便方式。 如果基础数据库发生更改,则该标记对象从缓存中移删除,而且调用回调方法并采取它认为是适合的任何操作 — 在本例中,刷新该站点映射(请参见图 2)。

您可以通过将 SQLSiteMapProvider.cs 复制到您 Web 站点的 App_Code 文件夹来部署 SQLSiteMapProvider。 (在 ASP.NET 2.0 中,该目录中的源代码文件自动编译。) 当一旦部署了该提供程序之后,您就需要注册它并使其成为默认站点映射提供程序。 如果您要想使用它的 SQL 缓存依赖项功能,您也必须进行这种配置。 该您的操作方式取决于站点映射是存储在 SQL Server 7.0 或 SQL Server 2000 数据库中,还是存储在 SQL Server 2005 数据库中。

使用 SQLSiteMapProvider


图 3 中的 web.config 文件显示了如果当站点映射存储在 SQL Server 7.0 或 SQL Server 2000 数据库中名为 SiteMap 的 的表中时,如何配置 SQLSiteMapProvider 以使用 SQL 缓存依赖项。<connectionStrings> 部分定义了一个标识该数据库的名为 SiteMapConnectionString 的连接字符串。 该提供程序使用它查询数据库并监视其站点映射部分的更改。 显然,您将需要使用实际连接字符串替换省略号(“_”)。

<SiteMap>部分注册 SQLSiteMapProvider 并使其成为默认站点映射提供程序。 它也还包括一个 SQLCacheDependency 属性 (Attribute),该属性标识存储站点映射信息的数据库和表。 该属性 (Attribute) 的存在通知 SQLSiteMapProvider 创建一个 SQLCacheDependency 来监视站点映射数据库的更改;如果您想使用不带缓存依赖项的 SQLSiteMapProvider,只需省略 SQLCacheDependency 属性 (Attribute)。

<SQLCacheDependency>部分启用 ASP.NET 中的 SQL 缓存依赖项并提供所需的配置信息,包括检查轮询时间间隔,它指定 ASP.NET 检查数据库更改的频率(在本例中,每五秒钟一次)。 该部分中的数据库名映射为到该提供程序的 SQLCacheDependency 属性 (Attribute) 中指定的数据库名;该连接字符串名映射为到<connectionStrings>部分中的连接字符串。

为了使这些配置设置正常工作生效,您必须首先准备包含站点映射数据的站点映射数据库和表,以便支持 SQL 缓存依赖项。 使用 ASP.NET 2.0 附带的 aspnet_regSQL.exe 实用工具,该操作只需两分钟。 如果该数据库是一个名为 SiteMapDatabase 的本地数据库,则首先运行以下命令准备该数据库:

aspnet_regSQL –S localhost –E –d SiteMapDatabase -ed

然后,如果站点映射数据存储在数据库的 SiteMap 表中,则运行以下命令准备该表:

aspnet_regSQL –S localhost –E –d SiteMapDatabase –t SiteMap -et

第一个命令将一个更改通知表添加到该数据库以及用于访问该表的存储过程中。 第二个命令将一个插入/更新/删除触发器添加到 SiteMap 表中。 当激发之后,该触发器将一个项添加到该更改通知表中,指示 SiteMap 表的内容已经更改。ASP.NET 2.0 在预编程的间隔检查轮询更改通知表,以检测对 SiteMap 表的更改,以及对要使用 SQL 缓存依赖项监视的其他任何表的更改。

如果您使用带有 SQL Server 2005 数据库的 SQLSiteMapProvider,您无需准备该数据库来使用 SQL 缓存依赖项。 您甚至无需不必标识包含站点映射数据的数据库和表;只需使用<sqlCacheDependency>元素启用 SQL 缓存依赖项并将 SQLSiteMapProvider 的 SQLCacheDependency 属性 (Attribute) 设置为“CommandNotification”,如图 4 所示。 (您也还需要运行具有 dbo 特权的 ASP.NET 辅助进程以便使 SQL Server 2005 缓存依赖项自动工作。)

这样配置之后,SQLSiteMapProvider 通过用 SQLCacheDependency 对象包装 SQLCommand 对象(针对用于查询站点映射数据的数据库)的 SQLCommand 对象包装 SQLCacheDependency 对象,可以利用内置在 ASP.NET 中内置的 SQL Server 2005 支 支持。 SQLCacheDependency 进而使用 SQL Server 2005 查询通知接收指示该查询返回的数据已更改的异步回调。 不发生任何查轮询,也不需要任何特殊的表、存储过程或触发器。 如果您要找个借口来将您当前的数据库升级到 SQL Server 2005,则对于数据驱动的 ASP.NET 应用程序来说而言,该功能本身绝对是物有所值的。

创建站点映射数据库


针对为 SQLSiteMapProvider 创建的站点映射表必须遵循一个预定义的架构,该架构有助于本身可以用于表示相关系数据库中的分层数据的表示形式。 图 5 中的 SQL 脚本创建了这样一个名为 SiteMap 的此类表,并为它提供了示例站点映射节点。

添加到该表中的每个记录都代表示一个站点映射节点,而且每个记录具有映射为到具有相同名称的 SiteMapNode 属性 (Property) 的字段,并具有表示节点间关系的字段。 每个节点必须具有一个唯一的 ID,它存储在 ID 字段中。 要使一个节点成为另一个节点的子节点,您需要将子节点的 Parent 子字段设置为其父节点的 ID。 除根节点之外,其他所有节点都必须有一个父节点。 此外,由于 BuildSiteMap 的实现方式,一个节点只能成为 ID 比它小的节点的子节点。 (请注意该数据库查询中的 ORDER BY 子句。) 例如,ID 为 100 的节点可以成为 ID 为 99 的节点的子节点,但不能成为 ID 为 101 的节点的子节点。

默认情况下,SQLSiteMapProvider 假设存储站点映射数据的表称名为 SiteMap。SQLSiteMapProvider 使用名为 proc_GetSiteMap 的 的存储过程查询站点映射节点的数据库,而且指示该操作的目标是针对 SiteMap 表的。 如果您要想更改站点映射表的名称,只需在数据库和存储过程中更改该表名。


http://www.netscum.dk/china/msdn/library/data/sqlserver/0602WickedCode.mspx?mfr=true