(转)ASP.NET 2.0 GridView 范例集
原文地址:http://www.microsoft.com/taiwan/msdn/columns/huang_jhong_cheng/ASP_NET_GridView.htm
例子下载地址:http://www.dreams.idv.tw/~code6421/files/GridView1.zip
作者:黄忠成
这篇文章从何来?
在写【极意之道 - 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 来的好。
浙公网安备 33010602011771号