WEB开发网
开发学院软件开发C语言 C#发现之旅 - 高性能ASP.NET树状列表控件(上) 阅读

C#发现之旅 - 高性能ASP.NET树状列表控件(上)

 2010-09-30 21:03:22 来源:WEB开发网   
核心提示:已有树状列表控件分析发现问题近期发现有人在ASP.NET项目开发中使用一种叫dtree的树状列表组件加载缓慢,这也是笔者撰写本章的动机,C#发现之旅 - 高性能ASP.NET树状列表控件(上),毛主席教导我们,做事要发现问题,已经输出的内容是不可更改的,因此Render或RenderContents中不能调用Regis

已有树状列表控件分析发现问题

近期发现有人在ASP.NET项目开发中使用一种叫dtree的树状列表组件加载缓慢。这也是笔者撰写本章的动机。毛主席教导我们,做事要发现问题,分析问题和解决问题。首先我们发现了已有的树状列表WEB控件加载缓慢的问题,接下来就很自然的是分析问题了。

下图就是dtree 运行界面的例子

C#发现之旅 - 高性能ASP.NET树状列表控件(上)

分析问题

现在我们分析问题,对使用dtree生成树状列表的程序代码的分析,可以了解程序运行过程如下图所示

C#发现之旅 - 高性能ASP.NET树状列表控件(上)

图片看不清楚?请点击这里查看原图(大图)。

在这样的程序中,首先服务器端的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字符串过程缓慢,很自然我们就针对这个原因来解决问题。

首先我们可以完全抛弃JavaScript脚本,使用C#在服务器端生成 展现树状列表的HTML代码,然后发往客户端 ,客户端浏览器获得HTML代码并展示出树状列表。

C#发现之旅 - 高性能ASP.NET树状列表控件(上)

图片看不清楚?请点击这里查看原图(大图)。

在这种模式下,服务器端的C#程序查询数据库获得数据,并使用字符串拼凑来生成用于展现树状列表的HTML代码,由于C#功能强大,而且速度比较快,可以使用 StringBuilder来加速字符串拼凑操作,而客户端浏览器获得的这个HTML代码,立即解析并显示出树状列表。因此整个过程是相当快的。这是一个可以采用的解决方法。

当然这个方式有一定的限制性,若服务器程序运行也比较缓慢,比如ASP,它比客户端的JavaScript脚本快不了很多,此时这种方法优势就不明显了。

另外一个方法就是加速JavaScript脚本生成HTML代码的过程。这时我们可以考虑使用其他的可快速运行的程序来辅助JavaScript来生成HTML代码,于是我们想到COM组件,我们可以设计出这样的程序结构。

C#发现之旅 - 高性能ASP.NET树状列表控件(上)

图片看不清楚?请点击这里查看原图(大图)。

在这个软件结构中,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浏览器进行密切的协作。

那么我们又如何使用MSXML组件来生成HTML代码呢?我们可以采用XSLT技术,首先系统提供一个XML文档,该文档定义了树状结构信息,然后我们调用一个事先定义好的XSLT文档,将两者进行XSLT转化,一下子就能生成HTML字符串,然后将生成的HTML字符串填充到页面中。在这个过程中,大部分运算量是由MSXML完成,而MSXML组件是用C++开发的,运行速度快,这样就能大大加快整个生成HTML字符串的过程,从而加快树状列表的加载过程。

由于XSLT是国际标准,因为我们在服务器端也可以使用这种方法。而且客户端和服务器端的代码类似,因此我们有可能开发出同时支持服务器端运行和客户端运行的树状列表WEB控件。

运行软件

笔者已经根据上述的解决问题的方式经过上述的软件设计开发出了这个树状列表WEB控件,并编写了演示页面,现对该控件的功能进行展示,使得读者对这个控件有一些初步的印象。打开浏览器直接输入演示页面地址,打开页面,可以看到如下的用户界面

