ASP.NET 資料繫結控制項裡的子控制項如何準確抓取【上】

ASP.NET 資料繫結控制項裡的子控制項如何準確抓取【上】

常常看到有人問:如何取得 GridView 某一列 TextBox 的值?或為什麼在某某事件底下抓取 DetailsView 裡的 DropDownList 會出現 NullReferenceException?…其他諸如 DataList、FormView、ListView 怎麼使用 FindControl 方法?…等問題,幾乎可以說是新手必問的問題了。當然這一類的問題因為太常碰到,所以隨便在網路上搜尋一下都找得到解法,那麼今天我是來騙收視率的嗎?當然不是!...身為一個可愛小女孩的爸爸,我總希望能多積點陰德回饋社會,盡一點棉薄之力,或許能讓新手少走點冤枉路,多點時間在拯救地球的議題上,或許小女孩 20 年後仍然可以看見活跳跳的北極熊跟企鵝逛大街...(女王說:又不是選舉造勢晚會,幹嘛發動親情攻勢...)

趕緊進入主題好了...。

資料繫結 Web 伺服器控制項的種類與介紹,可以參考 MSDN Library 上的概觀,這邊提出以下兩點特徵:
  1. 都是複合控制項,可以將其他控制項放在其中,更進一步來說都是所謂的命名容器 (NamingContainer),本身且/或底層物件實作 INamingContainer 介面,因此會重新定義子控制項的 ID 命名空間,以確保頁面上所有控制項的 UniqueIDClientID 屬性是唯一值。
  2. 皆具備樣版 (Template) 功能 (但實作細節不盡相同),以提供特定目的、自訂配置之用,這會影響子控制項出現的時機還有型態。
這兩個特徵暫且先有個底,接下來會用實例一一解釋相關細節。

不知道各位是否有過這樣的經驗,新增一個自訂控制項,裡面只配置一個 TextBox1,完成後拉到網頁上,另外在網頁上再配置一個 TextBox1,但執行起來兩者卻不會互相干擾,例如底下的例子:

<%@ Control Language="C#" ClassName="TextBoxWebUserControl" %>

<script runat="server">

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
            TextBox1.Text = TextBox1.ClientID;
    }
</script>

<asp:TextBox ID="TextBox1" runat="server" Width="360px"></asp:TextBox>


<%@ Page Language="C#" %>
<%@ Register Src="TextBoxWebUserControl.ascx" TagName="TextBoxWebUserControl" TagPrefix="uc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            TextBox1.Text = TextBox1.ClientID;
        }
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <hr />
        <asp:TextBox ID="TextBox1" runat="server" Width="360px"></asp:TextBox><br />
        <uc1:TextBoxWebUserControl ID="TextBoxWebUserControl1" runat="server" />
    </div>
    </form>
</body>
</html>

為了明顯區分兩個 TextBox,我分別在 Page.Load 事件將 TextBox.ClientID 指定給自己的 Text 屬性,建置之後執行的效果及原始碼如下截圖:

NamingContainer_01

你發現了嗎?自訂控制項的 id 有一點不一樣,是以 "自訂控制項類別名稱_子控制項ID" 的型式呈現,看起來似乎有一點從屬關係的意味。接下來為了更清楚展示這一點,我寫一個公用類別 Utitly,裡面包含靜態方法 DrillDownCtrl,用來往下遞迴找出指定控制項的階層結構,為了聚焦之故,這個方法只收集 Button、Label、TextBox、DropDownList 幾個常用控制項的命名容器資訊,通常這些控制項也是最底層的子控制項。以下重點列出 DrillDownCtrl 程式碼,並且試著在 Default.aspx 程式碼裡叫用:

/// 鑽探控制項階層結構
/// </summary>
/// <param name="current">目標控制項</param>
public static void DrillDownCtrl(Control current)
{
    HttpContext.Current.Response.Write("<pre>");

    foreach (Control child in current.Controls)
    {
        // 只收集 Button、Label、TextBox、DropDownList 等常用控制項資訊
        if (child.GetType() == typeof(Button)
            || child.GetType() == typeof(Label)
            || child.GetType() == typeof(TextBox)
            || child.GetType() == typeof(DropDownList)
            )
        {
            HttpContext.Current.Response.Write(">Container: <span style='color: blue;'>" + child.NamingContainer.ToString() + "</span><br />");
            HttpContext.Current.Response.Write(String.Format("{0}>Control: ", space) + child.ClientID + "<br />");
        }
        else
        {
            DrillDownCtrl(child);
        }
    }

    HttpContext.Current.Response.Write("</pre>");
}


<%@ Page Language="C#" %>
<%@ Register Src="TextBoxWebUserControl.ascx" TagName="TextBoxWebUserControl" TagPrefix="uc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            TextBox1.Text = TextBox1.ClientID;
        }

        // 截取 Page 階層結構
        Utility.DrillDownCtrl(Page);
    }
</script>
<%-- ...<以下略>... --%>

執行結果請看下圖:

NamingContainer_02

第一個 TextBox1 的命名容器是 _Page,第二個 TextBox1 的命名容器是 TextBoxWebUserControl1。還記得前面說過的第一個特徵嗎?命名容器具有重新定義子控制項 ID 命名空間的特性,MSDN Library 也可以查到 Page 以及 UserControl 類別確實有實作 INamingContainer 介面(註1)。

同理,我們利用相同的邏輯截取 GridView 控制項階層,但這裡加入一個 DrillDownList 方法的多載版本顯示所有實作 INamingContainer 介面的類別,直接來看結果:  

NamingContainer_03

在只有一筆資料的 GridView 裡,你可以看到 GridView 是命名容器,底下包含 Header、DataRow、Footer 三個 GridViewRow 物件,GridViewRow 類別也是命名容器,Header、Footer 不包含我們要的控制項因此略過,DataRow 底下則包含了 EditButton、NameLabel、TickerLabel 三個子控制項。最底層控制項,被賦予的 ClientID 都是以 "MyGridView_ctl02_ControlID" 的型式出現,"MyGridView" 的部分是 GridView 賦予的, "ctl02" 則是 GridViewRow 未指定 ID 時,由 ASP.NET 自動產生的值,加上子控制項本身的 ID,各部分用底線串連起來就是最後產生的唯一識別項 (UniqueID 的產生方式雷同但是用 $ 號串連,UniqueID 與 ClientID 的區別及用途可自行查詢 MSDN 文件)。

到目前為止,我們了解命名容器定義了主要的階層關係(註2),換句話說在同一層命名容器底下的所有控制項,其識別值必定是唯一,這一點很重要!ASP.NET 就是靠這樣的機制來避免頁面上的控制項產生命名衝突,當然你也可以利用這一點來抓取任何你想要的控制項。然而是不是這樣就天下太平,北極熊跟企鵝從此過著幸福快樂的日子?…還差一點,下一篇我們來看看資料繫結控制項的樣板功能如何影響子控制項出現的時機。


系列文章:

備註:
  1. Page、UserControl 類別都繼承自 TemplateControl,該基底類別實作了 INamingContainer。儘管如此,UserControl 還是另外再實作一次 INamingContainer,推測是因為 Page 為網頁階層架構的最上層命名容器,為簡潔之故在 TemplateControl 底下覆寫了定義 ID 命名空間的動作,這樣直接放在 Page 上的控制項 ID 就會簡短一些,也因此 UserControl 必須再度實作 INamingContainer。
  2. 事實上只觀察 NamingContainer 構成的階層架構是簡化過的,從實用的角度來說,這樣就足以鑑別出唯一的控制項;你也可以修改一下上述 DrillDownCtrl 程式邏輯,改由觀察 Control.Parent 屬性取得完整階層從屬關係,你會發現複雜許多。


參考資料: