网站导航(自定义站点地图)
目前你看到的所有的站点地图中为节点所提供的信息只有标题、描述、URL。然而,XML 的结构是开放的,也就是说,你可以自由的插入含有你自己数据的自定义特性。例如,你可以添加指定目标框架的特性或指定链接需要在弹出窗口中打开。现在,唯一的问题是你必须自行处理这些信息。换句话说,你必须配置用户界面来让它使用这些额外信息。
下面的代码显示了一个使用 Target 特性指定链接要打开的框架的站点地图:
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode title="Home" description="Root" url="~/Default.aspx">
<siteMapNode title="Products" description="Our products" url="~/Products.aspx" target="_blank" >
...
现在你的代码中有几个选择。如果导航控件里使用模板,可以直接绑定到已添加的新特性。如果导航控件不支持模板(或者你不愿意创建模板),就要采用另一个方法。TreeView 和 Menu 都在每个绑定项绑定(DataBound 事件)时引发一个事件:
protected void treeNav_TreeNodeDataBound(object sender, TreeNodeEventArgs e)
{
// 这里不能通过强类型的属性获得 target 特性值,SiteMapNode 类没有这个属性
// 必须按名称通过索引器获取
e.Node.Target = ((SiteMapNode)e.Node.DataItem)["target"];
}
创建自定义的 SiteMapProvider
要真正改变 ASP.NET 导航模型的工作方式,你必须创建自己的站点地图提供程序。你可能会因为以下几个原因选择自定义的站点地图提供程序:
- 需要把站点地图保存在不同的数据源(如关系型数据库)中。
- 要按不同的架构保存站点地图信息,它和 ASP.NET 预期的 XML 格式不同。
- 需要一个高度动态的站点地图,随时可进行创建。例如,你可能希望按当前用户、查询字符串参数等产生不同的站点地图。
- 需要改变 XmlSiteMapProvider 实现中的某一个限制。例如,你可能希望具有带重复 URL 的节点。
实现自定义站点地图提供程序时,你有两个选择。
- 继承 System.Web 命名空间的抽象基类 SiteMapProvider (从头实现一个新的提供程序)。
- 继承 StaticsSiteMapProvider 类(它提供了很多方法的基本实现,包括保存和搜索节点的逻辑)。
随后,在这里我将介绍一个允许在数据库中保存站点地图信息的自定义提供程序。
1. 在数据库中保存站点地图信息
下图只是重复了前一章所看到的站点地图数据。
对于这个解决方案,站点地图程序不应直接访问表。应使用存储过程。这增加了灵活性,而且你以后可以用不同的架构保存导航信息,只要存储过程返回的表包含预期的列即可。下面是本例中使用的存储过程:
create proc GetSiteMap as
select * from SiteMap order by ParentID,Title
2. 创建站点地图提供程序
站点地图没有改变站点地图导航的底层逻辑,所以可以不必由 SiteMapProvider 继承并重新实现所有的追踪和导航行为(这个工作更乏味),而直接从 StaticsSiteMapProvider 继承。
首先,修改配置文件 web.config 文件,一会提供程序需要读取相关的配置:
<system.web>
<siteMap defaultProvider="SqlSiteMapProvider">
<providers>
<add name="SqlSiteMapProvider" type="SqlSiteMapProvider" providerName="System.Data.SqlClient"
connectionString="Data Source=localhost;Initial Catalog=Northwind;Integrated Security=SSPI"
storedProcedure="GetSiteMap"/>
</providers>
</siteMap>
</system.web>
这里附上自定义站点地图提供程序的代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Common;
using System.Data;
public class SqlSiteMapProvider : StaticSiteMapProvider
{
// 首先,需要改写 Initialize() 方法,读取配置文件与站点地图相关的信息
// 提供程序需要读取下面 3 个信息并保存它们留待稍后使用
private string connectionString;
private string providerName;
private string storedProcedure;
private bool initialized = false;
public virtual bool IsInitialized
{
get { return initialized; }
}
public override void Initialize(string name,
System.Collections.Specialized.NameValueCollection attributes)
{
if (!IsInitialized)
{
base.Initialize(name, attributes);
providerName = attributes["providerName"];
connectionString = attributes["connectionString"];
storedProcedure = attributes["storedProcedure"];
if (string.IsNullOrEmpty(providerName))
{
throw new ArgumentException("The provider name is not found.");
}
if (string.IsNullOrEmpty(connectionString))
{
throw new ArgumentException("The connection string is not found.");
}
if (string.IsNullOrEmpty(storedProcedure))
{
throw new ArgumentException("The stored procedure is not found.");
}
initialized = true;
}
}
// 真正的工作在 BuildSiteMap() 方法中,它创建构造导航树的 SiateMapNode 对象
// 在应用程序的生命周期里通常只创建一次并多次重用它
// 因此,提供程序必须在内存里保存它
private SiteMapNode rootNode;
public override SiteMapNode BuildSiteMap()
{
// 因为多个页面会共享站点地图提供程序的同一个实例
// 因此,在更新任何共享信息(如内存中的导航树)前最好锁定它
lock (this)
{
// Don't rebuild the map unless using caching.
// If your site map changes often, consider using caching
if (rootNode == null)
{
// Start with a clean slate
Clear();
// 获取数据库信息保存在 DataSet 中(因为需要往返导航遍历,不可用 DataReader)
DbProviderFactory provider = DbProviderFactories.GetFactory(providerName);
DbConnection con = provider.CreateConnection();
con.ConnectionString = connectionString;
DbCommand cmd = provider.CreateCommand();
cmd.Connection = con;
cmd.CommandText = storedProcedure;
cmd.CommandType = CommandType.StoredProcedure;
DbDataAdapter adapter = provider.CreateDataAdapter();
adapter.SelectCommand = cmd;
DataSet ds = new DataSet();
adapter.Fill(ds, "SiteMap");
DataTable dtSiteMap = ds.Tables["SiteMap"];
// 导航 DataTable 创建 SiteMapNode 对象,可以搜索没有父节点的节点(找到根节点)
DataRow rowRoot = dtSiteMap.Select("ParentID is null")[0];
// 这里也借用了默认模式中 Key 和 Url 值一样的模式(注意: 它们可以不一样)
rootNode = new SiteMapNode(this, rowRoot["Url"].ToString(), rowRoot["Url"].ToString(),
rowRoot["Title"].ToString(), rowRoot["Description"].ToString());
// 填充层次的剩余部分,这里需要递归.
// 我们使用了一个私有方法 AddChildren(),它每次填充一层.
string rootID = rowRoot["ID"].ToString();
AddNode(rootNode);
// fill down the hiearachy(层次,分层)
AddChildren(rootNode, rootID, dtSiteMap);
}
}
return rootNode;
}
private void AddChildren(SiteMapNode rootNode, string rootID, DataTable dtSiteMap)
{
DataRow[] childRows = dtSiteMap.Select("ParentID = " + rootID);
foreach (DataRow row in childRows)
{
SiteMapNode childNode = new SiteMapNode(this,
row["Url"].ToString(),
row["Url"].ToString(),
row["Title"].ToString(),
row["Description"].ToString());
string rowID = row["ID"].ToString();
AddNode(childNode, rootNode);
AddChildren(childNode, rowID, dtSiteMap);
}
}
// 最后填写其他几个获取站点地图信息必需的重载方法
protected override SiteMapNode GetRootNodeCore()
{
return BuildSiteMap();
}
public override SiteMapNode RootNode
{
get
{
return BuildSiteMap();
}
}
protected override void Clear()
{
lock (this)
{
rootNode = null;
base.Clear();
}
}
}
现在你的网站不需要建立 Web.Sitemap 文件也可以使用站点地图了。自定义提供程序方便而整洁的插入,新的信息由自定义提供程序提供并到达页面,丝毫看不出底层信息通道已完全改变了。
3. 添加排序
目前,返回按标题字母排序的结果。对于一个实际的站点,你可能需要控制页面出现的顺序。幸好有最简单的解决方案。你不需要动 SiteMapProvider 的代码,只需要在表中增加一个排序的字段,如 OrdinalPosition 并修改存储过程 GetSiteMap 即可。
alter proc GetSiteMap as
select * from SiteMap order by ParentID,OrdinalPosition,Title
这样,首先按父节点进行了分组,然后按照你的控制列进行了排序,当然,这个排序只对同一层的页面有效。
4. 添加缓存
你可能注意到了有一个问题,SqlSiteMapProvider 的一个问题是它永远在内存中保存当前站点地图的根节点。除非应用程序重启(重新编译网站或修改了它的配置),否则就使用同一个站点地图。
如果打算经常修改站点地图,有几个办法可以确保应用程序被通知到变化并刷新站点地图。最好的办法是使用数据缓存把根节点保存一段有限的时间。
<add name="SqlSiteMapProvider" ... cacheTime="600"/>
在提供程序中获取这个时间:
cacheTime = Int32.Parse(attributes["cacheTime"]);
修改后的 BuildSiteMap()在所需的时间内将站点地图信息保存在缓存中:
public override SiteMapNode BuildSiteMap()
{
SiteMapNode rootNode;
lock (this)
{
rootNode = HttpContext.Current.Cache["rootNode"] as SiteMapNode;
if (rootNode == null)
{
...
HttpContext.Current.Cache.Insert("rootNode", rootNode, null,
DateTime.Now.AddSeconds(cacheTime), TimeSpan.Zero);
}
}
return rootNode;
}
最后,SqlSiteMapProvider.Clear()需要做少量改动以便从缓存中删除站点地图信息:
protected override void Clear()
{
lock (this)
{
HttpContext.Current.Cache.Remove("rootNode");
base.Clear();
}
}