DataGrid 控件中的汇总行

Dino Esposito

Wintellect

2002 年 1 月 15 日

查看 Summary.cs

查看 Summary.aspx

ASP.NET DataGrid 控件呈现一个多列、完全模板化的网格,它是 .NET 框架中所有数据绑定 Web 控件中最通用和最灵活的控件。DataGrid 的用户界面在某种程度上类似于 Microsoft Excel 工作表。尽管 DataGrid 具有高级的编程接口以及完整的属性集,但是它只生成包含交错超级链接的 HTML 表,从而提供互操作功能(例如,排序和分页命令)。

使用 DataGrid 控件,可以创建简单的数据绑定列(显示从数据源检索的数据)、模板化列(允许您设计单元格内容的布局),最后但也很重要的是基于命令的列(允许您向网格中添加特定的功能)。

DataGrid 控件很适合报告数据,而且其足够的灵活性允许您构建复杂且具有专业外观的数据表,在这些数据表中,可随意实现诸如分页和排序之类的功能。然而,其他功能(例如,深化和主/细)只需要较少的工作。在本月的专栏中,我将讨论一个无法由该控件本身提供、但是颇受许多人欢迎的功能。因此,让我们研究一下如何自动生成复杂的报表,在这些表中,必须显示具有部分合计的汇总行。

DataGrid 项

可通过设置 DataGrid 控件的 DataSource 属性来将实际数据绑定到该控件的实例。此属性是一般的 Object 类型,并且支持两个配置方案。通常用实现 ICollection 接口的数据对象来设置它。通常将使用 DataTableDataView 对象。另一种方法是,用另一个对象类型(例如,data reader 对象)来设置它。不过,在这种情况下,必须打开自定义分页机制;否则将引发异常。简单地说,您要么将 DataGrid 绑定到分页数据源(即用来实现枚举数的集合对象),要么必须为自己提供分页。

对于 Web 控件,数据绑定处于启用状态,只有在调用 DataBind 方法时,用户界面才进行刷新。在刷新过程中,该控件会遍历数据源并将一些行复制到它的 Items 集合中。Items 属性代表当前显示页的内容。如果数据源支持分页(即,实现 ICollection),则 DataGrid 从 DataSource 选择适合当前页的正确行子集。否则,它假设 DataSource 的全部内容都适合当前页并将它们全部加载到 Items 中。在填充完 Items 之后,就会呈现该控件的用户界面。

此处有何教训?DataGrid 控件能够安全而又一致地显示的全部内容就是绑定数据源中包含的行。因此,如果您希望插入汇总行,以便按照公共键对某些记录进行分组并显示部分合计,则必须指出将这些汇总行直接插入数据源的方法。

然而,将汇总行插入数据源是不够的。实际上,必须能够区分汇总行和普通行,并用不同的视觉样式呈现前者。

在将数据附加到该控件之前,确保数据源包含其所需的所有汇总行。接着,挂钩 ItemCreated 事件,检测每个汇总行,然后用不同的布局和样式绘制它们。让我们看一看如何在 SQL 查询的不同行之间插入汇总行。我将使用基于 Northwind SQL Server 数据库的示例应用程序来阐释我的观点。该应用程序列出每个客户已在给定年份发出的所有定单。定单按年份和客户 ID 进行分组。对于每个客户,都额外有一行来汇总定单的总数和总额。

数据分组

下面的 SQL 命令选择所有客户在给定年份发出的所有定单。只显示每个定单所有项目价格的总和。

SELECT o.customerid, od.orderid, SUM(od.quantity*od.unitprice) AS price 
FROM Orders o, [Order Details] od
WHERE Year(o.orderdate) = @TheYear AND od.orderid=o.orderid
GROUP BY o.customerid, od.orderid
ORDER BY o.customerid

在 T-SQL 语言中,SELECT 语句的 GROUP BY 子句提供将预定义的汇总行添加到结果集的 WITH ROLLUP 子句。当然,这样的汇总行具有所有其他列的布局,但每一列的内容都可以进行某种程度的自定义。下面的语句阐释如何修改上面的命令,以使其允许使用汇总行。

DECLARE @TheYear int
SET @TheYear = 1998

SELECT 
	CASE GROUPING(o.customerid) WHEN 0 
THEN o.customerid ELSE '(Total)' END AS MyCustomerID, 
	CASE GROUPING(od.orderid) WHEN 0 
THEN od.orderid ELSE -1 END AS MyOrderID, 
	SUM(od.quantity*od.unitprice) AS price
FROM Orders o, [Order Details] od
WHERE Year(orderdate) = @TheYear AND od.orderid=o.orderid
GROUP BY o.customerid, od.orderid WITH ROLLUP
ORDER BY o.customerid, price

如果您将该代码片段复制并粘贴到 SQL 查询分析器中,将看到如下图所示的内容。

ms810274.data01102002-fig01(zh-cn,MSDN.10).gif

图 1. WITH ROLLUP 子句将汇总行添加到结果集

GROUPING 是一种 T-SQL 聚合函数,它与 ROLLUP 一起工作于 GROUP BY 子句的主体中。使用 GROUPING 运算符将使新列添加到结果集。如果该行已经由 ROLLUP 运算符添加并因此而成为汇总行,则新列中将包含值 1。否则,该列将包含值 0。使用 CASE..WHEN..END 语句可以将这个新列与分组列合并在一起。

在上面的示例中,MyCustomerID 列在所有因其进行分组而创建的行中包含 CustomerID 列的值以及字符串 “(Total)”。同样,当该行代表小计时,MyOrderID 列包含定单 ID 和 –1。

为了对数据进行汇总,SQL Server 还提供了几个选项,例如,WITH CUBE 运算符和 COMPUTE BY 子句。正如您所想象的那样,尽 管一个选项的功能以某种方式与另一个选项的功能相交叉,但是所有这些选项并不完全等价。特别是,WITH CUBE 针对结果集内组与子组的每个可能的组合都生成一个汇总行。而 WITH ROLLUP 按照分组列的指定顺序来进行分组。最后,COMPUTE BY(SQL Server 2000 支持它的目的仅在于获得向后兼容性)的工作方式与 WITH ROLLUP 大体相同,不同的是它返回多个结果集,而且在由查询优化器处理时不如 ROLLUP 效率高。

显示已分组数据

当绑定到 DataGrid 控件时,由 SQL 命令返回的结果集看上去如下图所示。

ms810274.data01102002-fig02(zh-cn,MSDN.10).gif

图 2. 通过 DataGrid 控件显示的结果集

示例应用程序中使用的 DataGrid 控件按如下方式进行声明:

<asp:DataGrid id="grid" runat="server" 
AutoGenerateColumns="false"
AllowPaging="true" PageSize="15"
Font-Size="xx-small"
CellSpacing="0" CellPadding="4" GridLines="both"
BorderStyle="solid" BorderColor="skyblue" BorderWidth="1" 
OnItemCreated="ItemCreated"
OnPageIndexChanged="PageIndexChanged">

<headerstyle backcolor="skyblue" font-size="9pt" font-bold="true" />
<itemstyle backcolor="#eeeeee" />
<pagerstyle backcolor="skyblue" font-name="webdings" 
font-size="10pt" PrevPageText="3" NextPageText="4" />

<Columns>
<asp:BoundColumn DataField="MyCustId" HeaderText="Customer" />
<asp:BoundColumn DataField="MyOrderId" HeaderText="Order #" />
<asp:BoundColumn DataField="price" HeaderText="Amount" 
DataFormatString="{0:c}">
<itemstyle horizontalalign="right" />
</asp:BoundColumn>
</Columns>
</asp:DataGrid>

使用 WITH ROLLUP 运算符获取的数据源已经包含生成有效的报告所必需的全部信息。您可能已经注意到了,该语句添加一个顶行,其中包含由所有客户发出的全部定单的合计。在使用 WITH ROLLUP 运算符时,如果您修改了分组行的顺序,则所生成行的数量和结构可能会发生显著变化。这额外的一行是我选择使用特定语法的结果。如果您不需要这段信息,只需在绑定之前将它从结果集删除。或者,可以将该行移到数据集的底部。

下面显示的代码阐释如何执行 rollup 语句。从文本框中读出的参数是要考虑的年份。结果集临时存储在 DataSet 对象中。在这个示例应用程序中,我将在 Session 槽中缓存 DataSet 对象。在实际环境中,这应该是受重视的选择。通常,存储在 Session 中的任何字节都有一个位于那里的充分理由。

private DataSet PhysicalDataRead()
{
String strCnn = "SERVER=localhost;DATABASE=northwind;UID=sa;";
SqlConnection conn = new SqlConnection(strCnn);

// Command text using WITH ROLLUP
StringBuilder sb = new StringBuilder("");
sb.Append("SELECT ");
sb.Append("  CASE GROUPING(o.customerid) WHEN 0 ");
sb.Append("    THEN o.customerid ELSE '(Total)' END AS MyCustID, ");
sb.Append("  CASE GROUPING(od.orderid) WHEN 0 ");
sb.Append("    THEN od.orderid ELSE -1 END AS MyOrderID, ");
sb.Append("  SUM(od.quantity*od.unitprice) AS price ");
sb.Append("FROM Orders o, [Order Details] od ");
sb.Append("WHERE Year(orderdate)=@nYear AND od.orderid=o.orderid ");
sb.Append("GROUP BY o.customerid, od.orderid WITH ROLLUP ");
sb.Append("ORDER BY o.customerid, price");
String strCmd = sb.ToString();
sb = null;

SqlCommand cmd = new SqlCommand();
cmd.CommandText = strCmd;
cmd.Connection = conn;   
SqlDataAdapter da = new SqlDataAdapter(strCmd, strConn);
da.SelectCommand = cmd;

// Set the "year" parameter
SqlParameter p1 = new SqlParameter("@nYear", SqlDbType.Int);
p1.Direction = ParameterDirection.Input;
p1.Value = Convert.ToInt32(txtYear.Text);
cmd.Parameters.Add(p1);

DataSet ds = new DataSet();
da.Fill(ds, "Orders");
return ds;
}

为了使汇总行在该网格的页面中清楚地显示,需要更改汇总行的样式和布局。这可在 ItemCreated 事件处理程序中完成。设计思路是,通过检查定单 ID 来检测汇总行,然后修改单元格的布局和样式。在结果集内,汇总行的特征是定单 ID 为 –1。值 –1 是来自所使用语句的任意值。

CASE GROUPING(od.orderid) WHEN 0 THEN od.orderid ELSE -1 END AS MyOrderID

如果不针对 orderid 列使用 GROUPING 运算符,则对于汇总行来说,该列的值将为 NULL。

修改布局和样式

DataGrid 允许您修改成分单元格的样式和布局,这可通过挂钩 ItemCreated 事件来完成。该控件每次处理子项(页眉、页脚、行、页导航)时,该事件都会被激发。事件处理程序接收类型为 DataGridItemEventArgs 的参数,您可以从该参数提取所处理项目的类型。

汇总行是 DataGrid 行,同样,它的类型可以是 ItemAlternatingItem。因此,在编写 ItemCreated 处理程序时,要确保只有在该项的类型正确时才处理相应的单元格。下面的列表概述所需的代码。

public void ItemCreated(Object sender, DataGridItemEventArgs e)
{
	// Get the type of the newly created item
	ListItemType itemType = e.Item.ItemType;
	if (itemType == ListItemType.Item || 
	itemType == ListItemType.AlternatingItem) 
	{
		// Get the data bound to the current row
		DataRowView drv = (DataRowView) e.Item.DataItem;
	if (drv != null)
	{
		// Check here the app-specific way to detect whether the 
		// current row is a summary row
		:
	}
	}
}

如果所创建的项是 DataGrid 项(或交替项),则可以通过 DataItem 属性访问绑定到行的数据。根据 DataGrid 绑定到的对象的类型,DataItem 属性会指向不同的行对象。如果网格绑定到 DataView,会获取 DataRowView 对象;如果该源用 DataTable 对象来表示,会获取 DataRow 对象。在该示例应用程序中,我使用 DataView 对象填充了网格。后来,单行的数据对象成为 DataRowView 对象。

在拥有了数据行对象之后,可以应用一些应用程序特定的规则来确定该行是否为汇总行。在该示例应用程序中,汇总行的 MyOrderID 字段设置为 –1。

if ((int) drv["MyOrderID"] == -1)
{
   // Modify style and layout here. 
   //    --> Set the background color to white and use bold font
   e.Item.BackColor = Color.White; 
e.Item.Font.Bold = true;    
}

DataGrid 现在看上去如下图所示。

ms810274.data01102002-fig03(zh-cn,MSDN.10).gif

图 3. 以粗体显示且背景为白色的汇总行

DataGrid 行实际上只是表中的一行。同样,使用它可以很好地进行单元格删除以及其他调整。让我们看一看如何使用跨越所有现有列的单一单元格来呈现汇总行。

if ((int) drv["MyOrderID"] == -1)

ms810274.data01102002-fig04(zh-cn,MSDN.10).gif

图 4. 具有自定义布局的汇总行

在这三个原始单元格中,前两个被删除,第三个(现在包含索引 0)被正确对齐并跨越外部表的宽度。如果您希望在汇总行上显示一些自定义文本,则需要做好面对其他问题的准备。

假设您需要添加一些文本以对小计进行注释,而且与此同时,让小计与单个定单量出现在同一列中。在这种情况下,只需删除一个单元格。

e.Item.Cells.RemoveAt(1);         // remove the order # cell
e.Item.Cells[0].ColumnSpan = 2;      // span the custID cell
e.Item.Cells[1].HorizontalAlign = HorizontalAlign.Right;
e.Item.Cells[0].Text = "Total is";

此代码的结果如下所示。正如您所看到的那样,它与您的预期结果不完全相同。汇总行的第一个单元格中并没有您刚刚设置的文本。这是怎么回事呢?

ms810274.data01102002-fig05(zh-cn,MSDN.10).gif

图 5. 具有修改后的自定义布局的汇总行

此处需要考虑的重要一点是,ItemAlternatingItem 行均为绑定行。它们的明确文本只是在 OnItemDataBound 事件的过程中设置。您可能已经猜到了,OnItemDataBound 事件会在创建该项之后激发。因此,在处理 ItemCreated 时分配给单元格的任何文本在后来都由某个事件以静默方式改写。可通过设置 DataGrid 的 OnItemDataBound 属性来挂钩 OnItemDataBound 事件。

<asp:DataGrid id="grid" runat="server" 
AutoGenerateColumns="false"
:
OnItemCreated="ItemCreated"
OnItemDataBound="ItemDataBound"
OnPageIndexChanged="PageIndexChanged">
The structure of the code for 
																																																					ItemDataBound is shown below.
public void ItemDataBound(Object sender, DataGridItemEventArgs e)
{
DataRowView drv = (DataRowView) e.Item.DataItem;
if (drv == null)
	return;

if ((int) drv["MyOrderID"] == -1)
{
if (drv["MyCustomerID"].ToString() == "(Total)")
{
	e.Item.BackColor = Color.Yellow;
	e.Item.Cells[0].Text = "Orders total";
}
else
	e.Item.Cells[0].Text = "Customer subtotal";
}
}

最上面的一行是在黄色背景上绘制的,它显示其他汇总行中的另一个文本。最终的 DataGrid 显示如下。

ms810274.data01102002-fig06(zh-cn,MSDN.10).gif

图 6. 最终的 DataGrid

小结

以应用程序特定的剂量很好地混合 SQL 代码和 ASP.NET 技术可以实现有效的 Web 数据库应用程序。DataGrid 控件是一个前沿工具,可用来为它所提供的编程功能构建完美而又功能强大的 Web 应用程序,而且对于它所支持的自定义级别来说用途更多。

对话栏:关键任务的确认

我如何能够显示一个强制用户在启动关键任务(例如,记录删除)之前进行确认的对话框?

假设您需要在用户单击某个按钮后显示这样的对话框。好,这只需通过一些用来处理 onclick 事件的客户端 JavaScript 代码来获得。

任何 ASP.NET 控件都可以求值为一个或多个 HTML 标记。下压按钮映射到"按钮" 标记。链接按钮映射到由脚本驱动的超级链接。使用 ASP.NET 控件的 Attributes 集合,可以同时为这两个标记注册脚本代码段。例如,如果您有一个按钮(无论它是 LinkButton 对象还是 Button 对象),则可以定义在该按钮被单击之后运行的 JavaScript 代码,如下所示:

String js = "return confirm('Do you really want to delete the record?');";
btn.Attributes["onclick"] = js;
这样一来,HTML 标记包含一个属性:
<a  ? onclick="return confirm(' ? ')"> 

当用户单击该链接时,会运行 onclick 客户端代码,而且如果用户单击 No,该事件将会自动放弃。此行为嵌入到浏览器的逻辑中,它几乎与 ASP.NET 无关。如果 onclick 客户端处理程序成功退出,则会执行 __doPostBack 函数,而且页面会回发到服务器。

Dino EspositoWintellect 的 ADO.NET 专家兼培训教员和顾问,工作地点位于意大利的罗马。Dino 是 MSDN Magazine 的特约编辑,是 Cutting Edge 专栏的撰稿人。他还定期向 Developer Network Journal 和 MSDN News 投稿。Dino 是 Microsoft Press 出版的?Building Web Solutions with ASP.NET and ADO.NET??的作者,也是 http://www.vb2themax.com/ 的创始人之一。如果希望与 Dino 联系,可发送电子邮件至 dinoe@wintellect.com

转到原英文页面