C#发现之旅 - 高性能ASP.NET树状列表控件
本章介绍
本课程研究了使用XSLT技术来开发一个树状列表WEB控件,展示了XSTL的一些高级应用,以及ASP.NET控件的开发过程,本课程内容比较多。本章的配套演示程序为https://files.cnblogs.com/xdesigner/ASPNETTreeView.zip。
已有树状列表控件分析
发现问题
近期发现有人在ASP.NET项目开发中使用一种叫dtree的树状列表组件加载缓慢。这也是笔者撰写本章的动机。毛主席教导我们,做事要发现问题,分析问题和解决问题。首先我们发现了已有的树状列表WEB控件加载缓慢的问题,接下来就很自然的是分析问题了。
下图就是dtree 运行界面的例子【袁永福原创(http://www.xdesigner.cn/)】
分析问题
现在我们分析问题,对使用dtree生成树状列表的程序代码的分析,可以了解程序运行过程如下图所示
在这样的程序中,首先服务器端的C#代码查询数据库,然后根据查询所得数据拼凑出一个JavaScript代码字符串,然后发往客户端,客户端浏览器获得这个JavaScript代码字符串并开始执行它,而在JavaScript脚本中也是字符串拼凑出一段HTML代码字符串,然后使用浏览器提供的document.write方法或obj.innerHTML属性将生成的HTML字符串填充到HTML页面中进行展示。
这是是分析了dtree的流程,但使用其他的一些树状列表控件也大体如此。
现在我们根据这个流程图来判断是哪个环节速度缓慢。基本上数据库本身查询速度是没问题;将查询结果传递到C#程序中问题也不大,因为一般的数据库服务器和ASP.NET程序是在一台电脑上或者同一个高速局域网中;C#程序生成JavaScript字符串的过程也是没多大问题,因为C#运行速度是相当的快的,而且还有StringBuilder来加速字符串拼凑操作,因此只要逻辑算法没有问题,速度是有保障的。总体来说服务器端内部是没有速度问题。
将JavaScript字符串通过网络从服务器端发送到客户端,所花的的时间是字符串长度除以网络传输速度,若WEB系统运行在高速的局域网中,则速度没多大问题,但若WEB程序运行在缓慢的广域网或英特网中,则JavaScript字符串长度会比较大的影响程序运行速度。由于公司系统主要运行在局域网中,因此网络传输速度不是主要问题。
在客户端浏览器中,浏览器接受并执行JavaScript脚本代码,在JavaScript脚本中使用字符串拼凑来生成用于展现树状列表的HTML字符串。JavaScript代码是解释方式执行的,速度相当慢,而字符串拼凑操作也是比较缓慢的操作,JavaScript中没有任何手段来优化字符串拼凑操作。因此由JavaScript代码生成HTML字符串的过程是缓慢的,这是一个速度瓶颈。
JavaScript代码还调用浏览器提供的document.write函数或innerHTML属性将生成的HTML字符串填充到页面中,浏览器会解析这个HTML代码并展现出树状列表。由于document.write或innerHTML是运行在浏览器内部的,外部程序无法控制,而且速度也不算慢,因此这里也就没有什么好优化的。
经过上述分析,可以看到整个展现树状列表的过程中最缓慢的环节就是使用JavaScript脚本来生成HTML代码字符串,其次就是数据从服务器端发送到客户端的过程。若一个树状列表要显示数千个节点,则JavaScript脚本将拼凑出几百K甚至过MB的HTML字符串,这个过程是相当缓慢的,很容易导致IE浏览器由于脚本运行过于缓慢而提示用户是否继续执行脚本。
因此JavaScript脚本生成HTML字符串将是我们主要的优化环节,也是新开发的树状列表控件的重点关注部分。
解决问题
经过上述分析,我们可以了解到树状列表加载缓慢主要原因就是JavaScript脚本生成HTML字符串过程缓慢,很自然我们就针对这个原因来解决问题。
【袁永福原创(http://www.xdesigner.cn/)】首先我们可以完全抛弃JavaScript脚本,使用C#在服务器端生成 展现树状列表的HTML代码,然后发往客户端 ,客户端浏览器获得HTML代码并展示出树状列表。
在这种模式下, 服务器端的C#程序查询数据库获得数据,并使用字符串拼凑来生成用于展现树状列表的HTML代码,由于C#功能强大,而且速度比较快,可以使用StringBuilder来加速字符串拼凑操作,而客户端浏览器获得的这个HTML代码,立即解析并显示出树状列表。因此整个过程是相当快的。这是一个可以采用的解决方法。
当然这个方式有一定的限制性,若服务器程序运行也比较缓慢,比如ASP,它比客户端的JavaScript脚本快不了很多,此时这种方法优势就不明显了。
【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
另外一个方法就是加速JavaScript脚本生成HTML代码的过程。这时我们可以考虑使用其他的可快速运行的程序来辅助JavaScript来生成HTML代码,于是我们想到COM组件,我们可以设计出这样的程序结构。
在这个软件结构中,C#程序连接数据库查询数据,然后生成JavaScript脚本字符串,而客户端浏览器获得并执行JavaScript脚本,在JavaScript脚本调用外部的COM组件,生成HTML字符串,然后使用document.write或innerHTML将HTML字符串填充到页面中显示出树状结构。由于COM组件一般是用C++等编译性语言开发的,因此运行速度比JavaScript快得多,这样能加速JavaScript生成HTML代码的速度。
由于便于B/S系统的开发和部署,我们尽量避免自己开发的COM组件或使用第三方组件,而是使用Windows操作系统自带的标准COM组件,浏览器认为该组件比较安全,运行速度快,而且还能方便的生成HTML字符串。这个组件是什么呢?这就是MSXML组件。
MSXML组件是用C++开发的,是Windows操作系统的标准部分,而且是IE浏览器认为比较安全的ActiveX组件,能和IE浏览器进行密切的协作。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
那么我们又如何使用MSXML组件来生成HTML代码呢?我们可以采用XSLT技术,首先系统提供一个XML文档,该文档定义了树状结构信息,然后我们调用一个事先定义好的XSLT文档,将两者进行XSLT转化,一下子就能生成HTML字符串,然后将生成的HTML字符串填充到页面中。在这个过程中,大部分运算量是由MSXML完成,而MSXML组件是用C++开发的,运行速度快,这样就能大大加快整个生成HTML字符串的过程,从而加快树状列表的加载过程。
由于XSLT是国际标准,因为我们在服务器端也可以使用这种方法。而且客户端和服务器端的代码类似,因此我们有可能开发出同时支持服务器端运行和客户端运行的树状列表WEB控件。
运行软件
笔者已经根据上述的解决问题的方式经过上述的软件设计开发出了这个树状列表WEB控件,并编写了演示页面,现对该控件的功能进行展示,使得读者对这个控件有一些初步的印象。打开浏览器直接输入演示页面地址,打开页面,可以看到如下的用户界面【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
可以看到页面上主要显示了两个树状列表,其显示内容都是按照客户,定单,货物三层结构的树状列表。其中左边的树状列表是一次性加载了所有的数据,共生成3072个节点。而右边的列表只加载了第一层节点,共91个节点,但右边的列表能动态加载子节点列表。用户可以使用鼠标点击操作来展开和收缩节点,点击货物节点会显示一个消息框。
软件设计
为了便于开发人员使用,笔者开发出一个通用的树状列表WEB控件,该控件名称为SkyTreeViewControl,是一种从System.Web.UI.WebControls.WebControl派生的WEB控件,这样开发人员以后在ASP.NET 程序中需要树状列表时只要将这个控件拖拽到页面即可开始使用了。现在开始进行控件的基本设计。
结构设计
根据上面分析的结果,笔者采用XML/XSLT技术,于是就可以设计出如下的程序运行流程。
在这个流程中,服务器的C#程序查询数据库获得树状结构信息,并将树状结构信息保存到一个XML文档中,然后发往客户端。而客户端执行的JavaScript脚本中,调用MSXML组件加载服务器端生成的节点XML文档,并从服务器上下载事先准备好的XSLT模板文档,然后调用MSXML组件执行XSLT转换,转换结果就是HTML字符串,然后将这个字符串填充到页面上展示出树状列表。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
目标HTML代码设计
无论WEB控件或者JavaScript等等经过怎样的处理,浏览器最终都是依据HTML代码来显示文档界面的,因此设计WEB控件,首先得设计WEB控件最终生成的HTML代码,也就是WEB控件的执行目标。
为了展现树状结构,业界已经设计出很多种HTML代码格式。有使用DIV标签的,有使用P标签的,笔者这里经过一些尝试,决定采用TABLE标签,使用表格套表格的方式来展现树状列表的层次结构。其设计的HTML页面范例如下。
培训演示程序中有一个名为treeviewsample.htm 的文件,其中就是这个树状列表的HTML样本。
这个演示HTML文档中,展现节点“10359 成先生”及其子节点的HTML代码片段如下
<tr>
<td valign="top" align="left" width="16" background="SkyTreeViewControl_line.gif"
height="16">
<img id="NodeID_expend" src="SkyTreeViewControl_expend.gif">
</td>
<td valign="top">
<img id="NodeID_icon" height="16" src="SkyTreeViewControl_open.bmp">
<a id="NodeID_text">10359 成先生</a>
<table id="NodeID_table" cellspacing="0" cellpadding="0" border="1" bordercolor="black">
<tr>
<td valign="top" align="left" width="16" height="16">
<img src="SkyTreeViewControl_child.gif">
</td>
<td valign="top" nowrap>
<img id="IDAIL4QC_icon" src="SkyTreeViewControl_default.bmp">
<a id="IDAIL4QC_text">饼干</a>
</td>
</tr>
<tr>
<td valign="top" align="left" width="16" height="16">
<img src="SkyTreeViewControl_child.gif">
</td>
<td valign="top" nowrap>
<img id="IDARL4QC_icon" src="SkyTreeViewControl_default.bmp">
<a id="IDARL4QC_text">花奶酪</a>
</td>
</tr>
<tr>
<td valign="top" align="left" width="16" height="16">
<img src="SkyTreeViewControl_lastchild.gif">
</td>
<td valign="top" nowrap>
<img id="IDAWL4QC_icon" src="SkyTreeViewControl_default.bmp">
<a id="IDAWL4QC_text">温馨奶酪</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
则这段HTML代码的显示效果为
分析这段HTML代码,读者可以看到,每一个节点都占据一个表格行,节点文本后面还跟着一个Table来容纳子节点。如此循环则使用表格套表格的方式实现了一个树状列表结构。
HTML中,对于每一个节点都定义了一个惟一的编号,比如“NodeID”,在实际应用中则可以是任意样式的ID号,而且每一个节点分成4个部分。首先是前面的展开收缩控制元素。用一个图片元素来表示,当节点展开时使用图标,当节点收缩时使用图标。该图片元素编号为“NodeID_expend”,占据了节点表格行的第一个单元格。由于树状结构有同层节点连接线,若节点不是上级节点的最后一个子节点则设置第一个单元格的背景图片来模拟显示同层节点连接线。
节点表格行的第二个单元格用来放置节点的图标,文本和子节点的表格。每一个节点都有一个图标,使用IMG元素来展示,元素编号是“NodeID_icon”,而且若有子节点,则节点可以展开和收缩,此时节点的图标是不同的,需要动态设置。最典型的就是节点展开时使用图标,而节点收缩时使用图标,当然两个图标是可以一样的。这里使用扩展属性“SrcBack”来设置节点第二个图标的图片地址,当节点展开或收缩时可以将图片元素的SrcBack属性值和标准的Src属性值进行互换即可实现节点图标的变化。
节点图标后面就是用一个A标签来显示节点文本了,这个元素的编号为“NodeID_text”。
若节点有子节点,则在节点文本后面放置一个Table 元素来显示子节点。该表格的编号为“NodeID_table”。这个表格里面也是一个表格行来显示一个节点数据。当用户点击节点的展开和收缩标记时,笔者就可以编写脚步来控制容纳子节点的Table对象的display的样式值来显示或隐藏这个表格,从而实现子节点的展开和收缩。
HTML代码设计后,笔者开发的WEB控件的最终的目标就是生成这样的HTML代码,其生成过程有很多种,有字符串拼凑的,有在服务器端用C#程序生成,也有在客户端用JavaScript脚本代码来生成。在这里使用XSLT来生成HTML代码,并支持在服务器端和客户端生成代码。
脚本设计
基本的HTML代码设计出来后,接着就开始设计客户端脚本,使得这个静态的树状列表具有动态的效果。这里采用JavaScript脚本语言,树状列表的动态效果就是用户鼠标点击某个节点时,若该节点有子节点,则设置包含子节点的表格元素在可见和不可见的状态的切换,而且同时更新元素的图标来表示节点的展开收缩状态,此外还设置节点的文本为高亮度显示来表示该节点处于选择状态。
读者知道HTML的CSS样式控制中有一个名为“display”的样式,若设置“display”的值为“none”时则HTML元素不可见而且不占地方,就好像这个元素从来就没有存在过,若设置“display”样式为空字符串时,则HTML元素是正常显示。JavaScript脚本就可以修改子节点表格元素的“display”样式来显示或隐藏子节点列表。
在JavaScript中,很多HTML元素有“getAttribute”和“setAttribute”函数,用来读取和设置扩展属性,在上面的HTML设计中,笔者对显示节点图标的图片元素定义了一个名为“SrcBack”的非标准属性。在JavaScript中,可以使用代码“obj.getAttribute(“SrcBack”)”来获得该属性值,使用“obj.setAttribute(“SrcBack”,”新数据”)”来设置该属性值;在IE浏览器中,开发人员可以使用更简洁的方式,直呼其名的获得和设置扩展属性值,也就是使用“obj.SrcBack”属性,但这是微软对JavaScript和HTML DOM的扩展,只适用于IE,其他浏览器是不支持的,为了使得控件具备一定的兼容性,笔者这里使用“getAttribute”和“setAttribute”这种符合HTML国际标准的编程接口。
在脚本程序中有个很重要的问题是如何加载XML和XSLT文档。很多时候这里的XML文件是动态生成的,而XSLT文档是事先编制好的。一般的需要三个文档,一个是包容树状列表的主HTML页面,一个是生成XML文档的动态服务器页面,还有一个是保存在服务器上的静态的XSLT文件。这三个文件分开提供,则不利于程序的编写和部署,为此这里采用微软IE浏览器所特有的XML数据岛(XML Island)的功能,将XML文档和XSLT文档嵌入到HTML文档中,从而仅仅依赖一个HTML文档即可生成树状列表。
在HTML中使用标签XML来定义一个XML数据岛元素。其范例为
<METADATA>
<AUTHOR>John Smith</AUTHOR>
<GENERATOR>Visual Notepad</GENERATOR>
<PAGETYPE>Reference</PAGETYPE>
<ABSTRACT>Specifies a data island</ABSTRACT>
</METADATA>
</XML>
开发人员既可以使用它的src属性来指定动态加载的XML文档的URL地址,也可以直接在XML标签之间填写XML代码。在JavaScript中,开发人员可以使用XML数据岛对象的XMLDocument属性来获得XML文档对象,也就是调用“XMLIslandID.XMLDocument”语句,开发人员还可以使用响应它的“onreadystatechange”事件来执行该XML数据加载完毕后的处理。
XML数据岛是IE浏览器的特有的功能,其他浏览器不支持XML数据岛。关于XML数据岛的详细信息可参考MSDN中的相关说明。若要使得控件能在多个浏览器中运行,则不得使用XML数据岛。此处为了开发方便就采用XML数据岛技术,不过这使得控件只能用于IE浏览器。
节点XML文档设计
在这个WEB控件中将采用XML/XSLT技术来生成HTML代码。首先得设计出定义树状结构的XML文档,由于XML文档本身是树状结构,因此这里的XML文档设计就比较简单的了。大家可以提出很多种设计方案,在此我提出如下的设计方案。先看一下XML文档的范例。
<RootNodes>
<Node>
<Icon>customer.bmp</Icon>
<Text>艾德高科技</Text>
<Nodes>
<Node>
<Icon>order.bmp</Icon>
<Text>10359 成先生</Text>
<Nodes>
<Node>
<Icon>product.bmp</Icon>
<Text>饼干</Text>
<OnClick>alert('饼干')</OnClick>
<Nodes />
</Node>
<Node>
<Icon>product.bmp</Icon>
<Text>温馨奶酪</Text>
<OnClick>alert('温馨奶酪')</OnClick>
<Nodes />
</Node>
</Nodes>
</Node>
</Nodes>
</Node>
<Node>
<Icon>customer.bmp</Icon>
<Text>霸力建设</Text>
<Nodes>
<Node>
<Icon>order.bmp</Icon>
<Text>10858 余小姐</Text>
<Nodes>
<Node>
<Icon>product.bmp</Icon>
<Text>海鲜粉</Text>
<OnClick>alert('海鲜粉')</OnClick>
<Nodes />
</Node>
</Nodes>
</Node>
</Nodes>
</Node>
</RootNodes>
详细读者看到这个范例后能很容易的理解这个XML文档的结构,Node元素表示一个节点,它下面有Icon元素指定元素图标文件名,Text元素指定节点文本,OnClick元素指定节点的OnClick事件处理,Nodes元素用于放置子节点。根元素RootNodes下放置了树状列表第一层节点。
XSLT文档设计
XSLT文档是这个控件中比较复杂的部分,在后面将详细说明其内容。
软件说明
根据软件设计,笔者已经完成了该程序,程序主要包含以下几个文件
Default.aspx |
演示树状列表WEB控件的一个ASPX页面。 |
SkyTreeNode.cs |
定义了表示树状列表中一个节点的类型。 |
SkyTreeNodeList.cs |
定义了一种树状节点列表的类型。 |
TreeViewNodeXml.aspx |
为动态加载子节点的控件提供节点XML文档的服务页面。 |
SkyTreeViewControl.bmp |
控件在设计器工具箱上的图标的图片。 |
SkyTreeViewControl.cs |
控件C#源代码文件。 |
SkyTreeViewControl.xslt |
和控件配套使用的XSLT文件。 |
SkyTreeViewControl_*.gif/bmp |
一系列的显示树状列表结构所需的小图片。 |
现对该软件进行详细说明。
SkyTreeNode.cs
该文件中定义了类型SkyTreeNode,用于表示树状列表中的一个节点,其主要代码如下
/// 树状列表节点对象
/// </summary>
/// <remarks>
/// 本对象表示高性能ASP.NET树状列表中的一个节点,每个节点
/// 有个Nodes属性用于保存子节点,由此可以形成树状结构。
/// </remarks>
[System.Serializable()]
[System.Xml.Serialization.XmlType("Node")]
public class SkyTreeNode
{
/// <summary>
/// 初始化对象
/// </summary>
public SkyTreeNode()
{
myNodes = new SkyTreeNodeList(this);
}
private string strID = null;
/// <summary>
/// 节点编号
/// </summary>
/// <remarks>
/// 在生成HTML代码时,系统会调用XSLT的函数 generate-id() 来生成节点HTML
/// 编号,在同一个XML文档时,自动生成的编号是唯一的不会重复。但当页面上有
/// 多个树状列表或者需要客户端动态加载节点时则会在不同的XML文档上调用
/// generate-id() 函数,这会导致节点的HTML编号重复,此时需要明确的设置该ID
/// 属性以确保生成的节点的HTML编号不重复。一般可以设置为
/// System.GUID.NewGUID().ToString() 值
/// </remarks>
public string ID
{
get
{
return strID;
}
set
{
strID = value;
}
}
private string strText = null;
/// <summary>
/// 节点文本
/// </summary>
public string Text
{
get
{
return strText;
}
set
{
strText = value;
}
}
private string strIcon = null;
/// <summary>
/// 节点图标URL地址
/// </summary>
public string Icon
{
get
{
return strIcon;
}
set
{
strIcon = value;
}
}
private string strLink = null;
/// <summary>
/// 节点链接地址
/// </summary>
public string Link
{
get
{
return strLink;
}
set
{
strLink = value;
}
}
private string strValue = null;
/// <summary>
/// 节点数值
/// </summary>
public string Value
{
get
{
return strValue;
}
set
{
strValue = value;
}
}
private string strOnClick = null;
/// <summary>
/// 节点点击事件处理代码
/// </summary>
public string OnClick
{
get
{
return strOnClick;
}
set
{
strOnClick = value;
}
}
private string strXMLSource = null;
/// <summary>
/// 子节点信息XML来源
/// </summary>
/// <remarks>
/// 当客户端要动态的加载节点的子节点,则必须要设置该属性为一个
/// 相对或绝对的URL地址,该地址必须使用一个页面参数来传递该节点的
/// ID属性。
/// </remarks>
public string XMLSource
{
get
{
return strXMLSource;
}
set
{
strXMLSource = value;
}
}
private SkyTreeNode myParent = null;
/// <summary>
/// 父节点对象
/// </summary>
/// <remarks>
/// Parent属性不能参与XML序列化和反序列化,否则会出现树状节点
/// 循环引用而导致程序结构错误,因此使用 XmlIgnore 特性来说明
/// 不参与XML序列化和反序列化。
/// </remarks>
[System.Xml.Serialization.XmlIgnore()]
[System.ComponentModel.Browsable(false)]
public SkyTreeNode Parent
{
get
{
return myParent;
}
set
{
myParent = value;
}
}
private SkyTreeNodeList myNodes = null;
/// <summary>
/// 子节点列表
/// </summary>
/// <remarks>
/// 此处使用XmlArrayItem特性说明Nodes属性是一个列表,该列表对应
/// 的XML节点下Node名称的子XML节点对应一个SkyTreeNode类型的对象。
/// </remarks>
[System.Xml.Serialization.XmlArrayItem("Node", typeof(SkyTreeNode))]
public SkyTreeNodeList Nodes
{
get
{
return myNodes;
}
}
}//public class SkyTreeNode
本类型比较简单,定义了一些树状节点拥有的属性,此外还定义了一个Nodes子节点列表,一个节点可以有若干个子节点,则多个节点组合起来就可以构成树状列表。
该类型前面使用代码“[System.Serializable()]”来指定类型可以进行二进制序列化,在ASP.NET2.0中,所有可以保存在页面Session或ViewState中的对象必须可以执行二进制序列化,若SkyTreeNode类型没有使用代码“[System.Serializable()]”标记为可执行二进制序列化,则不能将其保存在页面Session或ViewState中。类型SkyTreeNode前面还使用代码“[System.Xml.Serialization.XmlType("Node")]”来指定类型进行XML序列化时的使用的XML标签名为“Node”。
对“Parent”属性在使用代码“[System.Xml.Serialization.XmlIgnore()]”表明该属性不执行XML序列化和反序列化。由于XML序列化是递归处理对象的所有的可读写属性,而“Parent”属性指向该节点的父节点,而父节点的“Nodes”属性又包含了这个节点,如此形成了对象循环引用,若“Parent”属性参与XML序列化则必然会造成无限递归循环,导致程序错误。
SkyTreeNodeList.cs
该文件定义了SkyTreeNodeList类型,该类型是树状节点列表,能存储若干个树状节点对象,该类型的代码为
/// 树状列表节点列表
/// </summary>
[System.Serializable()]
public class SkyTreeNodeList : System.Collections.CollectionBase
{
/// <summary>
/// 初始化对象
/// </summary>
public SkyTreeNodeList()
{
}
/// <summary>
/// 初始化对象
/// </summary>
/// <param name="node">列表所属节点对象</param>
public SkyTreeNodeList(SkyTreeNode node)
{
myOwnerNode = node;
}
private SkyTreeNode myOwnerNode = null;
/// <summary>
/// 拥有该列表的节点对象
/// </summary>
[System.ComponentModel.Browsable(false)]
public SkyTreeNode OwnerNode
{
get
{
return myOwnerNode;
}
}
/// <summary>
/// 返回指定序号处的节点对象
/// </summary>
public SkyTreeNode this[int index]
{
get
{
return (SkyTreeNode)this.List[index];
}
}
/// <summary>
/// 向列表添加节点
/// </summary>
/// <param name="node">节点对象</param>
/// <returns>新节点在列表中的序号</returns>
public int Add(SkyTreeNode node)
{
if (node == null)
throw new ArgumentNullException("node");
if (myOwnerNode != null)
node.Parent = myOwnerNode;
return this.List.Add(node);
}
/// <summary>
/// 向列表添加若干个节点
/// </summary>
/// <param name="nodes">节点列表,该列表中的元素类型必须为SkyTreeNode类型</param>
public void AddRange(System.Collections.ICollection nodes)
{
if (nodes == null)
throw new ArgumentNullException("nodes");
foreach (SkyTreeNode node in nodes)
{
if (myOwnerNode != null)
node.Parent = myOwnerNode;
this.List.Add(node);
}
}
/// <summary>
/// 删除节点
/// </summary>
/// <param name="node">节点对象</param>
public void Remove(SkyTreeNode node)
{
this.List.Remove(node);
}
/// <summary>
/// 判断节点在列表中的序号
/// </summary>
/// <param name="node">节点对象</param>
/// <returns>节点在列表中的从0开始的序号,若不存在则返回-1</returns>
public int IndexOf(SkyTreeNode node)
{
return this.List.IndexOf(node);
}
/// <summary>
/// 判断列表是否存在指定的节点对象
/// </summary>
/// <param name="node">节点对象</param>
/// <returns>是否存在该节点</returns>
public bool Contains(SkyTreeNode node)
{
return this.List.Contains(node);
}
}//public class SkyTreeNodeList : System.Collections.CollectionBase
本类型比较简单,它是从类型“System.Collections.CollectionBase”上派生的针对SkyTreeNode类型的强类型的列表,它使用代码“[System.Serializable()]”表明可以进行二进制序列化,它提供了一些属性和方法用于维护列表中的树状节点元素。
SkyTreeViewControl.cs
树状列表控件所有的C#代码就放置在这个文件中。打开这个文件,首先我们看到一条指令
// 在ASPX的HTML代码中默认使用SkyWebControl作为其HTML标签的前缀
[assembly:System.Web.UI.TagPrefix("CS_Discovery" , "SkyWebControl")]
这条指令前面有“assembly:”的前缀,表示这是一个针对程序集的指令,它具有两个参数,第一个参数为某个名称控件,第二个参数指定该名称空间下的所有的Web控件在ASPX的HTML源代码中的标签前缀,这里为“SkyWebControl”。
这个文件中定义了3个类型。
SkyTreeViewControlBuilder
这个类型是从System.Web.UI.ControlBuilder上派生的。本类型用于对VS.NET的WEB窗体设计器提供支持。
SkyTreeViewControlDesigner
这个类型是从System.Web.UI.Design.ControlDesigner 上派生的,用于对VS.NET的WEB窗体设计器提供支持。
SkyTreeViewControl
这个类型就是树状列表WEB控件了。首先看到它的定义头。
[System.ComponentModel.Designer( typeof( SkyTreeViewControlDesigner ))]
[System.Drawing.ToolboxBitmap( typeof( SkyTreeViewControl ))]
public class SkyTreeViewControl : System.Web.UI.WebControls.WebControl
这个类型是从System.Web.UI.WebControls.WebControl上派生的。它还附加了3个特性,其中ControlBuilder特性用于指明控件配套的控件创建者类型为SkyTreeViewControlBuilder,Designer特性用于指明控件配套的设计器类型为SkyTreeViewControlDesigner,而特性ToolboxBitmap用于指明控件类型在VS.NET的窗体设计器的工具箱中使用什么样的图标。这里指明使用图标“”。
本控件定义了Nodes属性,其代码为
/// <summary>
/// 子节点列表
/// </summary>
[System.ComponentModel.Browsable( false )]
public SkyTreeNodeList Nodes
{
get
{
return myNodes ;
}
set
{
myNodes = value;
}
}
Nodes属性保存了树状列表控件的根节点。该属性使用代码“[System.ComponentModel.Browsable( false )]”声明该属性在设计器中的属性列表中是看不见的。本控件还定义了AllNodes属性用于获得树状列表所包含的所有节点组成的列表。
本控件定义了IndentXML属性,其定义代码为
/// <summary>
/// XML是否进行缩进
/// </summary>
/// <remarks>
/// 若控件的IndentXML属性值为True,则控件内部生成的XML文本将带缩进,便于开发人员调试
/// 程序,但这将增加页面大小,因此当程序调试完毕后可以设置IndentXML属性值为false来
/// 减小页面大小,提高性能。
/// </remarks>
[System.ComponentModel.DefaultValue( false )]
[System.ComponentModel.Description("生成XML是否进行缩进")]
[System.ComponentModel.Category("Behavior")]
public bool IndentXML
{
get
{
return bolIndentXML ;
}
set
{
bolIndentXML = value;
}
}
该属性用于表示生成的XML源代码是否进行缩进。若XML源代码进行缩进,则方便开发人员直接查看XML源代码,但这样会增加页面大小,因此当应用程序处于开发时可以设置树状列表的控件的IndentXML属性值为true,当开发完成部署时可设置该属性值为false。
此外控件还定义了以下几个属性
AutoScroll |
获得或设置控件是否自动显示横向和纵向滚动条,若该属性值为false,则无论控件显示多少内容,控件都不会显示滚动条。 |
GenerateAtServer |
获得或设置控件是否在服务器端生成显示树状列表的HTML代码,若该属性值为true,则控件会在ASP.NET服务器端生成显示树状列表的HTML代码,这会加大服务器的工作量,并导致页面比较大;若该属性值为false,则控件会在客户端浏览器中使用JavaScript/XSLT来生成HTML代码,此时会减少服务器工作量,并减少输出的页面的大小。 |
DynamicLoadChildNodes |
获取或设置控件是否动态加载子节点列表,若该属性值为True,则控件允许动态加载节点的子节点,此时控件不会刷新页面,而加载树状节点对象的XMLSource属性指定的XML文档来动态的生成子节点;若该属性值为False则禁止这种功能。 |
TagKey |
控件重载了TagKey属性,设置该控件最外层使用“DIV”标签。 |
TreeNodeStyleString |
树状列表节点使用的CSS样式字符串。 |
SelectedNodeStyleString |
处于选中状态的树状节点使用的CSS样式字符串。 |
OnLoad 函数
控件重写了WebControl的OnLoad函数,其代码为
/// 控件加载时的处理,此时控件尚未向页面输出HTML代码。
/// </summary>
/// <param name="e">事件参数</param>
protected override void OnLoad(EventArgs e)
{
base.OnLoad (e);
// 包含树状列表节点样式的HTML代码
string strStyleHtml = "\r\n.SkyTreeViewControl_TreeNode { " + this.TreeNodeStyleString + "}"
+ "\r\n.SkyTreeViewControl_SelectedNode{" + this.SelectedNodeStyleString + "}\r\n";
// 添加树状节点使用的CSS样式代码
if (this.Page.Header != null)
{
// 若在ASPX的HTML代码中使用了“<head runat=server>.</head>”则
// this.Page.Header属性有效,可以向head标签下添加新内容。
// 按照比较严格的HTML规范,style标签只能放置在head标签下面。
// ASP.NET2.0支持this.Page.Header属性,但ASP.NET1.0/1.1不支持。
bool find = false;
foreach (System.Web.UI.Control ctl in this.Page.Header.Controls)
{
// 搜索Header下面的所有的子元素,看看是否已经输出过树状列表
// 节点样式元素。
if (ctl.ID == "SkyTreeViewControl_Style")
{
find = true;
break;
}
}
if (find == false)
{
// 若以前没有输出则向Header元素下添加新的style元素,并设置其内容。
HtmlGenericControl style = new HtmlGenericControl("style");
style.ID = "SkyTreeViewControl_Style";
style.InnerHtml = strStyleHtml;
this.Page.Header.Controls.Add(style);
}
}
else
{
// 不能在Render或RenderContents函数中使用RegisterClientScriptBlock
// 因为那时RegisterClientScriptBlock函数的功能已经不在状态,无效了。
// 而OnLoad函数中页面尚未开始输出,此时RegisterClientScriptBlock函数
// 是有效的。
this.Page.ClientScript.RegisterClientScriptBlock(
this.GetType(),
"SkyTreeViewControl_Style",
"<style>" + strStyleHtml + "</style>");
}
}
这里的树状节点列表需要根据其选择状态而使用属性“TreeNodeStyleString”或“SelectedNodeStyleString”中指定的CSS样式,为了减少HTML代码量,将生成一个style的HTML标签,该标签包含了属性“TreeNodeStyleString”或“SelectedNodeStyleString”指定的CSS样式,而对树状列表节点采用“class=’样式名称’”来选择其CSS显示样式。
按照比较严格的HTML语法,style标签必须放置在HTML文档中的head标签里面,因此本控件尽量将style标签放置在head标签中。
在ASP.NET2.0中,WEB控件可以使用属性“this.Page.Header”获得ASPX中的head标签,若ASPX的HTML代码中使用了“<head runat=server>….</head>”,则属性“this.Page.Header”属性是有效的,若head标签没有标记为“runat=server”则“this.Page.Header”属性值是空引用,由于笔者无法强制开发人员使用“<head runat=server>”标记,因此在此需要进行判断“this.Page.Header”属性值是否为空。注意ASP.NET1.0/1.1不支持“this.Page.Header”属性。
若“this.Page.Header”属性有效,则还需要遍历页面head标签下面的所有的子元素,若没有找到ID号为“SkyTreeViewControl_Style”则元素则向其添加一个ID号为“SkyTreeViewControl_Style”的标签为“style”的HTML元素,此举是为了放置当同一个页面上有多个树状列表控件时重复的向“head”标签输出“style”标签。
若“this.Page.Header”属性为空,则只能使用非标准的HTML语法来输出style标签了。这里使用了函数“this.Page.ClientScript.RegisterClientScriptBlock”,这个函数用于向页面特定的部分输出HTML代码。
在ASP.NET中,任何标记为“runat=server”的WEB控件必须包含在标记为“runat=server”的FORM元素中。当页面程序或WEB控件内部调用RegisterClientScriptBlock 函数时,ASP.NET框架会紧跟着Form元素的起始标记(也就是HTML代码“<form runat=server … >”)后插入指定的HTML代码。这个函数有三个参数,第一个参数为WEB控件的类型,第二个是用于标记HTML代码块的名称,第三个是HTML代码字符串。若在同一种(注意,不是同一个)WEB控件中调用了多次RegisterClientScriptBlock函数,但使用了相同的HTML代码块名称,则仍然只输出一次。这样能避免一个页面上同一个类型的多个WEB控件多次输出相同的HTML代码。
类似的,ASP.NET还提供一个RegisterStartupScript 函数,函数参数也是HTML代码块的名称和HTML代码字符串。但它会紧挨着Form元素的结束标签(也就是HTML代码“</form>”)的前面插入指定的HTML代码。下面的代码说明了函数RegisterClientScriptBlock和RegisterStartupScript的输出区域。
<form runat=server method=post> [RegisterClientScriptBlock函数输出区域] 定义内容的HTML代码,定义WEB控件的HTML代码 [RegisterStartupScript函数输出区域] </form> |
Render 函数
WEB控件使用Render函数向页面输出HTML代码。其代码为
/// 输出控件HTML内容
/// </summary>
/// <param name="writer">HTML书写器</param>
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
if( this.AutoScroll )
{
this.Style["overflow"] = "auto" ;
}
base.Render( writer );
}
在这里会进行判断,若控件设置了AutoScroll属性,也就是当树状列表要显示的节点比较多时,控件自动显示滚动条,这里就设置控件的“overflow”样式值为“auto”。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
ASP.NET框架处理原始的ASPX文件时,遇到WEB控件标签时会去掉这个标签,然后转而调用WEB控件的Render函数。比如在Default.ASPX中有一段HTML
<SkyWebControl:SkyTreeViewControl id="myTreeView" runat="server" 其他属性…… > |
当ASP.NET框架处理这个HTML代码片段时,会将“SkyWebControl:SkyTreeViewControl”标签整个的删除掉,然后转而调用WEB控件的Render函数,将这个函数输出的HTML代码替换掉ASPX中的WEB控件标签。此时ASPX中的WEB控件标签成了WEB控件在HTML文档中的占位符。这就是所有WEB控件输出HTML代码的原理,不会有例外。
RenderContents 函数
控件的Rander函数调用了“base.Rander”函数,而“base.Rander”函数内部会调用RenderContents函数来输出控件的内容。因此这里控件重写了RenderContents函数用来输出详细内容。这个函数是树状列表控件C#代码的主要内容。
本函数的第一个部分就是判断控件是否处于设计模式,也就是判断控件是否运行在VS.NET的Web窗体设计器中,其代码如下
{
if( base.Page.Site.DesignMode )
{
// 若ASPX页面是处于设计状态,比如处于VS.NET集成开发环境的WEB表单设计器
// 中,则本WEB控件不显示实际内容,只是显示控件的一些状态。
Type t = this.GetType();
writer.WriteLine("<b>" + this.ID + "</b>" );
writer.WriteLine("<br />Type=" + t.FullName );
writer.WriteLine("<br />Version=" + t.Assembly.FullName );
writer.WriteLine("<br />GenerateAtServer=" + this.GenerateAtServer );
writer.WriteLine("<br />DynamicLoadChildNodes=" + this.DynamicLoadChildNodes );
writer.WriteLine("<br />AutoScroll=" + this.AutoScroll );
writer.WriteLine("<br />IndentXML=" + this.IndentXML );
writer.WriteLine("<br />Yfyuan release at 2008-2-19");
return ;
}
}
在这里判断 base.Page.Site.DesignMode 属性。若该属性值为 true , 则表明控件处于设计模式,出现在VS.NET的窗体设计器中。此时控件就是简单地输出控件的名称类型和一些重要属性值。
若控件不处于设计器中,那就是真正的运行了。若允许客户端动态加载子节点,则输出支持动态加载子节点的HTML代码块,这里使用了RegisterStartupScript函数。将在客户端的form标签结束前输出这些HTML代码。
这里要注意一下,在Render和RenderContents函数中调用RegisterClientScriptBlock函数是无意义的,因为早在任何WEB控件输出前,form标签已经开始并输出了一些内容了,已经输出的内容是不可更改的,因此Render或RenderContents中不能调用RegisterClientScriptBlock函数,而应当在控件的的OnLoad方法或Load事件处理中调用RegisterClientScriptBlock函数。
第一段HTML代码块
若控件的“DynamicLoadChildNodes”属性值为true,也就是控件运行在客户端动态加载子节点,则输出支持客户端动态加载子节点的HTML代码,首先输出一个名为“SkyTreeViewControlTempXML”的XML数据岛标签,并将它的“onreadystatechange“事件绑定到”SkyTreeViewControlDynamicLoadChildNodes”的JavaScript函数上。这里还定义了一个名称为“SkyTreeViewControlDyanmicRootNodeID”的全局变量,用于指明是哪个节点正在加载在节点。这里输出的HTML代码内容为
定义一个XML数据岛标签
当动态加载子节点时使用该XML数据岛来加载定义子节点的XML文档,
XML数据岛是IE特有技术,其他浏览器可能不支持。
-->
<xml id='SkyTreeViewControlTempXML'
onreadystatechange='SkyTreeViewControlDynamicLoadChildNodes()' ></xml>
<script language=javascript>
//--------- 开始定义动态加载子节点使用的JavaScript 代码---------------------
// 当前动态加载子节点的根节点对象编号
var SkyTreeViewControlDyanmicRootNodeID ;
// 动态加载子节点
function SkyTreeViewControlDynamicLoadChildNodes()
{
// 获得加载子节点定义数据的XML数据岛对象
var xml = document.getElementById( 'SkyTreeViewControlTempXML' );
if( xml == null )
{
return ;
}
if( xml.readyState != 'complete' )
{ // 此时说明XML数据岛正处于工作状态,取消当前操作
return ;
}
// 获得当前正在处理的树状列表节点对象
var RootNode = document.getElementById( SkyTreeViewControlDyanmicRootNodeID + '_text' );
if( RootNode == null)
return ;
var html = '';
var errorflag = false;
// 获得包含XSLT代码的XML数据岛对象
var xsl = document.getElementById( 'SkyTreeViewControlXSLT');
if( xsl == null )
{
// 未找到XSLT文档则设置错误信息
html = '缺失XSLT数据';
errorflag = true ;
}
else if( xml.XMLDocument.parseError.errorCode != 0)
{
// 若加载定义子节点的XML文档错误则设置错误信息
html = '错误:' + xml.XMLDocument.parseError.reason
+ '[' + xml.XMLDocument.parseError.url + ']' ;
errorflag = true ;
}
else
{
if( xml.XMLDocument.documentElement == null )
{
html = '未找到根节点';
errorflag = true ;
}
else
{
// 将当前节点的HTML标签的编号保存到XML文档中。
xml.XMLDocument.documentElement.setAttribute(
'RootID' ,
SkyTreeViewControlDyanmicRootNodeID );
// 执行XSLT转换,生成用于显示子列表的HTML字符串
html = xml.XMLDocument.transformNode( xsl.XMLDocument );
}
}
// 将当前节点的“正在加载”的字样删掉
var lbl = document.getElementById( SkyTreeViewControlDyanmicRootNodeID +'_Loading');
if( lbl != null )
{
if( errorflag )
{
lbl.innerHTML = '<br />' + html ;
return ;
}
// 将动态生成的HTML代码输出到HTML页面中
lbl.insertAdjacentHTML('afterEnd', html );
lbl.parentNode.removeChild( lbl );
}
else
{
// 将动态生成的HTML代码输出到HTML页面中
RootNode.insertAdjacentHTML('afterEnd', html );
}
// 获得节点的展开点图片对象
var myExpend = document.getElementById( SkyTreeViewControlDyanmicRootNodeID + '_expend');
// 获得节点的图标图片对象
var myIcon = document.getElementById( SkyTreeViewControlDyanmicRootNodeID + '_icon');
// 获得节点的文本对象
var myText = document.getElementById( SkyTreeViewControlDyanmicRootNodeID + '_text');
// 获得子节点表格
var myTable = document.getElementById( SkyTreeViewControlDyanmicRootNodeID + '_table');
if( myExpend != null
&& myIcon != null
&& myText != null
&& myTable != null )
{
// 将当前节点的控制图标从收缩状态改变为展开状态
var SrcBack = myIcon.src ;
myIcon.src = myIcon.getAttribute('SrcBack');
myIcon.setAttribute( 'SrcBack' , SrcBack );
SrcBack = myExpend.src ;
myExpend.src = myExpend.getAttribute('SrcBack');
myExpend.setAttribute( 'SrcBack' , SrcBack );
//myTable.scrollIntoView( false );
}
SkyTreeViewControlDyanmicRootNodeID = null;
}//function SkyTreeViewControlDynamicLoadChildNodes()
//-------- 本段JavaScript 定义结束--------------------------
</script>
当用户展开一个节点而导致动态加载子节点时,其他的JavaSciprt代码会设置节点的XMLSource属性值到这个名为“SkyTreeViewControlTempXML”的XML数据岛的src属性上。这会导致IE浏览器立即开始异步的加载XML文档,并根据其文档加载状态而多次触发“onreadystatechange”事件,从而调用JavaScript函数“SkyTreeViewControlDynamicLoadChildNodes”,在这个JavaScript函数中,首先获得XML数据岛对象,然后判断其状态,若状态值不等于“complete”时,则文档还没有完成加载,从而退出等待下一次调用。
若XML数据岛完成加载后,获得父节点的文本元素,然后获得嵌入在HTML文档中另外一个名为“SkyTreeViewControlXSLT”的XML数据岛,这个XML数据岛包含了XSLT文档。然后使用刚刚加载的XML文档,调用它的“transfromNode”函数执行XSLT转换,则转换结果就是显示子节点的HTML字符串。然后使用HTML元素的“insertAdjacentHTML”函数将生成的HTML字符串输出到HTML页面上。
成功的动态加载子节点后,JavaScript脚本还更新父节点的图标,使其表示为展开状态。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
第二段HTML代码块
WEB控件输出完第一段HTML代码后,会根据需要输出第二段代码,第二段代码包含在客户端执行XSLT转换所需的XSLT模板代码以及初始化生成树状列表的HTML代码的JavaScript代码,其C#代码为
{
// 若允许动态加载子节点而且不是在服务器段生成HTML代码
// 则输出动态加载子节点使用的JavaScript代码
if (!base.Page.ClientScript.IsStartupScriptRegistered(
this.GetType() ,
"SkyTreeViewControlXSLT"))
{
// 输出第二段HTML代码块
base.Page.ClientScript.RegisterStartupScript(
this.GetType() ,
"SkyTreeViewControlXSLT" ,
@"
<!-- 使用一个XML数据岛保存客户端动态生成HTML代码时使用的XSLT代码-->
<xml id='SkyTreeViewControlXSLT'>"
+ ReadXSLTString() // 此处从资源文件SkyTreeViewControl.xslt中获得XSLT代码
+ @"</xml>
<script language=javascript>
//--------------- 生成树状列表--------------------------------------
// 参数id 就是树状列表控件的编号
function RefreshSkyTreeViewControl( id )
{
var xml = document.getElementById( id + '_xml');
var xsl = document.getElementById( 'SkyTreeViewControlXSLT');
var container = document.getElementById( id + '_container' );
if( container == null )
{
alert('未找到目标');
return ;
}
if( container != null )
{
if( xml == null )
{
container.innerText = '缺失XML数据';
return ;
}
if( xsl == null )
{
container.innerText = '缺失XSLT数据';
return ;
}
var html = xml.XMLDocument.transformNode( xsl.XMLDocument );
container.innerHTML = html ;
}
}//function RefreshSkyTreeViewControl( id )
</script>"
);
}
}
在这里程序会判断控件的GenerateAtServer和DynamicLoadChildNodes 的属性值,若WEB控件不是在服务器端生成HTML代码或者允许在客户端动态加载子列表就会输出第二段HTML代码。
在输出第二段HTML代码时,首先会输出一个名为“SkyTreeViewControlXSLT”的XML数据岛元素,该数据岛内容来自函数“ReadXSLTString”,该函数就是简单的从一个名为“SkyTreeViewControl.xslt”的嵌入式程序集资源中读取所有的文本内容。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
此处还输出一个为“RefreshSkyTreeViewControl”的JavaScript函数,用于初始化一个树状列表,其参数就是树状列表的编号。也就是“myTreeView”,“myTreeView2”之类的控件客户端编号。在JavaScript函数中,首先获得第三个XML数据岛,获得树状节点定义信息XML文档,然后和“SkyTreeViewControlXSLT”数据岛中的XSLT文档执行XSLT转换,生成HTML字符串,然后调用HTML元素的“innerHTML”属性向HTML文档填充刚刚生成的HTML代码,从而展现出一个树状结构。
本过程配套使用的“ReadXSLTString”函数的代码为
/// <summary>
/// 从程序集资源文件SkyTreeViewControl.xslt中加载XSLT代码。
/// </summary>
/// <returns>加载的XSLT代码字符串</returns>
private string ReadXSLTString()
{
if (strXSLString == null)
{
foreach (string name in this.GetType().Assembly.GetManifestResourceNames())
{
// 查询程序集中所有的嵌入的资源的名称,找到以"SkyTreeViewControl.xslt"
// 结尾的程序集资源并以UTF8的文本编码格式加载其中的文本内容。
// 程序集资源的全名为“程序集默认名称空间.保存文件的各级目录.文件名”
// 其中文件名中不能出现下划线。
if (name.EndsWith(".SkyTreeViewControl.xslt"))
{
using (System.IO.Stream stream =
this.GetType().Assembly.GetManifestResourceStream(name))
{
System.IO.StreamReader reader = new System.IO.StreamReader(
stream,
System.Text.Encoding.UTF8);
strXSLString = reader.ReadToEnd();
}
return strXSLString;
}
}
throw new Exception("未找到程序集资源SkyTreeViewControl.xslt");
}
return strXSLString;
}
本函数中,程序会遍历程序集中所有的嵌入式的资源名称,若该名称以“.SkyTreeViewControl.xslt”结尾则以UTF8的编码格式读取该资源的文本内容。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
第三段HTML代码块
WEB控件还会输出一个名为“SkyTreeViewControlExpendNodebyID”的JavaScript函数,该函数用于展开或收缩节点,而与之配套的定义了一个名为SkyTreeViewControlCurrentTreeNode的全局变量,用于保存当前高亮度显示的节点的编号。这段JavaScript代码为
var SkyTreeViewContrlCurrentTreeNode ;
//
//---------------- 展开指定编号的树状列表的节点---------------
// 参数strID 指明节点的ID号
// 参数bolSelect 指明是否高亮度显示这个树状列表节点
//
function SkyTreeViewContrlExpendNodeByID(strID , bolSelect )
{
// 获得节点的展开点图片对象
var myExpend = document.getElementById(strID + '_expend');
// 获得节点的图标图片对象
var myIcon = document.getElementById(strID + '_icon');
// 获得节点的文本对象
var myText = document.getElementById(strID + '_text');
// 获得子节点表格
var myTable = document.getElementById(strID + '_table');
if( myText == null )
return ;
if( bolSelect )
{
// 设置树状列表节点高亮度显示
if( SkyTreeViewContrlCurrentTreeNode != myText
&& SkyTreeViewContrlCurrentTreeNode != null)
{
SkyTreeViewContrlCurrentTreeNode.className = 'SkyTreeViewControl_TreeNode';
}
SkyTreeViewContrlCurrentTreeNode = myText;
SkyTreeViewContrlCurrentTreeNode.className = 'SkyTreeViewControl_SelectedNode';
}
// 展开或收缩子节点
if( myExpend != null
&& myIcon != null
&& myTable != null )
{
// 切换节点前面的展开或收缩样式的图标
var SrcBack = myIcon.src ;
myIcon.src = myIcon.getAttribute('SrcBack');
myIcon.setAttribute( 'SrcBack' , SrcBack );
// 切换节点前面的+ 或者- 样式的图标
SrcBack = myExpend.src ;
myExpend.src = myExpend.getAttribute('SrcBack');
myExpend.setAttribute( 'SrcBack' , SrcBack );
// 显示或隐藏包含子节点的表格对象
if( myTable.style.display != 'none' )
{
myTable.style.display='none';
}
else
{
myTable.style.display='';
}
}
var dyload = false;
if( myTable == null )
{
// 若不存在包含子节点的表格对象则尝试动态加载子节点
// 此时节点的XMLSource扩展属性就保存着定义子节点的XML文档URL地址。
var XMLSource = myText.getAttribute('XMLSource');
if( XMLSource != null && XMLSource.length > 0 )
{
// 若设置了该XML文档地址则删除XMLSource 扩展属性并
myText.removeAttribute('XMLSource');
var xml = document.getElementById( 'SkyTreeViewControlTempXML');
if( xml != null )
{
// 设置“正在加载”字样为显示状态
SkyTreeViewControlDyanmicRootNodeID = strID ;
var lbl = document.getElementById( SkyTreeViewControlDyanmicRootNodeID +'_Loading');
if( lbl != null )
{
lbl.style.display = '';
}
// 调用编号为SkyTreeViewControlTempXML 的XML数据岛来异步加载XML文档
xml.src = XMLSource ;
dyload = true ;
}
}
}
if( dyload == false )
{
//myText.scrollIntoView( false );
}
}//function SkyTreeViewContrlExpendNodeByID(strID)
这个JavaScript函数有两个参数,第一个参数是节点编号,第二个参数是是否设置该节点为当前节点,也就是是否设置高亮度显示(一般的就是蓝底白字)。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
在这个JavaScript函数里面,首先根据节点编号获得节点的图标,文本和包含子节点列表的表格对象。并根据需要设置当前高亮度显示的节点对象。
若子节点列表存在则需要展开和收缩子节点列表,此时将节点图标的src属性和它们的扩展属性SrcBack值进行互换,从而切换了图标,还对包含子节点列表的表格对象在可见状态和不可见状态间进行切换。
若子节点列表不存在而且节点的XMLSource属性有效则开始动态加载子节点。首先获得名为“SkyTreeViewControlTempXML”的XML数据岛对象,本控件输出的第一段HTML代码中已经包含了该XML数据岛。设置JavaScript全局变量“SkyTreeViewControlDyanmicRootNodeID”的值为当前节点的编号,然后设置这个XML数据岛的src属性值为节点的XMLSource属性值。之后IE浏览器就异步的加载XML文档,转而执行第一段HTML代码块中的JavaScript脚本了。
上面的代码都是使用RegisterStartupScript函数输出了三段HTML代码块。接下来就是输出控件的HTML代码内容了。
输出控件HTML代码
C#代码首先在内存中创建一个XML文档书写器,创建一个System.Xml.Serialization.XmlSerializer类型的对象,使用XML序列化技术将控件Nodes属性中的树状节点保存到一个XML文档中,实现这个功能的代码为
System.IO.StringWriter myStrWriter = new System.IO.StringWriter();
System.Xml.XmlTextWriter myXMLWriter = new System.Xml.XmlTextWriter( myStrWriter );
if( this.IndentXML && this.GenerateAtServer == false )
{
myXMLWriter.Indentation = 3 ;
myXMLWriter.IndentChar = ' ';
myXMLWriter.Formatting = System.Xml.Formatting.Indented ;
}
// 使用XML序列化将控件的树状节点全部保存到一个XML文档中并输出到XML文档书写器中
System.Xml.Serialization.XmlSerializer xser =
new System.Xml.Serialization.XmlSerializer( typeof( SkyTreeNodeList ));
xser.Serialize( myXMLWriter , this.Nodes );
myXMLWriter.Close();
// 获得定义控件树状节点的XML字符串
string xml = myStrWriter.ToString();
若控件的“GenerateAtServer”属性值为true,也就是在服务器端生成HTML代码,实现该功能的代码为
// 这里从资源文件SkyTreeViewControl.xslt中获得XSLT代码
string xslt = ReadXSLTString();
System.Xml.XmlDocument xsltDocument = new System.Xml.XmlDocument();
xsltDocument.LoadXml( xslt );
// 获得树状节点的数据XML文档
System.Xml.XmlDocument NodeXmlDocument = new System.Xml.XmlDocument();
NodeXmlDocument.LoadXml( xml );
// 创建一个字符串书写器,XSLT转换结果将输出到这个字符串书写器中
myStrWriter = new System.IO.StringWriter();
// 创建XSLT转换引擎
System.Xml.Xsl.XslCompiledTransform transform =
new System.Xml.Xsl.XslCompiledTransform();
// 引擎加载XSLT模板
transform.Load(xsltDocument);
// 执行XSLT转换
transform.Transform( NodeXmlDocument , null , myStrWriter );
// 直接向ASPX页面输出转换结果
writer.Write( myStrWriter.ToString());
这段代码中,首先调用函数ReadXSLTString从资源文件“SkyTreeViewControl.xslt”加载一个XML文档,然后创建一个NodeXmlDocument变量,调用它的LoadXml函数对根节点进行XML序列化所得的XML字符串来生成XML文档,然后创建一个System.Xml.Xsl.XslCompiledTransform类型的XSLT转换引擎,调用它的Load方法加载XSLT模板,调用它的Transform方法来执行XSLT转换,转换结果输出到myStrWriter的字符串书写器中,这个字符串书写器中的内容就是用于显示树状结构的HTML代码。然后程序使用RenderContent函数的writer参数将这个HTML代码输出到页面中。
若不是在服务器端生成HTML代码,那就是在客户端生成代码了,于是程序输出一个XML数据岛的HTML代码,该数据岛的内容就是节点XML文档的内容。实现这个功能的C#代码为
writer.Write("<span id='" + this.ClientID + "_container' >正在加载,请稍候</span>");
// 将数据XML字符串输出到ASPX页面中的一个XML数据岛中
writer.Write("<xml id='" + this.ClientID + "_xml'>" + xml + "</xml>");
// 输出将要初始化树状列表的JavaScript函数
this.Page.ClientScript.RegisterStartupScript(
this.GetType() ,
"SkyTreeViewControL_Init_" + this.ClientID , @"
<script language=javascript>
RefreshSkyTreeViewControl( '" + this.ClientID + @"' );
</script>
");
这里调用了RegisterStartupScript函数,使得HTML页面加载时立即调用名为 RefreshSkeyTreeViewControl 的JavaScript函数来初始化这个树状列表,本控件输出的第二段HTML代码就包含了该JavaScript函数。
操作数据视图
本WEB控件还在数据视图中保存数据,其功能代码为
/// 保存数据视图状态
/// </summary>
/// <returns>操作结果</returns>
protected override object SaveViewState()
{
this.ViewState["nodes"] = this.myNodes ;
return base.SaveViewState ();
}
/// <summary>
/// 加载数据视图状态
/// </summary>
/// <param name="savedState">操作结果</param>
protected override void LoadViewState(object savedState)
{
myNodes = this.ViewState["nodes"] as SkyTreeNodeList ;
base.LoadViewState (savedState);
}
当ASP.NET框架要求WEB控件保存数据到视图中,系统会自动调用控件的SaveViewState函数,当ASP.NET框架要求WEB控件从数据视图中加载数据时,系统会自动调用WEB控件的LoasViewState函数。能保存在数据视图中的对象必须能进行二进制序列化。这就是在SkyTreeNode和SkyTreeNodeList类型前面都附加声明类型能进行二进制序列化的“[System.Serializable()]”的原因。
分析SkyTreeViewControl的源代码,读者可以看到这个WEB控件还是比较复杂的,它需要了解ASP.NET自定义控件的一些知识,此外还需要掌握JavaScript和IE浏览器XML数据岛的知识。
XML数据岛是IE浏览器特有的功能,【袁永福原创(http://www.xdesigner.cn/),请尊重知识】是微软对HTML标准的扩展,其他浏览器是不支持XML数据岛的,实际上我们可以使用XMLHttpRequest 的ActiveX组件来从服务器上下载XML文档,而FireFox是支持XMLHttpRequest的,这样可以做到对FireFox的兼容。在这里我特地演示使用了XML数据岛的功能,而且使用XML数据岛的功能能将XML文档嵌入到HTML文档中,减少WEB系统的文件数,从而降低系统复杂度,而且方便部署。若规定客户端浏览器限制为IE浏览器时,则可以采用XML数据岛的功能。
SkyTreeViewControl.xslt
这个WEB控件中有一个很重要的文档就是XSLT模板文档。它保存在文件SkyTreeViewControl.xslt中,并作为嵌入的资源参与程序的编译。这个XSLT模板文档的主体结构为
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<!--
和 SkyTreeViewControl 树状列表控件配合使用的XSLT文档,本文档的生成操作必须为“嵌入的资源”。
编制 袁永福 2008-1-29
-->
<xsl:output method="html" indent="no" />
<!-- ***************** 主模板,为XSLT转换的入口点 ******************** -->
<xsl:template match="/*">
------------- 主模板的内容 -----------------------
</xsl:template>
<!-- ******************* 输出一个树状列表节点 *************************** -->
<xsl:template name="TreeNode">
-------------- 子模板的内容 ----------------------
</xsl:template>
</xsl:stylesheet>
这里使用了xsl:output指令
<xsl:output method="html" indent="no" /> |
表明此XSLT转换生成是HTML代码,而且不进行缩进处理。
该XSLT模板文档定义了两个XSLT模板,一个是默认模板,还有一个名为“TreeNode”的子模板。默认模板内容为
<xsl:template match="/*">
<!-- 定义一个NodeID变量,表示当前节点的编号 -->
<xsl:variable name="NodeID">
<xsl:choose>
<xsl:when test="string-length( @RootID ) > 0 ">
<xsl:value-of select="@RootID" />
</xsl:when>
<xsl:when test="string-length(ID) > 0">
<xsl:value-of select="ID" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="generate-id( . ) " />
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- 若有子节点则显示子节点 -->
<table border="0" cellspacing="0" cellpadding="0">
<xsl:attribute name="id">
<xsl:value-of select="concat( $NodeID , '_table' ) " />
</xsl:attribute>
<xsl:for-each select="Node">
<xsl:call-template name="TreeNode">
<xsl:with-param name="Level">1</xsl:with-param>
</xsl:call-template>
</xsl:for-each>
</table>
</xsl:template>
主模板是XSLT转换的入口处,在主模板中,首先创建了一个名为NodeID的XSLT参数,若定义了树状列表的根节点则创建table元素,然后循环遍历所有的根节点,并对每一个列表节点元素调用TreeNode模板,并传递了一个名为Level的值为1的参数,表示生成的节点层次数。这样就开始了递归创建HTML元素的过程。
TreeNode 模板
TreeNode模板是XSLT模板文档的主要部分,其代码为
<xsl:template name="TreeNode">
<!-- 定义一个 Level 参数,表示节点层次,默认 0 -->
<xsl:param name="Level">0</xsl:param>
<!-- 定义一个NodeID变量,表示当前节点的编号 -->
<xsl:variable name="NodeID">
<xsl:choose>
<xsl:when test="string-length(ID) > 0">
<xsl:value-of select="ID" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="generate-id( . ) " />
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- 定义一个showChildNodes变量,表示当前节点是否默认显示子节点 -->
<xsl:variable name="ShowChildNode">
<xsl:if test="$Level < 1 ">1</xsl:if>
</xsl:variable>
<!-- 获得所有子节点的个数 -->
<xsl:variable name="ChildCount">
<xsl:value-of select="count(Nodes/Node) " />
</xsl:variable>
<!-- 判断是否存在子节点 -->
<xsl:variable name="HasChild">
<xsl:if test="$ChildCount > 0 or string-length( XMLSource ) > 0 ">1</xsl:if>
</xsl:variable>
<tr>
<td valign="top" align="left" width="16" height="16">
<!-- 显示前面的树状结构线 -->
<xsl:choose>
<xsl:when test=" $HasChild = '1' ">
<xsl:if test="position()!=last()">
<xsl:attribute name="background">SkyTreeViewControl_line.gif</xsl:attribute>
</xsl:if>
<img>
<xsl:attribute name="id">
<xsl:value-of select="concat($NodeID,'_expend')" />
</xsl:attribute>
<xsl:choose>
<xsl:when test="$ShowChildNode != '1' and position() != last()">
<xsl:attribute name="src">SkyTreeViewControl_collapse.gif</xsl:attribute>
<xsl:attribute name="SrcBack">SkyTreeViewControl_expend.gif</xsl:attribute>
</xsl:when>
<xsl:when test="$ShowChildNode = '1' and position() != last()">
<xsl:attribute name="src">SkyTreeViewControl_expend.gif</xsl:attribute>
<xsl:attribute name="SrcBack">SkyTreeViewControl_collapse.gif</xsl:attribute>
</xsl:when>
<xsl:when test="$ShowChildNode != '1' and position() = last()">
<xsl:attribute name="src">SkyTreeViewControl_lastcollapse.gif</xsl:attribute>
<xsl:attribute name="SrcBack">SkyTreeViewControl_lastexpend.gif</xsl:attribute>
</xsl:when>
<xsl:when test="$ShowChildNode = '1' and position() = last()">
<xsl:attribute name="src">SkyTreeViewControl_lastexpend.gif</xsl:attribute>
<xsl:attribute name="SrcBack">SkyTreeViewControl_lastcollapse.gif</xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:attribute name="onclick">
<xsl:text>SkyTreeViewContrlExpendNodeByID('</xsl:text>
<xsl:value-of select="$NodeID" />
<xsl:text>' , false );</xsl:text>
</xsl:attribute>
</img>
</xsl:when>
<xsl:otherwise>
<xsl:if test="position()!=last()">
<img src="SkyTreeViewControl_child.gif" />
</xsl:if>
<xsl:if test="position()=last()">
<img src="SkyTreeViewControl_lastchild.gif" />
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</td>
<td valign="top" nowrap="1">
<!-- 用于生成节点图标HTML代码 -->
<img align="absmiddle" width="16" height="16">
<xsl:attribute name="onclick">
<xsl:text>SkyTreeViewContrlExpendNodeByID('</xsl:text>
<xsl:value-of select="$NodeID" />
<xsl:text>' , true );</xsl:text>
</xsl:attribute>
<xsl:attribute name="id">
<xsl:value-of select="concat($NodeID,'_icon')" />
</xsl:attribute>
<xsl:choose>
<xsl:when test="string-length( Icon ) != 0">
<xsl:attribute name="SrcBack">
<xsl:value-of select="Icon" />
</xsl:attribute>
<xsl:attribute name="src">
<xsl:value-of select="Icon" />
</xsl:attribute>
</xsl:when>
<xsl:when test="$HasChild = '1' and $ShowChildNode = '1'">
<xsl:attribute name="SrcBack">SkyTreeViewControl_close.bmp</xsl:attribute>
<xsl:attribute name="src">SkyTreeViewControl_open.bmp</xsl:attribute>
</xsl:when>
<xsl:when test="$HasChild = '1' and $ShowChildNode != '1'">
<xsl:attribute name="SrcBack">SkyTreeViewControl_open.bmp</xsl:attribute>
<xsl:attribute name="src">SkyTreeViewControl_close.bmp</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="src">SkyTreeViewControl_default.bmp</xsl:attribute>
</xsl:otherwise>
</xsl:choose>
</img>
<!-- 生成节点的文本 -->
<a class="SkyTreeViewControl_TreeNode" valign="top">
<xsl:attribute name="id">
<xsl:value-of select="concat( $NodeID, '_text')" />
</xsl:attribute>
<xsl:attribute name="onclick">
<xsl:text>SkyTreeViewContrlExpendNodeByID('</xsl:text>
<xsl:value-of select="$NodeID" />
<xsl:text>' , true );</xsl:text>
<xsl:if test="OnClick !=''">
<xsl:value-of select="OnClick" />
</xsl:if>
</xsl:attribute>
<xsl:if test="Link != ''">
<xsl:attribute name="href">
<xsl:value-of select="Link" />
</xsl:attribute>
</xsl:if>
<xsl:if test="string-length(XMLSource) > 0 ">
<xsl:attribute name="XMLSource">
<xsl:value-of disable-output-escaping="yes" select="XMLSource" />
</xsl:attribute>
</xsl:if>
<xsl:value-of select="concat( ' ' ,Text)" />
</a>
<xsl:if test="$ChildCount > 0 ">
<!-- 若有子节点则显示子节点 -->
<table border="0" cellspacing="0" cellpadding="0">
<xsl:attribute name="id">
<xsl:value-of select="concat( $NodeID , '_table' ) " />
</xsl:attribute>
<xsl:if test="$ShowChildNode != '1'">
<xsl:attribute name="style">display:none</xsl:attribute>
</xsl:if>
<xsl:for-each select="Nodes/Node">
<!-- 递归调用TreeNode模板本身来生成下一级的节点的HTML代码 -->
<xsl:call-template name="TreeNode">
<xsl:with-param name="Level"> <!-- 传递节点层次参数 -->
<xsl:value-of select="$Level + 1" />
</xsl:with-param>
</xsl:call-template>
</xsl:for-each>
</table>
</xsl:if>
<xsl:if test="string-length(XMLSource) > 0 ">
<!-- 若节点的XMLSource存在则预先生成好“正在加载”字样,但隐藏起来 -->
<span style='display:none' class='SkyTreeViewControl_TreeNode'>
<xsl:attribute name="id">
<xsl:value-of select="concat( $NodeID , '_Loading' ) " />
</xsl:attribute>
<br />
<xsl:text>正在加载</xsl:text>
</span>
</xsl:if>
</td>
</tr>
</xsl:template>
在这个模板中首先接受了一个名为Level的参数,该参数表示生成的树状列表的层次序号。
此处定义了一个名为“NodeID”的XSLT变量,将作为树状列表节点在HTML文档中的编号。
这里使用了XSLT函数generate-id,该函数属于创建一个惟一的编号。对相同的XML节点调用generate-id函数会得到相同的结果,对不同的XML节点调用该函数一定会得到不同的编号。注意,这里的编号在同一个XML文档中是惟一的,但对多个XML文档则不一定了。若对两个XML文档中的XML节点调用该函数是有可能得到相同的编号。因此当同一个页面上有多个WEB控件,或者WEB控件支持动态加载子节点时,【袁永福原创(http://www.xdesigner.cn/),请尊重知识】整个页面会对多个XML文档执行XSLT转换,从而导致多个节点对象可能使用一样的节点编号的情况。若多个节点的编号一样会导致树状列表工作异常。这就是为什么在本演示程序中的“TreeViewNode.aspx”中使用“System.Guid.NewGuid”来手动的设置节点编号的原因。因为“NewGuid”函数会创建一个全球惟一编号,多次调用“NewGuid”函数而创建相同的编号的可能性基本上不存在,这从而保证了同一个页面中所有树状列表的节点编号的惟一性。
此处定义了名为“ShowChildNode”的XSLT变量。这里判断了Level参数值是否小于1,若小于1则参数值为1,表示初始化时该节点的子节点就显示出来。在这里可以控制树状列表初始化时显示多少层节点。
此处定义了“ChildCount”变量,用于存放子节点的个数,以后需要获得子节点个数时就用这个变量,避免重复计算。
此处定义了“HasChild”变量,用于判断是否有子节点,以后需要判断是否有子节点时就不需要重新计算了。
准备工作完毕后开始输出HTML代码了。首先输出一个表格行,然后输出第一个单元格。
若节点存在子节点,若节点不是父节点的最后一个子节点时设置单元格的背景图片,从而模拟显示树状列表的层次结构线。此外由于节点存在子节点,因此可以展开和收缩,因此还输出展开和收缩控制点,并对控制点图片添加“onclick”属性,该属性中调用了控件输出的第三段HTML代码中定义的名为“SkyTreeViewContrlExpendNodeByID”的JavaScript函数。
若节点不存在子节点则输出图片来模拟树状结构叶子节点连接线。
接下来就是输出第二个单元格了。首先输出节点前面的图标,若树状节点定义了Icon数据,则设置图标的SrcBack和src属性值为Icon数据。若没有指定Icon数据,则使用默认值。并设置显示图标的图片的“onclick”属性,这个“onclick”属性将会调用JavaScript函数“SkyTreeViewContrlExpendNodeByID”。
然后输出节点文本,输出超链接和OnClick属性值,若节点还有XMLSource值,这设置该值到HTML元素的XMLSource属性中。
系统判断XSLT变量“ChildCount”的值,若该节点存在子节点,则创建一个table元素,然后遍历所有的子节点,递归调用TreeNode模板,并传递Level参数,并使得Level参数值每次递归调用都增加1。
若节点的XMLSource值有效,还输出一个隐藏的提示“正在加载”的文本标签。
经过上述过程,一种高性能的可动态加载子列表的ASP.NET树状列表控件开发完毕,接着笔者将这个WEB控件投入使用。
Default.aspx
演示程序中有一个Default.aspx页面,为默认页面,这个页面就演示使用树状列表WEB控件,读者可以使用IE浏览器运行这个页面,可以看到两个树状列表,都是使用了3层节点来显示数据库中的客户订单信息。
左边的列表内容是一下子加载了数据库中的所有的数据,页面加载后该树状列表已经包含了3072个节点。用户鼠标操作可以展开和收缩节点,可以点击货物节点来弹出一个消息框。
而右边的列表内容是动态加载的,页面加载后该列表只加载了客户信息,并没有加载定单信息和货物信息。当用户鼠标展开节点时,页面会根据需要自动的从后台加载相关的信息并动态的生成子节点。动态加载子节点能加快树状列表的初始化加载速度。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
使用VS.NET 2005打开演示程序,重新编译一下,然后打开演示页面Default.aspx 的设计界面。其设计界面如下图
可以看到页面上已经放置了两个树状列表控件。控件名为myTreeView和myTreeView2,鼠标点击某个树状列表控件,可以在旁边的属性窗口中列出了该控件的一些属性,其界面如下图所示,
树状列表控件比较重要的属性有
AutoScroll 是否自动显示滚动条。
DynamicLoadChild表示是否使用动态加载子节点。
GenerateAtServer 是否在服务器端生成HTML代码。
IndentXML 生成的XML是否进行缩进。
SelectedNodeStyleString 选中的节点的CSS样式字符串。
TreeNodeStyleString 节点的CSS样式字符串。
查看该页面的HTML代码,可以看到这里使用了标签“SkyWebControl:SkyTreeViewControl”来定义一个树状列表控件。一些控件的属性值保存在HTML标签的属性中。
一般的。NET框架中的WEB控件在HTML代码中的前缀是“asp”,比如“asp:label”,“asp:button”,“asp:TextBox”等。开发人员在开发自己的WEB控件也可以使用自己的HTMJL标签前缀,在树状列表控件的C#源代码中开头有一条指令
[assembly:System.Web.UI.TagPrefix("CS_Discovery" , "SkyWebControl")] |
这条指令就指明自定义控件采用什么样的标签前缀。
查看这个页面的C#代码,可以看到代码还是不复杂的。主要包含了一个Page_Load函数,该函数的代码为
{
if( myTreeView.Nodes.Count == 0 )
{
// 连接数据库
using( OleDbConnection conn = new OleDbConnection())
{
conn.ConnectionString = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source="
+ this.Server.MapPath("SkyDemo.mdb");
conn.Open();
using( OleDbCommand cmd = conn.CreateCommand())
{
// 查询数据库,获得所有的客户名称、订单号和产品名称,
// 向树状列表myTreeView 填充内容
cmd.CommandText = @"
select
trim(Customers.CompanyName) ,
trim(orders.orderid & chr(32) & orders.shipname) ,
trim(products.productname)
from
customers ,
orders ,
orderdetails ,
products
where
customers.customerid = orders.customerid
and orders.orderid = orderdetails.orderid
and orderdetails.productid = products.productid
order by
trim(Customers.CompanyName) ,
orders.orderid,
trim(products.productname)";
OleDbDataReader reader = cmd.ExecuteReader();
// 客户公司节点
SkyTreeNode CompanyNode = null;
// 客户订单节点
SkyTreeNode OrderNode = null;
while( reader.Read())
{
// 读取客户公司名称
string cname = Convert.ToString( reader.GetValue( 0 ));
// 读取订单编号和订货人名称
string oname = Convert.ToString( reader.GetValue( 1 ));
// 读取产品名称
string pname = Convert.ToString( reader.GetValue( 2 ));
if( CompanyNode == null || cname != CompanyNode.Text )
{
// 创建新的客户公司节点并添加到控件myTreeView 中
OrderNode = null;
CompanyNode = new SkyTreeNode();
CompanyNode.Text = cname ;
CompanyNode.Icon = "customer.bmp";
myTreeView.Nodes.Add( CompanyNode );
}
if( OrderNode == null || oname != OrderNode.Text )
{
// 创建新的订单节点并添加到客户公司节点下
OrderNode = new SkyTreeNode();
OrderNode.Text = oname ;
OrderNode.Icon = "order.bmp";
CompanyNode.Nodes.Add( OrderNode );
}
// 创建新的货物节点并添加到订单节点下
SkyTreeNode ProductNode = new SkyTreeNode();
ProductNode.Text = pname ;
ProductNode.Icon = "product.bmp";
ProductNode.OnClick = "alert('" + pname + "')" ;
OrderNode.Nodes.Add( ProductNode );
}//while
reader.Close();// 树状列表myTreeView 填充完毕
// 查询数据库获得所有的客户编号和客户名称,准备填充树状列表控件myTreeView2
cmd.CommandText = "Select customerid , companyname from customers "
+" order by companyname";
reader = cmd.ExecuteReader();
while( reader.Read())
{
// 创建新的客户节点并添加到控件myTreeView2 下。
string NodeID = System.Guid.NewGuid().ToString();
SkyTreeNode node = new SkyTreeNode();
node.ID = NodeID ;
node.Text = Convert.ToString( reader.GetValue( 1 ));
node.Icon = "customer.bmp";
// 设置节点的XMLSource属性准备客户端动态加载子节点
// 此处需要配合使用TreeViewNodeXml.aspx 页面。
node.XMLSource = "TreeViewNodeXml.aspx?KeyType=customerid&KeyValue="
+ Convert.ToString( reader.GetValue( 0 ) ) ;
myTreeView2.Nodes.Add( node );
}
reader.Close();
}//using
}//using
}
this.lblInfo.Text = "本控件共有节点" + myTreeView.AllNodes.Count + " 个";
this.lblInfo2.Text = "本控件初始化有" + myTreeView2.AllNodes.Count
+ " 个节点,能动态加载子节点";
}
在这个函数中,首先连接程序目录下的演示数据库 SkyDemo.mdb,然后执行一个比较复杂的SQL查询,获得数据库中所有的客户名称,订单信息和订单货物信息,然后填充到myTreeView的Nodes属性中,从而在内存中构造了一个三层的树状结构。
这里面可以看到树状列表没有自动的数据源绑定功能。由于树状结构处理过程复杂,简单的数据源绑定难于实现,因此这个树状列表控件不提供数据源绑定功能,而是需要使用者编程向列表添加节点。
填充第一个树状列表后,程序然后再次查询数据库,获得所有的客户名称和编号,然后填充到myTreeView2 的Nodes中,并设置每个节点的XMLSource属性。而XMLSource属性就规定了客户端动态加载子节点使用的XML文档的URL地址。这里使用了另外一个页面“TreeViewNodeXml.aspx”作为XML文档的提供者,并向该页面传递参数来决定生成什么样的节点定义XML文档。而且该页面生成的XML文档格式符合刚才设计的节点XML文档格式。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
在这个页面中,没有任何生成HTML代码的代码,所做的只是向页面拖拽放置树状列表,然后查询数据库填充控件的节点结构,可以说使用比较方便,这个树状列表控件内部自动完成了所有的底层工作。
TreeViewNodeXml.aspx
本页面是一个后台服务页面,用于向第二个树状列表动态的提供子节点信息。该页面也不复杂,它没有HTML代码,其ASPX文件只有一行代码,其代码如下
<%@ Page language="c#" Inherits="CS_Discovery.TreeViewNodeXml" CodeFile="TreeViewNodeXml.aspx.cs" %> |
查看该页面的C#代码,只有一个Page_Load函数,其代码为
{
// 关键字的类型
string KeyType = this.Request.QueryString["KeyType"] ;
// 关键字的数值
string KeyValue = this.Request.QueryString["KeyValue"];
// 设置页面输出格式
this.Response.ContentEncoding = System.Text.Encoding.GetEncoding( 936 );
this.Response.ContentType = "text/xml";
// 根据页面输出流创建XML文档书写器
System.Xml.XmlTextWriter writer = new System.Xml.XmlTextWriter( this.Response.Output );
// 设置带缩进
writer.IndentChar = ' ' ;
writer.Indentation = 3;
writer.Formatting = System.Xml.Formatting.Indented ;
// 开始输出XML文档
writer.WriteStartDocument();
writer.WriteStartElement("Nodes");
// 连接数据库
using( System.Data.OleDb.OleDbConnection conn = new System.Data.OleDb.OleDbConnection())
{
conn.ConnectionString = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source="
+ this.Server.MapPath("SkyDemo.mdb");
conn.Open();
// 查询数据库
using( System.Data.OleDb.OleDbCommand cmd = conn.CreateCommand())
{
if (KeyType == "customerid")
{
// 关键字为客户编号,则此时查询该客户编号下的所有的订单信息
cmd.CommandText = "select orderid , shipname from orders where customerid='"
+ KeyValue +"' order by orderid";
System.Data.IDataReader reader = cmd.ExecuteReader();
while( reader.Read())
{
writer.WriteStartElement("Node");
string NodeID = System.Guid.NewGuid().ToString();
// 创建节点编号
writer.WriteElementString("ID" , NodeID );
writer.WriteElementString("Icon","order.bmp");
// 节点文本就是订单编号和订单人姓名
writer.WriteElementString("Text" ,
Convert.ToString( reader.GetValue( 0 ) )
+ "-" + Convert.ToString( reader.GetValue( 1 )));
// 输出生成子节点使用的XML文档URL地址,从功能上看相当于递归调用本页面
writer.WriteElementString("XMLSource" ,
"TreeViewNodeXml.aspx?KeyType=orderdetails&KeyValue="
+ Convert.ToString( reader.GetValue( 0 )) );
writer.WriteEndElement();
}
reader.Close();
}
else if( KeyType == "orderdetails" )
{
// 关键字为订单编号,则此时查询该订单编号下的所有的货物的信息
cmd.CommandText = @"
Select
products.productname ,
orderdetails.quantity
from
orderdetails ,
products
where
orderdetails.productid = products.productid
and orderdetails.orderid=" + KeyValue + " order by productname ";
System.Data.IDataReader reader = cmd.ExecuteReader();
while( reader.Read())
{
writer.WriteStartElement("Node");
string NodeID = System.Guid.NewGuid().ToString();
writer.WriteElementString("ID" , NodeID );
writer.WriteElementString("Icon" , "product.bmp" );
writer.WriteElementString("Text" ,
Convert.ToString( reader.GetValue( 0 )));
writer.WriteElementString("OnClick" ,
"alert('" + Convert.ToString( reader.GetValue( 0 )) + "')" );
writer.WriteEndElement();
}
reader.Close();
}
}//using
}//using
writer.WriteEndElement();
writer.WriteEndDocument();
writer.Close();
}
这段代码过程也不复杂,首先获得名为KeyType和KeyValue的页面参数。然后在页面输出流上创建一个XML文档书写器,然后连接数据库准备查询数据。
若参数KeyType的值等于“customerid”,则表示KeyValue为一个客户编号,此时页面是要求输出输出该编号客户名下的所有的订单信息,此时代码连接数据库查询查询Orders数据表,对每一个查询记录输出一个名为Node的XML元素,使用Guid.NewGuid()创建一个不重复的节点编号,输出节点图标,文本信息。此外还输出XMLSource信息。这里的XMLSource信息表示订单节点的子节点信息来源,也就是指定订单的货品信息XML文档地址,这里还是指向页面TreeViewNodeXml.aspx,使用的KeyType参数值为orderdetails,而KeyValue 参数值就是订单编号。
若页面参数KeyType的值等于orderdetails,则KeyValue为一个订单编号,此时页面被要求输指定编号的订单中的详细货物清单,此时程序执行一个简单的联合查询,获得指定订单编号的所有货品信息,然后输出XML文档。由于货品信息没有子节点,因此也就不输出XMLSource元素了。
其他文件
这个树状列表控件还包含了一些其他文件,比如SkyTreeViewControl.bmp是控件在VS.NET窗体设计器工具箱上显示的小图标。而其他的以 SkyTreeViewControl_开头的图片文件用于模拟显示树状列表的层次结构,还有一些默认图标文件。
其他说明
读者可以试着运行演示页面Default.aspx,可以发现对树状控件进行不同的设置,生成的HTML页面大小是不相同的。比如笔者设置左边的列表的GenerateAtServer属性为false,IndentXML为false时,也就是启动客户端生成HTML代码,则生成的客户端加载的HTML页面大小为三百多K,查看其HTML源代码,发现树状列表的节点XML文档占据着HTML文档的大部分。若笔者设置左边的列表的GenerateAtServer属性为true,也就是在服务器端生成HTML代码,则客户端显示的HTML页面大小接近2兆。体积瞬间增长到6倍,也就是说当树状列表节点比较多时(这里有三千多个),则在服务器端生成代码和在客户端生成代码这两种模式存在很大的差别。这是因为当在客户端生成代码时,服务器端向客户端发送的是XML文档,只包含比较纯粹的数据,体积小;而在服务器端生成代码时,服务器端发送的是HTML代码,除了包含数据外,还有大量的用于控制界面样式的HTML代码,这导致HTML代码量大。【袁永福原创(http://www.xdesigner.cn/),请尊重知识】
在以前的XML/XSLT章节中,笔者提过,在WEB开发中采用XML/XSLT技术能比较大的改善WEB程序的网络传输性能,在这里用于显示大量节点的树状列表的应用就是一个范例。
部署控件
这个树状列表控件开发完毕后包含在一个DLL文件中,编译程序前要注意设置文件“SkyTreeViewControl.bmp”和“SkyTreeViewControl.xslt”的“生成操作”属性值为“嵌入的资源”。
开发人员在开发其他系统时可以添加这个DLL的引用,在VS.NET的窗体设计器的工具箱上可以看到这个树状列表的图标,若没有则在工具箱上右击显示快捷菜单,选中“添加/移除项目”,在对话框中选择浏览,显示包含控件的DLL 文件,然后选择SkyTreeViewControl即可。
开发人员从工具箱中拖拽一个树状列表控件到ASP.NET页面上就可以开始使用了。
在开发和部署包含树状列表的WEB 程序前,需要将所有“SkyTreeViewControl_”前缀的图片文件拷贝到程序第一级目录下。
小结
在本章中,大家一起研究了目前一些ASP.NET项目中使用的树状列表的原理和出现的问题,并开发新的树状列表控件。这种WEB树状列表控件支持在客户端和服务器端使用XSLT技术生成HTML代码,并能在客户端动态加载子列表,相对于旧控件,新控件加载速度快,能显示大量的节点。
此外读者还学习了IE浏览器所特有的XML数据岛的功能,并使用JavaScript脚本在客户端执行XSLT转换。这里读者可以看到XSLT技术作为国际标准技术已经得到广泛的支持。
【袁永福原创(http://www.xdesigner.cn/),请尊重知识】