GridView
是ASP.NET 1.x的DataGrid控件的后继者。它提供了相同的基本功能集,同时增加了大量扩展和改进。如前所述,DataGrid(ASP.NET 2.0仍然完全支持)是一个功能非常强大的通用控件。然而,它有一个重大缺陷:它要求我们编写大量定制代码,甚至处理比较简单而常见的操作,诸如分页、排序、编辑或删除数据等也不例外。GridView控件旨在解决此限制,并以尽可能少的数据实现双向数据绑定。该控件与新的数据源控件系列紧密结合,而且只要底层的数据源对象支持,它还可以直接处理数据源更新。
这种实质上无代码的双向数据绑定是新的GridView控件最著名的特征,但是该控件还增强了很多其他功能。该控件之所以比DataGrid控件有所改进,是因为它能够定义多个主键字段、新的列类型以及样式和模板选项。GridView还有一个扩展的事件模型,允许我们处理或撤销事件。
GridView
控件为数据源的内容提供了一个表格式的类网格视图。每一列表示一个数据源字段,而每一行表示一个记录。该类声明如下:
public class GridView : CompositeDataBoundControl,
ICallbackContainer,
ICallbackEventHandler
该基类确保数据绑定和命名容器支持。ICallbackContainer和ICallbackEventHandler接口提供了比现在支持的更有效的分页和排序功能。它通过使用新的脚本回调技术的客户端的out-of-band调用来完成。(稍候将会更详细地讨论这一点。)首先让我们来看看GridView控件的编程接口。
1. GridView控件的属性
GridView
支持大量属性,这些属性属于如下几大类:行为、可视化设置、样式、状态和模板。表10.6详细描述了影响的行为的属性。
表10.6 GridView控件的行为属性
属性
|
描述
|
AllowPaging
|
指示该控件是否支持分页。
|
AllowSorting
|
指示该控件是否支持排序。
|
AutoGenerateColumns
|
指示是否自动地为数据源中的每个字段创建列。默认为true。
|
AutoGenerateDeleteButton
|
指示该控件是否包含一个按钮列以允许用户删除映射到被单击行的记录。
|
AutoGenerateEditButton
|
指示该控件是否包含一个按钮列以允许用户编辑映射到被单击行的记录。
|
AutoGenerateSelectButton
|
指示该控件是否包含一个按钮列以允许用户选择映射到被单击行的记录。
|
DataMember
|
指示一个多成员数据源中的特定表绑定到该网格。该属性与DataSource结合使用。如果DataSource是有一个DataSet对象,则该属性包含要绑定的特定表的名称。
|
DataSource
|
获得或设置包含用来填充该控件的值的数据源对象。
|
DataSourceID
|
指示所绑定的数据源控件。
|
EnableSortingAndPagingCallbacks
|
指示是否使用脚本回调函数完成排序和分页。默认情况下禁用。
|
RowHeaderColumn
|
用作列标题的列名。该属性旨在改善可访问性。
|
SortDirection
|
获得列的当前排序方向。
|
SortExpression
|
获得当前排序表达式。
|
UseAccessibleHeader
|
规定是否为列标题生成 | 标签(而不是 | 标签)。
|
SortDirection
和SortExpression属性规定当前决定行的排列顺序的列上的排序方向和排序表达式。这两个属性都是在用户单击列的标题时由该控件的内置排序机制设置的。整个排序引擎通过AllowSorting属性启用和禁用。EnableSortingAndPagingCallbacks属性打开和关闭该控件的使用脚本回调进行分页和排序,而不用往返于服务器并改变整个页面的功能。
GridView
控件内显示的每一行对应于一种特殊的网格项。预定义的项目类型几乎等于DataGrid的项目类型,包括标题、行和交替行、页脚和分页器等项目。这些项目是静态的,因为它们在控件的生命期内在应用程序中保持不变。其他类型的项目在短暂的时间(即,完成某种操作所需的时间)内是活动的。动态项目是编辑行、所选的行和EmptyData项。当网格绑定到一个空的数据源时,EmptyData标识该网格的主体。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image002.jpg">
注意
GridView
控件提供了几个专门为了可访问性而设计的属性。这些属性是
UseAccessibleHeader
、
Caption
、
CaptionAlign
和
RowHeaderColumn
。设置
RowHeaderColumn
时,将用默认的标题样式
(
黑体字
)
输出该列的所有单元。然而,
ShowHeader
、
HeaderStyle
和其他与标题相关的属性并不会影响由
RowHeaderColumn
指示的列。
表10.7详细描述了GridView控件上可用的样式属性。
表10.7 GridView控件的样式属性
样式
|
描述
|
AlternatingRowStyle
|
定义表中每隔一行的样式属性。
|
EditRowStyle
|
定义正在编辑的行的样式属性。
|
FooterStyle
|
定义网格的页脚的样式属性。
|
HeaderStyle
|
定义网格的标题的样式属性。
|
EmptyDataRowStyle
|
定义空行的样式属性,这是在GridView绑定到空数据源时生成。
|
PagerStyle
|
定义网格的分页器的样式属性。
|
RowStyle
|
定义表中的行的样式属性。
|
SelectedRowStyle
|
定义当前所选行的样式属性。
|
表10.8列出了影响控件外观的大多数属性,而表10.9列出了模板属性。
表10.8 GridView控件的外观属性
属性
|
描述
|
BackImageUrl
|
指示要在控件背景中显示的图像的URL。
|
Caption
|
在该控件的标题中显示的文本。
|
CaptionAlign
|
标题文本的对齐方式。
|
CellPadding
|
指示一个单元的内容与边界之间的间隔(以像素为单位)。
|
CellSpacing
|
指示单元之间的间隔(以像素为单位)。
|
GridLines
|
指示该控件的网格线样式。
|
HorizontalAlign
|
指示该页面上的控件水平对齐。
|
EmptyDataText
|
指示当该控件绑定到一个空的数据源时生成的文本。
|
PagerSettings
|
引用一个允许我们设置分页器按钮的属性的对象。
|
ShowFooter
|
指示是否显示页脚行。
|
ShowHeader
|
指示是否显示标题行。
|
PagerSettings
对象把所有可以对分页器设置的可视化属性组织在一起。其中有很多属性在DataGrid程序员看来应该是熟悉的。PagerSettings类还添加了一些新属性以满足新的预定义的按钮(第1页和最后一页),并在链接中使用图像代替文本。(我们需要合计出一条和使用DataGrid时相同的诀窍。)
表10.9 GridView控件的模板属性
模板
|
描述
|
EmptyDataTemplate
|
指示该控件绑定到一个空的数据源时要生成的模板内容。如果该属性和EmptyDataText属性都设置了,则该属性优先采用。如果两个属性都没有设置,则把该网格控件绑定到一个空的数据源时不生成该网格。
|
PagerTemplate
|
指示要为分页器生成的模板内容。该属性覆盖我们可能通过PagerSettings属性作出的任何设置。
|
最后剩下的是状态属性,表10.10列出了这些属性。状态属性返回有关控件的内部状态的信息。
表10.10 状态属性
属性
|
描述
|
BottomPagerRow
|
返回表格该网格控件的底部分页器的GridViewRow对象。
|
Columns
|
获得一个表示该网格中的列的对象的集合。如果这些列是自动生成的,则该集合总是空的。
|
DataKeyNames
|
获得一个包含当前显示项的主键字段的名称的数组。
|
DataKeys
|
获得一个表示在DataKeyNames中为当前显示的记录设置的主键字段的值。
|
EditIndex
|
获得和设置基于0的索引,标识当前以编辑模式生成的行。
|
FooterRow
|
返回一个表示页脚的GridViewRow对象。
|
HeaderRow
|
返回一个表示标题的GridViewRow对象。
|
PageCount
|
获得显示数据源的记录所需的页面数。
|
PageIndex
|
获得或设置基于0的索引,标识当前显示的数据页。
|
PageSize
|
指示在一个页面上要显示的记录数。
|
Rows
|
获得一个表示该控件中当前显示的数据行的GridViewRow对象集合。
|
SelectedDataKey
|
返回当前选中的记录的DataKey对象。
|
SelectedIndex
|
获得和设置标识当前选中行的基于0的索引。
|
SelectedRow
|
返回一个表示当前选中行的GridViewRow对象。
|
SelectedValue
|
返回DataKey对象中存储的键的显式值。类似于SelectedDataKey。
|
TopPagerRow
|
返回一个表示网格的顶部分页器的GridViewRow对象。
|
GridView
旨在利用新的数据源对象模型,并在通过DataSourceID属性绑定到一个数据源控件时效果最佳。GridView还支持经典的DataSource属性,但是如果那样绑定数据,则其中一些特征(诸如内置的更新或分页)变得不可用。
2. GridView控件的事件
GridView
控件没有不同于DataBind的方法。然而,如前所述,在很多情况下我们不需要调用GridView控件上的方法。当我们把GridView绑定到一个数据源控件时,数据绑定过程隐式地启动。
在ASP.NET 2.0中,很多控件,以及Page类本身,有很多对doing/done类型的事件。控件生命期内的关键操作通过一对事件进行封装:一个事件在该操作发生之前激发,一个事件在该操作完成后立即激发。GridView类也不例外。表10.11列出了GridView控件激发的事件。
表10.11 GridView控件激发的事件
事件
|
描述
|
PageIndexChanging,
PageIndexChanged
|
这两个事件都是在其中一个分页器按钮被单击时发生。它们分别在网格控件处理分页操作之前和之后激发。
|
RowCancelingEdit
|
在一个处于编辑模式的行的
Cancel
按钮被单击,但是在该行退出编辑模式之前发生。
|
RowCommand
|
单击一个按钮时发生。
|
RowCreated
|
创建一行时发生。
|
RowDataBound
|
一个数据行绑定到数据时发生。
|
RowDeleting, RowDeleted
|
这两个事件都是在一行的
Delete
按钮被单击时发生。它们分别在该网格控件删除该行之前和之后激发。
|
RowEditing
|
当一行的
Edit
按钮被单击时,但是在该控件进入编辑模式之前发生。
|
RowUpdating,
RowUpdated
|
这两个事件都是在一行的
Update
按钮被单击时发生。它们分别在该网格控件更新该行之前和之后激发。
|
SelectedIndexChanging,
SelectedIndexChanged
|
这两个事件都是在一行的
Select
按钮被单击时发生。它们分别在该网格控件处理选择操作之前和之后激发。
|
Sorting, Sorted
|
这两个事件都是在对一个列进行排序的超链接被单击时发生。它们分别在网格控件处理排序操作之前和之后激发。
|
RowCreated
和RowDataBound事件与DataGrid的ItemCreated和ItemDataBound事件相同,只是换了个新名称。它们的行为完全与它们在ASP.NET 1.x中的一样。对于RowCommand事件也一样,它与DataGrid的ItemCommand事件一样。
可以使用宣布某种操作的事件,极大地增强了我们的编程能力。通过连接RowUpdating事件,可以交叉检查正在更新什么并对新值进行验证。同样,我们可能需要处理RowUpdating事件,用HTML对客户端提供的值进行编码,然后把它们持久地保存在底层数据存储中。这一简单技巧有助于防御脚本侵入。
3. 简单的数据绑定
如下代码片断说明了把数据绑定到一个GridView控件的最简单方法。数据源对象使该页面几乎不需要任何代码。
TypeName="ProAspNet20.DAL.Customers"
SelectMethod="LoadAll">
设置DataSourceID属性触发数据绑定过程,它运行数据源查询,并填充该网格的用户界面。我们不需要编写任何绑定代码。(注意,我们仍然必须编写LoadAll方法和DAL。)
在默认情况下,GridView控件自动生成足够多的列,以包含来自数据源的所有数据。在其他情况下,我们可能需要单独控制和设计每一列。为此,要对绑定过程稍加提炼。
如果没有设置任何数据源属性,则GridView控件不会生成任何东西。如果绑定了一个空的数据源并且规定了EmptyDataTemplate模板,则向用户显示的结果有一个较友好的外观:
There's no data to show in this view.
如果该控件所绑定的数据源不空,则忽略EmptyDataTemplate属性。图10.2展示了空模板生成的输出。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image005.jpg">
图10.2 绑定到一个空数据源的正在运行GridView控件
如果使用一个已声明的列集合,则网格的AutoGenerateColumns属性通常设置为false。然而,这不是一个严格的要求——网格可以声明的列和自动生成的列。在这种情况下,声明的列先显示。还要注意自动生成的列不是添加到Columns集合。因此,使用列自动生成时,Columns集合通常是空的。
1. 对列进行配置
Columns
属性是一个DataControlField对象集合。DataControlField对象类似于DataGrid的DataGridColumn对象,但是它有一个更一般的名称,因为这些字段对象可以在其他不必生成列的数据绑定控件中重用。(例如,在DetailsView控件中,相同的类用来生成一行。)
我们既可以以声明的方式定义列,也可以以编程方式声明列。如果以编程的方式声明列,则只要实例化任何必需的数据字段对象,并把它们添加到Columns集合。如下代码把一个数据绑定的列添加到网格中:
BoundField field = new BoundField();
field.DataField = "companyname";
field.HeaderText = "Company Name";
grid.ColumnFields.Add(field);
各列数据按列字段在集合众出现的顺序进行显示。要在.aspx源文件中静态地声明列,则使用标签,如下面所示:
表10.12列出GridView控件中使用列字段类。所有的类都继承DataControlField。
表10.12 GridView控件支持的列类型
类型
|
描述
|
BoundField
|
默认的列类型。作为纯文本显示一个字段的值。
|
ButtonField
|
作为命令按钮显示一个字段的值。我们可以选择链接按钮或按钮开关样式。
|
CheckBoxField
|
作为一个复选框显示一个字段的值。它通常用来生成布尔值。
|
CommandField
|
ButtonField
的增强版本,表示一个特殊的命令,诸如Select、Delete、Insert或Update。该属性对GridView控件几乎每什么用;该字段是为DetailsView控件定制的。(GridView和DetailsView共享从DataControlField派生的类集。)
|
HyperLinkField
|
作为超链接显示一个字段的值。单击该超链接时,浏览器到航道指定的URL。
|
ImageField
|
作为一个 HTML标签的Src属性显示一个字段的值。绑定字段的内容应该是物理图象的URL。
|
TemplateField
|
为列中的每一项显示用户定义的内容。当我们需要创建一个定制的列字段时,则使用该列类型。模板可以包含任意多个数据字段,还可以结合文字、图像和其他控件。
|
表10.13列出了所有的列类型共享的主要属性。
表10.13 GridView列的公共属性
属性
|
描述
|
AccessibleHeaderText
|
表示Assistive Technology设备的屏幕阅读器读取的缩写文本的文本。
|
FooterStyle
|
获得该列的页脚的样式对象。
|
FooterText
|
获得和设置该列的页脚的文本。
|
HeaderImageUrl
|
获得和设置放在该列的标题中的图像的URL。
|
HeaderStyle
|
获得该列的标题的样式对象。
|
HeaderText
|
获得和设置该列的标题的文本。
|
InsertVisible
|
指示当它的父数据绑定控件处于插入模式时,该字段是否可见。该属性不适用于GridView控件。
|
ItemStyle
|
获得各列的单元的样式对象。
|
ShowHeader
|
指示是否生成该列的标题。
|
SortExpression
|
获得和设置该列的标题被单击时用来排序网格内容的表达式。通常,该字符串属性被设置为所绑定的数据字段的名称。
|
表10.13所列的属性代表每个列类型实际提供的属性的一个子集。特别是,每个列类型定义了一个定制的属性集,用以定义和配置所绑定的字段。有关GridView的列类型的编程接口的详情,请参考MSDN文档。
2. 绑定字段
BoundField
类表示在一个数据绑定控件(诸如GridView或DetailsView)中作为纯文本显示的一个字段。为了规定要显示的字段,把DataField属性设置为该字段的名称。通过设置DataFormatString属性,可以应用一个定制的格式化字符串于所显示的值。如果NullDisplayText属性的值为null,则允许我们规定要显示的交替文本。最后,通过把ConvertEmptyStringToNull属性设置为true,强制该类把空字符串看作null值。
BoundField
可以通过Visible属性以编程的方式隐藏起来,而ReadOnly属性防止所显示的值在编辑模式被修改。要在头部或页脚部分显示一个标题,请分别设置HeaderText和FooterText属性。我们还可以选择在头部显示一个图像,而不是文本,这时要设置HeaderImageUrl属性。
3. 按钮字段
按钮字段适合于把一个可单击的元素放入一个网格的列中。通常使用一个按钮字段触发针对当前行的一个操作。按钮字段表示我们希望通过一个服务器端事件处理的任何操作。当该按钮被单击时,页面回发并激发一个RowCommand事件。图10.3展示了一个示例。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image006.jpg">
图10.3 GridView控件中的按钮字段
如下清单给出了上图所示网格的标记代码:
AutoGenerateColumns="false" AllowPaging="true"
OnRowCommand="GridView1_RowCommand">
headertext="Product" />
headertext="Packaging" />
headertext="Price" DataFormatString="{0:c}">
产品信息使用几个BoundField对象显示出来。该示例按钮列允许我们把产品添加到购物车中。当用户单击该按钮时,激发RowCommand服务器事件。在多个按钮列可用的情况下,CommandName属性允许我们推断出哪个按钮被单击了。我们赋给CommandName是代码隐藏类可以理解的任何惟一的字符串。下面给出了一个实例:
void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (e.CommandName.Equals("Add"))
{
// Get the index of the clicked row
int index = Convert.ToInt32(e.CommandArgument);
// Create a new shopping item and add it to the cart
AddToShoppingCart(index);
}
}
在该示例中,按钮列为所有的数据项显示一个固定文本。通过设置ButtonField类上的Text属性可以做到这一点。如果需要把按钮文本绑定到当前数据项上的一个特定字段,则把DataTextField属性设置为该字段的名称:
我们可以选择不同样式的按钮:按压式按钮、链接按钮或图像按钮。要以图像样式生成该按钮,则使用如下代码:
ImageUrl="/proaspnet20/images/cart.gif" />
要把一个ToolTip添加到该按钮(或图像)上,则需要处理RowCreated事件。(我将在本章后面详细介绍该事件。)
4. 超链接字段
超链接列把用户指向一个不同的URL,该URL可以有选择地在一个内部框架中显示出来。该链接的文本和URL可以从所绑定的数据源中获得。特别是,URL可以按以下两种方法之一进行设置:通过直接绑定到一个数据源字段,或通过使用一个硬编码的带有定制查询字符串的URL。如果URL存储在数据源的一个字段字段中,则选择直接绑定。在这种情况下,把DataNavigateUrlFields属性设置为该列的名称。然而,在某些情况下,要访问的URL与特定的应用程序有关,并不是存储在数据源中。在这种情况下,可以用一个硬编码的URL和查询字符串中的一个参数数组设置DataNavigateUrlFormatString属性:
HeaderText="Product"
DataNavigateUrlFields="productid"
DataNavigateUrlFormatString="productinfo.aspx?id={0}"
Target="ProductView" />
当用户单击该按钮时,浏览器用productinfo.aspx?id=xxx URL的内容填充规定的框架窗口,其中xxx取自productid字段。该URL可以包含多个参数。要包含多个数据绑定值,只要把DataNavigateUrlFields属性设置为一个逗号隔开的字段名列表。该行为扩展了DataGrid的超链接列的行为,因为它支持多个参数。
超链接的文本也可以进行格式化。DataTextFormatString属性可以包含任何有效的标记,并使用{0}占位符保留数据绑定值的位置。(参见图10.4。)
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image007.jpg">
图10.4 GridView控件中的超链接字段
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image008.jpg">
提示
为超链接的页面的选择一个目标时,还可以使用下面所列的任何一种标准目标:
_self
、
_parent
和
_new
。
Microsoft Internet Explorer
和
Firefox
都支持
_search
,它使用一个停靠在浏览器左端的配套
Web
面板。
(
参见图
10.5
。
)
5. CheckBox字段
我们迄今所考虑的列类型对于经验丰富的ASP.NET 1.x开发人员来讲并不觉得新奇。虽然被重命名了,但是它们的总体行为仍然非常类似于DataGrids的类似列类型的行为。另一方面,CheckBoxField类型是在ASP.NET 2.0中是一个新类型,并且只适用于GridView和其他视图控件。在ASP.NET 1.x中获得一个复选框的最简单的方法是通过模板(一般是针对DataGrids)。
CheckBoxField
列是显示一个复选框的一个较简单的绑定列。我们可以只把它绑定到一个包含布尔值的数据字段。有效的布尔值取自一个SQL Server表中的一个Bit类型(和其他数据库中的类似类型)的列,如果该控件绑定到一个定制集合,则取自一个bool类型的属性。特别是,如果把一个CheckBoxField列绑定到一个整数属性,则会得到一个异常,从而隐式地假设0为false,非0为true。
6. 图像字段
ImageField
列类型表示一个在数据绑定控件中作为图像显示的字段。该单元包含一个元素,因此底层的字段必须引用一个有效的URL。然而,我们可以任意组合URL。例如,我们可以使用DataImageUrlField执行直接绑定,其中该字段的内容填充标签的Src属性。另外,我们可以使该列的单元指向一个外部页面(或者HTTP处理程序),从任何来源获取该图像的字节,并把它们下传给浏览器。如下代码说明了这种方法:
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image009.jpg">
图10.5 GridView控件中的CheckBbox字段
DataImageUrlFormatString="showemployeepicture.aspx?id={0}"
DataAlternateTextField="lastname">
<%# Eval("titleofcourtesy") + " " +
Eval("lastname") + ", " +
Eval("firstname") %>
<%# Eval("title")%>
<%# Eval("notes")%>
ImageField
列中的单元用下一个URL的输出进行填充:
ShowEmployeePicture.aspx?id=xxx
不用说,xxx是employeeid字段中与DataImageUrlField关联的值。有趣的是,替代文本也可以是数据绑定的。我们对替代文本使用DataAlternateTextField属性。图10.6给出了该特征的一个内部预览。图10.6中的页面利用一个模板列生成雇员的信息。我稍候将会介绍模板列的主题。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image010.jpg">
图10.6 GridView控件中的图像字段
如下代码说明了从一个数据库表中获取一个图像并提供它的最简单的代码:
void Page_Load(object sender, EventArgs e)
{
int id = Convert.ToInt32(Request.QueryString["id"]);
string connString = "...";
string cmdText = "SELECT photo FROM employees WHERE employeeid=@empID";
using (SqlConnection conn = new SqlConnection(connString))
{
SqlCommand cmd = new SqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("@empID", id);
byte[] img = null;
conn.Open();
try
{
img = (byte[])cmd.ExecuteScalar();
if (img != null)
{
Response.ContentType = "image/png";
Response.OutputStream.Write(img, EMP_IMG_OFFSET, img.Length);
}
}
catch
{
Response.WriteFile("/proaspnet20/images/noimage.gif");
}
conn.Close();
}
如果规定的字段是null,则上述代码提供一幅标准图像。如果正在使用直接绑定,则通过设置NullImageUrl属性,可以获得相同的结果——即,不是通过外部页面或处理程序传递。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image002.jpg">
注意
该代码片断中的
EMP_IMG_OFFSET
常量一般应该正好为
0
。然而,给定
Northwind
的
Employees
数据库的
photo
列的特定结构,它必须是
78
。不过再次强调,这仅仅是那个表必需的。
7. 模板字段
图10.6展示了一个定制列,其中组合了几个字段的值。这完全是通过使用模板得到的结果。TemplateField列为该网格中的每一行提供一个个性化用户界面,这完全是由页面开发人员定义的。我们可以为各生成阶段定义模板,包括默认视图、原地编辑、标题和页脚。表10.14列出了受支持的模板。
表10.14 受支持的模板
模板
|
描述
|
AlternatingItemTemplate
|
定义交替行的内容和外观。如果没有规定该模板,则使用ItemTemplate。
|
EditItemTemplate
|
定义当前正在编辑的行的内容和外观。该模板应当包含输入字段,而且还可能包含验证程序。
|
FooterTemplate
|
定义该行的页脚的内容和外观。
|
HeaderTemplate
|
定义该行的标题的内容和外观。
|
ItemTemplate
|
定义该行的默认内容和外观。
|
一 个模板化视图可以包含对我们正在创建的应用程序有意义的东西:服务器控件、文字和数据绑定表达式。数据绑定表达式允许我们插入当前数据行中包含的值。我们可以使用一个模板中所需的尽可能多的字段。然而请注意,并非所有的模板都支持数据绑定表达式。标题和页脚模板不是数据绑定的,并且对它使用表达式的任何企 图都将导致一个异常。
如下代码说明了如何为一个产品列定义项目模板。该列显示在两行上,并包括产品的名称和一些有关产品包装的信息。我们将使用数据绑定表达式(参见第9章中的讨论)来引用数据字段:
<%# Eval("productname")%>
available in <%# Eval("quantityperunit")%>
图10.7说明了正在运行的模板字段。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image002.jpg">
注意
TemplateField
类还有一个
InsertTemplate
属性。然而,
GridView
控件从来不使用这种模板。相反,
FormView
控件使用
InsertTemplate
。如前所述,在
ASP.NET 2.0
中,视图控件共享一些字段类,诸如
TemplateField
等。因此,
TemplateField(
以及其他几个类
)
提供了这些属性的一个超集,满足多个视图控件的需要。我们将在下一章介绍
FormView
控件。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image011.jpg">
图10.7 GridView控件中的模板字段
GridView
旨在利用底层数据源控件的具体功能。这样网格控件就能够应对诸如排序、分页、更新和删除等数据操作。一般而言,并非所有的数据源组件都支持所有可能的和可行的数据操作。数据源组件提供布尔属性(诸如CanSort属性),以表明它们是否能够执行一个给定的操作。
重要提示:
如果
GridView
控件通过
DataSource
属性绑定到数据源——即,不利用数据源控件——则就分页和其他操作
(
例如,排序和编辑
)
而言,它的总体行为几乎与
DataGrid
控件的行为一样。在这种情况下,
GridView
激发事件,并期望页面中的绑定代码提供指令和新数据。在本章的其余部分,除了明确说明外,我们指的是一个绑定到数据源控件的
GridView
。
GridView
多少使页面开发人员透明地实现通常所需的特征,诸如排序和分页等。在大多数情况下,我们只需要DataGrid所需代码的一部分;在某些情况下,根本不需要代码。这就是说,不要忘了一个古老而智慧的谚语所说的,“发亮的并不一定全是金子”。换句话说,自己编写的代码越少,就越要依赖于现有基础结构来完成事情。这么做是让系统代替我们做出重大的决策。分页和排序是Web应用程序中的关键操作。我们仍然可以接受GridView默认完成的工作,但是如果确切地知道究竟发生了什么,就能更好地及时诊断和修复在应用程序的生命期内出现的任何性能问题。
1. 无代码数据分页
能够滚动一个潜在的大型数据集,对于现代分布式应用来说是一个重要而具挑战性的特征。一种有效的数据分页机制允许顾客与一个数据库进行交互,而不用占据资源。要启用一个GridView控件上的数据分页功能,只需把AllowPaging属性设置为true。当AllowPaging设为true时,该网格显示一个分页器条,并准备检测用户对分页器按钮的单击。
当用户单击分页器按钮以查看一个新的页面时,该页面回发,但是GridView捕获该事件,并在内部处理它。这就标志着GridView和DataGrid与我们从ASP.NET 1.x获悉的编程模型之间的一个重大区别。对于GridView,不需要为PageIndexChanged事件编写处理程序。它仍然提供该事件(并且与PageIndexChanging配合),但是我们只有在执行额外的操作时才要处理该事件。GridView知道如何检索和显示请求的新页面。让我们分析一下如下控件声明:
DataSourceID="SqlDataSource1" AllowPaging="true" />
SqlDataSource1
绑定到该网格的任何数据立即可分页。如图10.8所示,该控件显示一个具有几个预定义链接(第一个、前一个、下一个和最后一个)的分页器,并且自动选择适合所选页面的行的正确子集。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image012.jpg">
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image013.jpg">
图10.8 遍历GridView控件中的页面
我们与GridView交互的默认用户界面包括页号。添加一个页号标签犹如编写一个PageIndexChanged事件的处理程序那么简单:
protected void GridView1_PageIndexChanged(object sender, EventArgs e)
{
ShowPageIndex();
}
private void ShowPageIndex()
{
CurrentPage.Text = (GridView1.PageIndex + 1).ToString();
}
再次强调,注意PageIndexChanged事件处理程序并不涉及数据绑定或页面选择,而DataGrids的对应事件处理程序则要涉及。如果不需要任何分页后的操作,就可以充满喜悦地全部删除它。
这一明显免费(而神奇的)分页机制需要什么代价呢?
GridView
控件并非真正知道如何获得一个新页面。它只是请求绑定的数据源控件返回i适合规定页面的行。分页最终由数据源控件完成。当一个网格被绑定到一个SqlDataSource控件时,则分页机制要求整个数据源绑定到该控件。当一个网格绑定到一个ObjectDataSource控件时,分页机制取决于我们连接的业务对象的能力。
让我们首先介绍SqlDataSource。我们必须把DataSourceMode设置为DataSet(默认设置)。这就意味着检索整个数据集,并且只显示适合当前页面大小的记录数。在一种极端情况下,最终可能下载1 000条记录,而每次回发只显示10条记录。如果通过把EnableCaching设置为true,启用SqlDataSource上的缓存,则情况会更好一些。在这种情况下,整个数据集只下载一次,并在指定的期限内存储在ASP.NET缓存中。只要数据保持缓存状态,显示任何页面几乎都是免费的。然而,可能有大量数据存储在内存中。因此,只推荐对所有用户共享的较小数据集采用这种方案。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image008.jpg">
提示
如果需要在数据库级对记录进行分页,则我们能做的最好的事情就是把期望的行为编写在一个存储过程中,并把该存储过程绑定到
SqlDataSouce
控件的
SelectCommand
属性上。在这种情况下,关闭缓存。
2. 把分页的负担移交给DAL
如第9章所述,ObjectDataSource控件提供了一个非常通用的接口,适合于严重依赖于底层的业务和数据访问层(DAL)功能的分页。
要点是应该有一个基于分页的业务对象,根据业务对象方法的特征配置ObjectDataSource控件。一旦标识了选择方法,就可以用一个带两个额外参数(页面大小和该页面的起始索引)的版本重载它。最终,该选择方法必须能够检索这些记录的页面。在声明ObjectDataSource控件时,分别把StartRowIndexParameterName和MaximumRowsParameterName属性设置为表示起始索引和页面大小的参数。
还需要的一个步骤是使GridView能够对ObjectDataSource控件提供的数据源进行分页。还需要把ObjectDataSource控件的EnablePaging属性设置为true:
EnablePaging="true"
TypeName="ProAspNet20.DAL.Customers"
StartRowIndexParameterName="firstRow"
MaximumRowsParameterName="totalRows"
SelectMethod="LoadByCountry">
PropertyName="SelectedValue" />
DataSourceID="ObjectDataSource1" AllowPaging="true"
OnPageIndexChanged="GridView1_PageIndexChanged">
在上述代码中,只显式地规定了使该方法起作用很重要的内容。两个与分页相关的参数留给GridView去设置。页面大小参数自动地绑定到GridView的PageSize属性;页面大小乘以页面索引决定了要检索的第一个索引。下面给出了LoadByCountry方法的原型:
public static CustomerCollection LoadByCountry(string country) {
LoadByCountry(country, -1, 0);
}
public static CustomerCollection LoadByCountry(string country,
int totalRows, int firstRow) {
// Retrieve the specified subset of records
}
ObjectDataSource
的机制并没有涉及很多有关分页算法的有效性的问题。业务对象实际上是如何检索被请求页面中的记录是一个与具体的实现和应用程序有关的问题。在示例代码中,LoadByCountry运行原始查询,并对整个数据集获取一个数据阅读器。接着,它丢弃所有不在规定范围内的记录。这种实现方法是简单性和有效性的良好折衷。它可能不是最好的解决方案,但是它易于实现和证明。内存消耗每次只限于一个记录,但是数据库返回整个数据集。
3. 分页算法
GridView
不支持DataGrids上的AllowCustomPaging属性。然而,对分页算法进行定制的确是可能的。定制分页算法的核心是提供一种最小化记录缓存的记录页提取方法。在理想情况下,我们应要求数据库对特定查询的结果进行分页。然而,很少有数据库支持该特征。还存在其他几种方法,各有优缺点。
一种可能的策略需要创建临时表,仅用于选择我们真正需要的记录的子集。我们创建一个存储过程,并把指示页面大小和索引的参数传给它。另外,我们可以使用嵌套的SELECT命令和TOP语句,检索被请求页面中最后一个记录前的所有记录,然后颠倒顺序,丢弃不需要的记录。再次强调,TOP子句并非是所有数据库共有的。另一种可能的方法基于如下博客贴中讨论的动态创建的SQL代码:http://weblogs.sqlteam.com/jeffs/archive/2004/ 03/22/1085.aspx。
如果能够与数据库管理员(DBA)合作,则可以要求添加一个特别的列,以便对这些查询进行索引。在这种情况下,DAL必须保证该列中的值构成这些值的一个正则表达式,并且是可计算的。完成此任务的最简单的方法是赋予该列累进数字。
4. 分页器配置
当AllowPaging属性设置为true时,网格显示一个分页器条。通过和标签或者它们的相当属性,可以在很大程度上控制分页器的特征。GridView控件的分页器还支持第一页和最后一页按钮,并允许我们把一个图像赋给每个按钮。(这对于DataGrids也是可能的,但是它需要大量代码。)分页器能够以如下两种模式进行工作:显示显式的页面编号,或者提供一个相对的导航系统。在前一种情况下,分页器包含数字连接,表示一个页面索引。在后一种情况下,按钮的存在是为了导航到下一页或前一页,甚至导航到第一页或最后一页。Mode属性规定分页器的用户界面。表10.15列出了可用的模式。
表10.15 网格分页器的模式
模式
|
描述
|
NextPrevious
|
显示下一页和前一页按钮,用于防问网格中的下一页和前一页。
|
NextPreviousFirstLast
|
显示下一页和前一页按钮,以及第一页和最后一页按钮,用于直接访问网格的第一页和最后一页。
|
Numeric
|
显示与网格的页面对应的数字链接按钮。
|
NumericFirstLast
|
显示与网格的各页以及与直接访问网格的第一页和最后一页的第一页和最后一页按钮对应的数字链接按钮。
|
特别的属性对,xxxPageText和xxxPageImageUrl,允许我们任意设置这些按钮的标签。xxx代表如下含义:First(第一页)、Last(最后一页)、Next(下一页)或Previous(前一页)。图10.9展示了一个正在运行的示例页面。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image014.jpg">
图10.9 带有两个分页器的可分页GridView
根据网格的大小,一个网格中的第一行和最后以行可能不一定适合屏幕范围。为了使用户更容易分页,而不管滚动条的位置,可以启用网格的顶部和底部分页器。通过设置元素上的Position属性可以做到这一点:
其他方案是仅在网格的顶部显示分页器,或者只在网格的底部显示分页器。
GridView
控件的分页器,必要时完全可以用一个新的分页器代替。(参见图10.10。)通过添加元素到该控件的声明中可以做到这一点。下面给出了一个示例:
Text="First" />
Text="<<" />
Text=">>" />
Text="Last" />
为了处理这些按钮上的单击事件,编写一个RowCommand事件处理程序,并显式地设置页面索引:
void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (e.CommandName == "Last")
GridView1.PageIndex = GridView1.PageCount - 1;
if (e.CommandName == "First")
GridView1.PageIndex = 0;
if (e.CommandName == "Next")
GridView1.PageIndex ++;
if (e.CommandName == "Prev")
GridView1.PageIndex --;
}
不可否认,该代码非常简单,应对它略加充实,至少要使它能够在到达第一个获最后一个索引时禁用其他按钮。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image015.jpg">
图10.10 带有一个定制分页器的可分页GridView
排序是一种需慎重处理、非线性的操作,如果在客户端执行其代价通常非常高。一般而言,实际上,对记录进行排序的最佳地方是在数据库环境中,由于我们在大多数时候最终运行的是超优化的代码。在分析GridView控件和数据源控件的排序基础结构时,要注意这一点。GridView没有实现排序算法,而是依赖于数据源控件(或者页面,如果绑定到一个可列举的对象)提供排序数据。
1. 无代码的数据排序
为了启用GridView的排序功能,把AllowSorting属性设置为true。如果启用了排序,则GridView能够作为链接生成这些列的标题文本。通过使用SortExpression属性,可以将每一列与一个排序表达式进行关联。排序表达式任何用逗号分开的列名序列。每个列名可以使用DESC或ASC等顺序限定符。DESC表示降序,而ASC表示升序。ASC限定符是默认的;如果忽略该限定符,则该列按升序排列。如下代码建立按productname数据源列进行排序的GridView列:
AllowSorting="true" AutoGenerateColumns="false">
sortexpression="productname" />
headertext="Packaging" />
正如分页一样,用GridView实现排序不需要人工编写任何代码。在正确配置下,GridView的排序基础结构可以在不用进一步干预的情况下以双向的方式起作用——即,如果单击一个按降序排列的列,则按升序对该列进行排序,反之亦然。只有在需要实现更高级的功能(诸如在标题中显示一幅指示排序方向的符号)时才需要添加一些定制代码。(稍候会讨论更多有关这方面的内容。)
正如分页一样,排序的主要障碍是底层的数据源控件如何实现它。让我们看看把网格绑定到一个SqlDataSource对象时会发生什么。除了把AllowSorting设置为true,并把排序表达式添加到可排序列外,不需要任何其他操作。(参见图10.11。)
当用户单击列以对它进行排序时,网格要求SqlDataSource控件返回已排序的数据。如前所述,SqlDataSource控件默认地返回一个DataSet。如果这样,则该控件检索数据,由它建立一个DataView,并调用该DataView的Sort方法。这种方法可以很好地起作用,但是绝对不是最好的排序方法。我们可能会发现这种方法非常适合我们的应用程序,但是请注意,排序是使用Web服务器的内容执行的。在与缓存机制相结合的情况下,在内存中进行分页和排序都是较小的共享的记录集的可行方案。
有没有可能从数据库服务器中获取预排序的数据呢?第1步是把SqlDataSource控件的DataSourceMode属性设置为DataReader。如果把它设置为DataSet,则排序将在内存中进行。第2步要求我们编写一个检索数据的存储过程。为了得到经过排序的数据,还要把数据源控件的SortParameterName属性设置为指示排序表达式的存储过程参数的名称。显然,我们需要该存储过程动态地建立它的命令文本,以结合正确的ORDER BY子句。下面说明了如何修改Northwind的存储过程CustOrderHist,使它的结果可以任意排序:
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image016.jpg">
图10.11 绑定到一个SqlDataSource控件的可排序GridView
CREATE PROCEDURE CustOrderHistSorted
@CustomerID nchar(5), @SortedBy varchar(20)='total' AS
SET QUOTED_IDENTIFIER OFF
IF @SortedBy = ''
BEGIN
SET @SortedBy = 'total'
END
EXEC (
'SELECT ProductName, Total=SUM(Quantity) ' +
'FROM Products P, [Order Details] OD, Orders O, Customers C ' +
'WHERE C.CustomerID = "' + @CustomerID + '" ' +
'AND C.CustomerID = O.CustomerID AND O.OrderID = OD.OrderID ' +
'AND OD.ProductID = P.ProductID GROUP BY ProductName ' +
'ORDER BY ' + @SortedBy)
GO
这时,网格准备显示已排序的数据列,并把排序的负担转交给数据库管理系统(DBMS):
DataSourceMode="DataReader"
ConnectionString='<%$ ConnectionStrings:LocalNWind %>'
SortParameterName="SortedBy"
SelectCommand="CustOrderHistSorted"
SelectCommandType="StoredProcedure">
Name="CustomerID" PropertyName="SelectedValue" />
在数据库上对数据进行排序(如这里所示)与缓存是不可兼得的,我们必须知道这一点。我们需要把EnableCaching设置为false;否则会抛出一个异常。结果是,用户每次单击一列以对数据进行排序时就要返回到数据库。
如果使用DataSet模式并启用缓存,最初从数据库获取数据,并如期望的那样进行排序,但是随后的排序操作在内存中解决。最后,如果使用DataSet模式并禁用缓存,每次仍然要下到数据库中去排序。注意,这里之所以提及这种方案只是为了论述完备性:效果与使用DataReader是相同的,但是在需要缓存时,数据阅读器是一种更有效的方法。
一般而言,SortParameterName属性的推出,开辟了可以对其他一些主要使用数据而不需要分页或缓存的数据绑定控件(如Repeater和定制控件)的内容进行排序的可能性。
2. 把排序负担转交给DAL
如果使用ObjectDataSource控件会怎样呢?在这种情况下,排序的负担应转交给DAL或业务层,并通过所绑定的业务对象的编程接口提供给数据源控件。让我们修改前面分析的用于分页的LoadByCountry方法,向它添加一个指示排序表达式的新参数:
public static CustomerCollection LoadByCountry(
string country, int totalRows, int firstRow, string sortExpression)
{
CustomerCollection coll = new CustomerCollection();
using (SqlConnection conn = new SqlConnection(ConnectionString))
{
SqlCommand cmd;
cmd = new SqlCommand(cmdLoadByCountry, conn);
cmd.Parameters.AddWithValue("@country", country);
if (!String.IsNullOrEmpty(sortExpression))
cmd.CommandText += " ORDER BY " + sortExpression;
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
HelperMethods.FillCustomerList(coll, reader, totalRows, firstRow);
reader.Close();
conn.Close();
}
return coll;
}
cmdLoadByCountry
常量表示我们用来检索数据的SQL命令或存储过程。正如我们可以看到的,该方法的实现只是向已有的命令中添加一个可选的ORDER BY子句。这可能不是曾经设计的最好的方法,但它无疑满足把排序的负担转交给DAL并从DAL交给数据库的需要。这时,要把ObjectDataSource控件上的SortParameterName设置为该方法的决定排序的参数——本例中为sortExpression:
EnablePaging="true"
TypeName="ProAspNet20.DAL.Customers"
SortParameterName="sortExpression"
StartRowIndexParameterName="firstRow"
MaximumRowsParameterName="totalRows"
SelectMethod="LoadByCountry">
...
这种方法的优势是充分利用了排序机制,并且我们可以决定如何实现它、在哪里实现它以及何时实现它。我们可能要在DAL中编写一些排序代码,但只编写高度集中的代码。实际上,不需要任何基础结构代码,因为ASP.NET为我们奠定了这种基础。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image002.jpg">
注意
关于
GridView
控件上的排序还值得一提的一点是,如果需要可以撤销排序操作。为此,为
Sorting
事件编写一个处理程序,获得该事件的参数数据
(GridViewSortEventArgs
类型
)
,并把
Cancel
属性设置为
true
。
3. 提供用户反馈
GridView
控件不会自动地把任何可视元素添加到指示排序方向的输出中。下面给出了需要进行一些编码才能完成排序的少数几种情况之一:
void GridView1_RowCreated (object sender, GridViewRowEventArgs e) {
if (e.Row.RowType == DataControlRowType.Header)
AddGlyph(MyGridView, e.Row);
}
void AddGlyph(GridView grid, GridViewRow item) {
Label glyph = new Label();
glyph.EnableTheming = false;
glyph.Font.Name = "webdings";
glyph.Font.Size = FontUnit.Small;
glyph.Text = (grid.SortDirection==SortDirection.Ascending ?"5" :"6");
// Find the column you sorted by
for(int i=0; i
string colExpr = grid.Columns[i].SortExpression;
if (colExpr != "" && colExpr == grid.SortExpression)
item.Cells[i].Controls.Add (glyph);
}
}
其思想是为RowCreated事件创建一个处理程序,并寻找创建标题的时刻。接着创建一个新的Label控件,表示我们需要添加的符号。该Label应添加到哪里呢?
新建的Label控件设置了字体和文本,足以产生一个指示排序方向的符号(通常是▲和▼)。(这两个符号对应于Microsoft Webdings字体中的5和6。)我们必须将它与被单击列的文本一起添加。该列的索引可以在Sorting事件中存储到视图状态中。另外,可以仅仅检索它,将当前排序表达式(网格的SortExpression属性)与该列的表达式进行比较。一旦知道该列的索引,则检索对应的表单元并添加该标签:
item.Cells[i].Controls.Add (glyph);
结果如图10.12所示。如果页面基于一个主题,则Label控件的字体(对于正确地显示符号是必不可少的)可能被覆盖。为了避免这一点,应当禁用标签控件的主题支持。EnableTheming属性正好解决了这个问题。
4. 对分页和排序使用回调
排序和分页操作都需要一次页面回发,随后完全刷新该页面。在大多数情况下,这是一个重型操作,因为页面通常包含大量图形。为了让用户感受到更好的体验,如果该网格可以下探到Web服务器,获取新记录集,并只更新界面的某一部分,这样不是更好吗?多亏了ASP.NET脚本回调(我在另一本最近出版的书,《Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics [Microsoft Press, 2005]》中,更为详细地介绍了脚本回调),GridView控件能够提供该特征。而我们只需打开布尔属性EnableSortingAndPagingCallbacks。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image017.jpg">
图10.12 增强GridView控件的排序功能
如前所述,该特征依赖于ASP.NET脚本回调引擎的服务,该特征也可以应用于非Internet Explorer浏览器,包括Firefox、Netscape 6.x和更新的版本、Safari 1.2,以及最新的Opera浏览器。
SqlDataSource与ObjectDataSource
几方面的考虑因素将有助于澄清何时使用SqlDataSource和ObjectDataSource控件。首先,记住这些数据源控件并非是想要执行稳定的数据绑定的开发人员的惟一两种选择。然而,到目前为止,它们是最流行、最常用的。在ASP.NET 2.0中,数据绑定绝非只能使用数据源控件,记住这一点也是重要的。这就是说,SqlDataSource和ObjectDataSource只是ASP.NET工具箱中的工具,如果合适就可以使用它们。
在我看来,SqlDataSource最适合于非连接的数据绑定方法,并在通过DataSet获取数据时起到最佳作用。只有在这种情况下,才能启用分页、排序和缓存功能。在三种功能中,只有排序功能在数据阅读器模式有些重复。如果使用DataSets适合我们的应用程序,则使用SqlDataSource是一种不错的选择。它为我们提供了现成的解决方案,其中大多数是易于编写的声明式代码,但是这种方法在实际应用中并不一定有效。换句话说,在一个应用程序中使用SqlDataSource可能对某些特征有好处,但对于驱动整个DAL是不够的。
相反,如果意识到需要对分页和排序操作(诸如定制分页或服务器端排序)保持更多的控制,则改用ObjectDataSource似乎是一种更好的思想。在这种情况下,首先设计并实现一个完整的DAL和(有选择地)一个业务层。在该层中,我们精心制作需要从网格那里得到支持的任何功能:分页、排序,甚至数据缓存。注意,如果使用定制集合代替ADO.NET容器类,则不支持缓存功能,但是实现一个个性化的缓存层并不没有什么困难。
如果使用ObjectDataSource,则需要我们自己负责实现那些多少有点类似于ASP.NET 1.x中的DataGrids的关键特征。这样如何呢?我们并非只是在一些代码隐藏类中插入少量代码;而是在应用程序的DAL中插入逻辑。我们仍然要编写代码,但是我们要编写的代码的质量非常不同!
此外,ObjectDataSource控件完全支持定制实体类和定制集合。在.NET Framework 2.0中,对范型的支持使编写定制集合容易多了,并且显著减少了编写一个完全定制的、建立在量身定制的并与特定领域有关的对象基础之上的DAL的成本。
GridView
控件的主要优点(弥补了DataGrid的主要缺点)是能够更新数据源。DataGrid控件仅提供数据编辑的基础结构。它提供了必要的用户界面元素,并在用户修改某个数据字段的值时激发合适的事件,但它没有把那些变更提交回到数据源。开发人员失望地发现他们必须编写大量刻板的代码以真正地持久存留变更。
有了GridView控件,如果所绑定的数据源支持更新,则该控件可以自动地执行该操作,从而提供真正的开箱即用(out-of-the-box)解决方案。数据源控件通过CanUpdate布尔属性表明它的更新功能。
非常类似于DataGrid,GridView可以为网格中的每一行生成一个命令按钮列。这些特殊的命令列包含编辑或删除当前记录的按钮。如果使用DataGrid控件,则必须使用一个专门的列类型(EditCommandColumn类)显式地创建一个编辑命令列。GridView大大地简化了更新和删除操作。
1. 原地编辑和更新
原地编辑指网格能够支持对当前显示记录的变更的能力。通过打开AutoGenerateEditButton布尔属性,启用一个网格视图上的原地编辑功能:
autogeneratecolumns="false" autogenerateeditbutton="true">
...
当AutoGenerateEditButton属性设置为true时,GridView显示一个附加列,就像图10.13所示的那样。通过单击Edit按钮,使所选行进入编辑模式,并且可以任意输入新数据。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image018.jpg">
图10.13 一个支持原地编辑的GridView控件
要停止编辑并丢弃任何变更,用户只要单击Cancel按钮。GridView可以处理该单击,而不需要任何外部支持;这些返回到原来的只读状态;而EditIndex属性返回到它的默认值-1,表示当前没有编辑任何行。但是,如果用户单击了更新链接会怎样呢?GridView首先激发RowUpdating事件,然后在内部检查数据源控件上的CanUpdate属性。如果CanUpdate返回false,则抛出一个异常。如果数据源控件没有定义更新命令,则CanUpdate返回false。
假设我们的网格绑定到一个SqlDataSource对象。为了在用户更新时持久地保存变更,必须如下面这种设计自己代码:
ConnectionString="<%$ ConnectionStrings:LocalNWind %>"
SelectCommand="SELECT employeeid, firstname, lastname FROM employees"
UpdateCommand="UPDATE employees SET
firstname=@firstname, lastname=@lastname
WHERE employeeid=@original_employeeid">
AutoGenerateColumns="false"
DataKeyNames="employeeid" AutoGenerateEditButton="true">
UpdateCommand
属性设置为用来执行更新的SQL命令。编写该命令时,根据需要可以声明任意多个参数。然而,如果忠实于某个具体的命名约定,则参数值自动进行解析。代表要更新字段(诸如firstname)的参数必须匹配一个网格列的DataField属性的名称。该参数在WHERE子句中用来确认工作记录必须匹配DataKeyNames属性(已显示记录的键)。original_XXX格式字符串是标识参数所必需的。通过数据源控件上的OldValuesParameterFormatString属性可以改变该方案。
成功地完成一个更新命令通过RowUpdated事件通知整个网格。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image002.jpg">
注意
GridView
从输入字段收集值,并填充一个由名称
/
值对组成的字典,指示该行中每个字段的新值。
GridView
还提供了一个
RowUpdating
事件,它允许程序员验证传递给数据源对象的值。此外,
GridView
在启动关联数据源上的更新操作之前自动地调用
Page.IsValid
。如果
Page.IsValid
返回
false
,则取消该操作。如果使用一个带验证器的定制模板,则该属性特别有用。
如果网格绑定到一个ObjectDataSource控件,则情况有些不同。绑定的业务对象必须有一个更新方法。该方法将接受满足其工作所需的任意多个参数。我们可以决定逐个传递参数,或者用一个惟一的数据结构组织这些参数。如果有一个非常好的DAL,则优先使用第二种方案。下面给出了一个实例:
TypeName="ProAspNet20.DAL.Customers"
SelectMethod="LoadAll"
UpdateMethod="Save"
DataObjectTypeName="ProAspNet20.DAL.Customer">
DataKeyNames="id" AutoGenerateColumns="false">
AutoGenerateEditButton="true"
Save
方法可以有如下原型和实现:
public static void Save(Customer cust)
{
using (SqlConnection conn = new SqlConnection(ConnectionString))
{
SqlCommand cmd = new SqlCommand(cmdSave, conn);
cmd.Parameters.AddWithValue("@id", cust.ID);
cmd.Parameters.AddWithValue("@companyname", cust.CompanyName);
cmd.Parameters.AddWithValue("@city", cust.City);
cmd.Parameters.AddWithValue("@address", cust.Street);
...
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
return;
}
}
要运行的实际SQL命令(或存储过程)只不过是带有一个SET子句列表的经典的UPDATE语句。DataObjectTypeName属性指示一个类的名称,ObjectDataSource在一个数据操作中用该类作为一个参数。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image002.jpg">
注意
如果设置了
DataObjectTypeName
属性,则所有的数据方法不是无参数的,就是接受一个指定类型的对象。无论是否声明式地填充该方法的参数集,都这样。
DataObjectTypeName
属性优先于参数集合。
2. 删除显示的记录
从GridView的角度看,删除记录与更新记录差不多。在这两种情况下,GridView利用数据源的执行数据操作的能力。通过将AutoGenerateDeleteButton属性的值设置为true,启用记录删除功能。GridView生成一个按钮列,如果单击这些按钮,则针对所绑定的数据源控件上的对应行调用删除命令;并向该数据源方法传递一个由名称/值组成的键字段对字典,用来惟一地标识要删除的行:
ConnectionString="<%$ ConnectionStrings:LocalNWind %>"
SelectCommand="SELECT employeeid, firstname, lastname FROM employees"
UpdateCommand="UPDATE employees SET
firstname=@firstname, lastname=@lastname
WHERE employeeid=@original_employeeid"
DeleteCommand="DELETE employees WHERE
employeeid=@original_employeeid" />
GridView
不会提供有关将要发生的操作的任何反馈信息。它在执行删除之前调用Page.IsValid,如果有一个带验证器的定制模板,这么做是有用的。此外,RowDeleting事件为我们提供了另一个以编程方式控制该操作的合法性的机会。
如果由于数据库特定的限制没有删除该记录,则删除操作失败。例如,如果子记录通过某个关系引用一个记录,则不能删除该纪录。这时会抛出一个异常。
要通过ObjectDataSource控件删除一个记录,则为业务对象提供一对方法,如下所示:
public static void Delete(Customer cust)
{
Delete(cust.ID);
}
public static void Delete(string id)
{
using (SqlConnection conn = new SqlConnection(ConnectionString))
{
SqlCommand cmd = new SqlCommand(cmdDelete, conn);
cmd.Parameters.AddWithValue("@id", id);
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
return;
}
}
重载删除方法并非是必要条件,但这么做是有用的,这无疑会使我们的DAL更灵活、更易于使用。
3. 插入新记录
目前,GridView控件并不支持向数据源对象插入数据。缺少这个功能是由于GridView实现的结果,而不是由于底层数据源的功能和特征。实际上,所有的数据源控件都支持一个插入命令属性。正如我们在下一章将会看到的,DetailsView和FormView控件完全支持新记录的插入。
在ASP.NET 1.x中,使DataGrid控件支持记录插入的常见做法要求我们修改页脚或分页器,为空的文本框和按钮腾出空间。GridView支持相同的模型,并通过PagerTemplate属性使涉及分页器的情况更简单。通过RowCreated事件修改页脚的内容是可能的(稍后再作详细介绍)。然而请注意,如果网格绑定到一个空的数据,则页脚条被隐藏。如果希望用户能够把一个新记录添加到一个空的网格中该怎么办呢?利用EmptyDataTemplate,具体如下所示:
There's no data to show in this view.
Text="Add New Record" />
要捕获该按钮上的用户单击,为RowCommand事件编写一个处理程序:
void Gridview1_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (e.CommandName == "AddNew")
{ ... }
}
为了使GridView控 件的概述更完整,我们完全需要分析两种常见的编程场景:下钻和行定制。一个网格把一系列数据项提交给用户;在很多情况下,用户需要从这些数据项中选择一个,并开始对它的操作。如前所述,按钮列是为了简化此任务而存在的。我们过一会将深入讨论该主题。行定制是另一个常见特征,它使我们有机会修改网格的标准 外观。我们可以修改行布局,添加或删除数据单元,或者在每行的基础上修改可视属性,使某些行看起来不同于其他行(例如,表示负值的行)。
1. 对给定的行执行一个操作
让我们回到本章前面讨论按钮列时简单提到的一个问题。假设我们正在创建一个电子商务应用程序;其中一个页面显示产品网格以及一些允许用户把产品添加到购物车中的按钮。我们添加一个按钮列,并编写RowCommand事件的处理程序:
void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (e.CommandName.Equals("Add"))
{
// Get the index of the clicked row
int index = Convert.ToInt32(e.CommandArgument);
// Create a new shopping item and add it to the cart
AddToShoppingCart(index);
}
}
这是我们前文讲到而没有深入下去的地方。现在让我们再往前迈一步,扩展AddToShoppingCart的代码。该方法的目的是什么?通常,它检索一些关于被单击产品的信息,并把它存储在表示购物车的数据结构中。在示例代码中,购物车是一个名为ShoppingCart的定制集合:
public class ShoppingCart : List
{
public ShoppingCart()
{
}
}
ShoppingItem
是一个定制类,描述一件已购买的产品。它包含几个属性:产品ID、产品名称、单价和购买量。购物车存储在会话状态中,并通过一个名为MyShoppingCart的页级属性提供给外界:
protected ShoppingCart MyShoppingCart
{
get
{
object o = Session["ShoppingCart"];
if (o == null) {
InitShoppingCart();
return (ShoppingCart) Session["ShoppingCart"];
}
return (ShoppingCart) o;
}
}
private void InitShoppingCart()
{
ShoppingCart cart = new ShoppingCart();
Session["ShoppingCart"] = cart;
}
AddToShoppingCart
的核心目标只是创建一个用被单击产品的信息填充的HoppingItem对象。如何检索该信息呢?
正如我们可以看到的,GridView存储GridViewCommandEventArgs结构的CommandArgument属性中被单击行的索引。此信息是必需的,但不足以满足我们的目的。我们需要把该索引转换为该网格行背后的产品。最好是把该网格行索引转换为数据集索引,以获取被单击的网格行中生成的数据项对象。
GridView
的DataKeyNames属性指示要持久保存在视图状态中的数据字段的名称,以便在回发事件(诸如RowCommand)的后期获取它们。作为一个字符串数组实现的DataKeyNames,是DataGrid控件的DataKeyField属性在GridView中的对应属性。它包含一个DataGrid中显示的行的主键和GridView的许多属性:
DataSourceID="SqlDataSource1"
DataKeyNames="productid,productname,unitprice" ... />
我们在DataKeyNames中应列出多少个字段?考虑到那里所列的每个字段占据了一些视图状态空间。另一方面,如果只限于自己存储主键字段,则需要运行一个查询以获取我们所需的所有数据。哪种方法更好,取决于我们真正需要做什么。在我们的示例环境中,我们需要制作已经缓存在Web服务器的内存中的产品的副本。但不需要运行查询来获取我们已经知道的数据。要填充一个ShoppingItem对象,需要产品ID、名称和单价:
private void AddToShoppingCart(int rowIndex)
{
DataKey data = GridView1.DataKeys[rowIndex];
ShoppingItem item = new ShoppingItem();
item.NumberOfItems = 1;
item.ProductID = (int) data.Values["productid"];
item.ProductName = data.Values["productname"].ToString();
item.UnitPrice = (decimal) data.Values["unitprice"];
MyShoppingCart.Add(item);
ShoppingCartGrid.DataSource = MyShoppingCart;
ShoppingCartGrid.DataBind();
}
DataKeyNames
中列出的字段的值包装在DataKeys数组中——这是DataGrid开发人员的老相识。DataKeys是一个DataKey对象的数组,而DataKey对象是一种有序字典。我们通过Values集合访问被持久保存的字段的值,如前面的代码所示。
为用户界面计,购物车的内容绑定到另一个GridView控件,以便用户可以在任何时候都看到他们的购物车中有什么。该绑定是通过经典的DataSource对象发生的。关于该特征的意图,可以回头看看图10.3。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image019.jpg">
警告
只有在生成该控件时,每个网格行才被绑定到一个数据项——数据源中的一个数据行。诸如
RowCommand
等的回发事件,在到达该阶段之前激发。因而,被单击的
GridViewRow
对象的
DataItem
属性
(
我们需要的数据应该保存在这里
)
,如果从
RowCommand
处理程序的内部访问它,则难免会为
null
。这就是为什么需要
DataKeyNames
和相关的
DataKeys
属性。
2. 选择一个给定行
选择被单击行的更一般的机制,可以通过一个特殊的命令按钮(即,选择按钮)实现。如同删除和编辑按钮的情况一样,通过设置AutoGenerateSelectButton布尔属性打开它。为了充分利用该选择特征,建议大家还要为所选行添加一个样式:
...
当用户单击一个选择按钮时,页面接受一个更具体的SelectedIndexChanged事件。一些属性,诸如SelectedIndex、SelectedRow和SelectedDataKey也被更新。为了完备性,注意到一行被选中时,该页面首先收到一个RowCommand事件,然后再收到SelectedIndexChanged事件。然而,当RowCommand事件激发时,还没有更新任何一个选择属性。
如下代码说明了如何重写前一个实例,以便把所选的产品添加到购物车中:
protected void GridView1_SelectedIndexChanged(object sender, EventArgs e)
{
AddToShoppingCart();
}
private void AddToShoppingCart()
{
DataKey data = GridView1.SelectedDataKey;
ShoppingItem item = new ShoppingItem();
item.NumberOfItems = 1;
item.ProductID = (int) data.Values["productid"];
item.ProductName = data.Values["productname"].ToString();
item.UnitPrice = (decimal) data.Values["unitprice"];
MyShoppingCart.Add(item);
ShoppingCartGrid.DataSource = MyShoppingCart;
ShoppingCartGrid.DataBind();
}
正如大家可以看到的,我们不需要传递行索引,因为SelectedDataKey属性提供了对应的DataKey对象。(参见图10.14。)
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image020.jpg">
图10.14 把所选的产品添加到购物车中
3. 行定制
需要一个说明为什么以定制的方式生成网格行通常是重要的简明实例吗?让我们看一看图10.14。用户正好把一件已经停售的产品添加到购物车中。如果能够禁用任何匹配某个标准的行,或者更简单地,能够根据运行时条件定制行布局,那不是更好吗?让我们看看如何做到这一点。
有两个GridView事件对此任务是必不可少的:RowCreated和RowDataBound。创建网格的任何行(无论是标题、页脚、项目、交替项、分页器还是其他)时,前一个事件激发。当新创建的行绑定到它的数据项(即,被绑定数据源中的对应记录)时,后一个事件激发。RowDataBound事件不会对网格中的所有行激发,而只对那些表示被绑定数据项的网格行激发。对于标题、页脚和分页器,不激发任何事件。
作为第一个实例,让我们看看如何禁用Discontinued字段返回true的行的Select链接。在这种情况下,我们需要一个RowDataBound事件处理程序,因为所需的定制取决于被绑定数据行上的值。如前所述,当RowCreated事件激发时还没有此信息:
void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
object dataItem = e.Row.DataItem;
bool discontinued = (bool) DataBinder.Eval(dataItem, "discontinued");
e.Row.Enabled = !discontinued;
}
}
一般而言,首先检查行的类型。确切地说,这种测试对于RowDataBound事件并非是严格必需的,因为该事件只为数据行激发。数据项(即,对应的记录)是通过GridViewRow对象的DataItem属性获取的。接着,获取所关心的字段,并应用自己的逻辑。我们可能事先不知道该行所绑定的数据对象的类型。DataBinder.Eval方法是一个范型访问器,通过反射起作用,而不管底层的对象。如果需要禁用整个数据行(及其所含的控件),则可以关闭网格行对象的Enabled属性。要访问一个特定控件,需要在该网格的对象模型中找到自己的方法。下面给出了如何访问(和禁用)Select链接:
((WebControl)e.Row.Cells[0].Controls[0]).Enabled = !discontinued;
该代码之所以能起作用,是因为Select链接总是为每个数据行的第一个单元的第一个控件。图10.15展示了前面的产品列表,但是禁用了停售产品。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href="http://book.csdn.net/BookFiles/73/10/image021.jpg">
图10.15 对应于停售产品的行现在被禁用了
只要理解了网格行对象模型,实际上可以做我们想做的任何事情。