首页 > 代码库 > GridView控件

GridView控件

 
GridView是ASP.NET 1.x的DataGrid控件的后继者。它提供了同样的基本功能集,同一时候添加?了大量扩展和改进。如前所述,DataGrid(ASP.NET 2.0仍然全然支持)是一个功能很强大的通用控件。然而,它有一个重大缺陷:它要求我们编写大量定制代码,甚至处理比較简单而常见的操作,诸如分页、排序、编辑或删除数据等也不例外。GridView控件旨在解决此限制,并以尽可能少的数据实现双向数据绑定。该控件与新的数据源控件系列紧密结合,并且仅仅要底层的数据源对象支持,它还能够直接处理数据源更新。
这样的实质上无代码的双向数据绑定是新的GridView控件最著名的特征,可是该控件还增强了非常多其它功能。该控件之所以比DataGrid控件有所改进,是由于它可以定义多个主键字段、新的列类型以及样式和模板选项。GridView另一个扩展的事件模型,同意我们处理或撤销事件。
GridView控件为数据源的内容提供了一个表格式的类网格视图。每一列表示一个数据源字段,而每一行表示一个记录。该类声明例如以下:
public class GridView : CompositeDataBoundControl,
                           ICallbackContainer,
                           ICallbackEventHandler
该基类确保数据绑定和命名容器支持。ICallbackContainer和ICallbackEventHandler接口提供了比方今支持的更有效的分页和排序功能。它通过使用新的脚本回调技术的client的out-of-band调用来完毕。(稍候将会更具体地讨论这一点。)首先让我们来看看GridView控件的编程接口。
1. GridView控件的属性
GridView支持大量属性,这些属性属于例如以下几大类:行为、可视化设置、样式、状态和模板。表10.6具体描写叙述了影响的行为的属性。 
 
表10.6  GridView控件的行为属性
属性
描写叙述
AllowPaging
指示该控件是否支持分页。
AllowSorting
指示该控件是否支持排序。
AutoGenerateColumns
指示是否自己主动地为数据源中的每一个字段创建列。默觉得true。
AutoGenerateDeleteButton
指示该控件是否包括一个button列以同意用户删除映射到被单击行的记录。
AutoGenerateEditButton
指示该控件是否包括一个button列以同意用户编辑映射到被单击行的记录。
AutoGenerateSelectButton
指示该控件是否包括一个button列以同意用户选择映射到被单击行的记录。
DataMember
指示一个多成员数据源中的特定表绑定到该网格。该属性与DataSource结合使用。假设DataSource是有一个DataSet对象,则该属性包括要绑定的特定表的名称。
DataSource
获得或设置包括用来填充该控件的值的数据源对象。
DataSourceID
指示所绑定的数据源控件。
EnableSortingAndPagingCallbacks
指示是否使用脚本回调函数完毕排序和分页。默认情况下禁用。
RowHeaderColumn
用作列标题的列名。该属性旨在改善可訪问性。
SortDirection
获得列的当前排序方向。
SortExpression
获得当前排序表达式。
UseAccessibleHeader
规定是否为列标题生成<th>标签(而不是<td>标签)。
SortDirection和SortExpression属性规定当前决定行的排列顺序的列上的排序方向和排序表达式。这两个属性都是在用户单击列的标题时由该控件的内置排序机制设置的。整个排序引擎通过AllowSorting属性启用和禁用。EnableSortingAndPagingCallbacks属性打开和关闭该控件的使用脚本回调进行分页和排序,而不用往返于server并改变整个页面的功能。
GridView控件内显示的每一行相应于一种特殊的网格项。提前定义的项目类型差点儿等于DataGrid的项目类型,包含标题、行和交替行、页脚和分页器等项目。这些项目是静态的,由于它们在控件的生命期内在应用程序中保持不变。其它类型的项目在短暂的时间(即,完毕某种操作所需的时间)内是活动的。动态项目是编辑行、所选的行和EmptyData项。当网格绑定到一个空的数据源时,EmptyData标识该网格的主体。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href=http://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image002.jpg"> 注意    GridView控件提供了几个专门为了可訪问性而设计的属性。这些属性是UseAccessibleHeaderCaptionCaptionAlignRowHeaderColumn。设置RowHeaderColumn时,将用默认的标题样式(黑体字)输出该列的全部单元。然而,ShowHeaderHeaderStyle和其它与标题相关的属性并不会影响由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
引用一个同意我们设置分页器button的属性的对象。
ShowFooter
指示是否显示页脚行。
ShowHeader
指示是否显示标题行。
 
PagerSettings对象把全部能够对分页器设置的可视化属性组织在一起。当中有非常多属性在DataGrid程序猿看来应该是熟悉的。PagerSettings类还加入?了一些新属性以满足新的提前定义的button(第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
这两个事件都是在当中一个分页器button被单击时发生。它们分别在网格控件处理分页操作之前和之后激发。
RowCancelingEdit
在一个处于编辑模式的行的Cancelbutton被单击,可是在该行退出编辑模式之前发生。
RowCommand
单击一个button时发生。
RowCreated
创建一行时发生。
RowDataBound
一个数据行绑定到数据时发生。
RowDeleting, RowDeleted
这两个事件都是在一行的Deletebutton被单击时发生。它们分别在该网格控件删除该行之前和之后激发。
RowEditing
当一行的Editbutton被单击时,可是在该控件进入编辑模式之前发生。
RowUpdating,
RowUpdated
这两个事件都是在一行的Updatebutton被单击时发生。它们分别在该网格控件更新该行之前和之后激发。
SelectedIndexChanging,
SelectedIndexChanged
这两个事件都是在一行的Selectbutton被单击时发生。它们分别在该网格控件处理选择操作之前和之后激发。
Sorting, Sorted
这两个事件都是在对一个列进行排序的超链接被单击时发生。它们分别在网格控件处理排序操作之前和之后激发。
RowCreated和RowDataBound事件与DataGrid的ItemCreated和ItemDataBound事件同样,仅仅是换了个新名称。它们的行为全然与它们在ASP.NET 1.x中的一样。对于RowCommand事件也一样,它与DataGrid的ItemCommand事件一样。
能够使用宣布某种操作的事件,极大地增强了我们的编程能力。通过连接RowUpdating事件,能够交叉检查正在更新什么并对新值进行验证。相同,我们可能须要处理RowUpdating事件,用HTML对client提供的值进行编码,然后把它们持久地保存在底层数据存储中。这一简单技巧有助于防御脚本侵入。
3. 简单的数据绑定
例如以下代码片断说明了把数据绑定到一个GridView控件的最简单方法。数据源对象使该页面差点儿不须要不论什么代码。
<asp:ObjectDataSource ID="MySource" runat="server"
    TypeName="ProAspNet20.DAL.Customers"
    SelectMethod="LoadAll">
</asp:ObjectDataSource>
<asp:GridView runat="server" id="grid" DataSourceID="MySource" />
设置DataSourceID属性触发数据绑定过程,它执行数据源查询,并填充该网格的用户界面。我们不须要编写不论什么绑定代码。(注意,我们仍然必须编写LoadAll方法和DAL。)
在默认情况下,GridView控件自己主动生成足够多的列,以包括来自数据源的全部数据。在其它情况下,我们可能须要单独控制和设计每一列。为此,要对绑定过程稍加提炼。
假设没有设置不论什么数据源属性,则GridView控件不会生成不论什么东西。假设绑定了一个空的数据源而且规定了EmptyDataTemplate模板,则向用户显示的结果有一个较友好的外观:
<asp:gridview runat="server" datasourceid="MySource">
    <emptydatatemplate>
        <asp:label runat="server">
            There‘s no data to show in this view.
        </asp:label>
    </emptydatatemplate>
</asp:gridview>
假设该控件所绑定的数据源不空,则忽略EmptyDataTemplate属性。图10.2展示了空模板生成的输出。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href=http://www.mamicode.com/"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源文件里静态地声明列,则使用<Columns>标签,如以下所看到的:
<columns>
    <asp:boundfield datafield="customerid" headertext="ID" />
    <asp:boundfield datafield="companyname" headertext="Company Name" />
</columns>
表10.12列出GridView控件中使用列字段类。全部的类都继承DataControlField。
 
表10.12  GridView控件支持的列类型
类型
描写叙述
BoundField
默认的列类型。作为纯文本显示一个字段的值。
ButtonField
作为命令button显示一个字段的值。我们能够选择链接button或button开关样式。
CheckBoxField
作为一个复选框显示一个字段的值。它通经常使用来生成布尔值。
CommandField
ButtonField的增强版本号,表示一个特殊的命令,诸如Select、Delete、Insert或Update。该属性对GridView控件差点儿每什么用;该字段是为DetailsView控件定制的。(GridView和DetailsView共享从DataControlField派生的类集。)
HyperLinkField
作为超链接显示一个字段的值。单击该超链接时,浏览器到航道指定的URL。
ImageField
作为一个<img> 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. button字段
button字段适合于把一个可单击的元素放入一个网格的列中。通常使用一个button字段触发针对当前行的一个操作。button字段表示我们希望通过一个server端事件处理的不论什么操作。当该button被单击时,页面回发并激发一个RowCommand事件。图10.3展示了一个演示样例。
 
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href=http://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image006.jpg">
 
图10.3  GridView控件中的button字段
例如以下清单给出了上图所看到的网格的标记代码:
<asp:GridView ID="GridView1" runat="server" DataSourceID="SqlDataSource1"
    AutoGenerateColumns="false" AllowPaging="true"
    OnRowCommand="GridView1_RowCommand">
    <HeaderStyle backcolor="gray" font-bold="true" height="200%" />
    <PagerStyle backcolor="gray" font-bold="true" height="200%" />
    <PagerSettings Mode="NextPreviousFirstLast" />
    <Columns>
      <asp:BoundField datafield="productname"
          headertext="Product" />
      <asp:BoundField datafield="quantityperunit"
          headertext="Packaging" />
      <asp:BoundField datafield="unitprice"
          headertext="Price" DataFormatString="{0:c}">
        <itemstyle width="80px" horizontalalign="right" />
      </asp:BoundField>
      <asp:ButtonField buttontype="Button" text="Add" CommandName="Add" />
    </Columns>
</asp:GridView>
产品信息使用几个BoundField对象显示出来。该演示样例button列同意我们把产品加入?到购物车中。当用户单击该button时,激发RowCommandserver事件。在多个button列可用的情况下,CommandName属性同意我们判断出哪个button被单击了。我们赋给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);
    }
}
在该演示样例中,button列为全部的数据项显示一个固定文本。通过设置ButtonField类上的Text属性能够做到这一点。假设须要把button文本绑定到当前数据项上的一个特定字段,则把DataTextField属性设置为该字段的名称:
我们能够选择不相同式的button:按压式button、链接button或图像button。要以图像样式生成该button,则使用例如以下代码:
<asp:buttonfield buttontype="Image" CommandName="Add"
    ImageUrl="/proaspnet20/images/cart.gif" />
要把一个ToolTip加入?到该button(或图像)上,则须要处理RowCreated事件。(我将在本章后面具体介绍该事件。)
4. 超链接字段
超链接列把用户指向一个不同的URL,该URL能够有选择地在一个内部框架中显示出来。该链接的文本和URL能够从所绑定的数据源中获得。特别是,URL能够按下面两种方法之中的一个进行设置:通过直接绑定到一个数据源字段,或通过使用一个硬编码的带有定制查询字符串的URL。假设URL存储在数据源的一个字段字段中,则选择直接绑定。在这样的情况下,把DataNavigateUrlFields属性设置为该列的名称。然而,在某些情况下,要訪问的URL与特定的应用程序有关,并非存储在数据源中。在这样的情况下,能够用一个硬编码的URL和查询字符串中的一个參数数组设置DataNavigateUrlFormatString属性:
<asp:HyperLinkField DataTextField="productname"
    HeaderText="Product"
    DataNavigateUrlFields="productid"
    DataNavigateUrlFormatString="productinfo.aspx?id={0}"
    Target="ProductView" />
当用户单击该button时,浏览器用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://www.mamicode.com/"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://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image008.jpg"> 提示    为超链接的页面的选择一个目标时,还能够使用以下所列的不论什么一种标准目标:_self_parent_newMicrosoft Internet ExplorerFirefox都支持_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列类型表示一个在数据绑定控件中作为图像显示的字段。该单元包括一个<img>元素,因此底层的字段必须引用一个有效的URL。然而,我们能够随意组合URL。比如,我们能够使用DataImageUrlField运行直接绑定,当中该字段的内容填充<img>标签的Src属性。另外,我们能够使该列的单元指向一个外部页面(或者HTTP处理程序),从不论什么来源获取该图像的字节,并把它们下传给浏览器。例如以下代码说明了这样的方法:
 
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href=http://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image009.jpg">
 
图10.5  GridView控件中的CheckBbox字段
<Columns>
  <asp:ImageField DataImageUrlField="employeeid"
    DataImageUrlFormatString="showemployeepicture.aspx?id={0}"
    DataAlternateTextField="lastname">
    <ControlStyle Width="120px" />
    </asp:ImageField>
  <asp:TemplateField headertext="Employee">
  <ItemStyle Width="220px" />
    <ItemTemplate>
        <b><%# Eval("titleofcourtesy") + " " +
            Eval("lastname") + ", " +
            Eval("firstname") %></b> <br />
            <%# Eval("title")%>
            <hr />
            <i><%# Eval("notes")%></i>
    </ItemTemplate>
  </asp:templatefield>
