首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 發現之旅:高效能 ASP.NET TreeView 控制項開發 (下)——TreeNode XSLT 模板深度解析

     編輯:關於C#

前言:XSLT 驅動的樹狀結構渲染引擎

承接前兩篇,本文深入剖析 SkyTreeViewControl 核心渲染引擎——TreeNode XSLT 模板。這個模板將 XML 資料節點轉換為可互動的 HTML 結構,支援動態載入子節點、展開/收合狀態保持、多層級巢狀結構、自訂圖示與點擊事件,並透過 generate-id() 與 GUID 雙重機制確保節點 ID 唯一性。理解此模板,即掌握了「資料驅動 UI」在伺服器/客戶端雙模式下的完整實作邏輯。

TreeNode 模板完整結構拆解

1. 參數與變數宣告區

<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 兩種來源。

2. 第一欄:樹狀連接線與展開/收合控制點

<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>

圖示狀態機:

節點類型位置初始狀態srcSrcBack
有子節點非最後收合collapse.gifexpend.gif
非最後展開expend.gifcollapse.gif
有子節點最後收合lastcollapse.giflastexpend.gif
最後展開lastexpend.giflastcollapse.gif
無子節點非最後-child.gif-
無子節點最後-lastchild.gif-

3. 第二欄:節點圖示、文字、超連結與遞迴子節點

<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>

動態載入機制:XMLSource 與客戶端 XSLT 轉換

資料流向

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 提示

關鍵 JS 函數邏輯 (見前文)

  • SkyTreeViewContrlExpendNodeByID(id, select):統一處理展開/收合/高亮/動態載入觸發
  • SkyTreeViewControlDynamicLoadChildNodes():XML 資料島回調,執行 transformNode(),DOM 插入
  • RefreshSkyTreeViewControl(id):頁面載入初始化,對根 XML 執行 XSLT

Default.aspx 實際應用範例

靜態完整樹狀結構 (myTreeView)

protected 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} 節點";
}

動態載入樹狀結構 (myTreeView2)

// 僅載入第一層:客戶
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} 節點,支援動態載入";

TreeViewNodeXml.aspx:動態 XML 供應端點

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();
}

效能與部署關鍵點

客戶端 vs 伺服器端生成對比

模式GenerateAtServer傳輸內容頁面大小 (3072 節點)伺服器 CPU
客戶端 XSLTfalse純 XML 資料 (~300KB)~300 KB低
伺服器端 XSLTtrue完整 HTML (~2MB)~2 MB (6.7x)高

結論:大量節點務必啟用客戶端生成,利用瀏覽器原生 XSLT 引擎分擔渲染壓力。

部署檢查清單

  1. 編譯輸出 DLL 並加入專案參考
  2. 工具箱右鍵「選擇項目」→ 瀏覽 DLL → 勾選 SkyTreeViewControl
  3. 將所有 SkyTreeViewControl_*.gif/bmp 圖片複製至 Web 應用程式根目錄
  4. 確認 SkyTreeViewControl.xslt 與 SkyTreeViewControl.bmp 專案屬性為 嵌入資源
  5. ASPX 頁面註冊前綴:<%@ Register TagPrefix="SkyWebControl" Namespace="CS_Discovery" Assembly="SkyTreeViewControl" %>

總結

SkyTreeViewControl 透過XML 資料島 + XSLT 模板 + JavaScript 協同架構,實現了在 IE 環境下的高效能樹狀列表渲染。其核心創新在於:

  1. 雙模式渲染:伺服器/客戶端 XSLT 切換,大數據量走客戶端
  2. 動態載入:XMLSource 機制實現按需載入,首屏極速
  3. 純資料傳輸:XML 比 HTML 小 6 倍以上,頻寬友善
  4. GUID 唯一性:解決多控制項/多 XML 文件 ID 衝突

雖然 XML 資料島為 IE 專屬技術,但「資料與呈現分離、客戶端模板渲染、按需載入」的架構思想,完美映射至現代的 React/Vue 虛擬 DOM、懶載入組件、GraphQL 精確查詢。這個 2008 年的控制項,實際上預演了前端工程化的核心範式。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved