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

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

我在上一篇文章說明了命名容器定義的階層,焦點放在「關係」,這一篇將著眼於「時機」。記得前文提到,資料繫結控制項的第二個特徵是具有樣板功能,意思是說能做到 CRUD 模式切換,依需求改變配置 (UI) 並填入內容 (資料),輕鬆建立資料異動程式。當然啦,內建的樣版主要用意也是針對異動所需,例如 ItemTemplate 是提供一般顯示資料用,EditItemTemplate 是修改資料的介面,InsertItemTemplate 則是新增資料用的,其它還有 HeaderTemplate、AlternatingItemTemplate、FooterTemplate、PagerTemplate…等樣板,但跟資料異動較無關係就是了。

至於樣板功能是怎麼運作的,我用一個範例來解釋,先在 DataList 配置 ItemTemplate、EditItemTemplate,然後利用前一篇的工具程式 DrillDownControl 來看看模式切換時有甚麼變化:
--- aspx ---
<div>
    <hr />
    <asp:DataList ID="MyDataList" runat="server" OnItemDataBound="MyDataList_ItemDataBound"
        OnItemCommand="MyDataList_ItemCommand" DataKeyField="Key">
        <ItemStyle BackColor="Silver" />
        <AlternatingItemStyle BackColor="White" />
        <SelectedItemStyle BackColor="LightBlue" />
        <ItemTemplate>
            <asp:Button ID="SelectButton" runat="server" Text=" 選取 " CommandName="Select" />
            <asp:Button ID="EditButton" runat="server" Text=" 編輯 " CommandName="Edit" />
            <asp:Label ID="DataLabel" Text='<%# Eval("Name") %>' runat="server" />
            (Container:
            <asp:Label ID="MyLabel" Text="<%# Container.ToString() %>" runat="server" />)
        </ItemTemplate>
        <AlternatingItemTemplate>
            <asp:Button ID="SelectButton" runat="server" Text=" 選取 " CommandName="Select" />
            <asp:Button ID="EditButton" runat="server" Text=" 編輯 " CommandName="Edit" />
            <asp:Label ID="DataLabel" Text='<%# Eval("Name") %>' runat="server" />
            (Container:
            <asp:Label ID="MyLabel" Text="<%# Container.ToString() %>" runat="server" />)
        </AlternatingItemTemplate>
        <SelectedItemTemplate>
            <asp:Button ID="SelectButton" runat="server" Text=" 選取 " CommandName="Select" />
            <asp:Button ID="EditButton" runat="server" Text=" 編輯 " CommandName="Edit" />
            <asp:Label ID="DataLabel" Text='<%# Eval("Name") %>' runat="server" />
            (Container:
            <asp:Label ID="MyLabel" Text="<%# Container.ToString() %>" runat="server" />)
        </SelectedItemTemplate>
        <EditItemTemplate>
            <asp:Button ID="CancelButton" runat="server" Text=" 取消 " CommandName="Cancel" />
            <asp:Button ID="UpdateButton" runat="server" Text=" 更新 " CommandName="Update" />
            <asp:TextBox ID="NameTextBox" Text='<%# Eval("Name") %>' runat="server"></asp:TextBox>
            (Container:
            <asp:Label ID="MyLabel" Text="<%# Container.ToString() %>" runat="server" />)
        </EditItemTemplate>
    </asp:DataList>
</div>
</form>

--- aspx.cs ---
{
    // 鑽探控制項階層
    Utility.DrillDownCtrl(Page, false);
}

protected void MyDataList_ItemCommand(object source, DataListCommandEventArgs e)
{
    DataListItem currentItem = e.Item;

    switch (e.CommandName)
    {
        case ("Select"):
            MyDataList.SelectedIndex = currentItem.ItemIndex;
            break;
        case ("Edit"):
            MyDataList.EditItemIndex = currentItem.ItemIndex;
            break;
        case ("Cancel"):
            MyDataList.EditItemIndex = -1;
            break;
        case ("Update"):
            // 在編輯模式下抓取 NameTextBox
            if (currentItem.ItemType == ListItemType.EditItem)
            {
                int key = (int)MyDataList.DataKeys[currentItem.ItemIndex];
                string name = ((TextBox)currentItem.FindControl("NameTextBox")).Text;

                ClientScript.RegisterStartupScript(
                    GetType(),
                    "ItemCommand_Context",
                    String.Format(
                        "alert('即將更新資料項 - Key:{0}, Name:{1}');",
                        key,
                        name),
                    true);
            }
            break;
    }

    // 繫結資料
    BindData();
}

