搜尋此網誌

2024年4月1日 星期一

ASP GridView 動態加入、刪除 Row,且維持使用者的輸入

緣起:


    3/26 那時不少的時間在弄文章標題說的事情,雖然在功能做到 70% 時才發現不需要這樣搞 (花了 7 小時),但是這個過程讓我又對 GridView 有更多的了解,所以覺得很有必要寫下來。

    話說,公司那時在做稽查,我負責的專案有被抽查,還好都沒事,聽說如果被記警告的話,那些獎金好像都會沒了。


RowDataBound:


    先講這個事件,之前我的這篇文章有談到,那時候的資料來源是 DataTable,然後在 RowDataBound 裡用 e.Row.Cells[index].Text 來拿值。我這次是要用 List<自訂Class> 來當資料來源,然後將資料綁到對應的控制項。

    前台程式

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Test.aspx.cs" Inherits="GridViewTest.Test" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:GridView runat="server" ID="myGridView" AutoGenerateColumns="false" OnRowDataBound="myGridView_RowDataBound">
                <Columns>
                    <asp:TemplateField HeaderText="姓名">
                        <ItemTemplate>
                            <asp:TextBox runat="server" ID="txtName"></asp:TextBox>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="性別">
                        <ItemTemplate>
                            <asp:DropDownList runat="server" ID="ddlGender">
                                <asp:ListItem Selected="True" Value="男">男</asp:ListItem>
                                <asp:ListItem Value="女">女</asp:ListItem>
                            </asp:DropDownList>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="學號">
                        <ItemTemplate>
                            <asp:TextBox runat="server" ID="txtID"></asp:TextBox>
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </asp:GridView>
        </div>
    </form>
</body>
</html>

    GridView 有三個 Column,第一個跟第三個是 TextBox,第二個是一個 DropDownList,性別的選項,有男跟女。

    再來是後台的程式

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace GridViewTest
{
    public partial class Test : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                List<MyData> listMyData = new List<MyData>();
                listMyData.Add(new MyData(123, "薯泥", GenderEnum.女));
                listMyData.Add(new MyData(456, "鳥旭", GenderEnum.男));
                listMyData.Add(new MyData(789, "阿祈", GenderEnum.男));
                listMyData.Add(new MyData(101112, "肥伶", GenderEnum.女));

                myGridView.DataSource = listMyData;
                myGridView.DataBind();
            }
        }

        protected void myGridView_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            if (e.Row.RowType != DataControlRowType.DataRow) return;
            TextBox txtName = (TextBox)e.Row.FindControl("txtName");
            TextBox txtID = (TextBox)e.Row.FindControl("txtID");
            DropDownList ddlGender = (DropDownList)e.Row.FindControl("ddlGender");

            txtName.Text = (string)DataBinder.Eval(e.Row.DataItem, "Name");
            txtID.Text = ((int)DataBinder.Eval(e.Row.DataItem, "ID")).ToString();
            ddlGender.SelectedValue = ((GenderEnum)DataBinder.Eval(e.Row.DataItem, "Gender")).ToString();
        }

        public class MyData
        {
            public int ID { get; set; }
            public string Name { get; set; }
            public GenderEnum Gender { get; set; }

            public MyData(int iD, string name, GenderEnum gender)
            {
                ID = iD;
                Name = name;
                Gender = gender;
            }
        }

        public enum GenderEnum
        {
            男,
            女
        }
    }
}

    30~51行 : 我們的資料 Class。
    53~57行 : 為下拉選單設計的 Enum。
    14~24行 : 模擬從資料庫撈出資料,用我們自己寫的 MyData 的 Constructor 來建立每個資料,並將它放進 List 中,最後指定 myGridView 的 DataSource,再 DataBind。
    30~32行 : 在 RowDataBind 事件裡,從 .e.Row 裡用 FindControl 找出每個控制項。
    34~36行 : 用 DataBinder.Eval 來取得資料,第一個傳入的項目是 e.Row.DataItem,第二個是 MyData Class 的欄位名稱,要注意資料型態的轉換。

    最後呈現的結果會是這樣


    TextBox 就單純指定它的 Text 而已,所以其實也可以在前台簡單做,用 Text='<%# Eval("欄位名稱")%>,也可以達到同樣的效果。


