筹划向 Visual Studio 2005 导航控件的迁移 --作者:Dave Donaldson Steven DeWalt
[导读]如何不编写一行代码就完成站点的导航工作,如何实现更加灵活的导航,在过去的日子对于开发人员来说是一个天方夜谭,从今开始,将不再是一个梦想。Visual Studio 2005提供的导航控件将为我们做好这一切的工作,本文详细介绍了在Visual Studio 2005下利用Breadcrumb控件实现导航。
在为Web应用程序生成UI时,首要工作之一是设计Web站点的总体结构,包括所有的站点导航。导航控件通常放在Web页的左侧或顶部,并且可能包括渐成气候的breadcrumb导航(它显示用户曾经浏览的路径),从而创建总体菜单系统。实现导航的技术有很多,但对于开发人员而言它们通常过于繁琐。这方面的例子有DHTML、include文件(对于那些使用传统ASP的人们而言)和ASP.NET中的用户控件。某些情况下导航元素被硬编码,但更常见的情形是,导航项目被保存在数据存储中以供在运行时进行只读访问——无论是在数据库(及高速缓存)中还是在XML配置文件中。
在ASP.NET 2.0中,实现站点导航所需的大多数工作均已内置,这使您能够利用丰富的功能集并且无须编写一行服务器端代码。
简单的Web站点
要证明使用ASP.NET 2.0导航控件生成Web站点是如此之简单,我们创建了一个例子——一家虚构的万事通型计算机公司:Northwind Traders。该公司Web站点的左侧有一个菜单,在右上方有一个浏览途径记录(breadcrumb)。请在Visual Studio 2005设计器中观察其中一个页面的设计(如图1所示),您可以看到有三个控件被圈了起来。蓝色圈起来的是SiteMapPath控件,它有效地显示了常见的Breadcrumb导航。红色圈起来的是TreeView控件,它当前被设置为显示平面视图,并且不能展开。绿色圈起来的是SiteMapDataSource控件,它充当TreeView控件的数据源。在我们演练新的导航控件、它们的属性以及其他有趣的项目时,请记住这张图。
图1 Visual Studio 2005设计器中的一个页面
Web.SiteMap文件
新导航控件的关键是新的web.SiteMap文件,如图2所示。这是一个标准的XML文件,它充当导航项目的默认数据存储。在Visual Studio 2005中创建新的Web站点时,默认情况下不会添加web.SiteMap文件,因此您必须自己手动添加该文件(在最终版本问世之前,可能会有所变化)。要添加该文件,请右键单击Web站点并选择“Add New Item”。在标准模板中下,选择XML文件,并将其重命名为web.SiteMap(最终版本支持将SiteMap文件作为一个特殊类型进行添加)。
一旦创建了该文件,就需要根据web.SiteMap的架构(请参见图3)来完成它,这既不困难也不麻烦。基本上,您需要一个根元素()以及零个或更多个嵌套的元素。请注意,元素的结构是逻辑性的,并且不需要映射到Web站点的物理目录结构。例如,Northwind Traders Web站点的文件都位于根目录中。另一件需要记住的事情是,对于每个元素而言,URL属性在SiteMap文件中必须是唯一的。还要注意,在Visual Studio 2005的2004年3月技术预览版中,在填写web.SiteMap文件时没有可用的智能感知。
要使web.SiteMap文件能为导航控件使用,应提供一个默认的站点映射提供程序。在Visual Studio 2005安装的machine.config文件中,默认的站点映射提供程序名为AspNetXmlSiteMapProvider,并且它使用的类类型是System.Web.XmlSiteMapProvider。该提供程序具有用于确定默认站点映射文件的属性。该属性(即,SiteMapFile)等同于“web.SiteMap”。一个名为SiteMapProvider的新抽象基类(XmlSiteMapProvider即派生于该类)已经被添加到下一版本的Microsoft .NET Framework中,使您可以实现自己的站点映射提供程序。
您应该注意,在Visual Studio 2005发布之前,web.SiteMap文件的名称有可能变更。例如,为了遵循.config命名约定,该文件可能最终被命名为SiteMap.config。
SiteMapPath控件
在详细讨论该控件之前,让我们先用一些时间来了解一下Breadcrumb踪迹在Web站点中的重要性。在中大型Web站点中,用户很容易即刻迷失在链接迷宫中,并且用户经常难以找到返回其踪迹起始位置的路线。这就是为什么breadcrumb对于用户体验如此重要的原因了。Breadcrumb导航使用户能够迅速了解他们在站点中的位置,而不必返回主页并从头开始。
要在下一版的Visual Studio中创建此类型的导航,您可以使用SiteMapPath控件。默认情况下,该控件使用web.SiteMap文件作为其数据存储,因此一旦您创建并填写了该文件,您就已经完成了在Web站点上实现Breadcrumb导航的一半工作。
SiteMapPath控件位于设计器工具箱的Navigation选项卡下方,您可以将其从设计器中拖放到Web窗体上。完成该工作之后,在您的Web站点(基于web.SiteMap文件中的元素)中便拥有基本的Breadcrumb踪迹。您或许希望调整SiteMapPath控件的属性,从而适应站点的总体设计。SiteMapPath控件的属性有很多,我们无法在此处对它们进行详细介绍,但我们将向您说明几个应该很快就能熟悉的属性。
PathDirection默认值为RootToCurrent,它以下列方式显示breadcrumb:Home > Products > Software。另一个选项是CurrentToRoot,它将前面的顺序颠倒过来:Software > Products > Home。
PathSeparator这是您希望在breadcrumb节点之间显示的任何文字。不要忘记包含前导空格和尾随空格。例如,要显示“Home > Products > Software”,您应该将该属性设置为“>”,而不是“>”。
RenderCurrentNodeAsLink这基本上决定了是否在breadcrumb中将您所在的当前页显示为指向自身的链接。默认值为False。
以下列表中的每一项都是一组属性,用于定义所有的字体和样式项以及总体外观。每组属性都包含相同的属性子集。
CurrentNodeStyle为breadcrumb中的当前节点提供外观(可替代NodeStyle)。
HoverNodeStyle当鼠标在某个breadcrumb节点上悬停时,决定该节点的外观。
NodeStyle当显示breadcrumb时,为breadcrumb中的所有节点提供统一的样式(除非它由RootNodeStyle或CurrentNodeStyle替代)。
PathSeparatorStyle是您在PathSeparator中定义的任意文字设置外观。
RootNodeStyle决定breadcrumb根节点的样式(可替代NodeStyle)。
如您所见,您可以完全控制breadcrumb的行为和外观,并且这些行为和外观都已经准备好供您使用,从而使您无需编写服务器端代码。例如,如果您希望用户快速识别Web站点的breadcrumb中的当前节点,可以将CurrentNodeStyle下的font bold(粗体)属性设置为True,或者将font size(字号)设置为Large,同时,可以将NodeStyle下的font bold属性设置为False,而将其font size设置为Small。快速查看一下Northwind Traders Web站点主页的源代码,就可以明白我们是如何实现SiteMapPath控件的:
<asp:SiteMapPath id="SiteMapPath1" runat="server" pathseparator=" -> ">
<hovernodestyle cssclass="crumbHoverNode" />
<nodestyle cssclass="crumbNode" />
<pathseparatorstyle cssclass="crumbPathSeparator" />
</asp:SiteMapPath>
SiteMapPath控件还有一个名为SiteMapProvider的属性。当该属性为空(默认)时,控件将使用前面提到的AspNetXmlSiteMapProvider(或XmlSiteMapProvider),随后它将读取web.SiteMap文件。如果您创建了自己的站点映射提供程序,并且在machine.config或web.config中添加了该项,则这就是您用来进行相应更改的属性。
SiteMapDataSource控件
当需要查看层次结构数据(例如,公共导航结构)时,该控件十分重要。SiteMapDataSource控件经常被用作供其他控件(如TreeView或DropDownList控件)绑定的数据源。要使用SiteMapDataSource控件,请在设计器工具箱的“Data”选项卡下找到它,并将它拖放到Web窗体中。
图4 SiteMapDataSource属性
如果看一下图4,您就会发现SiteMapDataSource控件的属性列表开销不是很大,下面让我们详细讨论一下该控件的每个属性。
EnableViewState如果使用过ASP.NET,您就会很熟悉该属性。它用于打开或关闭ViewState。默认情况下,该值设置为True。
FlatDepth该属性指定从分层网站地图检索节点的深度(由SiteMapProvider提供,默认情况下是web.SiteMap文件)。默认值为-1,这表示对深度没有限制。值0对应于层次结构的根节点级别,而正整数对应于低于根节点的多个级别。如果层次结构较深,并希望限制对一些级别的显示,则该属性十分有用。
ParentLevelsDisplayed指定要检索的父节点的等级数(相对于当前节点)。默认值为–1,这表示对所检索的父层没有限制。
SiteMapProvider决定供控件绑定的站点映射提供程序。默认值为空,这意味着控件使用前面讨论过的默认站点映射提供程序。如果您希望使用自己的站点映射提供程序,而不是默认的站点映射提供程序,则应该在此处键入它。
SiteMapViewType决定如何查看数据源。默认值为Tree,它使用与站点映射所指定的相同的层次结构来表示数据。其他值为Flat和Path。Flat只显示没有层次结构的节点平面视图,而Path将数据显示为当前节点和根节点之间的层次结构路径,这与SiteMapPath控件呈现其站点映射数据的方式相同。
StartingDepth指定控件开始从web.SiteMap文件中检索元素的深度。默认值为-1,表示对于数据源开始检索节点的深度没有限制。值0用于表示根节点。
StartingNodeType默认情况下,该属性设置为Root,这意味着起始节点是层次结构的根节点,但您可以将起始节点设置为任意节点。通过将该属性设置为Parent或Current,可以将起始节点设置为相对于站点映射中当前位置的某个节点。如果您使用Parent,则控件总是从当前显示页的父节点开始,除非当前显示页对应于根节点。如果将该属性设置为Current,则控件将始终从表示当前显示页的节点开始。
StartingNodeUrl您可以将web.SiteMap文件中元素的URL指定为起始节点,而不是将起始节点设置为Root、Current或Parent节点。设置该属性的过程应优先于StartingDepth和StartingNodeType属性。
ID在使用某个控件作为其他控件(如TreeView或DropDownList控件)的数据源时,记录该控件的ID。
Northwind Traders Web站点上SiteMapDataSource控件的典型声明,该声明使用所有属性(SiteMapViewType除外)的默认值,如下所示:
<asp:SiteMapDataSource
id="SiteMapDataSource1"
runat="server"
SiteMapviewtype="Flat"
/>
对于您可以拥有的SiteMapDataSource控件的数量,没有任何限制。例如,您可以在Web页的左侧拥有一个绑定到一个SiteMapDataSource控件的TreeView控件,并在右侧拥有绑定到另一个SiteMapDataSource控件的平面导航控件。
TreeView控件
对于曾经使用Windows窗体开发基于Windows应用程序的人来说,应该比较熟悉TreeView控件,然而,在Web应用程序领域中,使用TreeView还不太普遍。当前要在Web站点中实现与TreeView类似的功能,通常必须在客户端编写大量的DHTML,以使其正常工作。这被证明是非常痛苦的一件事,因为您必须考虑各种Web浏览器及其各个版本的所有特质,而测试将十分困难。在ASP.NET 2.0中,所有这些复杂性都不存在了,它们被封装在新增的TreeView控件中。
要使用TreeView控件进行站点导航,最容易的方法是从设计器工具箱中拖动该控件(位于Core选项卡下),并将它的数据源绑定到SiteMapDataSource控件。在完成该工作以后,剩下的实现就是设置属性——自始至终都不必编写任何服务器端代码。与SiteMapPath控件一样,在本文中难以对它的所有属性进行讨论,但图5列出了主要属性。在这里,我们将详细介绍个别几个属性。
图5 TreeView属性
ImageSet它可定义一组要在TreeView控件中使用的图像。它提供了大量图像集样式,最著名的有Custom、MSDN、XPFileExplorer和Windows_Help。采用其中一个预先定义的图像集,可以让TreeView使用熟悉的图像并继承所有样式。
ShowExpandCollapse起初,这看上去可能有些奇怪,因为我们都习惯于看到带有展开/折叠指示符(+号和–号)的TreeView,然而,该属性为您提供了不显示这些指示符的选项。但是,默认值为True。
ShowLines该属性决定是否在父节点树中显示连接子节点的行。默认值为False。
EnableClientScript这是一个相当重要的属性,尤其是在您希望TreeView控件能够展开和折叠时,这种情况很常见(虽然我们选择不在Northwind Traders Web站点中执行这些操作)。该属性指示是否让客户端脚本处理用于展开和折叠节点的事件(默认情况下处理)。将该属性设置为True时,将避免与服务器之间进行代价高昂的信息传递,但是如果您将该属性设置为False,则每当用户单击树中的节点时,都需要向服务器进行回发。
ExpandDepth在首次显示TreeView控件时设置展开的树层次的数目。例如,如果将该属性设置为2,则将展开根节点及根节点下方紧邻的所有父节点。默认值为–1,这表示将所有节点完全展开。
MaxDataBindDepth将此控件绑定到数据源(如SiteMapDataSource控件)时,可使用该属性来限制绑定到该控件的树级别的数目。例如,如果将该属性设置为2,则仅将根节点及根节点下方紧邻的所有节点绑定到TreeView控件。该数据源(SiteMapDataSource控件)中的所有其余节点将被忽略,这在较深的层次结构站点映射中会比较方便。默认值为–1,这会将数据源中的所有树级别绑定到该控件。
PopulateNodesFromClient默认情况下,该属性设置为True,这意味着TreeView将由web.SiteMap文件中的内容静态预先定义。因此,这可以避免到服务器的往返行程。为了使该属性为True,EnableClientScript属性也必须设置为True;否则,将发生到服务器的回发。
DataSourceID这是要绑定到的数据源的ID。如果您已经创建了一个SiteMapDataSource控件,并且希望将其用作TreeView的数据源,则请在此处键入SiteMapDataSource控件的ID。
TreeView控件所具有的属性要比在这里详细介绍的属性多很多,包括SiteMapPath控件中的相同节点样式属性(和子属性),从而使您可以对TreeView的行为和外观进行极为细致的控制。我们将探索其余TreeView属性的工作留给您自己去完成。图6 显示了Northwind Traders Web站点中某个Web页的源代码,使您能够对我们实现TreeView控件的方式有一个大致的了解。
<asp:treeview
id="TreeView1"
runat="server"
datasourceid="SiteMapDataSource1"
font-underline="False"
font-names="Verdana"
font-size="8pt"
font-italic="False"
font-bold="False"
imageset="MSDN"
forecolor="#000000"
nodeindent="30"
backcolor="#EEEEEE"
showexpandcollapse="False"
>
<selectednodestyle
verticalpadding="1"
bordercolor="#999999"
horizontalpadding="3"
backcolor="White"
font-underline="False"
font-italic="False"
font-bold="False"
borderwidth="1px"
borderstyle="Solid"
/>
<hovernodestyle
verticalpadding="1"
bordercolor="#999999"
horizontalpadding="3"
backcolor="#C7C7C7"
font-underline="True"
font-italic="False"
font-bold="False"
borderwidth="1px"
borderstyle="Solid"
/>
<parentnodestyle
font-underline="False"
font-italic="False"
font-bold="False"
/>
<leafnodestyle
font-underline="False"
font-italic="False"
font-bold="False"
/>
<nodestyle
font-underline="False"
forecolor="Black"
verticalpadding="2"
nodespacing="1"
horizontalpadding="4"
font-names="Verdana"
font-size="8pt"
font-italic="False"
font-bold="False"
/>
<rootnodestyle
font-underline="False"
font-italic="False"
font-bold="False"
/>
</asp:treeview>
图6 使用TreeView控件
目前的Breadcrumb控件
现在,您可能在想:这些控件都很棒并且我已经迫不及待地想去使用它们,我希望立即实现该功能。好,我们已经使用ASP.NET 1.1随意创建了一个简单的Breadcrumb控件,并且不是十分严格地将其设计建立在Visual Studio 2005 SiteMapPath控件的基础之上。通过遵循该控件的设计,我们已经创建了一条在需要从该控件切换到新控件时能够方便地进行迁移的途径。很自然地,我们调用了控件Breadcrumb。
对于Breadcrumb控件,我们认为将web.SiteMap文件(及其内部结构)保持为从Web站点根目录中加载的默认数据源会是一个好主意。该示例应该使您对如何逐步完成自己的控件有一个全面的了解,但ASP.NET 2.0最终版本中的实际API和实现可能有所不同。
因为web.SiteMap文件可能包含任意数量的非嵌套和嵌套元素,所以我们创建了SiteMapNode类以存放各个元素的属性,如Title、Url和Description。由于每个元素可以包含其他元素,我们在该类中添加了一个名为NodeList的属性,该属性是其他SiteMapNodes的集合。图7 包含了该类的代码。
Namespace DoughLibrary
Public Class SiteMapNode
Private m_strUrl As String = String.Empty
Private m_strTitle As String = String.Empty
Private m_strDescription As String = String.Empty
Private m_blnHasLeaf As Boolean = False
Private m_objNodeList As SiteMapNodeList
Public Sub New()
Me.New(False)
End Sub
Public Sub New(ByVal hasLeaf As Boolean, ByVal url As String, _
ByVal title As String, ByVal description As _
String)
Me.New(hasLeaf)
Me.m_strUrl = url
Me.m_strTitle = title
Me.m_strDescription = description
End Sub
Public Sub New(ByVal hasLeaf As Boolean)
If (hasLeaf = True) Then
Me.m_blnHasLeaf = True
Me.m_objNodeList = New SiteMapNodeList
Else
Me.m_blnHasLeaf = False
End If
End Sub
Public Property HasLeaf() As Boolean
Get
Return Me.m_blnHasLeaf
End Get
Set(ByVal Value As Boolean)
Me.m_blnHasLeaf = Value
End Set
End Property
Public Property NodeList() As SiteMapNodeList
Get
Return Me.m_objNodeList
End Get
Set(ByVal Value As SiteMapNodeList)
Me.m_objNodeList = Value
End Set
End Property
Public Property Url() As String
Get
Return Me.m_strUrl
End Get
Set(ByVal Value As String)
Me.m_strUrl = Value
End Set
End Property
Public Property Title() As String
Get
Return Me.m_strTitle
End Get
Set(ByVal Value As String)
Me.m_strTitle = Value
End Set
End Property
Public Property Description() As String
Get
Return Me.m_strDescription
End Get
Set(ByVal Value As String)
Me.m_strDescription = Value
End Set
End Property
End Class
End Namespace
图7 SiteMapNode类
图8显示了SiteMapNodeList类的代码,该类是从ArrayList继承的基本集合,但已强类型化为只包含SiteMapNodes。当确定某个SiteMapNode包含其他SiteMapNodes,然后通过向其简单地添加一些SiteMapNode对象来进行填充时,构建SiteMapNodeList类。
Namespace DoughLibrary
Public Class SiteMapNodeList : Inherits ArrayList
Default Public Shadows Property Item(ByVal index As Integer) _
As SiteMapNode
Get
Return CType(MyBase.Item(index), SiteMapNode)
End Get
Set(ByVal Value As SiteMapNode)
MyBase.Item(index) = Value
End Set
End Property
Public Shadows Function Add(ByVal value As SiteMapNode) _
As Integer
MyBase.Add(value)
End Function
End Class
End Namespace
图8 SiteMapNodeList类
与SiteMapNodeList类似,SiteMapPath类也是一个SiteMapNode对象的强类型集合,并且由Breadcrumb控件用来获得要呈现到浏览器的实际节点(实际上,除了类本身的名称以外,该代码与SiteMapNodeList类的代码完全相同)。例如,如果要向用户显示的路径应该是Home > Services > Training,则SiteMapPath对象将包含Home SiteMapNode、Services SiteMapNode和Training SiteMapNode。
请注意,SiteMapPath集合的顺序总是从根节点到当前节点。因此,Breadcrumb控件可以轻松地确定在浏览器中呈现节点时所遵循的方向。如果将PathDirection属性设置为RootToCurrent,控件将按照从第一个元素到最后一个元素的顺序读取SiteMapPath集合,而如果将PathDirection属性设置为CurrentToRoot,控件将按照从最后一个元素到第一个元素的顺序读取该集合。
Imports System.Xml
Namespace DoughLibrary
Public Class SiteMap
Private m_objSiteMapNode As SiteMapNode
Private m_objSiteMapPath As SiteMapPath
Private m_uriSiteMapFile As Uri
Private m_uriCurrentUrl As Uri
Private m_objXmlReader As XmlTextReader
Private m_intNodeDepth As Integer = 0
Public Sub New(ByVal siteMapFile As Uri, ByVal currentUrl As Uri)
Me.m_uriSiteMapFile = siteMapFile
Me.m_uriCurrentUrl = currentUrl
Me.m_objSiteMapNode = New SiteMapNode(True)
Me.LoadSiteMapNodes()
Me.m_objSiteMapPath = GetSiteMapPath()
End Sub
Public ReadOnly Property SiteMapPath() As SiteMapPath
Get
Return Me.m_objSiteMapPath
End Get
End Property
Private ReadOnly Property CurrentUrl() As Uri
Get
Return Me.m_uriCurrentUrl
End Get
End Property
Private ReadOnly Property SiteMapFile() As Uri
Get
Return Me.m_uriSiteMapFile
End Get
End Property
Private Property Depth() As Integer
Get
Return Me.m_intNodeDepth
End Get
Set(ByVal Value As Integer)
Me.m_intNodeDepth = Value
End Set
End Property
Private Sub LoadSiteMapNodes()
Me.m_objXmlReader = New XmlTextReader(SiteMapFile.ToString())
Me.LoadSiteMapXmlFile()
End Sub
Private Sub LoadSiteMapXmlFile()
' Ignore whitespace in web.sitemap file
Me.m_objXmlReader.WhitespaceHandling = WhitespaceHandling.None
' Skip xml declaration and any xml prolog
Me.m_objXmlReader.MoveToContent()
If (Me.m_objXmlReader.IsStartElement("siteMap") = True) Then
' Read past <siteMap> document root element
Me.m_objXmlReader.ReadStartElement("siteMap")
ReadSiteMapNode()
End If
End Sub
Private Sub ReadSiteMapNode()
' Temp branch and leaf nodes for tree traversal
Dim objBranch As SiteMapNode
Dim objLeaf As SiteMapNode
' Read the root node's attributes
ReadNodeAttribs(Me.m_objSiteMapNode)
' Cycle through the web.sitemap file nodes
While (Me.m_objXmlReader.Read())
If (Me.m_objXmlReader.NodeType = XmlNodeType.Element) Then
If (Me.m_objXmlReader.IsEmptyElement() = False) Then
Me.Depth += 1
' On a branch node, so create NodeList for leaves
objBranch = New SiteMapNode(True)
ReadNodeAttribs(objBranch)
Me.m_objSiteMapNode.NodeList.Add(objBranch)
Else
' At leaf, so don't create NodeList
objLeaf = New SiteMapNode(False)
objBranch.NodeList.Add(objLeaf)
ReadNodeAttribs(objLeaf)
End If
Else
If (Me.m_objXmlReader.NodeType = _
XmlNodeType.EndElement) Then
Me.Depth -= 1
End If
End If
End While
End Sub
Private Sub ReadNodeAttribs(ByRef node As SiteMapNode)
' Read title, description, and url attributes for the passed
' node
node.Title = Me.m_objXmlReader.GetAttribute("title")
node.Description = Me.m_objXmlReader.GetAttribute("description")
node.Url = Me.m_objXmlReader.GetAttribute("url")
End Sub
Private Function GetSiteMapPath() As SiteMapPath
Dim objPath As SiteMapPath = New SiteMapPath
Dim objCurrentNode As SiteMapNode = GetCurrentNode()
Dim objTempNode As SiteMapNode = New SiteMapNode
Dim i As Integer = 0
Dim blnFound As Boolean = False
' Always add the root node
objPath.Add(Me.m_objSiteMapNode)
If Not (objCurrentNode Is Me.m_objSiteMapNode) Then
' This loop takes advantage of knowing that the web.sitemap
' file is 3 levels deep. For robustness, this loop should
' make use of recursion instead.
While Not blnFound
objTempNode = Me.m_objSiteMapNode.NodeList.Item(i)
If (objTempNode Is objCurrentNode) Then
objPath.Add(objTempNode)
blnFound = True
Else
If (objTempNode.HasLeaf) Then
For Each objNode As SiteMapNode In _
objTempNode.NodeList
If (objNode Is objCurrentNode) Then
objPath.Add(objTempNode)
objPath.Add(objNode)
blnFound = True
Exit For
End If
Next
End If
End If
i += 1
End While
End If
Return objPath
End Function
Private Function GetCurrentNode() As SiteMapNode
Dim objResultNode As SiteMapNode = New SiteMapNode
Dim objTempNode As SiteMapNode = New SiteMapNode
Dim i As Integer = 0
Dim blnFound As Boolean = False
If (Me.m_objSiteMapNode.Url.Equals( _
Me.CurrentUrl.PathAndQuery)) Then
objResultNode = Me.m_objSiteMapNode
Else
' This loop takes advantage of knowing that the web.sitemap
' file is 3 levels deep. For robustness, this loop should
' make use of recursion instead.
While Not blnFound
objTempNode = Me.m_objSiteMapNode.NodeList.Item(i)
If (objTempNode.Url.Equals( _
Me.CurrentUrl.PathAndQuery)) Then
objResultNode = objTempNode
blnFound = True
Else
If (objTempNode.HasLeaf) Then
For Each objNode As SiteMapNode In _
objTempNode.NodeList
If _
(objNode.Url.Equals( _
Me.CurrentUrl.PathAndQuery)) Then
objResultNode = objNode
blnFound = True
Exit For
End If
Next
End If
End If
i += 1
End While
End If
Return objResultNode
End Function
End Class
End Namespace
图9 SiteMap类
SiteMap类是全部秘密所在;它负责从web.SiteMap文件加载全部节点并生成SiteMapPath对象,然后将该对象公开为只读属性(请参见图9)。因为我们知道Breadcrumb控件只关心呈现导航路径,所以提供SiteMapPath属性是有意义的,原因在于:从本质上说SiteMapPath是控件所关心的唯一对象。还要注意,我们的实现对图2 中显示的SiteMap文件的形状具有硬编码的依赖性。为了提高健壮性,GetSiteMapPath方法应改为使用递归。
当控件加载后,它将通过向SiteMap类传递站点映射文件的当前URL和名称,来创建该类的新实例。SiteMap类中的所有工作都是在构造函数中完成的:首先将所有元素加载到它们自己的SiteMapNode对象中(并将其链接在一起),然后生成SiteMapPath对象。在控件实例化SiteMap以后,它可以只请求SiteMapPath属性以呈现给浏览器。完成这一工作的代码非常简短:
Dim uriFile As Uri = New Uri("http://localhost/nwt/web.SiteMap")
Dim uriUrl As Uri = New Uri("http://localhost/nwt/products.aspx")
Dim objSiteMap As SiteMap = New SiteMap(uriFile, uriUrl)
Dim objPath As SiteMapPath = objSiteMap.SiteMapPath
所有SiteMapNodes的加载都是借助于XmlTextReader完成的,后者由一些私有Helper方法使用。通过检查web.SiteMap文件中的各个是否为空元素,我们可以确定哪些节点是分支,以及哪些节点是叶子(分支等同于嵌套节点,而叶子是非嵌套节点)。如果确定某个节点是分支,则将它添加到父节点的NodeList中,但如果该节点是叶子,则将其添加到当前分支的NodeList中。这同时使我们可以捕获树的深度。
当所有SiteMapNodes加载后,将生成SiteMapPath(这需要首先确定哪个SiteMapNode是当前节点)。像Breadcrumb实现的Visual Studio 2005版本一样,唯一性由给定的URL保证。在遍历各个SiteMapNode时,会将给定的URL与它们的URL属性进行比较,直至找到匹配项为止(随后将退出循环)。既然已经找到当前的SiteMapNode,我们将再次遍历所有节点(总是从根节点到当前节点)以跟踪路径,如GetSiteMapPath方法所示。
Breadcrumb类
Breadcrumb控件的所有服务器端代码都包含在它的自定义控件类中,并可以处理web.SiteMap文件的加载(OnLoad)和HTML的呈现(Render)。它公开了一个枚举—PathDirection,该枚举具有下列值:RootToCurrent(默认值)和CurrentToRoot。还提供了图10 中列出的属性,它们的名称与ASP.NET 2.0中的名称相同。请注意,每个“样式”属性的命名方式都与它们的ASP.NET 2.0对应属性类似,但它们的行为方式稍有不同。各个样式不再是一组属性,而是可以通过样式表设置的个别属性。
属性 | 描述 |
---|---|
PathDirection | 和ASP.NET 2.0相同 |
PathSeparator | 和ASP.NET 2.0相同 |
RenderCurrentNodeAsLink | 和ASP.NET 2.0相同 |
CurrentNodeStyle | 给出在 breadcrumb 的当前节点(重载 NodeStyle) |
HoverNodeStyle | 定义鼠标移动到breadcrumb节点上呈现的样式 |
NodeStyle | 提供所有节点显示时一致的样式 |
PathSeparatorStyle | 设置定义在PathSeparator的文本的样式 |
RootNodeStyle | 定义根节点的样式(重载NodeStyle) |
图10 breadcrumb控件属性
Imports System.ComponentModel
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Web
Namespace DoughLibrary
Public Enum PathDirection
RootToCurrent
CurrentToRoot
End Enum
Public Class Breadcrumb : Inherits WebControl
Private m_enmPathDirection As PathDirection = _
PathDirection.RootToCurrent
Private m_strPathSeparator As String = String.Empty
Private m_blnRenderCurrentNodeAsLink As Boolean = False
Private m_strCurrentNodeStyle As String = String.Empty
Private m_strHoverNodeStyle As String = String.Empty
Private m_strNodeStyle As String = String.Empty
Private m_strPathSeparatorStyle As String = String.Empty
Private m_strRootNodeStyle As String = String.Empty
Private m_SiteMapPath As SiteMapPath
Private Property SiteMapPath() As SiteMapPath
Get
Return Me.m_SiteMapPath
End Get
Set(ByVal Value As SiteMapPath)
Me.m_SiteMapPath = Value
End Set
End Property
Public Property PathDirection() As PathDirection
Get
Return Me.m_enmPathDirection
End Get
Set(ByVal Value As PathDirection)
Me.m_enmPathDirection = Value
End Set
End Property
Public Property PathSeparator() As String
Get
Return Me.m_strPathSeparator
End Get
Set(ByVal Value As String)
Me.m_strPathSeparator = Value
End Set
End Property
Public Property RenderCurrentNodeAsLink() As Boolean
Get
Return Me.m_blnRenderCurrentNodeAsLink
End Get
Set(ByVal Value As Boolean)
Me.m_blnRenderCurrentNodeAsLink = Value
End Set
End Property
Public Property CurrentNodeStyle() As String
Get
Return Me.m_strCurrentNodeStyle
End Get
Set(ByVal Value As String)
Me.m_strCurrentNodeStyle = Value
End Set
End Property
'Other properties removed for brevity
Public Property PathSeparatorStyle() As String
Get
Return Me.m_strPathSeparatorStyle
End Get
Set(ByVal Value As String)
Me.m_strPathSeparatorStyle = Value
End Set
End Property
Public Property RootNodeStyle() As String
Get
Return Me.m_strRootNodeStyle
End Get
Set(ByVal Value As String)
Me.m_strRootNodeStyle = Value
End Set
End Property
Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
Dim strFile As String = Page.Request.MapPath("web.sitemap")
Dim strPage As String = Page.Request.Url.ToString.ToLower
Dim objSiteMap As SiteMap = New SiteMap(New Uri(strFile), _
New Uri(strPage))
Me.m_SiteMapPath = objSiteMap.SiteMapPath
End Sub
Protected Overrides Sub Render(ByVal output As _
System.Web.UI.HtmlTextWriter)
' SiteMap Class always returns the SiteMapPath in RootToCurrent
' Order. If PathDirection attribute is set to CurrentToRoot,
' reverse the SiteMapPath direction before rendering.
If Me.PathDirection = PathDirection.CurrentToRoot Then
Me.SiteMapPath.Reverse()
End If
' outermost span start element for breadcrumb control
output.AddAttribute(HtmlTextWriterAttribute.Id, Me.ID)
output.RenderBeginTag(HtmlTextWriterTag.Span)
For Each objCrumb As SiteMapNode In Me.SiteMapPath
' start crumb span
output.RenderBeginTag(HtmlTextWriterTag.Span)
' start crumb hyperlink anchor
If Me.SiteMapPath.IndexOf(objCrumb) = 0 Then
If Me.PathDirection = PathDirection.RootToCurrent Then
' set css of 1st crumb: root if RootToCurrent
' direction
output.AddAttribute( _
HtmlTextWriterAttribute.Class, Me.RootNodeStyle)
Else
' set css of 1st crumb: current if CurrentToRoot
' direction
output.AddAttribute( _
HtmlTextWriterAttribute.Class, Me.CurrentNodeStyle)
End If
ElseIf Me.SiteMapPath.IndexOf(objCrumb) = _
(Me.SiteMapPath.Count - 1) Then
If Me.PathDirection = PathDirection.RootToCurrent Then
' set css of last crumb: current if RootToCurrent
' direction
output.AddAttribute(HtmlTextWriterAttribute.Class, _
Me.CurrentNodeStyle)
Else
' set css of last crumb: root if CurrentToRoot
' direction
output.AddAttribute(HtmlTextWriterAttribute.Class, _
Me.RootNodeStyle)
End If
Else
' set css of all other crumbs to the NodeStyle property
' value
output.AddAttribute(HtmlTextWriterAttribute.Class, _
Me.NodeStyle)
End If
If RenderCurrentNodeAsLink = False Then
If ((Me.PathDirection = PathDirection.RootToCurrent) _
AndAlso (Me.SiteMapPath.IndexOf(objCrumb) = _
(Me.SiteMapPath.Count - 1))) Or _
((Me.PathDirection = _
PathDirection.CurrentToRoot) AndAlso _
(Me.SiteMapPath.IndexOf(objCrumb) = 0)) Then
output.RenderBeginTag(HtmlTextWriterTag.Span)
' <span>
output.Write(objCrumb.Title)
output.RenderEndTag() ' </span>
Else
output.AddAttribute(HtmlTextWriterAttribute.Title, _
objCrumb.Description)
output.AddAttribute(HtmlTextWriterAttribute.Href, _
objCrumb.Url)
output.RenderBeginTag(HtmlTextWriterTag.A) ' <a>
output.Write(objCrumb.Title)
output.RenderEndTag() ' </a>
End If
Else
output.AddAttribute(HtmlTextWriterAttribute.Title, _
objCrumb.Description)
output.AddAttribute(HtmlTextWriterAttribute.Href, _
objCrumb.Url)
output.RenderBeginTag(HtmlTextWriterTag.A) ' <a>
output.Write(objCrumb.Title)
output.RenderEndTag() ' </a>
End If
output.RenderEndTag() ' </span> (end crumb span)
' append path separator if not on last crumb
If (Me.SiteMapPath.IndexOf(objCrumb) _
< (Me.SiteMapPath.Count - 1)) Then
output.AddAttribute(HtmlTextWriterAttribute.Class, _
Me.PathSeparatorStyle)
output.RenderBeginTag(HtmlTextWriterTag.Span)
output.Write(Me.PathSeparator)
output.RenderEndTag() ' </span>
End If
Next
output.RenderEndTag() ' </span> (outermost span)
End Sub
End Class
End Namespace
图11 breakcrumb类
在控件的加载事件过程中,创建了一个SiteMap实例,然后控件像刚才介绍的那样获取SiteMapPath。接下来,Render重写方法会按照正确的PathDirection将导航路径输出到浏览器中,并且带有所有关联的样式以完成其外观。图11 显示了该类的代码,而随后的代码片段演示了如何在Web表单上使用该自定义控件。请观察一下这些代码与使用ASP.NET 2.0 SiteMapPath控件所需的代码之间的相似性。
<nwt:breadcrumb
id="Breadcrumb1"
runat="server"
pathseparator=" -> "
currentnodestyle="crumbNode"
nodestyle="crumbNode"
pathseparatorstyle="crumbPathSeparator"
rootnodestyle="crumbNode"
rendercurrentnodeaslink="True"
/>
我们为Breadcrumb控件介绍的所有代码为您提供了一个向Web站点添加导航路径的简单方法。虽然还有提高其健壮性(通过缓存、错误处理和递归)的余地,但设计和编写该控件的主要目的是提高其可扩展性。
小结
如您所见,在目前实现SiteMapPath (Breadcrumb)控件,并使其具有ASP.NET 2.0相应控件所提供的某些功能,需要数百行代码。要在目前实现一个与ASP.NET 2.0版本同样强大的TreeView控件,需要花费大量的时间,并编写成千上万行代码。仅这一点,就足以使您为ASP.NET 2.0中包含的新导航控件而欢呼了,因为所有上述功能及其他功能都以现成的方式提供给您,只需要进行最少量的配置,而且更为重要的是,根本不需要自己编写代码。如果您装备了ASP.NET 2.0,那么将实现您以前做梦都不敢想的导航目标。
作者简介:Dave Donaldson和Steven DeWalt都是大型保险公司的IT架构师顾问。工作之余,他们会忙于为他们的小型软件工作室LoudCarrot Software生成基于.NET的应用程序和服务。您可以访问http://www.loudcarrot.com与他们进行联系。