C#发现之旅 - 高性能ASP.NET树状列表控件(上)

图片看不清楚?请点击这里查看原图(大图)。

可以看到页面上主要显示了两个树状列表,其显示内容都是按照客户,定单,货物三层结构的树状列表。其中左边的树状列表是一次性加载了所有的数据,共生成3072个节点。而右边的列表只加载了第一层节点,共91个节点,但右边的列表能动态加载子节点列表。用户可以使用鼠标点击操作来展开和收缩节点,点击货物节点会显示一个消息框。

软件设计

为了便于开发人员使用,笔者开发出一个通用的树状列表WEB控件,该控件名称为SkyTreeViewControl,是一种从 System.Web.UI.WebControls.WebControl派生的WEB控件,这样开发人员以后在ASP.NET 程序中需要树状列表时只要将这个控件拖拽到页面即可开始使用了。现在开始进行控件的基本设计。

结构设计

根据上面分析的结果,笔者采用XML/XSLT技术,于是就可以设计出如下的程序运行流程。

C#发现之旅 - 高性能ASP.NET树状列表控件(上)

图片看不清楚?请点击这里查看原图(大图)。

在这个流程中,服务器的C#程序查询数据库获得树状结构信息,并将树状结构信息保存到一个XML文档中,然后发往客户端。而客户端执行的 JavaScript脚本中,调用MSXML组件加载服务器端生成的节点XML文档,并从服务器上下载事先准备好的XSLT模板文档,然后调用MSXML 组件执行XSLT转换,转换结果就是HTML字符串,然后将这个字符串填充到页面上展示出树状列表。

目标HTML代码设计

无论WEB控件或者JavaScript等等经过怎样的处理,浏览器最终都是依据HTML代码来显示文档界面的,因此设计WEB控件,首先得设计WEB控件最终生成的HTML代码,也就是WEB控件的执行目标。

为了展现树状结构,业界已经设计出很多种HTML代码格式。有使用DIV标签的,有使用P标签的,笔者这里经过一些尝试,决定采用TABLE标签,使用表格套表格的方式来展现树状列表的层次结构。其设计的HTML页面范例如下。

C#发现之旅 - 高性能ASP.NET树状列表控件(上)

培训演示程序中有一个名为treeviewsample.htm 的文件,其中就是这个树状列表的HTML样本。

这个演示HTML文档中,展现节点“10359 成先生”及其子节点的HTML代码片段如下

<table cellspacing="0" cellpadding="0" border="1" bordercolor="black" width="187">
    <tr>
        <td valign="top" align="left" width="16" background="SkyTreeViewControl_line.gif"
            height="16">
            <img id="NodeID_expend" src="http://tech.ddvip.com/2009-09/SkyTreeViewControl_expend.gif">
        </td>
        <td valign="top">
            <img id="NodeID_icon" height="16" src="http://tech.ddvip.com/2009-09/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="http://tech.ddvip.com/2009-09/SkyTreeViewControl_child.gif">
                    </td>
                    <td valign="top" nowrap>
                        <img id="IDAIL4QC_icon" src="http://tech.ddvip.com/2009-09/SkyTreeViewControl_default.bmp">
                        <a id="IDAIL4QC_text">饼干</a>
                    </td>
                </tr>
                <tr>
                    <td valign="top" align="left" width="16" height="16">
                        <img src="http://tech.ddvip.com/2009-09/SkyTreeViewControl_child.gif">
                    </td>
                    <td valign="top" nowrap>
                        <img id="IDARL4QC_icon" src="http://tech.ddvip.com/2009-09/SkyTreeViewControl_default.bmp">
                        <a id="IDARL4QC_text">花奶酪</a>
                    </td>
                </tr>
                <tr>
                    <td valign="top" align="left" width="16" height="16">
                        <img src="http://tech.ddvip.com/2009-09/SkyTreeViewControl_lastchild.gif">
                    </td>
                    <td valign="top" nowrap>
                        <img id="IDAWL4QC_icon" src="http://tech.ddvip.com/2009-09/SkyTreeViewControl_default.bmp">
                        <a id="IDAWL4QC_text">温馨奶酪</a>
                    </td>
                </tr>
            </table>
        </td>
    </tr>