</Columns>
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://www.mamicode.com/"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://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image002.jpg"> 注意    该代码片断中的EMP_IMG_OFFSET常量一般应该正好为0。然而,给定NorthwindEmployees数据库的photo列的特定结构,它必须是78。不过再次强调,这不过那个表必需的。
7. 模板字段
图10.6展示了一个定制列,当中组合了几个字段的值。这全然是通过使用模板得到的结果。TemplateField列为该网格中的每一行提供一个个性化用户界面,这全然是由页面开发者定义的。我们能够为各生成阶段定义模板,包含默认视图、原地编辑、标题和页脚。表10.14列出了受支持的模板。
表10.14  受支持的模板
模板
描写叙述
AlternatingItemTemplate
定义交替行的内容和外观。假设没有规定该模板,则使用ItemTemplate。
EditItemTemplate
定义当前正在编辑的行的内容和外观。该模板应当包括输入字段,并且还可能包括验证程序。
FooterTemplate
定义该行的页脚的内容和外观。
HeaderTemplate
定义该行的标题的内容和外观。
ItemTemplate
定义该行的默认内容和外观。
一 个模板化视图能够包括对我们正在创建的应用程序有意义的东西:server控件、文字和数据绑定表达式。数据绑定表达式同意我们插入当前数据行中包括的值。我们能够使用一个模板中所需的尽可能多的字段。然而请注意,并不是全部的模板都支持数据绑定表达式。标题和页脚模板不是数据绑定的,而且对它使用表达式的不论什么企 图都将导致一个异常。
例如以下代码说明了怎样为一个产品列定义项目模板。该列显示在两行上,并包含产品的名称和一些有关产品包装的信息。我们将使用数据绑定表达式(參见第9章中的讨论)来引用数据字段:
<asp:templatefield headertext="Product">
    <itemtemplate>
        <b><%# Eval("productname")%></b> <br />
        available in <%# Eval("quantityperunit")%>
    </itemtemplate>
</asp:templatefield>
图10.7说明了正在执行的模板字段。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href=http://www.mamicode.com/"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://www.mamicode.com/"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时,该网格显示一个分页器条,并准备检測用户对分页器button的单击。
当用户单击分页器button以查看一个新的页面时,该页面回发,可是GridView捕获该事件,并在内部处理它。这就标志着GridView和DataGrid与我们从ASP.NET 1.x获悉的编程模型之间的一个重大差别。对于GridView,不须要为PageIndexChanged事件编写处理程序。它仍然提供该事件(而且与PageIndexChanging配合),可是我们仅仅有在运行额外的操作时才要处理该事件。GridView知道怎样检索和显示请求的新页面。让我们分析一下例如以下控件声明:
<asp:GridView ID="GridView1" runat="server"
    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://www.mamicode.com/"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://www.mamicode.com/"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://www.mamicode.com/"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:
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    EnablePaging="true"
    TypeName="ProAspNet20.DAL.Customers"
    StartRowIndexParameterName="firstRow"
    MaximumRowsParameterName="totalRows"
    SelectMethod="LoadByCountry">
    <SelectParameters>
        <asp:ControlParameter Name="country" ControlID="Countries"
            PropertyName="SelectedValue" />
    </SelectParameters>
</asp:ObjectDataSource>
 
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false"
    DataSourceID="ObjectDataSource1" AllowPaging="true"
    OnPageIndexChanged="GridView1_PageIndexChanged">
    <PagerSettings Mode="NextPreviousFirstLast" />
    <Columns>
        <asp:BoundField DataField="id" HeaderText="ID" />
        <asp:BoundField DataField="companyname" HeaderText="Company" />
        <asp:BoundField DataField="contactname" HeaderText="Contact" />
    </Columns>
</asp:GridView>
<b>Page: </b><asp:Label runat="server" ID="CurrentPage" />
在上述代码中,仅仅显式地规定了使该方法起作用非常重要的内容。两个与分页相关的參数留给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时,网格显示一个分页器条。通过<PagerSettings>和<PagerStyle>标签或者它们的相当属性,可以在非常大程度上控制分页器的特征。GridView控件的分页器还支持第一页和最后一页button,并同意我们把一个图像赋给每一个button。(这对于DataGrids也是可能的,可是它须要大量代码。)分页器可以以例如以下两种模式进行工作:显示显式的页面编号,或者提供一个相对的导航系统。在前一种情况下,分页器包括数字连接,表示一个页面索引。在后一种情况下,button的存在是为了导航到下一页或前一页,甚至导航到第一页或最后一页。Mode属性规定分页器的用户界面。表10.15列出了可用的模式。
表10.15  网格分页器的模式
模式
描写叙述
NextPrevious
显示下一页和前一页button,用于防问网格中的下一页和前一页。
NextPreviousFirstLast
显示下一页和前一页button,以及第一页和最后一页button,用于直接訪问网格的第一页和最后一页。
Numeric
显示与网格的页面相应的数字链接button。
NumericFirstLast
显示与网格的各页以及与直接訪问网格的第一页和最后一页的第一页和最后一页button相应的数字链接button。
特别的属性对,xxxPageText和xxxPageImageUrl,同意我们随意设置这些button的标签。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://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image014.jpg">
 
图10.9  带有两个分页器的可分页GridView
依据网格的大小,一个网格中的第一行和最后以行可能不一定适合屏幕范围。为了使用户更easy分页,而无论滚动栏的位置,能够启用网格的顶部和底部分页器。通过设置<PagerSettings>元素上的Position属性能够做到这一点:
<PagerSettings Position="TopAndBottom" />
其它方案是仅在网格的顶部显示分页器,或者仅仅在网格的底部显示分页器。
GridView控件的分页器,必要时全然能够用一个新的分页器取代。(參见图10.10。)通过加入?<PagerTemplate>元素到该控件的声明中能够做到这一点。以下给出了一个演示样例:
<PagerTemplate>
    <asp:Button ID="BtnFirst" runat="server" commandname="First"
        Text="First" />
    <asp:Button ID="BtnPrev" runat="server" commandname="Prev"
        Text="<<" />
    <asp:Button ID="BtnNext" runat="server" commandname="Next"
        Text=">>" />
    <asp:Button ID="BtnLast" runat="server" commandname="Last"
        Text="Last" />
</PagerTemplate>
为了处理这些button上的单击事件,编写一个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 --;
}
不可否认,该代码很easy,应对它略加充实,至少要使它可以在到达第一个获最后一个索引时禁用其它button。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href=http://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image015.jpg">
 
图10.10  带有一个定制分页器的可分页GridView
排序是一种需谨慎处理、非线性的操作,假设在client执行其代价通常很高。一般而言,实际上,对记录进行排序的最佳地方是在数据库环境中,因为我们在大多数时候终于执行的是超优化的代码。在分析GridView控件和数据源控件的排序基础结构时,要注意这一点。GridView没有实现排序算法,而是依赖于数据源控件(或者页面,假设绑定到一个可列举的对象)提供排序数据。
1. 无代码的数据排序
为了启用GridView的排序功能,把AllowSorting属性设置为true。假设启用了排序,则GridView能够作为链接生成这些列的标题文本。通过使用SortExpression属性,能够将每一列与一个排序表达式进行关联。排序表达式不论什么用逗号分开的列名序列。每一个列名能够使用DESC或ASC等顺序限定符。DESC表示降序,而ASC表示升序。ASC限定符是默认的;假设忽略该限定符,则该列按升序排列。例如以下代码建立按productname数据源列进行排序的GridView列:
<asp:GridView runat="server" id="MyGridView" DataSourceID="MySource"
    AllowSorting="true" AutoGenerateColumns="false">
    <Columns>
        <asp:BoundField datafield="productname" headertext="Product"
            sortexpression="productname" />
        <asp:BoundField datafield="quantityperunit"
            headertext="Packaging" />
    </Columns>
</asp:GridView>
正如分页一样,用GridView实现排序不须要人工编写不论什么代码。在正确配置下,GridView的排序基础结构能够在不用进一步干预的情况下以双向的方式起作用——即,假设单击一个按降序排列的列,则按升序对该列进行排序,反之亦然。仅仅有在须要实现更高级的功能(诸如在标题中显示一幅指示排序方向的符号)时才须要加入?一些定制代码。(稍候会讨论很多其它有关这方面的内容。)
正如分页一样,排序的主要障碍是底层的数据源控件怎样实现它。让我们看看把网格绑定到一个SqlDataSource对象时会发生什么。除了把AllowSorting设置为true,并把排序表达式加入?到可排序列外,不须要不论什么其它操作。(參见图10.11。)
当用户单击列以对它进行排序时,网格要求SqlDataSource控件返回已排序的数据。如前所述,SqlDataSource控件默认地返回一个DataSet。假设这样,则该控件检索数据,由它建立一个DataView,并调用该DataView的Sort方法。这样的方法能够非常好地起作用,可是绝对不是最好的排序方法。我们可能会发现这样的方法非常适合我们的应用程序,可是请注意,排序是使用Webserver的内容运行的。在与缓存机制相结合的情况下,在内存中进行分页和排序都是较小的共享的记录集的可行方案。
有没有可能从数据库server中获取预排序的数据呢?第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://www.mamicode.com/"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):
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
    DataSourceMode="DataReader"
    ConnectionString=‘<%$ ConnectionStrings:LocalNWind %>‘
    SortParameterName="SortedBy"
    SelectCommand="CustOrderHistSorted"
    SelectCommandType="StoredProcedure">
  <SelectParameters>
      <asp:ControlParameter ControlID="CustList"
          Name="CustomerID" PropertyName="SelectedValue" />
  </SelectParameters>
</asp:SqlDataSource>
在数据库上对数据进行排序(如这里所看到的)与缓存是不可兼得的,我们必须知道这一点。我们须要把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:
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    EnablePaging="true"
    TypeName="ProAspNet20.DAL.Customers"
    SortParameterName="sortExpression"
    StartRowIndexParameterName="firstRow"
    MaximumRowsParameterName="totalRows"
    SelectMethod="LoadByCountry">
    <SelectParameters>
    ...
    </SelectParameters>
