作者:黃忠成
這篇文章從何來?
在寫【極意之道 - ASP.NET AJAX/Silverlight】一書之前,我曾經動過念頭撰寫一本 ASP.NET 2.0 聖經類型的書籍,也付諸執行了一段時間,完成了近 500 頁的書稿 (500 頁,僅是此書的 3 章....,全書規劃有 15 章),但由於工作上的關係,我終究沒能在 ASP.NET 3.5 推出前完成這一本書,只是將書中的 ASP.NET/Silverlight 部份抽出成為另一本書,但東西寫都寫好了,不將其公諸於世,總覺得對不起她們 (我一直認為,文章在其完成時,即擁有作者所賦與的生命),雖然我可以將其收錄在未來可能撰寫的 ASP.NET 3.5 新書中,但由於近一年內的新書計劃中並沒有排定此書,遂決定將其中較實用的技巧抽出,與各位讀者分享,也算是送給各位長期支持我的讀者們,一份意外的聖誕/新年禮物吧。
漸層光棒
不喜歡 GridView 控件單調的 Header 區、單調的選取光棒嗎?這裡有個小技巧可以讓你的 GridView 控件看起來與眾不同,請先準備兩張圖形。
這種漸層圖形可以用 Photoshop 或 PhotoImpact 輕易做出來,接著將這兩個圖形檔加到專案的 Images 目錄中,左邊取名為 titlebar.gif、右邊取名為 gridselback.gif,然後開啟一個新網頁,組態 SqlDataSource 控件連結到任一資料表,再加入 GridView 控件繫結至此 SqlDataSource 控件,接著將 Enable Selection 打勾,切換至網頁 Source 頁面,加入 CSS 的程式碼。
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="GrandientSelGrid.aspx.cs" Inherits="GrandientSelGrid" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<style type="text/css">
.grid_sel_back
{
background-image :url(Images/gridselback.gif);
background-repeat : repeat-x
}
.title_bar
{
background-image :url(Images/titlebar.gif);
background-repeat : repeat-x
}
</style>
完成後切回設計頁面,設定 GridView 控件的 SelectedRowStyle 及 HeaderStyle 屬性。
完成後執行網頁,你會見到很不一樣的 GridView。
2 Footer or 2 Header
GridView 控件並沒有限制我們只能在裡面加入一個 Footer,因此我們可以透過程式的方式,添加另一個 Footer 至 GridView 控件中。
protected void GridView1_PreRender(object sender, EventArgs e)
{
//if no-data in datasource,GridView will not create ChildTable.
if (GridView1.Controls.Count > 0 && GridView1.Controls[0].Controls.Count > 1 )
{
GridViewRow row2 = new GridViewRow(-1, -1 ,
DataControlRowType.Footer, DataControlRowState.Normal);
TableCell cell = new TableCell();
cell.Text = "Footer 2" ;
cell.Attributes["colspan"] = GridView1.Columns.Count.ToString(); //merge columns
row2.Controls.Add(cell);
GridView1.Controls[0].Controls.AddAt(GridView1.Controls[0].Controls.Count - 1 , row2);
}
}
相同的,同樣的手法也可以用於添加另一個 Header 至 GridView 控件中,這個範例看起來無用,但是卻給了無限的想像空間,這是實現 GridView Insert 及 Collapsed GridView 功能的基礎。
Group Header
想合併 Header 中的兩個欄位為一個嗎?很簡單!只要在 RowCreated事 件中將欲被合併的欄位移除,將另一欄位的 colspan 設為 2 即可。 程式 4-8-12
protected void GridView1_RowCreated(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.Header)
{
e.Row.Cells.RemoveAt(3 );
e.Row.Cells[2].Attributes["colspan"] = "2" ;
e.Row.Cells[2].Text = "Contact Information" ;
}
}
下圖是執行畫面。
我想,應該不需要我再解釋 2 這個數字從何而來了吧。 ^_^
Group Row
想將同值的欄位合成一個嗎?下面的程式碼可以幫你達成。
private void PrepareGroup()
{
int lastSupID = -1 ;
GridViewRow currentRow = null ;
List tempModifyRows = new List();
foreach (GridViewRow row in GridView1.Rows)
{
if (row.RowType == DataControlRowType.DataRow)
{
if (currentRow == null )
{
currentRow = row;
int.TryParse(row.Cells[2].Text, out lastSupID);
continue ;
}
int currSupID = -1 ;
if (int.TryParse(row.Cells[2].Text, out currSupID))
{
if (lastSupID != currSupID)
{
currentRow.Cells[2].Attributes["rowspan"] = (tempModifyRows.Count+1 ).ToString();
currentRow.Cells[2].Attributes["valign"] = "center" ;
foreach (GridViewRow row2 in tempModifyRows)
row2.Cells.RemoveAt(2 );
lastSupID = currSupID;
tempModifyRows.Clear();
currentRow = row;
lastSupID = currSupID;
}
else
tempModifyRows.Add(row);
}
}
}
if (tempModifyRows.Count > 0 )
{
currentRow.Cells[2].Attributes["rowspan"] = (tempModifyRows.Count + 1 ).ToString();
currentRow.Cells[2].Attributes["valign"] = "center" ;
foreach (GridViewRow row2 in tempModifyRows)
row2.Cells.RemoveAt(2 );
}
}
protected void GridView1_PreRender(object sender, EventArgs e)
{
PrepareGroup();
}
這段程式碼應用了先前所提過的 GridViewRow 控件及 TableCell 的使用方式,下圖為執行結果。
Master-Detail GridView
Master-Detail,也就是主明細表的顯示,是資料庫應用常見的功能,運用 DataSource Control 及 GridView 控件可以輕易做到這點,請建立一個網頁,加入兩個 GridView 控件,一名為 GridView1,用於顯示主表,二名為GridView2,用於顯示明細表,接著加入兩個 SqlDataSource 控件,一個連結至 Northwind 資料庫的 Orders 資料表,另一個連結至 Order Details 資料表,於連結至 Order Details 資料表的 SqlDataSource 中添加 WHERE 條件來比對 OrderID 欄位,值來源設成 GridView1 的 SelectedValue 屬性。
接下來請將 GridView1 的 DataSoruce 設為 Orders 的 SqlDataSource,GridView2 的 DataSource 設為 Order Details 的 SqlDataSource,最後將 GridView1 的 Enable Selection 打勾即可完成 Master-Detail 的範例。
那這是如何辦到的呢?當使用者點選 GridView1 上某筆資料的 Select 連結時,GridView1 的 SelectedValue 屬性便會設成該筆資料的 DataKeyName 屬性所指定的欄位值,而連結至 Order Details 的 SqlDataSource 又以該屬性做為比對 OrderID 欄位時的值來源,結果便成了,使用者點選了 Select 連結,PostBack 發生,GridView2 向連結至 Order Details 的 SqlDataSource 索取資料,該 SqlDataSource以GridView1.SelectedValue 做為比對 OrderID 欄位的值,執行選取資料的 SQL 指令後,該結果集便是 GridView1 所選取那筆資料的明細了。
Master-Detail GridView Part 2
前面的 Master-Detail GridView 控件應用,相信你已在市面上的書、或網路上見過,但此節中的 GridView 控件應用包你沒看過,但一定想過!見下圖。
你一定很想驚呼?這是 GridView 嗎??不是第三方控件的效果吧?是的!這是 GridView 控件,而且只需要不到 100 行程式碼!!請先建立一個 UserControl:DetailsGrid.ascx,加入一個 SqlDataSource 控件連結至 Northwind 的 Order Details 資料表,選取所有欄位,接著在 WHERE 區設定如下圖的條件。
接著加入一個 GridView 控件繫結至此 SqlDataSource 控件,並將 Enable Editing 打勾,然後於原始碼中鍵入下面的程式碼。
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class DetailsGrid : System.Web.UI.UserControl
{
public int OrderID
{
get
{
object o = ViewState["OrderID" ];
return o == null ? -1 : (int )o;
}
set
{
ViewState[ "OrderID"] = value;
SqlDataSource1.SelectParameters[0].DefaultValue = value.ToString();
}
}
protected void Page_Load(object sender, EventArgs e)
{
}
}
接著建立一個新網頁,加入 SqlDataSource 控件繫結至 Northwind 的 Orders 資料表,然後加入一個 GridView 控件,並於其欄位編輯器中加入一個 TemplateField,於其內加入一個 LinkButton 控件,設定其屬性如下圖。
然後設定 LinkButton 的 DataBindings 如下圖。
然後於原始碼中鍵入下面的程式碼。
using System;
using System.Collections.Generic;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class CollapseGridView : System.Web.UI.Page
{
private List _collaspedRows = new List();
private List _delayAddRows = new List();
private bool RowIsCollasped(GridViewRow row)
{
if(_collaspedRows.Count > 0 )
return _collaspedRows.Contains((int )GridView1.DataKeys[row.RowIndex].Value);
return false ;
}
private void CreateDetailRow(GridViewRow gridRow)
{
if (RowIsCollasped(gridRow))
{
GridViewRow row = new GridViewRow(gridRow.RowIndex, -1 ,
DataControlRowType.DataRow, DataControlRowState.Normal);
TableCell cell = new TableCell();
row.Cells.Add(cell);
TableCell cell2 = new TableCell();
cell2.Attributes["colspan"] = (GridView1.Columns.Count - 1 ).ToString();
Control c = LoadControl("DetailsGrid.ascx" );
((DetailsGrid)c).OrderID = (int )GridView1.DataKeys[gridRow.RowIndex].Value;
cell2.Controls.Add(c);
row.Cells.Add(cell2);
_delayAddRows.Add(row);
}
}
protected void Page_Load(object sender, EventArgs e)
{
}
protected override void LoadViewState(object savedState)
{
Pair state = (Pair)savedState;
base .LoadViewState(state.First);
_collaspedRows = (List)state.Second;
}
protected override object SaveViewState()
{
Pair state = new Pair(base .SaveViewState(), _collaspedRows);
return state;
}
}
接下來在 TemplateField 中的 LinkButton 的 Click 事件中鍵入下面的程式碼。
protected void LinkButton1_Click(object sender, EventArgs e)
{
LinkButton btn = (LinkButton)sender;
int key = int .Parse(btn.CommandArgument);
if (_collaspedRows.Contains(key))
{
_collaspedRows.Remove(key);
GridView1.DataBind();
}
else
{
_collaspedRows.Clear(); // clear.
_collaspedRows.Add(key);
GridView1.DataBind();
}
}
最後在 GridView 控件的 RowCreated、PageIndexChanging 事件中鍵入下面的程式碼。
protected void GridView1_RowCreated(object sender, GridViewRowEventArgs e)
{
if(e.Row.RowType == DataControlRowType.DataRow)
CreateDetailRow(e.Row);
else if (e.Row.RowType == DataControlRowType.Pager && _delayAddRows.Count > 0 )
{
for (int i = 0; i < GridView1.Rows.Count; i++ )
{
if (RowIsCollasped(GridView1.Rows[i]))
{
GridView1.Controls[0].Controls.AddAt(GridView1.Rows[i].RowIndex + 2 ,
_delayAddRows[0 ]);
_delayAddRows.RemoveAt(0 );
}
}
}
}
protected void GridView1_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
_collaspedRows.Clear();
}
執行後就能看到前圖的效果了,那具體是如何做到的呢?我們知道,我們可以在 GridView 控件中動態的插入一個 GridViewRow 控件,而 GridViewRow 控件可以擁有多個 Cell,每個 Cell 可以擁有子控件,那麼當這個子控件是一個 UserControl 呢 ?相信說到這份上,讀者已經知道整個程式的運行基礎及概念了,剩下的細節如 LoadViewState、SaveViewState 等函式只是狀態的管理,看懂這個範例後!你應該也想到了其它的應用了(UserControl 中放DetailsView、FormView、MultiView、TabControl 或是再嵌上另一個 UserControl,成為巢狀式應用,想都會笑了吧!),對於 GridView!相信你已經毫無疑問了!本文所附的範例程式中將此技巧與 AJAX 結合,發揮到極致,下面列出此範例的截圖,你會發現我們其實錯估了 GridView 控件的強大威力。
如何?ASP.NET 2.0 其實給了我們一個很專業、強大的 GridView 控件不是嗎?此範例可由此下載:http://www.dreams.idv.tw/~code6421/files/GridView1.zip
4-8-4、GridView 的效能
OK,GridView 控件功能很強大,但是如果你仔細思考下 GridView 控件的分頁是如何做的,會發現她的做法其實隱含著一個很大的效能問題,GridView 控件在分頁功能啟動的情況下,會建立一個 PageDataSource 物件,由這個物件負責向 DataSource 索取資料,於索取資料時一併傳入 DataSourceSelectArgument 物件,此物件中便包含了起始的列及需要的列數,看起來似乎沒啥問題嗎?其實不然,當 DataSource 控件不支援分頁時,PageDataSource 物件只能以該 DataSource 所傳回的資料來做分頁,簡略的說!
SqlDataSource 控件是不支援分頁的,這時 PageDataSource 會要求 SqlDataSource 控件傳回資料,而 SqlDataSource 控件就用 SelectQuery 中的 SQL 指令向資料庫要求資料,結果便是,當該 SQL 指令選取 100000 筆資料時,SqlDataSource 所傳回給 PageDataSource 的資料也是 100000 筆!!這意味著,GridView 每次做資料繫結顯示時,是用 100000 筆資料在分頁,不管顯示的是幾筆,存在於記憶體中的都是 100000 筆!如果同時有 10 個人、100 個人在使用此網頁,可想而知 Server 的負擔有多重了,即使有 Cache 加持,一樣會有 100000 筆資料在記憶體中!以往在 ASP.NET 1.1 時,可以運用 DataGrid 控件的 CustomPaging 功能來解決此問題,但 GridView 控件並未提供這個功能,我們該怎麼處理這個問題呢?在提出解決方案前,我們先談談 GridView 控件為何將這麼有用的功能移除了?答案很簡單,這個功能已經被移往 DataSource 控件了,這是因為 DataSource 控件所需服務的不只是 GridView,FormView、DetailsView 都需要她,而且她們都支援分頁,如果將 CustomPaging 直接做在這些控件上,除了控件必須有著重複的程式碼外,設計師於撰寫分頁程式時,也需針對不同的控件來處理,將這些移往 DataSource 控件後,便只會有一份程式碼。說來好聽,那明擺著 SqlDataSource 控件就不支援手動分頁了,那該如何解決這個問題了,答案是 ObjectDataSource,這是一個支援分頁的 DataSource 控件,只要設定幾個屬性及對資料提供者做適當的修改後,便可以達到手動分頁的效果了。請建立一個 WebiSte 專案,添加一個 DataSet 連結到 Northwind 的 Customers 資料表,接著新增一個 Class,檔名為 NorthwindCustomersTableAdapter.cs,鍵入下面的程式碼。
using System;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
namespace NorthwindTableAdapters
{
public partial class CustomersTableAdapter
{
[System.ComponentModel.DataObjectMethodAttribute(
System.ComponentModel.DataObjectMethodType.Select, true )]
public virtual Northwind.CustomersDataTable GetData(int startRowIndex, int maximumRows)
{
this.Adapter.SelectCommand =
new System.Data.SqlClient.SqlCommand("SELECT {COLUMNS} FROM " +
"(SELECT {COLUMNS},ROW_NUMBER() OVER(ORDER BY {SORT}) As RowNumber FROM {TABLE} {WHERE}) {TABLE} " +
"WHERE RowNumber > {START} AND RowNumber < {FETCH_SIZE}" ,Connection);
this.Adapter.SelectCommand.CommandText = this.Adapter.SelectCommand.CommandText.Replace("{COLUMNS}", "*" );
this.Adapter.SelectCommand.CommandText = this.Adapter.SelectCommand.CommandText.Replace("{TABLE}", "Customers" );
this.Adapter.SelectCommand.CommandText = this.Adapter.SelectCommand.CommandText.Replace("{SORT}", "CustomerID" );
this.Adapter.SelectCommand.CommandText = this.Adapter.SelectCommand.CommandText.Replace("{WHERE}", "" );
this.Adapter.SelectCommand.CommandText = this.Adapter.SelectCommand.CommandText.Replace("{START}" , startRowIndex.ToString());
this.Adapter.SelectCommand.CommandText = this.Adapter.SelectCommand.CommandText.Replace("{FETCH_SIZE}", (startRowIndex+ maximumRows).ToString());
Northwind.CustomersDataTable dataTable = new Northwind.CustomersDataTable();
this .Adapter.Fill(dataTable);
return dataTable;
}
public virtual int GetCount(int startRowIndex, int maximumRows)
{
SqlCommand cmd = new System.Data.SqlClient.SqlCommand("SELECT COUNT(*) AS TOTAL_COUNT FROM Customers" , Connection);
Connection.Open();
try
{
return (int )cmd.ExecuteScalar();
}
finally
{
Connection.Close();
}
}
}
}
此程式提供了兩個函式,GetData 函式需要兩個參數,一個是起始的筆數,一個是選取的筆數,利用這兩個參數加上 SQL Server 2005 新增的 RowNumber,便可以向資料庫要求傳回特定範圍的資料。那這兩個參數從何傳入的呢?當 GridView 向 ObjectDataSource 索取資料時,便會傳入這兩個參數,例如當 GridView 的 PageSize 是 10 時,第 1 頁時傳入的 startRowIndex 便是 10,maximumRows 就是 10,以此類推。第二個函式是 GetCount,對於 GridView 來說,她必須知道繫結資料的總頁數才能顯示 Pager 區,而此總頁數必須由資料總筆數算出,此時 GridView 會向 PageDataSource 要求資料的總筆數,而 PageDataSource 在 DataSource 控件支援分頁的情況下,會要求其提供總筆數,這時此函式就會被呼叫了。大致了解這個程式後,回到設計頁面,加入一個 ObjectDataSource 控件,於 SELECT 頁次選取帶 startRowIndex 及 maximumRows 參數的函式。
按下Next按紐後,精靈會要求我們設定參數值來源,請直接按下Finish來完成組態。
接著設定 ObjectDataSource 的 EnablePaging 屬性為 True,SelectCountMethod 屬性為 GetCount (對應了程式 4-8-26 中的 GetCount 函式),最後放入一個 GridView 控件,繫結至此 ObjectDataSource 後將 Enable Paging 打勾,就完成了手動分頁的 GridView 範例了。
在效能比上,手動分頁的效能在資料量少時絕對比 Cache 來的慢,但資料量大時,手動分頁的效能及記憶體耗費就一定比 Cache 來的好。