已有樹狀列表控件分析發現問題
近期發現有人在ASP.NET項目開發中使用一種叫dtree的樹狀列表組件加載緩慢。這也是筆者撰寫本章的動機。毛主席教導我們,做事要發現問題,分析問題和解決問題。首先我們發現了已有的樹狀列表WEB控件加載緩慢的問題,接下來就很自然的是分析問題了。
下圖就是dtree 運行界面的例子
分析問題
現在我們分析問題,對使用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字符串過程緩慢,很自然我們就針對這個原因來解決問題。
首先我們可以完全拋棄Javascript腳本,使用C#在服務器端生成 展現樹狀列表的HTML代碼,然後發往客戶端 ,客戶端浏覽器獲得HTML代碼並展示出樹狀列表。
在這種模式下,服務器端的C#程序查詢數據庫獲得數據,並使用字符串拼湊來生成用於展現樹狀列表的HTML代碼,由於C#功能強大,而且速度比較快,可以使用 StringBuilder來加速字符串拼湊操作,而客戶端浏覽器獲得的這個HTML代碼,立即解析並顯示出樹狀列表。因此整個過程是相當快的。這是一個可以采用的解決方法。
當然這個方式有一定的限制性,若服務器程序運行也比較緩慢,比如ASP,它比客戶端的Javascript腳本快不了很多,此時這種方法優勢就不明顯了。
另外一個方法就是加速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浏覽器進行密切的協作。
那麼我們又如何使用MSXML組件來生成HTML代碼呢?我們可以采用XSLT技術,首先系統提供一個XML文檔,該文檔定義了樹狀結構信息,然後我們調用一個事先定義好的XSLT文檔,將兩者進行XSLT轉化,一下子就能生成HTML字符串,然後將生成的HTML字符串填充到頁面中。在這個過程中,大部分運算量是由MSXML完成,而MSXML組件是用C++開發的,運行速度快,這樣就能大大加快整個生成HTML字符串的過程,從而加快樹狀列表的加載過程。
由於XSLT是國際標准,因為我們在服務器端也可以使用這種方法。而且客戶端和服務器端的代碼類似,因此我們有可能開發出同時支持服務器端運行和客戶端運行的樹狀列表WEB控件。
運行軟件
筆者已經根據上述的解決問題的方式經過上述的軟件設計開發出了這個樹狀列表WEB控件,並編寫了演示頁面,現對該控件的功能進行展示,使得讀者對這個控件有一些初步的印象。打開浏覽器直接輸入演示頁面地址,打開頁面,可以看到如下的用戶界面
可以看到頁面上主要顯示了兩個樹狀列表,其顯示內容都是按照客戶,定單,貨物三層結構的樹狀列表。其中左邊的樹狀列表是一次性加載了所有的數據,共生成3072個節點。而右邊的列表只加載了第一層節點,共91個節點,但右邊的列表能動態加載子節點列表。用戶可以使用鼠標點擊操作來展開和收縮節點,點擊貨物節點會顯示一個消息框。
軟件設計
為了便於開發人員使用,筆者開發出一個通用的樹狀列表WEB控件,該控件名稱為SkyTreeViewControl,是一種從 System.Web.UI.WebControls.WebControl派生的WEB控件,這樣開發人員以後在ASP.NET 程序中需要樹狀列表時只要將這個控件拖拽到頁面即可開始使用了。現在開始進行控件的基本設計。
結構設計
根據上面分析的結果,筆者采用XML/XSLT技術,於是就可以設計出如下的程序運行流程。
在這個流程中,服務器的C#程序查詢數據庫獲得樹狀結構信息,並將樹狀結構信息保存到一個XML文檔中,然後發往客戶端。而客戶端執行的 Javascript腳本中,調用MSXML組件加載服務器端生成的節點XML文檔,並從服務器上下載事先准備好的XSLT模板文檔,然後調用MSXML 組件執行XSLT轉換,轉換結果就是HTML字符串,然後將這個字符串填充到頁面上展示出樹狀列表。
目標HTML代碼設計
無論WEB控件或者Javascript等等經過怎樣的處理,浏覽器最終都是依據HTML代碼來顯示文檔界面的,因此設計WEB控件,首先得設計WEB控件最終生成的HTML代碼,也就是WEB控件的執行目標。
為了展現樹狀結構,業界已經設計出很多種HTML代碼格式。有使用DIV標簽的,有使用P標簽的,筆者這裡經過一些嘗試,決定采用TABLE標簽,使用表格套表格的方式來展現樹狀列表的層次結構。其設計的HTML頁面范例如下。
培訓演示程序中有一個名為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="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數據島元素。其范例為
<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的窗體設計器的工具箱中使用什麼樣的圖標。這裡指明使用圖標“”。
本控件定義了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>.</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函數。