[ASP .NET] GridView 進階資料來源

開發人員可以直接從工具列中拉一個在「資料」標籤下的SqlDataSource或其他來源到網頁中,並點選GridView,在屬性中設定資料來源的控制項,但有時候我們不希望資料來源如此的死,倘若今天我們設計的是提供篩選的GridView如上圖左方的搜尋行程,使用者可以使用and的條件進階搜尋資料,此時,要動態組合SQL Command再開SqlConnection其實也有點危險,當公司組織沒有設定View來限制存取或您擁有特殊權限時,有時一個不小心就把資料給怎麼了也不一定,另外是當開發人員要新增功能時,應要避免修改已經測試過的程式碼來符合開發人員的需要,否則會蹦出一些突如其來的Bug,那如果要從現有資料來源加上一些篩選後,再動態繫結到GridView中,這樣不僅能保護資料來源,對程式開發也提供足夠的彈性

叮嚀:

  1. 在閱讀本篇之前,應先有 Page Life Cycle的概念,可以參考本篇文章-[ASP.NET]Page Life Cycle整理
  2. 要搭配GridView使用過繼承DataSourceControl的控制項(例:SqlDataSource)

 

ASP .NET不論是在2.0版、3.5版或4.0版都有個很簡易且很好的資料顯示工具-「GridView」。GridView的顯示方式真的是多采多姿,簡易的GridView可以表現出下圖這種樣式:

image

如果稍作修改的話,也可以顯示出以下兩種結果:(兩個GridView的組合)

imageimage

不外乎這些都必須要先有資料來源,才得以顯示這些資料,這邊不討論該怎麼使用SqlDataSource,這篇的重點就放在如何動態指定資料來源並繫結這些資料。那接著我們進入主題:

如果要動態繫結資料來源,就小弟目前所知,可以透過幾種方式:

  1. 將GridView中的DataSourceID設定為您要使用的繼承DataSourceControl的控制項,並在code中指定參數的值(參數的來源可以從網址、控制項…)
  2. 將GridView安置在網頁中,並在code中設定GridView的DataSource
  3. 在網頁中放多個繼承DataSourceControl的控制項,並在code中動態更換GridView的DataSourceID(不建議)

第一種方式比較容易實作,因為開發人員可以直接從工具列中拉一個在「資料」標籤下的SqlDataSource或其他來源到網頁中,並點選GridView,在屬性中設定資料來源的控制項,但有時候我們不希望資料來源如此的死,倘若今天我們設計的是提供篩選的GridView如上圖左方的搜尋行程,使用者可以使用and的條件進階搜尋資料,此時,要動態組合SQL Command再開SqlConnection其實也有點危險,當公司組織沒有設定View來限制存取或您擁有特殊權限時,有時一個不小心就把資料給怎麼了也不一定,另外是當開發人員要新增功能時,應要避免修改已經測試過的程式碼來符合開發人員的需要,否則會蹦出一些突如其來的Bug,那如果要從現有資料來源加上一些篩選後,再動態繫結到GridView中,這樣不僅能保護資料來源,對程式開發也提供足夠的彈性,是不是非常方便呢?

因此,我們想要自己設定資料來源,再經過特定的篩選,最後再用GridView顯示結果,步驟如下:

  • 模擬資料來源

為了要能夠教學,這邊用struct的List來示範,先建立下方程式碼


 /// <summary>
    /// 訂單項目資料
    /// </summary>
    public struct OrderItem
    {
        /// <summary>
        /// 設定或取得訂單編號
        /// </summary>
        public Guid OrderID { set; get; }
        /// <summary>
        /// 設定或取得產品ID
        /// </summary>
        public Guid ItemID { set; get; }
        /// <summary>
        /// 設定或取得購買數量
        /// </summary>
        public int Count { set; get; }
        /// <summary>
        /// 設定或取得產品名稱
        /// </summary>
        public string ItemName { set; get; }
    }

 


/// <summary>
    /// 處理訂單項目的資料類別
    /// </summary>
    public class OrderItemData
    {
        /// <summary>
        /// 取得模擬資料來源
        /// </summary>
        public List<OrderItem> DataSource
        {
            get
            {
                Guid itemId_1 = Guid.NewGuid(), itemId_2 = Guid.NewGuid(), itemId_3 = Guid.NewGuid(), itemId_4 = Guid.NewGuid();//隨機建立四個產品Id

                List<OrderItem> result = new List<OrderItem>();//結果
                Guid orderId_1 = new Guid("CB843080-755C-4F42-AE2B-A79DE62F503E");//第一筆訂單編號
                result.Add(new OrderItem() { OrderID = orderId_1, ItemID = itemId_1, ItemName = "腳踏車", Count = 2 });
                result.Add(new OrderItem() { OrderID = orderId_1, ItemID = itemId_2, ItemName = "暖氣", Count = 5 });
                result.Add(new OrderItem() { OrderID = orderId_1, ItemID = itemId_3, ItemName = "肥皂", Count = 10 });

                Guid orderId_2 = new Guid("FE390D81-BDF9-49C8-BD26-C51043FB6F0C");//第二筆訂單編號
                result.Add(new OrderItem() { OrderID = orderId_2, ItemID = itemId_2, ItemName = "暖氣", Count = 1 });
                result.Add(new OrderItem() { OrderID = orderId_2, ItemID = itemId_3, ItemName = "肥皂", Count = 7 });
                result.Add(new OrderItem() { OrderID = orderId_2, ItemID = itemId_4, ItemName = "滑鼠", Count = 3 });

                return result;
            }
        }
    }