</asp:ObjectDataSource>
这样的方法的优势是充分利用了排序机制,而且我们能够决定怎样实现它、在哪里实现它以及何时实现它。我们可能要在DAL中编写一些排序代码,但仅仅编写高度集中的代码。实际上,不须要不论什么基础结构代码,由于ASP.NET为我们奠定了这样的基础。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href=http://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image002.jpg"> 注意    关于GridView控件上的排序还值得一提的一点是,假设须要能够撤销排序操作。为此,为Sorting事件编写一个处理程序,获得该事件的參数数据(GridViewSortEventArgs类型),并把Cancel属性设置为true
3. 提供用户反馈
GridView控件不会自己主动地把不论什么可视元素加入?到指示排序方向的输出中。以下给出了须要进行一些编码才干完毕排序的少数几种情况之中的一个:
<script runat="server">
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<grid.Columns.Count; i++) {
        string colExpr = grid.Columns[i].SortExpression;
        if (colExpr != "" && colExpr == grid.SortExpression)
            item.Cells[i].Controls.Add (glyph);
    }
}
</script>
其思想是为RowCreated事件创建一个处理程序,并寻找创建标题的时刻。接着创建一个新的Label控件,表示我们须要加入?的符号。该Label应加入?到哪里呢?
新建的Label控件设置了字体和文本,足以产生一个指示排序方向的符号(一般是▲和▼)。(这两个符号相应于Microsoft Webdings字体中的5和6。)我们必须将它与被单击列的文本一起加入?。该列的索引能够在Sorting事件中存储到视图状态中。另外,能够只检索它,将当前排序表达式(网格的SortExpression属性)与该列的表达式进行比較。一旦知道该列的索引,则检索相应的表单元并加入?该标签:
item.Cells[i].Controls.Add (glyph);
结果如图10.12所看到的。假设页面基于一个主题,则Label控件的字体(对于正确地显示符号是不可缺少的)可能被覆盖。为了避免这一点,应当禁用标签控件的主题支持。EnableTheming属性正好攻克了这个问题。
4. 对分页和排序使用回调
排序和分页操作都须要一次页面回发,随后全然刷新该页面。在大多数情况下,这是一个重型操作,由于页面通常包括大量图形。为了让用户感受到更好的体验,假设该网格可下面探到Webserver,获取新记录集,并仅仅更新界面的某一部分,这样不是更好吗?多亏了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://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image017.jpg">
 
图10.12  增强GridView控件的排序功能
如前所述,该特征依赖于ASP.NET脚本回调引擎的服务,该特征也能够应用于非Internet Explorer浏览器,包含Firefox、Netscape 6.x和更新的版本号、Safari 1.2,以及最新的Opera浏览器。
SqlDataSourceObjectDataSource
几方面的考虑因素将有助于澄清何时使用SqlDataSource和ObjectDataSource控件。首先,记住这些数据源控件并不是是想要运行稳定的数据绑定的开发者的惟一两种选择。然而,到眼下为止,它们是最流行、最经常使用的。在ASP.NET 2.0中,数据绑定绝非仅仅能使用数据源控件,记住这一点也是重要的。这就是说,SqlDataSource和ObjectDataSource仅仅是ASP.NET工具箱中的工具,假设合适就能够使用它们。
在我看来,SqlDataSource最适合于非连接的数据绑定方法,并在通过DataSet获取数据时起到最佳作用。仅仅有在这样的情况下,才干启用分页、排序和缓存功能。在三种功能中,仅仅有排序功能在数据阅读器模式有些反复。假设使用DataSets适合我们的应用程序,则使用SqlDataSource是一种不错的选择。它为我们提供了现成的解决方式,当中大多数是易于编写的声明式代码,可是这样的方法在实际应用中并不一定有效。换句话说,在一个应用程序中使用SqlDataSource可能对某些特征有优点,但对于驱动整个DAL是不够的。
相反,假设意识到须要对分页和排序操作(诸如定制分页或server端排序)保持很多其它的控制,则改用ObjectDataSource似乎是一种更好的思想。在这样的情况下,首先设计并实现一个完整的DAL和(有选择地)一个业务层。在该层中,我们精心制作须要从网格那里得到支持的不论什么功能:分页、排序,甚至数据缓存。注意,假设使用定制集合取代ADO.NET容器类,则不支持缓存功能,可是实现一个个性化的缓存层并不没有什么困难。
假设使用ObjectDataSource,则须要我们自己负责实现那些多少有点相似于ASP.NET 1.x中的DataGrids的关键特征。这样怎样呢?我们并不是仅仅是在一些代码隐藏类中插入少量代码;而是在应用程序的DAL中插入逻辑。我们仍然要编写代码,可是我们要编写的代码的质量很不同!
此外,ObjectDataSource控件全然支持定制实体类和定制集合。在.NET Framework 2.0中,对范型的支持使编写定制集合easy多了,而且显著降低了编写一个全然定制的、建立在量身定制的并与特定领域有关的对象基础之上的DAL的成本。
GridView控件的主要长处(弥补了DataGrid的主要缺点)是可以更新数据源。DataGrid控件仅提供数据编辑的基础结构。它提供了必要的用户界面元素,并在用户改动某个数据字段的值时激发合适的事件,但它没有把那些变更提交回到数据源。开发者失望地发现他们必须编写大量刻板的代码以真正地持久存留变更。
有了GridView控件,假设所绑定的数据源支持更新,则该控件能够自己主动地运行该操作,从而提供真正的开箱即用(out-of-the-box)解决方式。数据源控件通过CanUpdate布尔属性表明它的更新功能。
很相似于DataGrid,GridView能够为网格中的每一行生成一个命令button列。这些特殊的命令列包括编辑或删除当前记录的button。假设使用DataGrid控件,则必须使用一个专门的列类型(EditCommandColumn类)显式地创建一个编辑命令列。GridView大大地简化了更新和删除操作。
1. 原地编辑和更新
原地编辑指网格可以支持对当前显示记录的变更的能力。通过打开AutoGenerateEditButton布尔属性,启用一个网格视图上的原地编辑功能:
<asp:gridview runat="server" id="GridView1" datasourceid="MySource"
    autogeneratecolumns="false" autogenerateeditbutton="true">
