承接前兩篇,本文深入剖析 SkyTreeViewControl 核心渲染引擎——TreeNode XSLT 模板。這個模板將 XML 資料節點轉換為可互動的 HTML 結構,支援動態載入子節點、展開/收合狀態保持、多層級巢狀結構、自訂圖示與點擊事件,並透過 generate-id() 與 GUID 雙重機制確保節點 ID 唯一性。理解此模板,即掌握了「資料驅動 UI」在伺服器/客戶端雙模式下的完整實作邏輯。
<xsl:template name="TreeNode">
<!-- 層級參數,根節點為 0 -->
<xsl:param name="Level">0</xsl:param>
<!-- 節點 ID:優先使用 XML 內建 ID,否則 generate-id() -->
<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>
<!-- 是否預設展開子節點:Level < 1 即根節點展開 -->
<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>
<!-- 是否有子節點 (含動態載入 XMLSource) -->
<xsl:variable name="HasChild">
<xsl:if test="$ChildCount > 0 or string-length(XMLSource) > 0">1</xsl:if>
</xsl:variable>
關鍵設計:
NodeID 雙重保險:XML 指定 ID 優先,否則 generate-id()。但同頁多控制項/動態載入會導致多 XML 文件,generate-id() 可能重複 → 伺服器端強制注入 System.Guid.NewGuid() 解決。ShowChildNode 控制初始展開層級,可擴充為屬性配置。HasChild 整合靜態子節點與動態 XMLSource 兩種來源。<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>
圖示狀態機:
| 節點類型 | 位置 | 初始狀態 | src | SrcBack |
|---|---|---|---|---|
| 有子節點 | 非最後 | 收合 | collapse.gif | expend.gif |
| 非最後 | 展開 | expend.gif | collapse.gif | |
| 有子節點 | 最後 | 收合 | lastcollapse.gif | lastexpend.gif |
| 最後 | 展開 | lastexpend.gif | lastcollapse.gif | |
| 無子節點 | 非最後 | - | child.gif | - |
| 無子節點 | 最後 | - | lastchild.gif | - |
<td valign="top" nowrap="1">
<!-- 節點圖示 -->
<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:attribute>
<xsl:if test="string-length(XMLSource) > 0">
<xsl:attribute name="XMLSource">
<xsl:value-of disable-output-escaping="yes" select="XMLSource"/>
</xsl:attribute>
</xsl:attribute>
<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">
<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>
<!-- 動態載入 Loading 提示 -->
<xsl:if test="string-length(XMLSource) > 0">
<span style="display:none" class="SkyTreeViewControl_TreeNode">
<xsl:attribute name="id"><xsl:value-of select="concat($NodeID, '_Loading')"/></xsl:attribute>
<br/>
<xsl:text>正在載入<img src="SkyTreeViewControl_loading.gif"/></xsl:text>
</span>
</xsl:if>
</td>
1. 伺服器輸出節點 XML,含 XMLSource="TreeViewNodeXml.aspx?KeyType=customerid&KeyValue=ALFKI" 2. 瀏覽器渲染節點,a 標籤帶 XMLSource 屬性 3. 使用者點擊展開圖示 → SkyTreeViewContrlExpendNodeByID() 4. JS 讀取 XMLSource → 設定 XML 資料島 src → 觸發 onreadystatechange 5. SkyTreeViewControlDynamicLoadChildNodes() 執行 XSLT 轉換 6. 生成 HTML 插入 _table,切換圖示狀態,移除 Loading 提示
SkyTreeViewContrlExpendNodeByID(id, select):統一處理展開/收合/高亮/動態載入觸發SkyTreeViewControlDynamicLoadChildNodes():XML 資料島回調,執行 transformNode(),DOM 插入RefreshSkyTreeViewControl(id):頁面載入初始化,對根 XML 執行 XSLTprotected void Page_Load(object sender, EventArgs e)
{
if (myTreeView.Nodes.Count == 0)
{
using (OleDbConnection conn = new OleDbConnection(
@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + Server.MapPath("SkyDemo.mdb")))
{
conn.Open();
OleDbCommand cmd = conn.CreateCommand();
// 三層聯查:Customers → Orders → OrderDetails → Products
cmd.CommandText = @"
SELECT TRIM(Customers.CompanyName),
TRIM(Orders.OrderID & ' ' & 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 Customers.CompanyName, Orders.OrderID, Products.ProductName";
SkyTreeNode companyNode = null, orderNode = null;
OleDbDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
string company = reader.GetString(0);
string order = reader.GetString(1);
string product = reader.GetString(2);
if (companyNode == null || company != companyNode.Text)
{
companyNode = new SkyTreeNode { Text = company, Icon = "customer.bmp" };
myTreeView.Nodes.Add(companyNode);
orderNode = null;
}
if (orderNode == null || order != orderNode.Text)
{
orderNode = new SkyTreeNode { Text = order, Icon = "order.bmp" };
companyNode.Nodes.Add(orderNode);
}
SkyTreeNode productNode = new SkyTreeNode
{
Text = product,
Icon = "product.bmp",
OnClick = "alert('" + product + "')"
};
orderNode.Nodes.Add(productNode);
}
}
}
lblInfo.Text = $"靜態樹共 {myTreeView.AllNodes.Count} 節點";
}
// 僅載入第一層:客戶
cmd.CommandText = "SELECT CustomerID, CompanyName FROM Customers ORDER BY CompanyName";
reader = cmd.ExecuteReader();
while (reader.Read())
{
SkyTreeNode node = new SkyTreeNode
{
ID = Guid.NewGuid().ToString(), // 強制唯一 ID
Text = reader.GetString(1),
Icon = "customer.bmp",
XMLSource = "TreeViewNodeXml.aspx?KeyType=customerid&KeyValue=" + reader.GetString(0)
};
myTreeView2.Nodes.Add(node);
}
lblInfo2.Text = $"動態樹初始 {myTreeView2.AllNodes.Count} 節點,支援動態載入";
protected void Page_Load(object sender, EventArgs e)
{
string keyType = Request.QueryString["KeyType"];
string keyValue = Request.QueryString["KeyValue"];
Response.ContentType = "text/xml";
Response.ContentEncoding = Encoding.GetEncoding(936); // GB2312
XmlTextWriter writer = new XmlTextWriter(Response.Output)
{
Formatting = Formatting.Indented,
Indentation = 3,
IndentChar = ' '
};
writer.WriteStartDocument();
writer.WriteStartElement("Nodes");
using (OleDbConnection conn = new OleDbConnection(...))
{
conn.Open();
OleDbCommand cmd = conn.CreateCommand();
if (keyType == "customerid")
{
// 返回該客戶的所有訂單
cmd.CommandText = "SELECT OrderID, ShipName FROM Orders WHERE CustomerID='" + keyValue + "' ORDER BY OrderID";
using (IDataReader r = cmd.ExecuteReader())
{
while (r.Read())
{
writer.WriteStartElement("Node");
writer.WriteElementString("ID", Guid.NewGuid().ToString());
writer.WriteElementString("Icon", "order.bmp");
writer.WriteElementString("Text", r.GetString(0) + "-" + r.GetString(1));
writer.WriteElementString("XMLSource",
"TreeViewNodeXml.aspx?KeyType=orderdetails&KeyValue=" + r.GetString(0));
writer.WriteEndElement(); // Node
}
}
}
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";
using (IDataReader r = cmd.ExecuteReader())
{
while (r.Read())
{
writer.WriteStartElement("Node");
writer.WriteElementString("ID", Guid.NewGuid().ToString());
writer.WriteElementString("Icon", "product.bmp");
writer.WriteElementString("Text", r.GetString(0));
writer.WriteElementString("OnClick", "alert('" + r.GetString(0) + "')");
writer.WriteEndElement();
}
}
}
}
writer.WriteEndElement(); // Nodes
writer.WriteEndDocument();
writer.Close();
}
| 模式 | GenerateAtServer | 傳輸內容 | 頁面大小 (3072 節點) | 伺服器 CPU |
|---|---|---|---|---|
| 客戶端 XSLT | false | 純 XML 資料 (~300KB) | ~300 KB | 低 |
| 伺服器端 XSLT | true | 完整 HTML (~2MB) | ~2 MB (6.7x) | 高 |
結論:大量節點務必啟用客戶端生成,利用瀏覽器原生 XSLT 引擎分擔渲染壓力。
SkyTreeViewControlSkyTreeViewControl_*.gif/bmp 圖片複製至 Web 應用程式根目錄SkyTreeViewControl.xslt 與 SkyTreeViewControl.bmp 專案屬性為 嵌入資源<%@ Register TagPrefix="SkyWebControl" Namespace="CS_Discovery" Assembly="SkyTreeViewControl" %>SkyTreeViewControl 透過XML 資料島 + XSLT 模板 + JavaScript 協同架構,實現了在 IE 環境下的高效能樹狀列表渲染。其核心創新在於:
雖然 XML 資料島為 IE 專屬技術,但「資料與呈現分離、客戶端模板渲染、按需載入」的架構思想,完美映射至現代的 React/Vue 虛擬 DOM、懶載入組件、GraphQL 精確查詢。這個 2008 年的控制項,實際上預演了前端工程化的核心範式。