加入新一列:


    我在前台加入一個按鈕,使用者點擊後,GridView 會在最下方加入新的一列。為了在 PostBack 後還保留每列的使用者輸入,所以需要把 GridView 每個 Row 的控制項的值給抓出來

   前台

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Test.aspx.cs" Inherits="GridViewTest.Test" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:Button runat="server" ID="btnAddRow" Text="新增資料" OnClick="btnAddRow_Click"/><br />
            <br />
            <asp:GridView runat="server" ID="myGridView" AutoGenerateColumns="false" OnRowDataBound="myGridView_RowDataBound">
                <Columns>
                    <asp:TemplateField HeaderText="姓名">
                        <ItemTemplate>
                            <asp:TextBox runat="server" ID="txtName"></asp:TextBox>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="性別">
                        <ItemTemplate>
                            <asp:DropDownList runat="server" ID="ddlGender">
                                <asp:ListItem Selected="True" Value="男">男</asp:ListItem>
                                <asp:ListItem Value="女">女</asp:ListItem>
                            </asp:DropDownList>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="學號">
                        <ItemTemplate>
                            <asp:TextBox runat="server" ID="txtID"></asp:TextBox>
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </asp:GridView>
        </div>
    </form>
</body>
</html>

    13行 : 加入一個 Button

    再來是後台程式碼

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace GridViewTest
{
    public partial class Test : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                List<MyData> listMyData = new List<MyData>();
                listMyData.Add(new MyData(123, "薯泥", GenderEnum.女));
                listMyData.Add(new MyData(456, "鳥旭", GenderEnum.男));
                listMyData.Add(new MyData(789, "阿祈", GenderEnum.男));
                listMyData.Add(new MyData(101112, "肥伶", GenderEnum.女));

                myGridView.DataSource = listMyData;
                myGridView.DataBind();
            }
        }

        protected void myGridView_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            if (e.Row.RowType != DataControlRowType.DataRow) return;
            TextBox txtName = (TextBox)e.Row.FindControl("txtName");
            TextBox txtID = (TextBox)e.Row.FindControl("txtID");
            DropDownList ddlGender = (DropDownList)e.Row.FindControl("ddlGender");

            txtName.Text = (string)DataBinder.Eval(e.Row.DataItem, "Name");
            txtID.Text = ((int)DataBinder.Eval(e.Row.DataItem, "ID")).ToString();
            ddlGender.SelectedValue = ((GenderEnum)DataBinder.Eval(e.Row.DataItem, "Gender")).ToString();
        }

        protected void btnAddRow_Click(object sender, EventArgs e)
        {
            List<MyData> listMyData = _RetrieveData();
            listMyData.Add(new MyData(1, "", GenderEnum.男));
            myGridView.DataSource = listMyData;
            myGridView.DataBind();
        }

        private List<MyData> _RetrieveData()
        {
            List<MyData> listReturn = new List<MyData>();
            foreach (GridViewRow gridViewRow in myGridView.Rows)
            {
                TextBox txtName = (TextBox)gridViewRow.FindControl("txtName");
                TextBox txtID = (TextBox)gridViewRow.FindControl("txtID");
                DropDownList ddlGender = (DropDownList)gridViewRow.FindControl("ddlGender");

                string sName = txtName.Text;
                int iID = 0;
                int.TryParse(txtID.Text, out iID);
                GenderEnum gender;
                Enum.TryParse(ddlGender.SelectedValue, out gender);

                listReturn.Add(new MyData(iID, sName, gender));
            }

            return listReturn;
        }

        public class MyData
        {
            public int ID { get; set; }
            public string Name { get; set; }
            public GenderEnum Gender { get; set; }

            public MyData(int iD, string name, GenderEnum gender)
            {
                ID = iD;
                Name = name;
                Gender = gender;
            }
        }

        public enum GenderEnum
        {
            男,
            女
        }
    }
}

    47~66行 : 將 myGridView 的每列控制項所存的值取出,存回 MyData class,最後回傳 List<MyData>。
    50行 : myGridView.Rows 取得所有的 Row,用 GridViewRow 來遊歷每一列。
    52~54行 : 呼叫 GridViewRow 的 findControl 來找到每個控制項。
    56~60行 : 將每個控制項的值取出,存到變數中。Enum 可以用 Enum.TryParse 來解析 String。
    62、65行 : 用記錄的變數來 new 一個 MyData,存到 List 中,最後回傳 List。

    39~45行 : 新增列按鈕的 click 事件。
    41行 : 用我們寫的 _RetrieveData 先把 myGridView 的資料取出。
    42行 : 在 List 最後面加入一個 MyData
    43、44行 : 重新指定 myGridView 的 DataSource
    

    完成後,開啟我們的網頁,點擊按鈕後,它會新增一列出來


    我們改一下那個新增列的值


    然後再點擊 "新增資料" 按鈕


    可以發現,我們剛剛輸入的值不會因為 PostBack 而被刷掉。但如果是按 F5 重整的話,那些新增的列就都會被刷掉了,所以如果是實務的狀況,在點擊按鈕新增一列後,資料庫應該就要插入新資料。