執行效果如下:

DataList_01
DataList_02

在第一筆資料點了「編輯」按鈕,後端程式碼在 DataList.ItemCommand 事件底下設定 DataList.EditItemIndex 屬性值,這樣第一筆資料就會切換為編輯模式,接著觀察輸出的階層可以看到以同一筆資料而言,同一時間只會使用一個樣板,而目前是用哪一個樣板呈現,取決於該筆資料處於何種模式。這樣的做動方式 ASP.NET 都已經幫你包好了,只須給定正確的條件,資料繫結控制項就會切換到對應模式,果然簡單!

看起來簡單輕鬆至極,但實際上動手寫卻仍不免有一些問題,接下來從幾個網路上常有人問的問題來挖掘其他需要注意的細節:
  • Q1. 選取 GridView 的某一列之後,如何抓取想要的欄位或主鍵?
    >>> 解法不只一種,GridView 每一列 (row) 都會對應到資料來源的一筆資料錄 (record),自成一個命名容器 (NamingContainer),更是同時切換樣板的單位。假設我們要達到下圖的效果:

    DataBoundCtrls_02

    在步驟 2 可以這樣寫 (實際應用時兩個事件擇一即可,在此是為說明之故):
    {
        // 由 UpdateButton 為起點抓取其他值;NamingContainer 屬性可找到上層命名容器
        GridViewRow currentRow = (GridViewRow)((Button)sender).NamingContainer;
    
        if (currentRow.RowType == DataControlRowType.DataRow
            && (currentRow.RowState & DataControlRowState.Edit) > 0
            )
        {
            // 在「更新」按鈕若加上 CommandArgument='<%# Eval("Key") %>'
            // 就可以用底下方式取得 Key 值。
            int key = Convert.ToInt32(((Button)sender).CommandArgument);
    
            // 在 GridView 若加上 DataKeyNames="Key" 
            // 就可以用底下方式取得 Key 值。
            //int key = (int)MyGridView.DataKeys[currentRow.RowIndex]["Key"];
    
            string name = ((TextBox)currentRow.FindControl("NameTextBox")).Text;
            string ticker = ((TextBox)currentRow.FindControl("TickerTextBox")).Text;
    
            ClientScript.RegisterStartupScript(
            GetType(),
            "_PreUpdate_Row_Contex",
            String.Format("alert('即將更新資料列 - Key:{0}, Name:{1}, Ticker:{2}。(送出更新)');", key, name, ticker),
            true);
        }
    }
    
    protected void MyGridView_RowUpdating(object sender, GridViewUpdateEventArgs e)
    {
        // GridViewUpdateEventArgs 直接可取得要更新的資料列索引
        GridViewRow currentRow = MyGridView.Rows[e.RowIndex];
    
        if (currentRow.RowType == DataControlRowType.DataRow
            && (currentRow.RowState & DataControlRowState.Edit) > 0
                )
        {
            int key = (int)MyGridView.DataKeys[currentRow.RowIndex]["Key"];
            string name = ((TextBox)currentRow.FindControl("NameTextBox")).Text;
            string ticker = ((TextBox)currentRow.FindControl("TickerTextBox")).Text;
    
            ClientScript.RegisterStartupScript(
            GetType(),
            "_Updating_Row_Context",
            String.Format("alert('即將更新資料列 - Key:{0}, Name:{1}, Ticker:{2}。(僅模擬)');", key, name, ticker),
            true);
        }
    }

    程式碼幾乎一模一樣,在不同的事件下處理會有不同的觸發者,惟出發點雖不同,只要了解「相對關係」以及「出現時機」,想找到要抓取的值也是一塊蛋糕而已 (一個衍生問題考考大家,你知道以上兩個事件執行的先後順序嗎?知道的話對於模式間的切換應當非常熟悉囉!)。提醒一下,DataControlRowType 列舉值可以排除 Header、Footer 等不是真正資料繫結的資料列,DataControlRowState 列舉值在本例則用來限定編輯狀態才去抓編輯樣板底下的控制項 (註1)。
  • Q2. FormView 底下的控制項該如何取值?(注意:DetailsView 做法一模一樣)
    >>> 用相同的概念理解,先來看看 FormView 的階層在模式切換時怎麼變化:

    FormView_01
    FormView_02

    很清楚了,FormView 本身是一個命名容器且是切換樣板的單位,所以要插入資料時我們可以這樣取值 (同樣二擇一即可):
    {
        string name = ((TextBox)MyFormView.FindControl("NameTextBox")).Text;
        string ticker = ((TextBox)MyFormView.FindControl("TickerTextBox")).Text;
    
        ClientScript.RegisterStartupScript(
                GetType(),
                "_InsertButton_Submit_Context",
                String.Format(
                    "alert('模式:{0}, 即將插入資料項 - Name:{1}, Ticker:{2}。(InsertButton.Click Event)');",
                    MyFormView.CurrentMode,
                    name,
                    ticker),
                true);
    }
    
    protected void MyFormView_ItemInserting(object sender, FormViewInsertEventArgs e)
    {
        string name = ((TextBox)MyFormView.FindControl("NameTextBox")).Text;
        string ticker = ((TextBox)MyFormView.FindControl("TickerTextBox")).Text;
    
        ClientScript.RegisterStartupScript(
                GetType(),
                "_Item_Inserting",
                String.Format(
                    "alert('模式:{0}, 即將插入資料項 - Name:{1}, Ticker:{2}。(FormView.ItemInserting Event)');",
                    MyFormView.CurrentMode,
                    name,
                    ticker),
                true);
    }
  • Q3. ListView 底下的控制項該如何取值?
    >>> ListView 是 ASP.NET 3.5 才出現的,越晚出的功能當然就越強大(註2),好在利用文章所附工具程式,你一樣可以仔細探詢其結構及運行模式:

    ListView_01
    ListView_02

    看得出來 ListViewDataItem 是我們要的命名容器,更新時的取值動作可以寫在 ListView.ItemCommand 事件底下:
    {
        ListViewItem currentItem = e.Item;
    
        // 限定為資料項目 (ListViewItem.DataItem)
        if (currentItem.ItemType == ListViewItemType.DataItem)
        {
            // 在更新動作才處理
            if (e.CommandName == "Update")
            {
                ListViewDataItem currentDataItem = (ListViewDataItem)currentItem;
    
                int key = (int)MyListView.DataKeys[currentDataItem.DisplayIndex]["Key"];
                string name = ((TextBox)currentDataItem.FindControl("NameTextBox")).Text;
                string ticker = ((TextBox)currentDataItem.FindControl("TickerTextBox")).Text;
    
                ClientScript.RegisterStartupScript(
                        GetType(),
                        "ItemCommand_Context",
                        String.Format(
                            "alert('即將更新資料項 - Key:{0}, Name:{1}, Ticker:{2}(ItemCommand Event - Update)');",
                            key,
                            name,
                            ticker),
                        true);
            }
        }
    }

    這只是其中一種方式,ListView 也有更新動作專用的 ItemUpdating 事件或者你想自行實作 UpdateButton.Click 事件也 OK,端看自己的喜好。

