理解并扩展 ASP.NET 2.0 中的站点导航系统
理解并扩展 ASP.NET 2.0 中的站点导航系统
David Gristwood
Developer & Platform Group, Microsoft
适用于:
Microsoft ASP.NET 2.0 (Beta 2)
摘要:ASP.NET 2.0 站点导航系统构建于一个功能强大、灵活的体系结构之上,设计这样的体系结构是为了使其具有可扩展性。本文探究站点提供程序的体系结构并提供一个示例提供程序,该提供程序将文件系统公开为站点导航的数据源,从而替代了标准的 Web.sitemap XML 文件。
请从此处下载示例。
本页内容
简介 | |
理解 ASP.NET 2.0 中的导航系统 | |
导航控件 | |
使用导航系统 | |
实现您自己的站点地图提供程序 | |
一个文件夹站点地图提供程序 | |
小结 |
简介
大多数 web 站点采用可视化导航的某种形式来帮助用户轻松地浏览站点,以及查找他们所需的信息和 Web 页。尽管不同站点之间的感观效果千差万别,但是通常会使用相同的基本元素 — 以导航栏或菜单列表的形式使用户定位到 web 站点的特定位置。
ASP.NET 1.x 提供的针对站点导航现成的支持很少,导致很多开发人员和 web 设计人员不是构建自己的导航系统,就是购买第三方控件以满足他们的需求。而 ASP.NET 2.0 对此作出了改进,它引入一个使用可插接式框架的导航系统,该框架能够公开站点层次结构和插入这个新模型的控件,因此易于构造一个高质量的菜单和导航系统。
本文描述 ASP.NET 2.0 导航系统的工作原理并展示如何对其进行扩展 — 不仅仅是使用简单的 XML 文件(Visual Studio 2005 中使用的默认机制)。
理解 ASP.NET 2.0 中的导航系统
ASP.NET 2.0 导航系统的一个目标是创建一个可以吸引开发人员和 web 站点设计人员的优秀的导航模型,除此之外,它还有一个目标是创建一个提供可扩展性功能的体系结构,该功能能够灵活地满足广泛的需求。该系统基于一个提供程序模型,该模型的使用贯穿于整个 ASP.NET 2.0 框架,由 ASP.NET 2.0 框架提供一个标准的机制用于插入不同的数据源。
ASP.NET 2.0 导航框架可以分解为几个部分:
• |
开发人员在实际 web 页面上使用的web 导航控件(Menu、TreeView 和 SiteMapPath)。这些控件可以通过自定义改变感观效果。 |
• |
TreeView 和菜单导航控件绑定的 SiteMapDataSource 控件,在 Web 导航控件和导航信息的底层提供程序之间提供一个抽象层。 |
• |
站点地图提供程序是可插接式提供程序,它用于公开描述 web 站点布局的实际信息。ASP.NET 提供了一个提供程序 XmlSiteMapProvider,它使用一个具有特定结构的 XML 文件作为其数据存储。 |
这种分层的体系结构在底层的站点层次结构和 web 站点上的控件之间制造了更为松散的耦合,提供了更大的灵活性,而且随着站点的不断发展,更容易实现体系结构和设计的改动。
以下表格说明提供程序和控件之间的关系。
图 1. 导航体系结构
对于导航系统,数据源描述用户能够定位的 web 站点页的层次结构,以及将这些信息显示给用户的方式。它作为一个站点地图被引用。一个简单的 web 站点的布局可以是以下形式:
Home Products Product A Product B Product C Latest Offers Contact Us Email Visit us
导航控件
在深入研究导航系统的内部工作机制之前,了解开发人员如何与之交互十分重要。最常见的方法是通过 ASP.NET 2.0 中的三个新导航控件。一个站点或页面上可以存在多个 Web 导航控件 — 如,主菜单控件放置在页面左侧,另一个菜单控件放在页面顶部,这种情况还是时有发生的,我们能够以编程方式使一个导航控件控制页面上另一个导航控件,或使它们各自独立操作。
导航系统通常与母版页功能一起使用,母版页功能也是 ASP.NET 2.0 中提供的新功能。通过将导航控件放置在站点的母版页上,可以确保整个站点具有统一的感观效果。但是,导航功能和母版页功能互不相关且各自独立。
这三个新 web 控件是:
• |
菜单控件 — 它提供一个传统的导航界面,通常的情况是沿 web 站点的一侧或横跨顶部。它能够显示任意数目的嵌套子菜单,而且,当用户的鼠标停悬在某一项上时,可以显示任意数目的可选弹出子菜单。 图 2. 菜单控件 |
• |
TreeView 控件 — 它提供一个垂直的树状用户界面,通过选择单个节点可以展开和折叠用户界面。它还提供用于选定某些项的复选框功能。 图 3. TreeView 控件 |
• |
SiteMapPath 控件 — 它通常作为“breadcrumb”控件进行引用,因为它可以跟踪用户在站点层次结构内的位置。它将当前位置显示为一个路径,通常是从主页到当前位置的路径,因而用户更容易了解自己所处的位置,并定位回路径上的其他页面。 图 4. SiteMapPath 控件 |
从体系结构的角度来看,菜单控件和 TreeView 控件有些相似,两者之间的主要区别是编程以及显示方式不同,因此,各自拥有独特的感观效果。要详细了解这两个控件,请参阅 MSDN 中的文章 Introducing the ASP.NET 2.0 TreeView and Menu Controls。值得注意的是,虽然使用菜单控件和 TreeView 控件的最常见情况离不开站点导航,但是它们也能够用于非导航情况,这有待于用户自己做选择。
SiteMapPath (或“breadcrumb”)控件则略为不同。它直接使用 SiteMapProvider,而不是菜单控件和 TreeView 控件使用的 SiteMapDataSource 控件。这说明它是导航系统内部更有针对性的控件,因此支持将其功能扩展到非导航情况的理论依据更少。
使用导航系统
理解站点导航工作原理的一种最简单的方法是,直接在一个应用程序内部访问它,而不是通过一个 Web 控件来访问。下列代码示例说明如何与 SiteMap 对象模型进行交互以显示一部分层次结构化的站点信息。
<%@ Page Language="C#" %> <script runat="server"> private void Page_Load(object sender, System.EventArgs e) { this.Label1.Text = "Current Page Title : " + SiteMap.CurrentNode.Title; if(SiteMap.CurrentNode.ChildNodes > 0) { this.HyperLink1.NavigateUrl = SiteMap.CurrentNode.ChildNodes[0].Url; this.HyperLink1.Text = SiteMap.CurrentNode.ChildNodes[0].Title; } } </script> <html> <head> </head> <body> <form id="Form1" runat="server"> <asp:Label ID="Label1" runat="server" Text="Label"></asp:Label><br /> First Child Node: <asp:HyperLink id="HyperLink1" runat="server">HyperLink</asp:HyperLink> </form> </body> </html>
导航系统将站点地图模型化为一系列节点,称为 SiteMapNodes,在一个树状结构内,每个节点通常表示用户能够定位到的 web 站点上的一个页面(可能有一些节点只是子页面的占位符,或与之类似,本身没有 Web 页)。
SiteMapNode 类具有一些属性和方法,其中最重要的一些是:
• |
Title — 当此节点通过一个 web 控件显示时将呈现的文本 |
• |
Url — SiteMapNode 在 web 站点内表示的实际页面的 URL |
• |
Description — 当鼠标悬停在 HTML 页面的节点上时显示的工具提示 |
SiteMapNode 类还有一些属性用于保存各个 SiteMapNodes 之间的关联,这些关联描述出站点地图的结构(ChildNodes、NextSibling、PreviousSibling、ParentNode 等等)。
实现您自己的站点地图提供程序
如前所述,ASP.NET 2.0 发行了一个名为 XmlSiteMapProvider 的导航数据存储提供程序,这是大部分开发人员熟悉的默认提供程序。该提供程序使用一个表示 SiteMapNodes 的 XML 文件中的数据。在使用 Visual Studio 2005 时,如果您在 Web 项目中添加了一个新站点地图,那么在默认情况下,在项目内将创建一个 Web.sitemap 文件,并按照下列模板格式进行填充。
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="" title="" description=""> <siteMapNode url="" title="" description="" /> <siteMapNode url="" title="" description="" /> </siteMapNode> </siteMap>
您一眼就可以看出,前面提到的核心 SiteMapNodes 属性(title、url 和 description)就是 Web.sitemap XML 架构中的属性,而且树结构(父级、子级等等)是由嵌套的 SiteMapNodes 表示的。
这是处理站点地图信息的一种简单且精练的方法,对于许多 Web 设计人员而言,使用起来绰绰有余。然而,ASP.NET 提供程序模型具有扩展性并不意味着您能够编写自己的提供程序。创建一个自定义站点地图提供程序要考虑三个可能的主要原因:
• |
站点地图信息保存在一个非 XML 文件的数据源中,例如数据库或目录服务。 |
• |
站点地图信息以 XML 的形式使用,但是使用的架构与 Web.sitemap 使用的架构不同。 |
• |
需要一个动态的站点地图结构,该结构需要在运行时构造,或者需要一个安全剪裁不能处理的自定义视图。 |
为了实现您自己的站点地图提供程序,您需要从 System.Web 命名空间的 SiteMapProvider 抽象类中派生一个自定义的提供程序类。虽然 SiteMapProvider 类具有大约二十几个抽象方法或虚方法,但是在您的自定义站点地图提供程序中,只有少数方法需要重写或实现。
如果您的自定义站点地图提供程序使用的数据存储与默认的 XmlSiteMapProvider 的 Web.sitemap XML 架构相似,则应该选择从 StaticSiteMapProvider 类派生,它提供 SiteMapProvider 类的部分实现,并反过来作为实际的 XmlSiteMapProvider 类的基类。
您的类必须至少实现下列方法:
• |
FindSiteMapNode — 返回一个与特定 URL 对应的 SiteMapNode。 |
• |
GetChildNodes — 返回一个特定 SiteMapNode 的子节点集合。 |
• |
GetParentNode — 返回一个特定 SiteMapNode 的父节点。 |
• |
GetRootNodeCore — 返回当前提供程序目前管理的所有节点的根节点。 |
您还应该重写 SiteMapProvider 的 Initialize 方法,在调用基类 Initialize 之后在该方法中执行您自己的初始化。
如果有必要,您可以选择重写与节点相关的一些属性,从而构造更为复杂的表示站点地图的模型:
• |
SiteMapNode — 返回映射到当前 HTTPContext URL 的节点。 |
• |
RootNode — 返回存储根部的节点。 |
• |
RootProvider — 返回根部的提供程序,以允许连锁提供程序。 |
这些属性和方法代表了 SiteMapProvider、Menu / TreeView 控件(通过 SiteMapDataSource)和 SiteMapPath 控件之间的协定。在以下两种情况下会发生这种交互:控件请求填充显示所需的信息,以及用户通过站点进行导航以跟踪站点导航内的当前位置。它是一个事件驱动模型,在交互过程中,导航系统调入提供程序。
如果编写自己的提供程序,您需要决定 SiteMapNodes 是只读还是可写 — 如果是可写的,您需要考虑线程安全,在适当的情况下锁定更新代码,从而保证实现是线程安全的,因为 ASP.NET 通过一个线程池调度请求,因此一个线程池会被多个线程访问。
一个更细节的问题是有关性能和可伸缩性的。一个 web 站点使用几个导航控件,或者底层导航数据存储容量很大的情况并不少见,因此,您需要保证提供程序可以响应,因为这将影响有关设计方面的事宜,包括存储和检索数据的最佳算法以及缓存的潜在作用。
安全问题也需要考虑。Web 站点的普遍要求是只允许会员或其他经过身份验证的用户查看特定页面,而 ASP.NET 2.0 的角色管理提供了定义明确的方法,以便根据安全角色限制对 Web 文件的访问。这一功能通过一个称为安全剪裁的机制扩展到站点导航系统中。安全剪裁强制在尝试访问页面时也应用 Url 和文件授权。在节点上定义一个 roles 属性用于扩展对该节点的访问 — 如果您是在 roles 属性中定义的角色之一,那么将返回该节点;如果您不是这些角色中的一个,那么将执行 Url 和 File Authoriation 检查。对于 XmlSiteMapProvider 而言,在一个 SiteMapNode 项中添加一个 roles="managers" 类型的属性将决定用户在站点地图中是否能够看到该节点。在编写您自己的站点地图提供程序时,这通过 IsAccessableToUser 方法进行处理,它根据用户的角色返回一个布尔值,指示该节点对用户是否可用。派生的提供程序能够使用基类 SiteMapProvider 中存在的默认实现。如果要支持安全性,那么站点地图必须提供某种方法来存储该信息。
一个文件夹站点地图提供程序
本文中的示例将一个 web 站点内的子目录公开为实际的站点地图,这使 web 站点导航能够直接映射到文件夹结构,这样的 web 站点结构模型不少见。该示例的重点是说明如何设计和构造一个站点地图提供程序,在这种情况下,它的目的不是覆盖行业优点方面所有可能的“边缘条件”。这些权衡在本文内的适当位置都已强调。
示例提供程序通过 web 站点的子目录进行递归枚举,检查每个文件夹(除了 App_* 和 bin 目录,它们是 ASP.NET 2.0 使用的特殊目录)是否存在一个 default.aspx 文件。如果存在这个文件,示例提供程序则将其添加到站点地图中,并使用包含文件夹的名称作为菜单描述。扩展该示例,使每个目录不仅支持 default.aspx 项,或者必要时在目录内的一个文本文件中存储一个日期工具提示,这实现起来相对比较简单。
提供程序的核心代码如下所示。
[AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)] public class FolderSiteMapProvider : SiteMapProvider { private SiteMapNode rootNode = null; // root of tree private Hashtable urlHash = null; // hash table based on URL private Hashtable keyHash = null; // hash table based on key string defaultPageName = "Default.aspx"; // only look for this file in each subdirectory string defaultTitle = "Home"; // description of root node // override SiteMapProvider Initialize public override void Initialize(string name, NameValueCollection attributes) { // do base class initialization first base.Initialize(name, attributes); // our custom initialization urlHash = new Hashtable(); keyHash = new Hashtable(); // get web site info string startFolder = HttpRuntime.AppDomainAppPath; string startUri = Uri.EscapeUriString(HttpRuntime.AppDomainAppVirtualPath); // want canonical format of URI // Create root node string key = startFolder; string url = startUri + @"/" + defaultPageName; rootNode = new SiteMapNode(this, key, url, defaultTitle); RootNode.ParentNode = null; urlHash.Add(url, rootNode); keyHash.Add(key, rootNode); // populate entire site EnumerateFolders(rootNode); } // Retrieves a SiteMapNode that represents a page public override SiteMapNode FindSiteMapNode(string rawUrl) { if (urlHash.ContainsKey(rawUrl)) { SiteMapNode n = (SiteMapNode)urlHash[rawUrl]; return n; } else { return null; } } // Retrieves the root node of all the nodes //currently managed by the current provider. // This method must return a non-null node protected override SiteMapNode GetRootNodeCore() { return rootNode; } // Retrieves the child nodes of a specific SiteMapNode public override SiteMapNodeCollection GetChildNodes(SiteMapNode node) { SiteMapNode n = (SiteMapNode)keyHash[node.Key]; // look up our entry, based on key return n.ChildNodes; } // Retrieves the parent node of a specific SiteMapNode. public override SiteMapNode GetParentNode(SiteMapNode node) { SiteMapNode n = (SiteMapNode)keyHash[node.Key]; // look up our entry, based on key return n.ParentNode; } // helper functions . . . // . . . }
当提供程序通过其 Initialize 方法进行调用时,示例提供程序生成一个文件夹列表,但是不刷新该列表,因此它不检查随时添加的新文件夹,这对一个产品的 web 站点是不合理的。
它不实现安全剪裁,因此所有子目录都可见,但是,要实现安全剪裁是很简单的,只要在 FindSiteMapNode、GetChildNodes 和 GetParentNode 方法内添加对基类 IsNodeAccessible 方法的调用就可以了,IsNodeAccessible 方法将自动获得为 web 站点配置的任何文件授权规则的优点。
提供程序在内部能够以任意种方法表示底层存储,但是,因为它通过 SiteMapNode 类与导航系统进行交互,特别是因为目录层次结构与节点层次结构如此匹配,所以在这种情况下,在内部使用这些方法才有意义。它还维护两个哈希表,其中一个以节点 URL 为关键字,另一个以节点关键字(文件夹名)为关键字,从而支持快速查询节点。对于本示例而言,在内存中保存所有这些信息是有意义的,因为它能够保持简单的编程模型和非常快的查询速度;但是,对于一个拥有成千上万个文件夹的大型站点而言,研究一个不在内存中保存全部信息的机制可能是值得的。
为了保持简单的代码,节点没有以只读的方式处理。在一个实际的具有行业优点的提供程序中,当应用程序不允许添加或修改站点地图节点的内部列表时,就将各个节点以及整个集合标识为只读。
私有的 helper 函数用于构造文件夹层次结构,如以下代码示例所示。
private void EnumerateFolders(SiteMapNode parentNode) { // create a node collection for this directory SiteMapNodeCollection nodes = new SiteMapNodeCollection(); parentNode.ChildNodes = nodes; // get list of subdirectories within this directory string targetDirectory = parentNode.Key; // we use the key to hold the file directory string[] subdirectoryEntries = Directory.GetDirectories(targetDirectory); foreach (string subdirectory in subdirectoryEntries) { // search for any sub folders in this directory string[] s = subdirectory.Split('\\'); string folder = s[s.Length-1]; string tmp = String.Copy(folder); tmp = tmp.ToLower(); // avoid any case sensitive matching issues // check for App_ and bin directories, and don't add them if (tmp.StartsWith("app_")) continue; if (tmp == "bin") continue; string testFileName = subdirectory + @"\" + defaultPageName; if (File.Exists(testFileName)) { // create new node string key = subdirectory; string url = CreateChildUrl(parentNode.Url, folder); string title = folder; SiteMapNode n = new SiteMapNode(this, key, url, title); n.ParentNode = parentNode; // add it into node collection and table nodes.Add(n); urlHash.Add(url, n); keyHash.Add(key, n); // and enummerate through this folder now EnumerateFolders(n); } } }
当提供程序在子目录中枚举时,它为每一个合法子目录创建一个 SiteMapNode 类,并设置它的 key、url 和 title 属性。它还为每一个子节点填充 ChildNodes 属性,这个属性是一个 SiteMapNodeCollection,而 ParentNode 属性则向上指回父节点。同时,它也将每个节点添加到哈希表中,以便进行快速查询。
一旦完成这些操作,提供程序需要使用 web.config 文件进行配置。以下项需要添加到 <system.web> 下。
<system.web> . . . <siteMap defaultProvider="SimpleProvider"> <providers> <add name="SimpleProvider" type="Test.SimpleProvider"/> </providers> </siteMap> . . . </system.web>
小结
本文的目标是使您了解并正确评价 ASP.NET 2.0 站点导航系统,理解不同元素之间协作的原理。代码示例说明了如何通过构造您自己的站点地图提供程序来扩展体系结构,该提供程序使用任意数据源来定义站点层次结构。
关于作者
David Gristwood 在 Microsoft 位于 UK 的开发人员和平台组工作,他大部分时间用来与客户和合作伙伴一起工作,帮助他们设计和构建充分利用 Microsoft .NET 平台的解决方案。他也经常在会议和研讨会上发言。