几乎不需缩写代码的双向数据绑定是GridView控件最瞩目的功能,但其他相对于DataGrid的改进之处也不胜枚举,这些改进包括:可定义多个主键字段、新的列类型、样式和模板选项。
GridView的属性
GridView的SortDirection和SortExpression属性用于指定当前列排序的方向和排序表达式。当用户单击某个列的标头时,这两个属性会由该控件的内建排序机制设置。整个排序引擎的开关取决于AllowSorting属性。EnableSortingAndPagingCallbacks属性用于指示该控件是否使用脚本回调功能进行分页和排序,有了该功能,我们不必执行到服务器的往返交互,也不必更改整个页面。
该功能不需要ASP.NET AJAV Extensions 1.0,也不会使用ASP.NET 3.5中内建的AJAV引擎。它是类似于AJAX的、轻型的、嵌入框架的功能,能够通过专门的基于XMLHttpRequest对象的引擎,发起特定上下文的Web服务器调用。
GridView控件中显示的每一行对应于一个特定类型的网格元素。在应用程序中,这些元素表面上是静态的,在该控件的生命周期各阶段基本保持原位。其他类型的元素呈现时间较短,只要需要完成某项操作时才会显示。这些动态元素包括编辑行、选定行及EmptyData元素。EmptyData用于定义网格被绑定到空数据源时在主体部分中显示的内容。
GridView控件能够充分发挥新数据源对象模型的功能,在通过DataSourceID属性绑定到数据源控件时,其表现更为出色。GridView还支持传统的DataSource属性,但如果以这种方式绑定数据,该控件的某些功能(如内建的更新和分页功能)将无法发挥。
GridView控件的事件
GridView控件除DataBind以外没有其他特别值得注意的方法。在许多情况下,我们不必调用GridView控件上的方法。当GridView绑定到数据源控件时,数据绑定过程会隐式地启动。
RowCreated和RowDataBound事件与DataGrid的ItemCreated和ItemDataBound作用一样,只不过换了新名称。RowCommand事件也是这样,对应于DataGrid控件的ItemCommand事件。
简单数据绑定
下面的代码演示了将数据绑定到GridView控件最简单的方法:
<asp:ObjectDataSource ID="MySource" runat="server" TypeName="Core35.DAL.Customers" SelectMethod="LoadAll">
</asp:ObjectDataSource>
<asp:GridView runat="server" id="grid" DataSourceID="MySource" />
设置DatasourceID属性会触发绑定过程,该过程运行数据源的查询并填充网格的用户界面。
将数据绑定到GridView控件
如果没有设置数据源属性,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>
GridView即可以有声明的列,也可以有自动生成的列。这种情况下,声明的列会在左侧显示。还应注意,自动生成的列不会被添加到Columns集合中。因此,若使用自动生成的列,Columns集合一般为空。
列的配置
Columns属性是DataControlField对象的集合。我们即可以以声明方式定义列,也能以编程方式进行。以编程方式定义列的示例:
BoundField field = new BoundField();
field.DataField = "companyname";
field.HeaderText = "Company Name";
grid.ColumnFields.Add(field);
使用声明方式定义列:
<columns>
<asp:boundfield datafield="customerid" headertext="ID" />
<asp:boundfield datafield="companyname" headertext="Company Name" />
</columns>
下表列出了可在GridView控件中使用的列类型,所有列的类型都继承于DataControlField类:
下表列出了所有列类型共有的主要属性:
绑定字段
BoundField类代表数据绑定控件中以纯文本方式显示的字段。为指定要显示的数据字段,应将DataField设置为该字段的名称。还可通过DataFormatString为显示的值设置自定义的格式字符串。通过NullDdisplayText属性,我们可指定在出现null值时显示的替代文本。通过将ConvertEmptyStringToNull设置为true,我们能将空字符串按null值处理。
通过BoundField的Visible属性可将该字段隐藏,而ReadOnly属性能阻止显示的值在编辑模式中被更改。为在标头和脚注栏中显示标题,可分别设置HeaderText和FooterText。我们还可设置HeaderImageUrl属性来选择替代文本的图像。
按钮字段
按钮字段适合于在网格的列中添加可点击元素。当某个按钮被单击后,页面会回发,并引发RowCommand事件。
我们可以选用不同类型的按钮--下压式按钮、链接按钮或图像按钮。图像按钮示例:
<asp:buttonfield buttontype="Image" CommandName="Add" ImageUrl="/core35/images/cart.gif" />
要为按钮添加提示框(ToolTip),则需要处理RowCreated事件。
如果没有额外的HtmlEncode="false"属性,BoundField类的DataFormatString是无法正常工作的。因为ASP.NET首先会对绑定字段的值进行HTML编码,然后才会应用格式字符串。这样,绑定的值不受指定格式字符串的影响。提前在该控件的生命周期中启用HTML编码是一种安全措施,旨在预防跨站点脚本攻击。
超链接字段
可以两种方式设置URL:直接从绑定源获取,或通过编码的带有自定义查询字符串的URL。但在某些情况下,要访问的URL跟特定的应用程序有关,并没有存储在数据源中。这时,我们可以将DataNavigateUrlFormatString属性设置为硬编码的URL,并在查询字符串中添加一组参数:
<asp:HyperLinkField DataTextField="productname" HeaderText="Product"
DataNavigateUrlFields="productid"
DataNavigateUrlFormatString="productinfo.aspx?id={0}"
Target="ProductView" />
DataTextFormatString属性可以包含任何有效的标记,用{0}作为占位符,为数据绑定的值留出位置。
为超链接所指向的页面选择目标时,我们还可以使用以下标准的目标值:_self、_parent、_new。
复选框字段
CheckBoxField类型能够显示复选框,我们只能将其绑定到布尔类型的数据字段上。
图像字段
这种列中的每个单元格包含一个<img>元素,因而底层的字段必须引用有效的URL。
模板字段
下表列出了GridView支持的模板:
我们可以在一个模板中绑定多个字段,但并不是所有的模板都支持数据绑定表达式。标头和脚注模板不支持数据绑定,若使用表达式,会引发异常。
下面的代码为产品列定义了项目模板,该列包含两行,分别用于显示产品名称和包装信息。
<asp:templatefield headertext="Product">
<itemtemplate>
<b><%# Eval("productname") %></b>
<br />
available in <%# Eval("quantityperunit") %>
</itemtemplate>
</asp:templatefield>
数据的分页
如果GridView控件通过DataSource属性绑定到数据源(即不使用数据源控件),那么分页和其他操作(如排序和编辑)的整体行为几乎与DataGrid控件一致。在这种情况下,GridView会引发若干事件,以此来通知页面,由页面中的数据绑定代码进行下一步的操作并刷新。
无需编程的数据分页
为启用GridView控件的分页功能,我们只需将AllowPaging属性设为true。这样,GridView会显示一个导航栏,并为响应用户单击导航按钮做好准备。
当用户要查看另一页面而单击其中的按钮时,页面会进行回发,GridView会捕获相应的事件并在内部进行处理。使用GridView,我们不必编写PageIndexChanged事件的处理程序。GridView本身知道如何获取数据并显示被请求的新页面。示例代码:
<asp:GridView ID="GridView1" runat="server" DataSourceID="SqlDataSource1" AllowPaging="true" />
GridView默认的用户界面不包含页码,添加页面标签只需要为PageIndexChanged事件编写代码:
protected void GridView1_PageIndexChanged(object sender, EventArgs e)
{
ShowPageIndex();
}
private void ShowPageIndex()
{
CurrentPage.Text = (GridView1.PageIndex + 1).ToString();
}
GridView控件本身并不知道如何获取新的页面,它仅仅会使绑定的数据源控件返回适合指定页面显示的记录。分页操作的细节最终由数据源控件决定。若绑定到SqlDataSource控件,分页需要将整个数据源绑定到网格控件上。若网格被绑定到ObjectDataSource控件,分页机制取决于该数据源控件连接到的业务对象的功能。
先让我们看看绑定到SqlDataSource时的情况。若要进行分页,必须将DataSourceMode设置为DataSet(也是默认设置)。这表明整个数据集都会被获取,但只有当前页面指定的记录被显示出来。如果将EnableCaching设置为ture,启用SqlDataSource缓存功能,则整个数据集只会被下载一次,并在指定的时间段内保持在ASP.NET缓存中。但这会导致大量数据驻留内存。因此,除非多个用户共享少量数据,否则不推荐这种方法。
如果要在数据库级进行分页,最好的方式是在存储过程中编写分页查询,并将存储过程绑定到SqlDataSource控件的SelectCommand属性上。这种情况下,应关闭数据源的缓存功能。
将分页负担转嫁给DAL
我们需要基于层业务对象方法的特性来配置ObjectDataSource控件。一旦确定Select方法后,再添加一个带有两个额外参数的重载(这两个参数分别用于指示页面大小和当前页的起始索引)。在ObjectDataSource的声明中,我们将StartRowIndexParameterName和MaximumRowsParameterName属性分别设置为指示起始索引和页面大小的方法参数的名称。
为使GridView能够通过ObjectDataSource控件对数据进行分页,我们还需将ObjectDataSource的EnablePaging属性设为true:
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
EnablePaging="true"
TypeName="Core35.DAL.Customers"
StartRowIndexParameterName="firstRow"
MaximumRowsParameterName="totalRow"
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)
{
return LoadByCountry(country, -1, 0);
}
public static CustomerCollection LoadByCountry(string country, int totalRows, int firstRow)
{
//此处实现分页查询
}
导航栏的配置
若将AllowPaging属性设为true,GridView将显示导航栏。通过<PagerSettings>和<PagerStyle>标签,或与二者等价的属性,我们能在很大程度上控制导航栏的特性。PagerSettings的Mode属性用于设置导航栏的用户界面。下表列出了可用的模式:
根据GridView的大小,GridView的第一行与最后一行间的距离可能超出屏幕实际的大小。为使用户能更容易导航,而不必频繁滑动滚动条,我们可使网格分别在顶端和底端显示导航栏。为此,只要设置<PagerSettings>元素的Position属性即可。
<PagerSetting Position="TopAndBottom" />
数据的排序
GridView没有实现排序算法,而是依赖于数据源控件来提供已排序的数据。
不需要编程的数据排序
为启用GridView的排序功能,我们应将AllowSorting属性设置为true。如果GridView的排序功能被启用,便可使各列的标头文本以超链接的形式呈现。
通过SortExpression属性,我们能使每列与一个排序表达式相关联。排序表达式是一个列名的序列,两列名间由逗号分隔,每个列名后还可以跟DESC或ASC限定词来对排序做进一步的说明。
示例代码:
<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>
以SqlDataSource控件为例:默认情况下,SqlDataSource控件会获取所有数据填入DataSet对象,然后用DataView来包装这些数据,并调用DataView的Sort方法。这种方式虽然可行,但占用的是Web服务器的内存。如果分页和排序针对同一组数据,且数据量较小,那么结合缓存功能使用该方法还比较适宜。
可否事先在数据库服务器上对数据进行排序?为此,我们首先应将SqlDataSource的DataSourceMode属性设置为DataReader。如将其设为DataSet,排序仍会在内存中进行。第二步需要我们编写获取数据的存储过程,为获取已排序数据,我们还要将数据源控件的SortParameterName设置为存储过程中指示排序表达式的参数名。显然,我们需要该存储过程动态地构建命令文本,使用查询语句带有ORDER BY子句。下面对NorthWind的CustOrderHist存储过程进行了修改,使其能按需对结果进行排序:
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, Customer 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
这样,GridView便能显示已排序数据列,排序的负担被转嫁给了数据库。
<asp:SqlDataSource ID="SqlDataSource1" runat="server" DataSourceMode="DataReader"
ConnectionString='<%$ ConnectionStrings:NWind %>'
SortParameterName="SortedBy"
SelectCommand="CustOrderHistSorted"
SelectCommandType="StoreProcedure">
<SelectParameters>
<asp:ControlParameter ControlID="CustList" Name="CustomerID"
PropertyName="SelectedValue" />
</SelectParameter>
</asp:SqlDataSource>
像这样在数据库上对数据进行排序与缓存功能是不兼容的,我们需要将EnableCaching设置为false,否则会抛出异常。
将排序操作负担转嫁给DAL
使用ObjectDataSource控件如何排序?下面对之前讨论分页时使用的LoadByCountry方法稍做修改,添加一个排序表达式参数:
public static CustomerCollection LoadByCountry(string country, int totalRows, int firstRow, string sortExpression)
{
//此处实现分页查询
}
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" EnablePaging="true"
TypeName="Core35.DAL.Customers"
SortParameterName="sortExpression"
StartRowIndexParameterName="firstRow"
MaximumRowsParameterName="totalRows"
SelectMethod="LoadByCountry">
<SelectParameters>
...
</SelectParameters>
</asp:ObjectDataSource>
对用户进行反馈
GridView控件不会自动向输出中添加指示排序方向的可视元素,为完善排序,我们需要自行编写代码:
<script runat="server">
void GridView1_RowCreated(object sender, GridViewRowEventArgs e)
{
//如果当前行是标头,则另行处理
if(e.Row.RowType == DataControlRowType.Header)
AddGlyph(sender as GridView, e.Row);
}
void AddGlyph(GridView grid, GridViewRow item)
{
//创建Label控件,并禁用其主题
Label glyph = new Label();
glyph.EnableTheming = false;
//设置Label控件属性
glyph.Font.Name = "webdings";
glyph.Font.Size = FontUnit.Small;//" 5"和" 6"表示正序或倒序排列的小三角标</script>
glyph.Text = (grid.SortDirection == SortDirection.Ascending ? " 5" : " 6");
//查找当前排序的列
for(int i = 0; i < grid.Columns.Count; i++)
{
//比较排序表达式来确定是否当前排序列
string colExpr = grid.Columns[i].SortExpression;
//如果找到当前排序列,将Label控件添加在上面
if(colExpr != "" && colExpr == grid.SortExpression)
item.Cells[i].Controls.Add(glyph);
}
}
通过回调进行分页和排序
ASP.NET中有脚本回调机制使GridView无需将整个页面回发就能进行分页和排序。要打开此功能,只需将EnableSortingAndPagingCallbacks属性设为true即可。
数据的编辑
GridView控件提供了完整的数据编辑解决方案。如果数据源支持更新,那么该控件会自动执行提交更改的操作。数据源控件会通过布尔类型的CanUpdate属性指示自身是否有更新功能。