差不多了…兩篇系列文章我試著用自己的理解方式,來向各位說明抓取資料繫結控制項裡的子控制項時,該有甚麼樣的概念,說穿了其實沒有甚麼高深的技巧,就只是「東西在哪?」、「甚麼時候出現?」罷了…,實際應用的時候多半會比我舉的例子繁複,但把握這兩個原則,基本上問題就解決了一半了,剩下的只是怎麼去拆解而已。有鑑於初學者大多很難對這些概念有全盤了解,有時候工作一多,就只是提槍快跑,衝就對了,不會有多餘時間去摸索,希望透過這兩篇系列文章可以讓還卡在這裡的人省掉錯誤嘗試的時間。文末附上完整專案檔,裡面除了系列文章貼過的程式碼以外,包含了所有資料繫結控制項的階層結構、模式切換展示,有需要的人可以下載回去測試。

DataBoundCtrls.rar


系列文章:

備註:
  1. 這兩個列舉值條件在 UpdateButton.Click、MyGridView.RowUpdating 底下可以省略,原因是以此例而言,兩個事件發生時資料列類型一定是 DataRow,狀態一定處於編輯模式。然而特別寫出這樣的程式碼是要大家注意,在其他事件底下 (例如:MyGridView.RowDataBound) 不下這兩個條件的話可能就會擲回錯誤。
  2. ListView 是一功能強大且非常有彈性的控制項,其全面性的功能足以做到既有資料繫結控制項所能做到的,甚至可以取代 ASP.NET 中的其他所有的資料繫結控制項,詳細介紹可以參考:MSDN Magazine - Extreme ASP.NET:全能的資料繫結控制項


參考資料: