第一段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腳本還更新父節點的圖標,使其表示為展開狀態。
第二段HTML代碼塊
WEB控件輸出完第一段HTML代碼後,會根據需要輸出第二段代碼,第二段代碼包含在客戶端執行XSLT轉換所需的XSLT模板代碼以及初始化生成樹狀列表的HTML代碼的Javascript代碼,其C#代碼為
if( this.GenerateAtServer == false || this.DynamicLoadChildNodes )
{
// 若允許動態加載子節點而且不是在服務器段生成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”的嵌入式程序集資源中讀取所有的文本內容。
此處還輸出一個為“RefreshSkyTreeViewControl”的Javascript函數,用於初始化一個樹狀列表,其參數就是樹狀列表的編號。也就是“myTreeView”,“myTreeView2”之類的控件客戶端編號。在Javascript函數中,首先獲得第三個XML數據島,獲得樹狀節點定義信息XML文檔,然後和“SkyTreeViewControlXSLT”數據島中的XSLT文檔執行XSLT轉換,生成HTML字符串,然後調用HTML元素的“innerHTML”屬性向HTML文檔填充剛剛生成的HTML代碼,從而展現出一個樹狀結構。
本過程配套使用的“ReadXSLTString”函數的代碼為
private static string strXSLString = null;
/// <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的編碼格式讀取該資源的文本內容。
第三段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函數有兩個參數,第一個參數是節點編號,第二個參數是是否設置該節點為當前節點,也就是是否設置高亮度顯示(一般的就是藍底白字)。
在這個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文檔中,實現這個功能的代碼為
// 在內存中創建一個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代碼,實現該功能的代碼為
// 若在服務器端生成HTML代碼則使用服務器端的XSLT轉換
// 這裡從資源文件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#代碼為
// 若在客戶端動態生成HTML代碼則將數據XML字符串輸出到ASPX頁面中
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>
/// 保存數據視圖狀態
/// </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浏覽器特有的功能,是微軟對HTML標准的擴展,其他浏覽器是不支持XML數據島的,實際上我們可以使用 XMLHttpRequest 的ActiveX組件來從服務器上下載XML文檔,而FireFox是支持XMLHttpRequest的,這樣可以做到對FireFox的兼容。在這裡我特地演示使用了XML數據島的功能,而且使用XML數據島的功能能將XML文檔嵌入到HTML文檔中,減少WEB系統的文件數,從而降低系統復雜度,而且方便部署。若規定客戶端浏覽器限制為IE浏覽器時,則可以采用XML數據島的功能。
SkyTreeViewControl.xslt
這個WEB控件中有一個很重要的文檔就是XSLT模板文檔。它保存在文件SkyTreeViewControl.xslt中,並作為嵌入的資源參與程序的編譯。這個XSLT模板文檔的主體結構為
<?xml version="1.0" encoding="UTF-8" ?>
<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”的子模板。默認模板內容為
<!-- ***************** 主模板,為XSLT轉換的入口點 ******************** -->
<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元素的過程。