</table>

则这段HTML代码的显示效果为

C#发现之旅 - 高性能ASP.NET树状列表控件(上)

分析这段HTML代码,读者可以看到,每一个节点都占据一个表格行,节点文本后面还跟着一个Table来容纳子节点。如此循环则使用表格套表格的方式实现了一个树状列表结构。

HTML中,对于每一个节点都定义了一个惟一的编号,比如“NodeID”,在实际应用中则可以是任意样式的ID号,而且每一个节点分成4个部分。首先是前面的展开收缩控制元素。用一个图片元素来表示,当节点展开时使用图标C#发现之旅 - 高性能ASP.NET树状列表控件(上),当节点收缩时使用图标C#发现之旅 - 高性能ASP.NET树状列表控件(上)。该图片元素编号为“NodeID_expend”,占据了节点表格行的第一个单元格。由于树状结构有同层节点连接线,若节点不是上级节点的最后一个子节点则设置第一个单元格的背景图片来模拟显示同层节点连接线。

节点表格行的第二个单元格用来放置节点的图标,文本和子节点的表格。每一个节点都有一个图标,使用IMG元素来展示,元素编号是“NodeID_icon”,而且若有子节点,则节点可以展开和收缩,此时节点的图标是不同的,需要动态设置。最典型的就是节点展开时使用图标C#发现之旅 - 高性能ASP.NET树状列表控件(上),而节点收缩时使用图标C#发现之旅 - 高性能ASP.NET树状列表控件(上),当然两个图标是可以一样的。这里使用扩展属性“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数据岛元素。其范例为

<XML ID="XMLIslandID">
  <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文档的范例。

<?xml version="1.0" encoding="utf-8"?>
<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>
/// 树状列表节点对象
/// </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>
/// 树状列表节点列表
/// </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#代码就放置在这个文件中。打开这个文件,首先我们看到一条指令

// 此时对程序集使用TagPrefix特性,表示CS_Discovery名称控件下的WEB控件
// 在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.Web.UI.ControlBuilder( typeof( SkyTreeViewControlBuilder ))]
[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的窗体设计器的工具箱中使用什么样的图标。这里指明使用图标“C#发现之旅 - 高性能ASP.NET树状列表控件(上)”。

本控件定义了Nodes属性,其代码为

private SkyTreeNodeList myNodes = new SkyTreeNodeList();
/// <summary>
/// 子节点列表
/// </summary>
[System.ComponentModel.Browsable( false )]
public SkyTreeNodeList Nodes
{
    get
    {
        return myNodes ;
    }
    set
    {
        myNodes = value;
    }
}

Nodes属性保存了树状列表控件的根节点。该属性使用代码“[System.ComponentModel.Browsable( false )]”声明该属性在设计器中的属性列表中是看不见的。本控件还定义了AllNodes属性用于获得树状列表所包含的所有节点组成的列表。

本控件定义了IndentXML属性,其定义代码为

private bool bolIndentXML = false;
/// <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函数,其代码为

/// <summary>
/// 控件加载时的处理,此时控件尚未向页面输出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>C#发现之旅 - 高性能ASP.NET树状列表控件(上).</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代码。其代码为

/// <summary>
/// 输出控件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”。

ASP.NET框架处理原始的ASPX文件时,遇到WEB控件标签时会去掉这个标签,然后转而调用WEB控件的Render函数。比如在Default.ASPX中有一段HTML

<SkyWebControl:SkyTreeViewControl id="myTreeView" runat="server" 其他属性…… >
</SkyWebControl:SkyTreeViewControl>

当 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 != null )
{
    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函数。

Tags:发现 之旅 高性能

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接