...
</asp:gridview>
当AutoGenerateEditButton属性设置为true时,GridView显示一个附加列,就像图10.13所看到的的那样。通过单击Editbutton,使所选行进入编辑模式,而且能够随意输入新数据。
0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" href=http://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image018.jpg">
 
图10.13  一个支持原地编辑的GridView控件
要停止编辑并丢弃不论什么变更,用户仅仅要单击Cancelbutton。GridView能够处理该单击,而不须要不论什么外部支持;这些返回到原来的仅仅读状态;而EditIndex属性返回到它的默认值-1,表示当前没有编辑不论什么行。可是,假设用户单击了更新链接会如何呢?GridView首先激发RowUpdating事件,然后在内部检查数据源控件上的CanUpdate属性。假设CanUpdate返回false,则抛出一个异常。假设数据源控件未定义更新命令,则CanUpdate返回false。
如果我们的网格绑定到一个SqlDataSource对象。为了在用户更新时持久地保存变更,必须如以下这样的设计自己代码:
<asp:sqldatasource runat="server" ID="EmployeesSource"
    ConnectionString="<%$ ConnectionStrings:LocalNWind %>"
    SelectCommand="SELECT employeeid, firstname, lastname FROM employees"
    UpdateCommand="UPDATE employees SET
            firstname=@firstname, lastname=@lastname
            WHERE employeeid=@original_employeeid">
</asp:sqldatasource>
<asp:gridview runat="server" id="GridView1" datasourceid="EmployeesSource"
      AutoGenerateColumns="false"
      DataKeyNames="employeeid" AutoGenerateEditButton="true">
      <columns>
          <asp:boundfield datafield="firstname" headertext="First" />
          <asp:boundfield datafield="lastname" headertext="Last" />
      </columns>
</asp:gridview>
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://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image002.jpg"> 注意    GridView从输入字段收集值,并填充一个由名称/值对组成的字典,指示该行中每一个字段的新值。GridView还提供了一个RowUpdating事件,它同意程序猿验证传递给数据源对象的值。此外,GridView在启动关联数据源上的更新操作之前自己主动地调用Page.IsValid。假设Page.IsValid返回false,则取消该操作。假设使用一个带验证器的定制模板,则该属性特别实用。
假设网格绑定到一个ObjectDataSource控件,则情况有些不同。绑定的业务对象必须有一个更新方法。该方法将接受满足其工作所需的随意多个參数。我们能够决定逐个传递參数,或者用一个惟一的数据结构组织这些參数。假设有一个很好的DAL,则优先使用另外一种方案。以下给出了一个实例:
<asp:ObjectDataSource ID="CustomersSource" runat="server"
    TypeName="ProAspNet20.DAL.Customers"
    SelectMethod="LoadAll"
    UpdateMethod="Save"
    DataObjectTypeName="ProAspNet20.DAL.Customer">
</asp:ObjectDataSource>
<asp:GridView ID="GridView1" runat="server" DataSourceID="CustomersSource"
    DataKeyNames="id" AutoGenerateColumns="false">
    AutoGenerateEditButton="true"
   <Columns>
        <asp:BoundField DataField="companyname" HeaderText="Company" />
        <asp:BoundField DataField="street" HeaderText="Address" />
        <asp:BoundField DataField="city" HeaderText="City" />
   </Columns>