由上述程式碼得知,若要得到資料來源,應使用下列方式取得


List<OrderItem> dataSource = new OrderItemData().DataSource;
  • 動態繫結到GridView中並顯示之

如果要將上述結果顯示到GridView中,最簡單的方式就是使用以下程式碼:


  <asp:GridView ID="GridView1" runat="server">
        </asp:GridView>

  protected void Page_Load(object sender, EventArgs e)
        {
            List<OrderItem> dataSource = new OrderItemData().DataSource;
            this.GridView1.DataSource = dataSource;
            this.GridView1.DataBind();
        }

這樣第一步就完成了,接著就是動態繫結資料,當資料來源無法變更時,又得下條件陳述式時,小弟會比較建議使用LINQ來做,舉例來說,我們目前只想要顯示訂單編號為 「FE390D81-BDF9-49C8-BD26-C51043FB6F0C」的資料,接著用GridView顯示結果,我們修改上述程式碼,成為以下所示


protected void Page_Load(object sender, EventArgs e)
        {
            List<OrderItem> dataSource = new OrderItemData().DataSource;
            //記得 using System.Linq;
            this.GridView1.DataSource = dataSource.Where(t => t.OrderID.Equals(new Guid("FE390D81-BDF9-49C8-BD26-C51043FB6F0C")));
            this.GridView1.DataBind();
        }

結果如下圖

image

如果把訂單編號都寫死在程式碼中,未免也太不人性化了,所以,我們希望能用一個DropDownList來顯示所有訂單,並顯示選取任一訂單的結果,先決條件是我們得知道資料來源中有那些訂單編號,接著才能在DropDownList中顯示,所以我們先寫出第一版本的程式碼


   <asp:DropDownList ID="Ddl_OrderId" runat="server" AutoPostBack="true">
        </asp:DropDownList>
        <br />
        <asp:GridView ID="GridView1" runat="server">
        </asp:GridView>

 


 protected void Page_Load(object sender, EventArgs e)
        {
            List<OrderItem> dataSource = new OrderItemData().DataSource;
            //記得 using System.Linq;
            if (!IsPostBack)
            {
                //因為沒有將DropDownList的EnableViewState設定為false,為了不要讓Item累加,因此只需在非控制項造成PostBack時加入資料
                foreach (Guid orderId in dataSource.Select(t => t.OrderID).Distinct())
                {
                    this.Ddl_OrderId.Items.Add(new ListItem(orderId.ToString("D")));
                }
            }
            this.GridView1.DataSource = dataSource.Where(t => t.OrderID.Equals(new Guid(this.Ddl_OrderId.SelectedValue)));
            this.GridView1.DataBind();

        }

以上程式碼將在使用者選擇任一個選項時PostBack並顯示該筆訂單的詳細資料,結果如下所示

image

如果想要選擇某筆資料,然後顯示OrderID在頁面中,修改一下程式碼後成為本結果


  <asp:DropDownList ID="Ddl_OrderId" runat="server" AutoPostBack="true">
        </asp:DropDownList>
        <br />
        <asp:GridView ID="GridView1" runat="server" DataKeyNames="OrderID,ItemID">
            <Columns>
                <asp:TemplateField HeaderText="選擇">
                    <ItemTemplate>
                      <asp:Button runat="server" ID="Btn_Select" Text="選擇" onclick="Btn_Select_Click" />
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>
        <asp:Label ID="Lbl_OrderID" runat="server"></asp:Label>

protected void Page_Load(object sender, EventArgs e)
        {
            List<OrderItem> dataSource = new OrderItemData().DataSource;
            //記得 using System.Linq;
            if (!IsPostBack)
            {
                //因為沒有將DropDownList的EnableViewState設定為false,為了不要讓Item累加,因此只需在非控制項造成PostBack時加入資料
                foreach (Guid orderId in dataSource.Select(t => t.OrderID).Distinct())
                {
                    this.Ddl_OrderId.Items.Add(new ListItem(orderId.ToString("D")));
                }
            }
            this.GridView1.DataSource = dataSource.Where(t => t.OrderID.Equals(new Guid(this.Ddl_OrderId.SelectedValue)));
            this.GridView1.DataBind();

        }

        protected void Btn_Select_Click(object sender, EventArgs e)
        {
            //從Label顯示選取的OrderID
            this.Lbl_ItemID.Text = this.GridView1.DataKeys[((sender as Button).NamingContainer as GridViewRow).RowIndex].Values["OrderID"].ToString();
        }

這樣看起來都沒什麼問題,但執行且按下"選擇"時卻出現以下錯誤:

無效的回傳或回呼引數。已在組態中使用 <pages enableEventValidation="true"/> 或在網頁中使用 <%@ Page EnableEventValidation="true" %> 啟用事件驗證。基於安全性理由,這項功能驗證回傳或回呼引數是來自原本呈現它們的伺服器控制項。如果資料為有效並且是必須的,請使用 ClientScriptManager.RegisterForEventValidation 方法註冊回傳或回呼資料,以進行驗證。

image

任何PostBack的事件要執行時都是在Page_Load之後,Page_PreRender之前,並且控制項在處理控制項事件時必須要有相關聯的控制項存在於頁面之中,但是上述程式碼已經在Page_Load時呼叫了GridView的DataBind(),因此,可以合理想像,要執行事件的控制項已經不存在了,但PostBack的事件仍然需要處理,此時,問題就出現了: 找不到要執行事件的控制項,基於安全性理由,駭客很有可能使用某攻擊手法(如XSS)要伺服器執行某事件,因此事件錯誤會被攔截下來。所以直覺上的解決方案是要在Button被取代之前先執行事件,所以會有下兩種解決方案:一種就是在執行完後才繫結,另一種是需要才繫結。要使用第一種解決方案,只需將DataBind()呼叫改到PreRender即可,也就是把剛剛的程式碼改成如下


protected void Page_PreRender(object sender, EventArgs e)
        {
            List<OrderItem> dataSource = new OrderItemData().DataSource;
            //記得 using System.Linq;
            if (!IsPostBack)
            {
//以下同上述程式碼

這樣看起來運作得很好,但要注意的是,當頁面上有許多控制項時,就算是沒有與GridView相關聯的控制項觸發PostBack,每次都會做DataBind(),無疑會對效能造成些許影響,那麼如果能在需要時再繫結,也會比較好一點,又因為不確定何時會取得資料,所以我們就會在GridView要繫結資料時提供資料來源給該控制項,那也可以聯想到應該就是在GridView中的DataBinding事件,換句話說,在任何時間點呼叫GridView1.DataBind()時,都一定會呼叫DataBinding,如此一來,就可以在需要時提供資料給GridView。接著,我們就把程式碼修改後如下


 <asp:DropDownList ID="Ddl_OrderId" runat="server" AutoPostBack="true" OnSelectedIndexChanged="Ddl_OrderId_SelectedIndexChanged"
            OnDataBinding="Ddl_OrderId_DataBinding">
        </asp:DropDownList>
        <br />
        <asp:GridView ID="GridView1" runat="server" DataKeyNames="OrderID,ItemID" OnDataBinding="GridView1_DataBinding">
            <Columns>
                <asp:TemplateField HeaderText="選擇">
                    <ItemTemplate>
                        <asp:Button runat="server" ID="Btn_Select" Text="選擇" OnClick="Btn_Select_Click" />
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>
        <asp:Label ID="Lbl_OrderId" runat="server"></asp:Label>

 

 


protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                //第一次執行才要做,其他狀況下適時操作
                this.Ddl_OrderId.DataBind();
                this.GridView1.DataBind();
            }

        }
  
        protected void GridView1_DataBinding(object sender, EventArgs e)
        {
            (sender as GridView).DataSource = new OrderItemData().DataSource
                .Where(t => t.OrderID.Equals(new Guid(this.Ddl_OrderId.SelectedValue)));
        }

        protected void Ddl_OrderId_DataBinding(object sender, EventArgs e)
        {
            //每次重新繫結時要先清空內容
            (sender as DropDownList).Items.Clear();
            List<OrderItem> dataSource = new OrderItemData().DataSource;
            foreach (Guid orderId in dataSource.Select(t => t.OrderID).Distinct())
            {
                this.Ddl_OrderId.Items.Add(new ListItem(orderId.ToString("D")));
            }
        }

        protected void Ddl_OrderId_SelectedIndexChanged(object sender, EventArgs e)
        {
            //因為不是每次PostBack都會做DataBind(),故當選取其他訂單時,才需要重新繫結資料。
            this.GridView1.DataBind();
        }

        protected void Btn_Select_Click(object sender, EventArgs e)
        {
            //從Label顯示選取的OrderID
            this.Lbl_OrderId.Text = this.GridView1.DataKeys[((sender as Button).NamingContainer as GridViewRow).RowIndex].Values["OrderID"].ToString();
        }

 

對於上述程式碼,我們將資料來源都設定為需要的時候才讀取,這樣對於效能以及程式邏輯上,是不是感覺比較好了呢?當然,還要考慮到刪除資料要重新繫結DropDownList的狀況等,從程式控制流程的彈性雖然很高,但要想的事情也相對的多,這篇也只是介紹如何使用這個技巧,其他的就要靠大家聰明的頭腦囉。

看完這篇,相信大家在動態繫結資料時,對於動態篩選資料也比較不陌生囉,若有任何不懂以及其他問題,也歡迎大家不吝指教。

範例程式碼載點: Download