刪除指定列:


    現在,我要在 myGridView 加入一個新的 column,裡面放的是一個 "刪除" 的按鈕。在實做刪除資料前,我要先能夠得知是哪一列的刪除按鈕被點擊,所以開始寫測試的程式碼

    前台程式碼

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Test.aspx.cs" Inherits="GridViewTest.Test" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:Button runat="server" ID="btnAddRow" Text="新增資料" OnClick="btnAddRow_Click"/><br />
            <br />
            <asp:Literal runat="server" ID="litTest"></asp:Literal>
            <br />
            <asp:GridView runat="server" ID="myGridView" AutoGenerateColumns="false" OnRowDataBound="myGridView_RowDataBound" OnRowCommand="myGridView_RowCommand">
                <Columns>
                    <asp:TemplateField HeaderText="姓名">
                        <ItemTemplate>
                            <asp:TextBox runat="server" ID="txtName"></asp:TextBox>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="性別">
                        <ItemTemplate>
                            <asp:DropDownList runat="server" ID="ddlGender">
                                <asp:ListItem Selected="True" Value="男">男</asp:ListItem>
                                <asp:ListItem Value="女">女</asp:ListItem>
                            </asp:DropDownList>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="學號">
                        <ItemTemplate>
                            <asp:TextBox runat="server" ID="txtID"></asp:TextBox>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="功能">
                        <ItemTemplate>
                            <asp:Button runat="server" ID="btnDelete" Text="刪除" CommandName="deleteRow"/>
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </asp:GridView>
        </div>
    </form>
</body>
</html>

    15行 : 加入 "litTest" 的 Literal,用來顯示是哪一列的刪除鈕被點擊。
    17行 : GridView 加入 OnRowCommand 事件。
    37~41行 : 放刪除按鈕的 Column。我注意到,如果 CommandName 叫 "delete" 的話,ASP 會直接幫我們觸發 Delete 的事件,所以我這邊才叫它 "deleteRow"。

    後台程式碼,由於只比上次多了 myGridView_RowCommand 這個方法,所以就單獨截它出來講

protected void myGridView_RowCommand(object sender, GridViewCommandEventArgs e)
{
    if(e.CommandName == "deleteRow")
    {
        Button button = (Button)e.CommandSource;
        GridViewRow gridViewRow = (GridViewRow)button.NamingContainer;

        litTest.Text = gridViewRow.RowIndex.ToString();
    }
}

    3行 : RowCommand 傳進來的是 GridViewCommandEventArgs 型態的參數,比對 CommandName 來選擇要執行的動作。
    5行 : 可以用 CommandSource 來取得事件的觸發者,這裡是 Button。
    6行 : 從這裡學到的,可以用 NamingContainer 來取得它所屬的 GridViewRow。
    8行 : 從 GridViewRow 的 RowIndex 可以得知它是第幾列 (從0開始算)。


    執行程式,點擊第一列的刪除按鈕,可以看到 Literal 顯示 0


    動態新增好幾列之後,點擊那些新增列的刪除鈕,功能也一樣正常。我這邊點的是最後一列的按鈕


    既然能成功取得要刪除的列的 index,那刪除也就簡單了,修改我們的 myGridView_RowCommand 事件,呼叫 myGridView 的 DeleteRow,傳入 gridViewRow.Index     


protected void myGridView_RowCommand(object sender, GridViewCommandEventArgs e)
{
    if(e.CommandName == "deleteRow")
    {
        Button button = (Button)e.CommandSource;
        GridViewRow gridViewRow = (GridViewRow)button.NamingContainer;

        myGridView.DeleteRow(gridViewRow.RowIndex);
    }
}

    但是呢,事情好像沒有我們想像的簡單,在我們開啟網頁,點擊按鈕後,它跳出錯誤。


    沒錯,我們必須處理 RowDeleting 事件,所以,前台程式最後整個寫下來會是這樣

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Test.aspx.cs" Inherits="GridViewTest.Test" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:Button runat="server" ID="btnAddRow" Text="新增資料" OnClick="btnAddRow_Click"/><br />
            <br />
            <asp:Literal runat="server" ID="litTest"></asp:Literal>
            <br />
            <asp:GridView runat="server" ID="myGridView" AutoGenerateColumns="false" OnRowDataBound="myGridView_RowDataBound" OnRowCommand="myGridView_RowCommand" OnRowDeleting="myGridView_RowDeleting">
                <Columns>
                    <asp:TemplateField HeaderText="姓名">
                        <ItemTemplate>
                            <asp:TextBox runat="server" ID="txtName"></asp:TextBox>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="性別">
                        <ItemTemplate>
                            <asp:DropDownList runat="server" ID="ddlGender">
                                <asp:ListItem Selected="True" Value="男">男</asp:ListItem>
                                <asp:ListItem Value="女">女</asp:ListItem>
                            </asp:DropDownList>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="學號">
                        <ItemTemplate>
                            <asp:TextBox runat="server" ID="txtID"></asp:TextBox>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="功能">
                        <ItemTemplate>
                            <asp:Button runat="server" ID="btnDelete" Text="刪除" CommandName="deleteRow"/>
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </asp:GridView>
        </div>
    </form>
</body>
</html>

    17行 : myGridView 加入 OnRowDeleting 事件。跟前次的主要差別是多了 myGridView_RowDeleting 事件


    後台的完整程式碼會是這樣

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace GridViewTest
{
    public partial class Test : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                List<MyData> listMyData = new List<MyData>();
                listMyData.Add(new MyData(123, "薯泥", GenderEnum.女));
                listMyData.Add(new MyData(456, "鳥旭", GenderEnum.男));
                listMyData.Add(new MyData(789, "阿祈", GenderEnum.男));
                listMyData.Add(new MyData(101112, "肥伶", GenderEnum.女));

                myGridView.DataSource = listMyData;
                myGridView.DataBind();
            }
        }

        protected void myGridView_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            if (e.Row.RowType != DataControlRowType.DataRow) return;
            TextBox txtName = (TextBox)e.Row.FindControl("txtName");
            TextBox txtID = (TextBox)e.Row.FindControl("txtID");
            DropDownList ddlGender = (DropDownList)e.Row.FindControl("ddlGender");

            txtName.Text = (string)DataBinder.Eval(e.Row.DataItem, "Name");
            txtID.Text = ((int)DataBinder.Eval(e.Row.DataItem, "ID")).ToString();
            ddlGender.SelectedValue = ((GenderEnum)DataBinder.Eval(e.Row.DataItem, "Gender")).ToString();
        }

        protected void btnAddRow_Click(object sender, EventArgs e)
        {
            List<MyData> listMyData = _RetrieveData();
            listMyData.Add(new MyData(1, "", GenderEnum.男));
            myGridView.DataSource = listMyData;
            myGridView.DataBind();
        }

        private List<MyData> _RetrieveData()
        {
            List<MyData> listReturn = new List<MyData>();
            foreach (GridViewRow gridViewRow in myGridView.Rows)
            {
                TextBox txtName = (TextBox)gridViewRow.FindControl("txtName");
                TextBox txtID = (TextBox)gridViewRow.FindControl("txtID");
                DropDownList ddlGender = (DropDownList)gridViewRow.FindControl("ddlGender");

                string sName = txtName.Text;
                int iID = 0;
                int.TryParse(txtID.Text, out iID);
                GenderEnum gender;
                Enum.TryParse(ddlGender.SelectedValue, out gender);

                listReturn.Add(new MyData(iID, sName, gender));
            }

            return listReturn;
        }

        protected void myGridView_RowCommand(object sender, GridViewCommandEventArgs e)
        {
            if(e.CommandName == "deleteRow")
            {
                Button button = (Button)e.CommandSource;
                GridViewRow gridViewRow = (GridViewRow)button.NamingContainer;

                myGridView.DeleteRow(gridViewRow.RowIndex);
            }
        }
        protected void myGridView_RowDeleting(object sender, GridViewDeleteEventArgs e)
        {
            List<MyData> listMyData = _RetrieveData();
            listMyData.RemoveAt(e.RowIndex);
            myGridView.DataSource = listMyData;
            myGridView.DataBind();
        }

        public class MyData
        {
            public int ID { get; set; }
            public string Name { get; set; }
            public GenderEnum Gender { get; set; }

            public MyData(int iD, string name, GenderEnum gender)
            {
                ID = iD;
                Name = name;
                Gender = gender;
            }
        }

        public enum GenderEnum
        {
            男,
            女
        }
    }
}

    78~84行 : myGridView_RowDeleting 事件。
    80行 : 把 myGridView 現存的資料取出。
    81行 : RowDeleting 會傳入 GridViewDeleteEventArgs 參數,可以從 RowIndex 得知是哪列被刪,搭配 List 的 RemoveAt 方法來移除資料。
    82、83行 : 重新指定 DataSource 並 Bind。


    這樣,之後不管是在新增或刪除列,其它列的資料就不會因為 PostBack 而消失不見。



沒有留言:

張貼留言