</asp:GridView>
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://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image002.jpg"> 注意    假设设置了DataObjectTypeName属性,则全部的数据方法不是无參数的,就是接受一个指定类型的对象。不管是否声明式地填充该方法的參数集,都这样。DataObjectTypeName属性优先于參数集合。
2. 删除显示的记录
从GridView的角度看,删除记录与更新记录差点儿相同。在这两种情况下,GridView利用数据源的运行数据操作的能力。通过将AutoGenerateDeleteButton属性的值设置为true,启用记录删除功能。GridView生成一个button列,假设单击这些button,则针对所绑定的数据源控件上的相应行调用删除命令;并向该数据源方法传递一个由名称/值组成的键字段对字典,用来惟一地标识要删除的行:
<asp:sqldatasource runat="server" ID="EmployeesSource"
    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控件支持记录插入的常见做法要求我们改动页脚或分页器,为空的文本框和button腾出空间。GridView支持同样的模型,并通过PagerTemplate属性使涉及分页器的情况更简单。通过RowCreated事件改动页脚的内容是可能的(稍后再作具体介绍)。然而请注意,假设网格绑定到一个空的数据,则页脚条被隐藏。假设希望用户可以把一个新记录加入?到一个空的网格中该怎么办呢?利用EmptyDataTemplate,具体例如以下所看到的:
<emptydatatemplate>
    <asp:label ID="Label1" runat="server">
        There‘s no data to show in this view.
        <asp:Button runat="server" ID="btnAddNew" CommandName="AddNew"
            Text="Add New Record" />
    </asp:label>
</emptydatatemplate>
要捕获该button上的用户单击,为RowCommand事件编写一个处理程序:
void Gridview1_RowCommand(object sender, GridViewCommandEventArgs e)
{
    if (e.CommandName == "AddNew")
    { ... }
}
为了使GridView控 件的概述更完整,我们全然须要分析两种常见的编程场景:下钻和行定制。一个网格把一系列数据项提交给用户;在非常多情况下,用户须要从这些数据项中选择一个,并開始对它的操作。如前所述,button列是为了简化此任务而存在的。我们过一会将深入讨论该主题。行定制是还有一个常见特征,它使我们有机会改动网格的标准 外观。我们能够改动行布局,加入?或删除数据单元,或者在每行的基础上改动可视属性,使某些行看起来不同于其它行(比如,表示负值的行)。
1. 对给定的行运行一个操作
让我们回到本章前面讨论button列时简单提到的一个问题。如果我们正在创建一个电子商务应用程序;当中一个页面显示产品网格以及一些同意用户把产品加入?到购物车中的button。我们加入?一个button列,并编写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<ShoppingItem>
{
    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的很多属性:
<asp:GridView ID="GridView1" runat="server"
    DataSourceID="SqlDataSource1"
    DataKeyNames="productid,productname,unitprice" ... />
我们在DataKeyNames中应列出多少个字段?考虑到那里所列的每一个字段占领了一些视图状态空间。还有一方面,假设仅仅限于自己存储主键字段,则须要执行一个查询以获取我们所需的全部数据。哪种方法更好,取决于我们真正须要做什么。在我们的演示样例环境中,我们须要制作已经缓存在Webserver的内存中的产品的副本。但不须要执行查询来获取我们已经知道的数据。要填充一个ShoppingItem对象,须要产品ID、名称和单位价格:
private void AddToShoppingCart(int rowIndex)
{
    DataKey data = http://www.mamicode.com/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://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image019.jpg"> 警告 仅仅有在生成该控件时,每一个网格行才被绑定到一个数据项——数据源中的一个数据行。诸如RowCommand等的回发事件,在到达该阶段之前激发。因而,被单击的GridViewRow对象的DataItem属性(我们须要的数据应该保存在这里),假设从RowCommand处理程序的内部訪问它,则难免会为null。这就是为什么须要DataKeyNames和相关的DataKeys属性。
2. 选择一个给定行
选择被单击行的更一般的机制,能够通过一个特殊的命令button(即,选择button)实现。如同删除和编辑button的情况一样,通过设置AutoGenerateSelectButton布尔属性打开它。为了充分利用该选择特征,建议大家还要为所选行加入?一个样式:
<asp:GridView ID="GridView1" runat="server" ... >
    <SelectedRowStyle BackColor="cyan" />
    ...
</asp:GridView>
当用户单击一个选择button时,页面接受一个更详细的SelectedIndexChanged事件。一些属性,诸如SelectedIndex、SelectedRow和SelectedDataKey也被更新。为了完备性,注意到一行被选中时,该页面首先收到一个RowCommand事件,然后再收到SelectedIndexChanged事件。然而,当RowCommand事件激发时,还没有更新不论什么一个选择属性。
例如以下代码说明了怎样重写前一个实例,以便把所选的产品加入?到购物车中:
protected void GridView1_SelectedIndexChanged(object sender, EventArgs e)
{
    AddToShoppingCart();
}
private void AddToShoppingCart()
{
    DataKey data = http://www.mamicode.com/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://www.mamicode.com/"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://www.mamicode.com/"http://book.csdn.net/BookFiles/73/10/image021.jpg">
 
图10.15  相应于停售产品的行如今被禁用了
仅仅要理解了网格行对象模型,实际上能够做我们想做的不论什么事情。