第10章 富数据控件

10 富数据控件(Rich Data Controls

 

在前面的章节中,你看到如何使用数据源控件来执行查询,包括使用或者不使用自定义数据访问类的帮助。在这过程中,你使用了一些富数据控件(rich data control),如GridView。但是,你对那些控件提供的功能了解得并不深。

本章中,你将更深入了解GridViewDetailViewFormView,并且了解如何进行格式化和使用控件的功能,如选择、排序、过滤和模板。你也会了解到高级的场景,如显示图片,计算总和,并且在单个控件里创建细节化的列表。

 

ASP.NET1.X模板化的控件

ASP.NET2.0仍然提供了ASP.NET1.0里引入的模板化的控件。这些控件包括DataGrid, DataList, Reapter。多数ASP.NET程序员不再使用这些控件(除了保持兼容之外,本书不讨论这些控件)。

DataGridDataGrd已由GridView完全替代,后者提供了相同(或者更多)的功能集,并且简化了编码模式。默认情况下,DataGridVisual Studio 2005的工具栏里不会出现。

DataListDataList多数情况下被GridView替代,后者提供了相似的模板集,但编码模型更简单。当然,如果你想创建多列的表(每个单元都是一条记录),仍可以使用DataListGridView不支持这种不平常的设计,因为它强制每一条记录占据一个单独的行。

RepeaterReapter仍然担负了原始的基于模板的控件的角色。尽管它没有提供多功能或者装饰,你仍可能用它来创建自定义的数据显示。Repeater没有添加任何内置的元素,因为你必将其限定于基于表的格式。但是,由于Repeater没有包含更高层的功能,如选择和编辑,因此,你需要做大量工作才能获得结果。

本章的核心是ASP.NET2.0中的新控件:GrieView, DetailsViewFormView。如果对旧版本的控件感兴趣,可以参阅本书以前的版本。

GridView控件(The GridView

如果你使用ASP.NET1.X编程,你可能使用相似的DataGrid控件。

在增强DataGrid的同时保持后向兼容是一个挑战ASP.NET小组于是决定创建一个全新的控件来实现增强的功能。这个控件就是GridView

GridView是一个显示数据的非常灵活的控件,它包含了大量的硬连接的功能,包括选择、分页和编辑,并且它还能通过模板实现了扩展性。GridView相对于DataGrid,最大的优点是它支持自由编码的场合。使用GridView,你可以完成许多常见的任务,如分页和选择,而这些并不需要编写一行代码。而使用DataGrid,你必须进行事件处理来完成相同的功能。

注意,DataGridASP.NET2.0中仍然可以找到,而且它现在支持绑定到数据源控件。

定义列(Defining Columns

到目前为止,你所见到的GridView示例中,都将GridView.AutoGenerateColumns属性设置为true,这意味着GridView使用映象来检查数据对象,并且查找记录的所有字段或自定义对象的属性。然后按照发现的顺序创建相应的列。

这种列的自动创建对于创建快速测试页面很有好处,但你无法获得想要的灵活性。例如,如果你想隐藏一些列,改变它们的顺序,或者配置它们的显示方式,如格式化或改变标题栏文本,该怎么实现呢?在这些情况下,你需要将AutoGenerateColumns设置为false,并且在GridView控件标签的<Columns>节中自己定义列。

提示,设置AutoGenerateColumnstrue的同时在<Colums>节中定义列是可能的。这种情况下,显性定义的列被添加到自动创建的列表中。这个技术在前面的章节中使用GridView自动创建绑定列和使用编辑控件手工定义列时已经用到。但是,对每一列都进行显性地定义可以获得最大的灵活性。

每一列都可以是任何类型,如表10-1描述那样。列标签的顺序决定了GridView中列的从左至右的顺序。

描述

BoundField

显示来自数据源的一个字段的文本

ButtonField

为列表中的每一项显示一个按钮

CheckBoxField

为列表中的每一项显示一个复选框。它自动应用于true/false的布尔字段(在SQL Server中,这些字段使用了bit数据类型)

CommandField

该列提供选择和编辑按钮

HyperlinkField

该列显示内容(来自于数据源的字段或静态文本)为一个超级连接

ImageField

该列显示二进制字段的图像数据(可以被描述为支持的图像格式)

TemplateField

该列允许你使用自定义模板,指定多个字段、自定义控件和任意HTML。这提供了对控件的最大控制,但需要更多的工作量

10-1 列类型

最基本的控件类型是BoundField,它绑定到数据对象中的一个字段。例如,下面的代码定义了一个单独数据绑定列来显示 EmployeeID字段。

<asp:BoundField DataField = “EmployeeID” HeaderText = “ID” />

这比自动创建的列具有更好的性能,这里,标题栏的文本用ID替代了EmployeeID

当你第一次创建GridView时,AutoGenerateColums属性设置为false。将其绑定到数据源控件时,不会有任何变化。但是,如果点击数据源控件的刷新视图(Refresh Schema)链接,AutoGenerateColumns属性翻转为trueVisual Studio为数据源的每个字段添加一个<BoundField>标签。这个方法有几个优点:

你可以通过设置列对象的属性方便地调整列顺序、列标题和其它细节。

你可以通过移除列标签来隐藏列。但是,不要过度使用这个技术,因为对于没必要显示的数据,不获取它更好。

提示,你也可以在程序中隐藏列。使用GridView中的Columns集可以实现这一点。例如,设置GridView1.Columns[2].Visiblefalse可以隐藏第3列。隐藏的列在渲染HTML时被忽略。

显性地定义列比自动创建列快。因为自动创建列强制GridView在运行时映射到数据源。

你可以添加额外的列来组合选择、编辑等。

提示,如果通过修改数据源来返回不同的列,你可以重新产生列。只要选择GridView,然后点击Refresh Schema链接。这样做会清除你所添加的所有自定义列(如编辑控件)。

下面是显性地声明列的完整的GridView:

<asp:GridView ID="gridEmployees" runat="server" DataSourceID="sourceEmployees"

AutoGenerateColumns="False">

<Columns>

<asp:BoundField DataField="EmployeeID" HeaderText="ID" />

<asp:BoundField DataField="FirstName" HeaderText="First Name" />

<asp:BoundField DataField="LastName" HeaderText="Last Name" />

<asp:BoundField DataField="Title" HeaderText="Title" />

<asp:BoundField DataField="City" HeaderText="City" />

</Columns>

</asp:GridView>

当你显性地声明绑定的字段时,你可以设置其它的属性,表10-2列出了这些属性。

属性

描述

DataField

想要在该列中显示的数据项的字段或属性的名字

DataFormatString

格式化字符串,用来格式化字段。获取数值和日期时很有用

ApplyFormatInEditMode

如果为true, 就使用格式化字符串来格式化值,即便它在编辑模式下显示于文本框中。默认值是false,意即只使用底层的默认设置。

FooterText, HeaderText, HeaderImageUrl

Sets the text in the header and footer region of the grid, if thisgrid has a heater (ShowHeader is true) and Footer (ShowFooteris true). The header is most commonly used for a descriptivelabel such as the field name, while the footer can contain a dynamically calculated value such as a summary (a technique demonstrated in the section “Summaries in the GridView” toward the end of this chapter). To show an image in the header instead of text, set the HeaderImageUrl property.

ReadOnly

 If true, the value for this column can’t be changed in edit mode. No edit control will be provided. Primary key fields are often  read-only.    

InsertVisible

If false, the value for this column can’t be set in insert mode. If you want a column value to be set programmatically or based on a default value defined in the database, you can use this feature.

Visible

If false, the column won’t be visible in the page (and no HTML will be rendered for it). This property gives you a convenient way to programmatically hide or show specific columns, changing the overall view of the data.

SortExpression

An expression that can be appended to a query to perform a sort based on this column. Used in conjunction with sorting, as described in the “Sorting the GridView” section.

HtmlEncode

If true (the default), all text will be HTML encoded to prevent special characters from mangling the page. You could disable HTML encoding if you want to embed a working HTML tag (such as a hyperlink), but this approach isn’t safe. It’s always a better idea to use HTML encoding on all values and provide other functionality by reacting to GridView selection events.

NullDisplayText

The text that will be displayed for a null value. The default is an empty string, although you could change this to a hard-coded value, such as “(not specified)”.

ConvertEmptyStringToNull

If this is true, before an edit is committed all empty strings will be converted to null values.

ControlStyle, HeaderStyle,  FooterStyle,  ItemStyle

Configures the appearance for just this column, overriding the styles for the row. You¡¯ll learn more about styles throughout this chapter.

10-2 BoundField 属性

如果你不想手工配置列,选择GridView,然后点击属性容的Columns属性边上的省略号(),你会看到Fields对话框,在里面你可以添加、移除或者重定义列(如图10-1)。

10-1 Visual Studio中配置列

现在你理解了GridView的基础,并且已经开始研究更高级的功能,在后面的章节中,你会处理这些主题:

Formating:如何格式化行和数据值。

Selecting:让用户如何选择GridView中的行,并且进行对应的响应。

Sorting:点击列的顶部标题时,如何动态地进行重新排序。

Paging:怎样使用自动或自定义的代码,将大的结果集数据分到多个页面,

Templates:通过重定义模板,完全自定义布局、格式化、编辑。

GridView格式化(Formatting the GridView

格式化包括几个相关的任务。首先,你希望对数据、货币或其它数值正确呈现。使用DataFormatString属性来进行处理。其次,你想在网格的不同部分混合应用颜色、字体、边框和对齐等选项,包括顶部栏和数据项。GridView通过样式来支持这些特征。最后,你能够描述事件,检查行的数据,并且在程序中将格式化应用到指定的数据点。下面的章节中,你将逐个了解这些技术。

GridView也提供了几个格式化属性,它们是自解释的,在此没有涉及。包括GridLine(用于添加或隐藏表格边框)CellpaddingCellSpacing(用于控制单元之间的跨度)CaptionCaptionAlign(用于向Grid的顶部添加一个标题)

提示,如何在网页中创建带滚动条的GridView?非常简单,将GridView放入一个Panel控件中,设置Panel的属性,然后设置Panel.ScrollbarsAuto, VerticalBoth

格式化字段(Formating Fields

每个BoundField列提供了一个DataFormatString属性,你可以用一个格式化字符串(format string)来配置数值和数据的外观。

格式化字符串一般由一个占位符(placeholder)和格式化指示器(format indicator)组成,放置于大括号中。一个典型的格式化字符串类似这样:

{0:C}

其中,0代表要格式化的值,字符指明一个预定义的格式化样式。这里,C代表货币格式,它将数值格式化为美元值(如3400.34就变成$3,400.34)。下面的列就使用了这种格式化字符串:

<asp:BoundField DataField="Price" HeaderText="Price" DataFormatString="{0:C}" />

10-3显示了数值的其它格式化选项。

Type 

 Format String 

 Example 

 Currency 

 {0:C} 

 $1,234.50 Brackets indicate negative values: ($1,234.50). Currency sign is locale-specific: (?1,234.50). 

 Scientific (Exponential) 

 {0:E} 

 1.234.50E+004 

 Percentage 

 {0:P} 

 45.6% 

 Fixed Decimal 

 {0:F?} 

 Depends on the number of decimal places you set. {0:F3} would be 123.400. {0:F0} would be 123. 

10-3 数值的格式化字符串

你可以在MSDN中找到其它示例。对于日期或时间值,也有一些扩展的列表。例如,如果你想将生日格式化为月//年(如 12/30/05 ),你可以使用下面的列:

<asp:BoundField DataField="BirthDate" HeaderText="Birth Date" DataFormatString="{0:MM/dd/yy}" />

10-4列出了更多示例。

Type 

 Format String 

 Example 

 Short Date 

 {0:d} 

 M/d/yyyy (for example: 10/30/2005 ) 

 Long Date 

 {0:D} 

 dddd, MMMM dd, yyyy (for example: Monday, January 30, 2005) 

 Long Date
Short Time 

 {0:f} 

 dddd, MMMM dd, yyyy HH:mm aa (for example: Monday, January 30, 2005 10:00 AM) 

 Long Date
 Long Time 

 {0:F} 

 dddd, MMMM dd, yyyy HH:mm:ss aa (for example: Monday, January 30, 2005 10:00:23 AM) 

 ISO Sortable Standard 

 {0:s} 

 yyyy-MM-dd HH:mm:ss (for example: 2005-01-30 10:00:23) 

 Month and Day 

 {0:M} 

 MMMM dd (for example: January 30) 

 General 

 {0:G} 

 M/d/yyyy HH:mm:ss aa (depends on locale-specific settings) (for example: 10/30/2002 10:00:23 AM) 

10-4 时间和日期格式化字符串

格式字符并没有指定到GridView,你可以和其它控件,模板中的数据绑定表达式一起使用它们(本章后面将有介绍),并且可以作为多数方法的参数。例如,DecimalDateTime类型可用于ToString()方法,接受一个格式化字符串,允许你手工格式化值。

样式(Styles

GridView提供了基于样式的丰富的格式化模型。同时,你可以设置8GridView样式,如表10-5所述。

样式

描述

 HeaderStyle

配置标题行的外观(ShowHeader要设置为true才能显示)

 RowStyle

配置每个一数据行的外观

 AlternatingRowStyle 

如果进行了设置,则每隔一行应用一次。This formatting acts in addition to the RowStyle formatting. For example, if you set a font using RowStyle, it is also applied to alternating rows, unless you explicitly set a different font through the AlternatingRowStyle. 

 SelectedRowStyle 

 Configures the appearance of the row thats currently selected. This formatting acts in addition to the RowStyle formatting. 

 EditRowStyle

配置在编辑模式下的行的外观。This formatting acts in addition to the RowStyle formatting. 

 EmptyDataRowStyle 

Configures the style thats used for the single empty row in the special case where the bound data object contains no rows. 

 FooterStyle

Configures the appearance of the footer row at the bottom of the GridView, if youve chosen to show it (if ShowFooter is true). 

 PagerStyle

Configures the appearance of the row with the page links, if youve enabled paging (set AllowPaging to true). 

10-5 数值格式化字符串

样式并不是单值属性。相反,每种样式提供了Style对象,包含选择颜色(ForeColorBackColor)、添加边框(BorderColor,BorderStyle, BorderWidth),改变行的大小(HeightWidth),对齐行(HorizontalAlignVerticalAlign)和配置文本外观(FontWrap)等属性。这些样式属性允许你几乎可重定义项目外观的每一个方面。如果你不想在网页中硬编码这些外观,你可以设置样式对象引用的CssClass属性到样式表类。该样式表类在链接的样式表中已经定义(第15章有更多关于样式的内容)

定义样式(Defining Styles

设置样式属性时,你可以使用两种相似的语法。第一,你可以使用object-walker语法来指明将样式属性扩展为标签属性,如下例:

<asp:GridView runat="server" ID="grid"

ItemStyle-ForeColor="DarkBlue" ... />

...

</asp:GridView>

同时,你可以添加嵌入的标签:

<asp:GridView runat="server" ID="grid" ...>

<ItemStyle ForeColor="DarkBlue" ... />

...

</asp:GridView>

这两种方法都是一样的。但是,在设置样式属性时,你还需要做更多的决定。你可以指定全局的样式属性,应用到grid中的每一列(如前面的示例),或者定义特定列的样式。为了创建特定列的样式,你需要添加样式属性或者一个嵌入标签到正确的列标签中,如下所示:

<asp:GridView runat="server" ID="grid" ...>

<Columns>

<asp:BoundField DataField="EmployeeID" HeaderText="ID" ItemStyle-Width="30px" />

...

</Columns>

</asp:GridView>

或者,你使用嵌入的标签,效果是一样的。:

<asp:GridView runat="server" ID="grid" ...>

<Columns>

<asp:BoundField DataField="EmployeeID" HeaderText="ID">

<ItemStyle Width="30px">

</asp:BoundField>

...

</Columns>

</asp:GridView>

这个技术通常也用来定义特定的列宽。如果你不定义特定的列宽,ASP.NET就使列宽自适应其中的数据(或者,在启动wrapping的情况下,可以根据换行符来适应文本)。如果值是一个范围,则宽度由最大值或者列标题来确定。但是,如果grid足够宽,你可能想要扩展列,使相邻的列看起来不会太拥挤。这种情况下,你需要显性地定义更大的列宽。

下面的代码是完全格式化的GridView标签:

<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceEmployees"

Font-Names="Verdana" Font-Size="X-Small" ForeColor="#333333"

CellPadding="4" GridLines="None" AutoGenerateColumns="False">

<HeaderStyle BackColor="#990000" Font-Bold="True" ForeColor="White" />

<RowStyle BackColor="#FFFBD6" ForeColor="#333333" />

<Columns>

<asp:BoundField DataField="EmployeeID" HeaderText="ID">

<ItemStyle Font-Bold="True" BorderWidth="1" />

</asp:BoundField>

<asp:BoundField DataField="FirstName" HeaderText="First Name" />

<asp:BoundField DataField="LastName" HeaderText="Last Name" />

<asp:BoundField DataField="City" HeaderText="City">

<ItemStyle BackColor="LightSteelBlue" />

</asp:BoundField>

<asp:BoundField DataField="Country" HeaderText="Country">

<ItemStyle BackColor="LightSteelBlue" />

</asp:BoundField>

<asp:BoundField DataField="BirthDate" HeaderText="Birth Date"

DataFormatString="{0:MM/dd/yyyy}" />

<asp:BoundField DataField="Notes" HeaderText="Notes">

<ItemStyle Wrap="True" Width="400"/>

</asp:BoundField>

</Columns>

</asp:GridView>

这个示例使用了GridView属性来设置和调整单元格跨度和网格线。它使用样式来加粗头部和配置行和交替行的背景。另外,特定列样式设置用不同的背景、加粗ID值,显性地改变Notes列的大小来突出显示位置信息。DataFormatString用于格式化在BirthDate字段中的所有数据值,图10-2显示了最终结果。

10-2 格式化的GridView

Visual Studio中配置样式

没有必要使用GridView控件标签来手工设置样式属性,因为GridView提供了丰富的设计时支持。要设置样式属性,可以使用Properties窗口来修改样式属性。例如,要配置首部的字体,扩展HeaderStyle属性来显示内嵌的Font属性来进行设置。唯一的限制是不允许你对单列进行设置样式。如果需要这样做,你可以通过编辑Columns属性来调用Fields对话框(如图10-1),然后选择相应的列,设置其样式属性。

你也可以使用预置的主题来设置组合样式,点击GridView右上角的任务按钮,选中自动套用格式。图10-3显示了自动套用格式对话框。

10-3 自动格式化GridView

一旦你选择了主题,样式设置就被插入到GridView标签中,可以通过属性窗口或手工修改它们。

值格式化(Formatting-Specific Values

到目前为止,所了解到的格式化并不精细。更精细的情况是,将格式化应用到值的一个单列中。如果想改变指定行,甚至一个单元的格式,应该怎么办?

解决办法就是对GridView.RowCreated事件进行响应。这个事件在Grid的一部分(如Header,Footer,Pager或者普通的、可选的、选择的项)被创建时唤起。你可以把当前行作为一个GridViewRow控件来进行访问。GridViewDataItem属性为给定的行提供了数据对象,GridViewRow.Cells集允许你获取行的内容。你可以使用GridViewRow来改变颜色和对齐,添加或移除子控件等。

下面的示例处理了RowCreated事件,并且根据下面的规则设置了颜色:

如果称谓(title of courtesy)是女士的称谓(本例中为Ms.或者Mrs,则该项的背景色设置为粉红色,而前景色设置为栗色。

如果称谓为Mr,则将该项的背景色设置为暗黑色,前景色设置为浅蓝色。

对其它的普通的称谓,如Dr.,则项的背景色根据DataGrid.BackColor属性指定的属性进行渲染。

下面这段代码是RowCreated事件处理程序,它实现了上述规则:

protected void GridView1_RowCreated(object sender, GridViewRowEventArgs e)

{

if (e.Row.RowType == DataControlRowType.DataRow)

{

// Get the title of courtesy for the item that's being created.

string title = (string)DataBinder.Eval(e.Row.DataItem, "TitleOfCourtesy");

// If the title of courtesy is "Ms.", "Mrs.", or "Mr.",

// change the item's colors.

if (title == "Ms." || title == "Mrs.")

{

e.Row.BackColor = System.Drawing.Color.LightPink;

e.Row.ForeColor = System.Drawing.Color.Maroon;

}

else if (title == "Mr.")

{

e.Row.BackColor = System.Drawing.Color.LightCyan;

e.Row.ForeColor = System.Drawing.Color.DarkBlue;

}

}

}

首先,程序检查正在创建的项是正式项还是预备项(alternate item)。如果都不是,这意味着这个项是其它的接口元素,如Pager,footer,header,程序就不做任何处理。如果项是正确的类型,程序从绑定的项中析取TitleOfCourtesy字段,并且将其与硬编码的字符串进行比较。

10-4显示了运行结果。

提示,这个示例使用了DataBinder.Eval()方法来获取一段数据项信息。同时,你可以将e.Row.DataItem转换为正确的类型(如,EmployeeDetails对应ObjectDataSource),DataRowViewDataSet模式里用于SqlDataSource),或者DbDataRecord(DataReader模式里用于SqlDataSource)。但是,DataBinder.Eval()方法适用于所有这些场合(速度上会有一点儿损失)。

这并不是使用RowCreated事件的最有用的示例,但是它演示了如何处理事件,并且为项读取所有重要信息。你可以使用更为精采的格式来改变pager连接的呈现方式,添加新按钮到Pagerheader,使用特定的字体和颜色来渲染需要高亮显示的值,创建总计(total)行和小计(subtotal)行,等等。

 

GridView行选择(GridView Row Selection

选择一行,意味着用户能够通过点击某个按钮或链接突出显示或者改变行的外观。当用户点击按钮时,不仅行改变了它的外观,而且你还可以在程序中处理事件。

GridView内嵌了对选择的支持。你只需要简单地添加CommandField列,并且设置ShowSelect属性为trueCommandFiels可以被演染为一个超级链接、按钮或者一幅固定图片。使用ButtonType属性来选择类型。然后可以通过SelectText属性来指定文本,并通过SelectImageUrl属性指定链接到图像。

这个示例显示了一个选择按钮:

<asp:CommandField ShowSelectButton="True" ButtonType="Button" SelectText="Select" />

显示一个小的可点击图标的示例:

<asp:CommandField ShowSelectButton="True" ButtonType="Image" SelectImageUrl="select.gif" />

10-5显示了两种类型的选择按钮,点击其中一个就可以选择行。

10-5 GridView选择

点击选择按钮时,页面被回传,然后完成一系列步骤。首先,GridView.SelectedIndexChanging事件释放,你可以对其进行截获,然后取消操作。第二步,GridView.SelectedIndex属性调整为指向选择的行。最后,GridView.SelectedIndexChanged事件释放,此时,如果你想手工更新其它控件来反映新的选择,你可以对其进行处理。当页面渲染后, SelectedRowStyle就应用到选取的行。

注意,为使选择能工作,你必须配置SelectedRowStyle,以便选择的行与其它行看起来有所不同。通常情况下,选择的行具有不同的BackColor属性。

使用选择来创建主控细节窗体(Using Selection to Create a Master-Details Form

如前一章所演示那样,你可以绑定其它数据源到具有参数的控件的属性。例如,你可以添加两个GridView控件,在第二个控件中,使用来自于第一个GridView的信息来执行一次查询。

GridView中,需要绑定的属性是SelectedIndex。但是,这有一个问题,SelectedIndex返回一个基于0的索引数字来代表Grid中出现的相应行。这并不是插入到查询中来获取相关记录的信息,相反,你需要相应行的关键字段。

所幸的是,GridView使用SelectedDataKeys属性简化了信息的获取。为使用这个特征,你必须使用逗号(,)分隔的多个字关键字段列表来设置GridView.DataKeyNames属性。你提供的每个名字必须匹配绑定对象的一个属性,或者绑定记录的一个字段。

一般地,你只有一个关键字段,如下所示:

<asp:GridView ID="gridEmployees" runat="server" DataSourceID="sourceEmployees"

DataKeyNames="EmployeeID" ... >

现在,你可以绑定第二个数据源到这个字段。下面的示例在连接查询中使用EmployeeID来在Territories表中查找所有匹配记录:

<asp:SqlDataSource ID="sourceRegions" runat="server"

ConnectionString="<%$ ConnectionStrings:Northwind %>"

ProviderName="System.Data.SqlClient" SelectCommand="SELECT Employees.EmployeeID,

Territories.TerritoryID, Territories.TerritoryDescription FROM Employees INNER JOIN

EmployeeTerritories ON Employees.EmployeeID = EmployeeTerritories.EmployeeID

INNER JOIN Territories ON EmployeeTerritories.TerritoryID = Territories.TerritoryID

WHERE (Employees.EmployeeID = @EmployeeID)" >

<SelectParameters>

<asp:ControlParameter ControlID="gridEmployees" Name="EmployeeID"

PropertyName="SelectedDataKey.Values[&quot;EmployeeID&quot;]" />

</SelectParameters>

</asp:SqlDataSource>

这个示例定义了一个对象数据源,它使用GetEmployeeRegions()方法。这个方法要求一个参数,即选择的雇员记录的EmployeeIDEmployeeID值从SelectedDataKey.Values集中获得。你可以根据它的索引号或者名字来查找EmployeeID字段(本例中为0,因为在DataKeyNames列表中仅有一个字段)。执行名字查询时有一个技巧,那就是你需要使用相应的HTML字符实体(&quot;)来替换引用标记。

10-6显示了主控细节窗体,它包含了一个雇员记录被选择时分配给雇员的区域。

10-6 主控细节页面

 

SelectedIndexChanged事件(The SelectedIndexChanged Event

正如前面的示例演示的那样,你可以清楚地设置主控细节窗体,而不需要编写任何代码。但是,在很多情况下,需要对SelectedIndexChanged事件作出反应。例如,你可能重定向用户到一个新的页面(在查询字符串中可能带了选择的值),或者,你可能想调整页面中的其它控件。

例如,下面的代码添加了一个标签来描述前面示例中的子表:

protected void gridEmployees_SelectedIndexChanged(object sender, EventArgs e)

{

int index = gridEmployees.SelectedIndex;

// You can retrieve the key field from the SelectedDataKey property.

int ID = (int)gridEmployees.SelectedDataKey.Values["EmployeeID"];

// You can retrieve other data directly from the Cells collection,

// as long as you know the column offset.

string firstName = gridEmployees.SelectedRow.Cells[2].Text;

string lastName = gridEmployees.SelectedRow.Cells[3].Text;

lblRegionCaption.Text = "Regions that " + firstName + " " + lastName +

" (employee " + ID.ToString() + ") is responsible for:";

}

10-7显示了结果。

10-7 处理SelectedIndexChanged事件

将数据字段作为选择按钮使用(Using a Data Field As a Selecte Button

你不需要创建一个新的列来支持行选择。相反,你可以将一个既存的列转换成链接。这个技术通常用于允许用户通过唯一的ID号来选择表中的行。

为使用这个技术,移除CommandField列,并且添加一个ButtonField列。然后,设置DataTextField为想要使用的字段的名字。

<asp:ButtonField ButtonType = “Button” DataTextField = “EmployeeID”/>

这个字段将有下划线,并且变成一个链接,当对其点击时,将会回传页面,并且触发GridView.RowCommand事件。你可以处理这个事件,确定选择了哪一行,然后通过程序设置SelectedIndex属性。但是,你可以使用更方便的办法。只需要指定CommandName的文本为Select,配置链接来唤起SelectedIndexChanged事件,如下所示:

<asp:ButtonField CommandName="Select" ButtonType="Button"

DataTextField="EmployeeID" />

现在点击数据字段,就自动选择了一条记录。

GridView排序(Sorting the GridView

GridView排序功能允许用户通过点击列标题来对GridView中的结果进行重新排序。它非常方便和容易实现。

要启用排序,必须设置GridView.AllowSorting属性为true。然后,需要每个可以排序的列定义SortExpression。理论上,排序表达式可以使用任何能够被数据源控件理解的语法。而在实践中,排序表达式几乎总是在SQL查询中使用的ORDER BY子句形式。这意味着排序表达式能够包含单个字段,或者逗号(,)分隔的多个字段。可选用ASCDESC添加到列名之后来使排序按升序或降序排列。

下面的示例对姓按照字符进行了排序:

<asp:BoundField DataField="FirstName" HeaderText="First Name"

SortExpression="FirstName"/>

注意,如果你想某列不能被排序,你不设置它的SortExpression属性就可以了。

提示,如果使用自动产生的列,每个绑定列都有其SortExpression属性设置来匹配DataField属性。

一旦在排序表达式和列之间建立了联系,并且设置AllowSorting属性为true,则GridView将会渲染标题为可点击链接。但是,它取决于数据源控件来实现真正的排序逻辑。排序如何实现,依赖于正在使用的数据源。并不是所有的数据源都支持排序。当然,SqlDataSourceObjectDataSource是支持的。

使用SqlDataSource排序(Sorting with SqlDataSource

使用SqlDataSource时,使用DataView类内建的能力来执行排序。本质上,当用户点击列链接时,DataView.Sort属性设置为该列的排序表达式。

提示,第8章已经阐明,每个DataTable都链接到一个默认的DataViewDataViewDataTable的窗口,它允许你应用排序和过滤,而不需要改变底层的表。你可以在程序中使用DataView,但是当你使用SqlDataSource时,它就在后台悄然使用了。但是,仅仅在DataSourceMode属性设置为SqlDataSourceMode.DataSet时,它才可获得。

使用DataView排序,从数据库获取的数据是无序的,对结果的排序在内存中进行。这并不是最快的方法(在内存在排序要求更多的负载,而且比SQL Server做相同的工作要慢),但综合使用缓存的时候,它的扩展性更强。因为你可以缓存数据的单个备份,以不同的方法进行动态排序。(第11章对此有深入介绍)。不使用DataView排序,就需要一个单独的查询来获取新的排序的数据。

10-8显示了可排序带了列链接的GridView。此时不需要任何自定义代码。

10-8 对姓自动排序

排序是根据列的数据类型来进行的。数值和数据列从小到大排列。假定底层的DataTable.CaseSensitive属性设置为false,字符串按照字符排序。包含二进制数据的列不能排序。

使用ObjectDataSource排序(Sorting with the ObjectDataSource

ObjectDataSource提供了两个选项:

如果select方法返回DataSet或者DataTableObjectDataSource能够使用与SqlDataSource相同的自动排序。

如果select方法返回一个自定义集,你需要提供一个选择方法来接收排序表达式和执行排序。除此之外,这个行为使你在构建解决方法时有足够的灵活性,但它不必是理想的排序。例如,不在GetEmployees()方法中执行排序,而是创建自定义带有Sort()方法的EmployeeDetails集类更有意义。不幸的是,ObjectDataSource不支持这种模式。

为了使用排序参数,你需要创建select方法来接收单个字符串参数。然后必须设置ObjectDataSource.SortParameterName属性来标识参数名,如下所示:

<asp:ObjectDataSource ID="sourceEmployees" runat="server"

TypeName="DatabaseComponent.EmployeeDB"

SelectMethod="GetEmployees" SortParameterName="sortExpression" />

注意,当你设置SortParameterName时,ObjectDataSource总是调用方法来接收排序表达式。如果数据没有排序(如grid首次创建时),ObjectDataSource简单地传递一个空字符串作为排序表达式。

现在,需要实现GetEmployees()方法,并且决定如何执行排序。最简单的方法是填充一个非连接的dataset这样你就可以依靠DataView的排序功能。下面是GetEmployees()方法的示例,在数据库组件中进行了排序:

public EmployeeDetails[] GetEmployees(string sortExpression)

{

SqlConnection con = new SqlConnection(connectionString);

SqlCommand cmd = new SqlCommand("GetAllEmployees", con);

cmd.CommandType = CommandType.StoredProcedure;

SqlDataAdapter adapter = new SqlDataAdapter(cmd);

DataSet ds = new DataSet();

try

{

con.Open();

adapter.Fill(ds, "Employees");

}

catch (SqlException err)

{

// Replace the error with something less specific.

// You could also log the error now.

throw new ApplicationException("Data error.");

}

finally

{

con.Close();

}

// Apply sort.

DataView view = ds.Tables[0].DefaultView;

view.Sort = sortExpression;

// Create a collection for all the employee records.

ArrayList employees = new ArrayList();

foreach (DataRowView row in view)

{

EmployeeDetails emp = new EmployeeDetails(

(int)row["EmployeeID"], (string)row["FirstName"],

(string)row["LastName"], (string)row["TitleOfCourtesy"]);

employees.Add(emp);

}

return (EmployeeDetails[])employees.ToArray(typeof(EmployeeDetails));

}

另一个方法是改变查询来应对排序表达式的变化。这样,数据库就能够执行排序。这个方法稍有点复杂,并且没有很好的可选项。下面是最可能的两种情况:

你可以动态构建带有ORDER BY子句的SQL语句。但是,这有SQL注入风险,需要你小心验证输入。

你可以编写条件逻辑来检查排序表达式,并且相应地执行查询(在select方法或者存储过程中)。这种代码很可能比较脆弱,并且包含了一些字符串解析。

排序和选择(Sorting and Selection

如果同时使用排序和选择,你会发现另一个问题。为了看到这个问题,选择一行,然后对任何一列进行排序。你会看到仍然保持了选择,但是它应该移动到选择时索引号相同的项。换句话说,如果你选择第二行并且执行排序,第二行将在新页面中仍保持选择,但所选择的记录并不是你先前选择的。要解决这个问题,需要在程序中每次点击头部链接时改变选择。

最简单的选项是响应GridView.Sorted事件,清除选择,如下所示:

protected void GridView1_Sorted(object sender, GridViewSortEventArgs e)

{

// Clear selected index.

GridView1.SelectedIndex = -1;

}

在一些情况下,你可能想做得更好,并且保证排序改变时仍保持行的正确选择。有一点技巧就是每次选择索引改变时,在视图状态中存储选择行的关键字段的值:

protected void GridView1_SelectedIndexChanged(object sender, EventArgs e)

{

// Save the selected value.

if (GridView1.SelectedIndex != -1)

{

ViewState["SelectedValue"] = GridView1.SelectedValue.ToString();

}

}

现在,Grid绑定到数据源(例如在排序操作之后),你可以重新应用最后选择的索引:

protected void GridView1_DataBound(object sender, EventArgs e)

{

if (ViewState["SelectedValue"] != null)

{

string selectedValue = (string)ViewState["SelectedValue"];

// Reselect the last selected row.

foreach (GridViewRow row in GridView1.Rows)

{

string keyValue = GridView1.DataKeys[row.RowIndex].Value.ToString();

if (keyValue == selectedValue)

{

GridView1.SelectedIndex = row.RowIndex;

return;

}

}

}

}

住,如果你启用了分页,这种方法会引起混乱。因为排序操作可能将当前行移到了另外一个页面,渲染之后虽然保持了选择,但并不可见。从代码的观点看,这是完美的验证,但在实践中,却会引起混乱。

高级排序(Advanced Sorting

GridView的排序是直观的,它支持对任何可排序的列进行升序排序。在一些应用中,用户有更多的排序选项,或者使用更复杂的排序表达式来对冗长的结果集进行排序。

GridView中改善排序的首选方法中处理GridView.Sorting事件,它在应用了排序后触发。此时,你可以改变排序表达式。例如,你可以使用它来实现升序/降序排序模式。这种模式下,你第一次点击列来应用升序排序,第二次应用降序排序。这与Windows资源管理器里的排序是相似的。

实现这个方法的代码如下:

protected void GridView1_Sorting(object sender, GridViewSortEventArgs e)

{

// Check to see the if the current sort (GridView1.SortExpression)

// matches the requested sort (e.SortExpression).

// This code tries to match the beginning of the GridView

// sort expression. The final ASC or DESC part is ignored.

if (GridView1.SortExpression.StartsWith(e.SortExpression))

{

// This sort is being applied to the same field for the second time.

// Reverse it.

if (GridView1.SortDirection == SortDirection.Ascending)

{

// This takes care of automatically adding the "DESC"

// to the end of the sort expression.

e.SortDirection = SortDirection.Descending;

}

}

}

你可以使用相似的逻辑,点击不同的列来进行复合排序。例如,你可能想是否用户点击了LastName,然后点击了FirstName。这种情况下,你可以应用LastName+FirstName排序。

protected void GridView1_Sorting(object sender, GridViewSortEventArgs e)

{

if (e.SortExpression == "FirstName" && GridView.SortExpression == "LastName")

{

// Based on the current sort and the requested sort, a compound

// sort makes sense.

e.SortExpression = "LastName, FirstName";

}

}

你可以将这种方法扩展到更多步骤,通过在视图状态中存储用户的选择,并传递给刷新后的页面,构建更大的排序表达式,来级联在任意列集上的搜索。

还有更多技术可用。你可以通过在程序中调用GridView.Sort()方法,并且提供一个排序表达式来对GridView进行排序。这在你想呈现给用户前对冗长的数据报告进行预排序时派上用场。如果你想允许用户选择预定义的排序选项列表(在另一个控件中列出),而不是通过列标题点击来进行排序,这会更有意义。

10-9显示了一个示例。当选择了列表中的一项时,排序就调用下面的代码:

protected void lstSorts_SelectedIndexChanged(object sender, EventArgs e)

{

GridView1.Sort(lstSorts.SelectedValue, SortDirection.Ascending);

}

10-9 通过其它控件来提供排序选项

GridView分页(Paging the GridView

到目前为止,重复值绑定的所有示例都将数据源的所有记录都显示在单个的页面上。但是,在现实世界中,这种状态并不理想。连接到包含了成百,甚至成千的记录的数据源将会产生一个巨大的网页,导致渲染和传输到客户端浏览器变得几乎不可能。

大多数站点在表中显示数据,或者在分页中列出记录,换句话说,就是在每页中显示固定数量的记录,并且提供到其它分页的导航链接。例如,在使用搜索引擎时,返回了成千上万的记录,分页功能体现得淋漓尽致了。

GridView控件内建分页支持。你可以在使用SqlDataSourceObjectDataSource时简单地创建分页。如果使用ObjectDataSource,你还可以自定义分页工作的方式,以获得更高的效率和扩展性。

自动分页(Automatic Paging

通过设置少量属性和处理事件,你可以使用GridView控件为你管理分页。GridView会创建到前一面或者下一面的链接,并且为当前页显示记录,而这一切并不需要你手动地析取记录。在讨论这个方法的优点和缺点之前,先看看需要准备些什么才可以进行自动分页。

GridView提供了几个属性来支持分页,在表10-6中列出。

Property  

 Description  

AllowPaging  

Enables or disables the paging of the bound records. It is false by default.  

PageSize  

Gets or sets the number of items to display on a single page of the grid. The default value is 10.  

CurrentPageIndex  

 Gets or sets the zero-based index of the currently displayed page, if paging is enabled.  

PagerSettings  

Provides a PagerSettings object that wraps a variety of formatting options for the pager controls. These options determine where the paging controls are shown and what text or images they contain. You can set these properties to fine-tune the appearance of the pager controls, or you can use the defaults.  

PagerStyle  

Provides a style object you can use to configure fonts, colors, and text alignment for the paging controls.  

PageIndexChanged Event

Occurs when one of the page selection elements is clicked.  

10-6 DataGrid的分页成员

要使用自动分页,你需要设置AllowPagingtrue(意即显示分页控件),还需要设置PageSize来确定每个页面显示的记录数。

下面是GridView控件的声明的示例,其中设置了这些属性。

<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceProducts"

PageSize="5" AllowPaging="True" ...>

...

</asp:GridView>

10-10显示了每页5条记录(总共16页)的示例。

10-10 每个分页5条记录

自动分页能够在任何实现了Icollection的数据源中工作。这意味着SqlDataSource支持自动分页,只要你使用DataSet模式。(DataReader模式不能工作)。另外,ObjectDataSource也支持自动分页,假定你的自定义数据访问类返回了一个实现ICollection对象(数组,强类型集)和非连接的DataSet,则都能够进行自动分页。

自动分页是一种模拟,它并不减少需要从数据库查询的数据的数量。分页面临的一个问题是,所有的数据在用户改变当前页时都需要进行绑定。换句话说,如果你将一个表分成10个页面,你将执行10次同样的工作,并且使数据库的总工作量增加10

幸运的是,你可以使用缓存(见第11章)来使自动分页更加高效。这允许多个请求重用相同的数据对象。当然,在处理特别巨大的查询时,将DataSet存储在缓存中可能并不是理想的解决方案。在这种情况下,需要用来缓存整个DataSet的内存量也是非常巨大的。此时,就需要使用分页了。

使用ObjectDataSource时自定义分页

自定义分页需要你小心处理和绑定当前页面的记录到GridViewGridView不再选择会自动显示的行。但是GridView仍提供了分页工具条,工具条有自动产生的链接,以便用户能够在页面间导航。

尽管自定义分页相当于自动分页要复杂得多,但它允许你减少带宽消耗,并且避免存储大的数据对象在服务器端的内存中。另一方面,大多数自定义策略要求数据库每次回传,这意味着你将对数据库做更多的工作。

提示,自定义分页是否比进行缓存的自动分页更好,这取决于你使用数据的方式。GridView使用的数据量越大,就越应使用自定义分页。另一方面,数据库服务器越慢,负载越重,则越需要使用缓存来减少重复调用。从本质上来讲,你需要根据你的应用来优化分页策略。

ObjectDataSource是唯一支持自定义分页的数据源。使用自定义分页的第一步是设置ObjectDataSourcePagingtrue然后通过至少3个属性来实现分页:StartRowIndexParameterName, MaximumRowsParameterNameSelectCountMethod

对记录计数(Counting the Records

为了使GridView的分页链接能够正确计数,你应该知道记录总数和每页的记录数。每页记录值通过PageSize属性设置,如前面的示例中那样。页面的总数需要一点技巧。

使用自动分页时,记录的总数由GridView根据数据源中的记录数自动确定。在自定义分页中,你必须显性地使用专门的方法来计算总数。下面的过程显示了应该怎样获取Employees表的记录总数,然后返回计数值:

public int CountEmployees()

{

// Create the Command and the Connection.

string sql = "SELECT COUNT(*) FROM Employees";

SqlConnection con = new SqlConnection(connectionString);

SqlCommand cmd = new SqlCommand(sql, con);

con.Open();

// Execute the command and use the return value for the

// VirtualItemCount property.

Datagrid1.VirtualItemCount = (int)cmd.ExecuteScalar();

con.Close();

}

例子中使用COUNT()计数方法来计算表的记录数,并使用Command对象的ExcuterScalar()方法来返回计数信息。这个方法使用SelectCountMethod属性绑定到ObjectDataSource

<asp:ObjectDataSource ID="sourceEmployees" runat="server" EnablePaging="True"

SelectCountMethod="CountEmployees" ... />

使用自定义分页时,SelectCountMethod在每次回传时执行。如果你想冒着不正确计数的风险来减少数据库的工作量,你可以缓存这个信息并且对其重用。

使用存储过程来获取分页的记录(A Stored Procedure to Get Paged Record

解决方案的下一部分稍有点技巧性。不是获取Employee记录的总数,GetEmployees()方法必须返回仅仅用于当前页的记录。为了实现这一点,这个示例使用了存储过程GetEmployeePage。该存储过程拷贝所有的雇员记录到一个临时表,并且在临时表中添加一个附加唯一ID自动增量的列。然后,存储过程从对应于请求的页面的数据表中获取选择,使用了@Start@Count参数。

存储过程的完整代码如下:

CREATE PROCEDURE GetEmployeePage

@Start int, @Count int

AS

-- create a temporary table with the columns we are interested in

CREATE TABLE #TempEmployees

(

ID int IDENTITY PRIMARY KEY,

EmployeeID int,

LastName nvarchar(20),

FirstName nvarchar(10),

TitleOfCourtesy nvarchar(25),

)

-- fill the temp table with all the employees

INSERT INTO #TempEmployees

(

EmployeeID, LastName, FirstName, TitleOfCourtesy

)

SELECT EmployeeID, LastName, FirstName, TitleOfCourtesy

FROM Employees ORDER BY EmployeeID ASC

-- declare two variables to calculate the range of records

-- to extract for the specified page

DECLARE @FromID int

DECLARE @ToID int

-- calculate the first and last ID of the range of records we need

SET @FromID = @Start

SET @ToID = @Start + @Count - 1

-- select the page of records

SELECT * FROM #TempEmployees WHERE ID >= @FromID AND ID <= @ToID

GO

这个存储过程使用了SQL Server特定的方法,其它数据库可能有别的优化方法。例如,Oracle数据库允许你在查询的WHERE子句中使用ROWNUM来返回一定范围的行。例如,SELECT * FROM Employees WHERE ROWNUM>100 AND ROWNUM<200返回101行到199行给页面。

分页选择方法(The Paged Selection Method

最后一步是创建GetEmployees()方法来执行分页。这个方法接收两个参数:首页的行索引号和页面大小(每页的最大行数)。通过StartRowIndexParameterNameMaximumRowsParameterName属性来指定这两个参数的名字。如果没有设置,则默认为startRowIndexmaximumRows

下面是使用了存储过程的GetEmployees()方法的实现:

public EmployeeDetails[] GetEmployees(int startRowIndex, int maximumRows)

{

SqlConnection con = new SqlConnection(connectionString);

SqlCommand cmd = new SqlCommand("GetEmployeePage", con);

cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add(new SqlParameter("@Start", SqlDbType.Int, 4));

cmd.Parameters["@Start"].Value = startRowIndex + 1;

cmd.Parameters.Add(new SqlParameter("@Count", SqlDbType.Int, 4));

cmd.Parameters["@Count"].Value = maximumRows;

// Create a collection for all the employee records.

ArrayList employees = new ArrayList();

try

{

con.Open();

SqlDataReader reader = cmd.ExecuteReader();

while (reader.Read())

{

EmployeeDetails emp = new EmployeeDetails(

(int)reader["EmployeeID"], (string)reader["FirstName"],

(string)reader["LastName"], (string)reader["TitleOfCourtesy"]);

CHAPTER 10 RICH DATA CONTROLS 359

employees.Add(emp);

}

reader.Close();

return (EmployeeDetails[])employees.ToArray(typeof(EmployeeDetails));

}

catch (SqlException err)

{

throw new ApplicationException("Data error.");

}

finally

{

con.Close();

}

}

运行这个页面的时候,你将看到与使用自动分页产生的页面相同的输出,分页控件的工作方式也是一样的。

自定义分页条(Customizing the Pager Bar

GridView分页控件是非常灵活的。在它们默认的外观中,你会看到一系列数字(见图10-10)。但是,你可以使用PagerStyle属性(自定义前景色、背景色、字体、颜色、尺寸等)和PagerSettings属性完全进行自定义。在PagerSettings.Mode属性中最重要的细节,在表10-7中描述。PagerSettings.Mode指明了根据某种样式来渲染分页的链接。

Mode  

 Description  

 Numeric  

 The grid will render as many links to other pages as specified by the PageButtonCount property. If that number of links is not enough to link to every page of the grid, the pager will display ellipsis links () that, when clicked, display the previous or next set of page links.  

 NextPrevious  

 The grid will render only two links for jumping to the previous and next pages. If you choose this option, you can also define the text for the two links through the NextPageText and PreviousPageText properties (or use image links through NextPageImageUrl and PreviousPageImageUrl).  

 NumericFirstLast  

 The same as Numeric, except there are additional links for the first page and the last page.  

 NextPreviousFirstLast  

 The same as NextPrevious, except there are additional links for the first page and the last page. You can set the text for these links through FirstPageText and LastPageText properties (or images through FirstPageImageUrl and LastPageImageUrl).  

10-7 分页模式

如果你不喜欢默认的分页条,你可以使用下一节描述的模板特征来实现自己的分页条。创建了PagerTemplate之后,你可以使用任何控件,如文本控件(以便用户输入页面索引号)和按钮来提交请求,并且加载新的页面。解析和绑定记录到当前页面的代码是相同的。

GridView模板(GridView Templates

到目前为止,前面的示例使用了GridView控件来显示数据,每一个字段对应一个单独的列。如果你想放置多个值到同一个单元,或者通过添加HTML标签和服务器控件来无限地自定义单元格中的内容,则需要使用TemplateField

TemplateField允许你为一列定义完全自定义的模板。在模板里,你可以添加控件标签、任意的HTML元素和数据绑定表达式。你可以自由地按任何方式来安排任何内容。

例如,假定你创建了一列,列中合并了first namelast namecourtesy字段。要完成这项具有挑战性的工作,你可以创建如下的ItemTemplate:

<asp:TemplateField HeaderText="Name">

<ItemTemplate>

<%# Eval("TitleOfCourtesy") %> <%# Eval("FirstName") %>

<%# Eval("LastName") %>

</ItemTemplate>

</asp:TemplateField>

现在,当你绑定GridView时,GridView从数据源抓取数据,并且遍历项集(collection of items)。它为每项都处理ItemTemplate,计算数据绑定表达式和添加演染的HTML到表中。这个模板非常简单,它简单地定义了三个数据绑定表达式。当进行计算时,这些表达式被转换为二进制文本。

注意,如果你想绑定一个不在结果集中显示的字段,你会收到一个运行时错误。如果你获取的附加字段不再绑定到任何模板时,则不会报错。

你可能已经注意到表达式使用了Eval()方法。它是System.Web.UI.Data-Binder类的一个静态方法。Eval()是非常重要的工具,它自动获取绑定到当前行的数据项,使用映射来查找匹配的字段(对于行)或属性(对于自定义对象),然后返回值。这个映射过程增加了少量额外工作。但是它在处理查询的时候,并不会使消耗的时间增加太多。如果没有Eval()方法,你需要通过Container.DataItem属性和类型转换代码访问数据对象,如下所示:

<%# ((EmployeeDetails)Container.DataItem)["FirstName"] %>

使用这个方法的问题是需要知道数据对象的精确类型。例如,在前面的数据绑定表达式中,假定你通过ObjectDataSource绑定到一个EmployeeDetails对象数组。如果你要转换成SqlDataSource,或者重命名EmployeeDetails类,你的页面就会崩溃。另一方面,如果你使用Eval()方法,只要数据对象的名字正确,数据绑定表达式就会正常工作。换句话说,使用Eval()方法,你就可以创建与数据访问层松耦合页面。

提示,在DataSet模式下绑定到SqlDataSource时,数据项就是DataRowView。绑定到DataReader模式的SqlDataSource时,数据项是DbDataRecord

Eval()方法也能够快速地对数据字段进行格式化。要使用这个功能,你必须使用Eval()方法的重载方法来接收附加的格式化字符串参数。示例如下:

<%# Eval("BirthDate", "{0:MM/dd/yy}") %>

你可以在Eval()方法中使用表10-3和表10-4中定义的任何格式化字符串

你可以自由组合模板列和其它类型的列。你也可以不使用任何其它列,而是将所有来自于Employees表中的信息放入一个格式化的模板:

<asp:GridView ID="gridEmployees" runat="server" DataSourceID="sourceEmployees"

AutoGenerateColumns="False" ...>

<!-- Styles omitted. -->

<Columns>

<asp:TemplateField HeaderText="Employees">

<ItemTemplate>

<b>

<%# Eval("EmployeeID") %> -

<%# Eval("TitleOfCourtesy") %> <%# Eval("FirstName") %>

<%# Eval("LastName") %>

</b>

<hr />

<small><i>

<%# Eval("Address") %><br />

<%# Eval("City") %>, <%# Eval("Country") %>,

<%# Eval("PostalCode") %><br />

<%# Eval("HomePhone") %></i>

<br /><br />

<%# Eval("Notes") %>

</small>

</ItemTemplate>

</asp:TemplateField>

</Columns>

</asp:GridView>

10-11显示了输出结果。

10-11 创建模板化的列

 

使用多个模板(Using Multiple Templates

前面的示例使用了单个模板来配置所有数据项的外观。但是,GridView并不是仅仅提供了ItemTemplate模板。事实上,GridView允许你使用大量的模板来配置不同的外观。在每一个模板列里,你都可以使用表10-8中列出的模板。

Mode  

 Description  

 HeaderTemplate  

 Determines the appearance and content of the header cell  

 FooterTemplate  

 Determines the appearance and content of the footer cell  

 ItemTemplate  

 Determines the appearance and content of each data cell (if you arent using the AlternatingItemTempalte) or every odd-numbered data cell (if you are)  

 AlternatingItemTemplate  

 Used in conjunction with the ItemTemplate to format even-numbered and odd-numbered rows differently  

 EditItemTemplate  

 Determines the appearance and controls used in edit mode  

10-8 GridView模板

除了表10-8中列出的模板外,EditItemTemplate是最重要的模板,它使你能够控制对于字段的编辑体验。如果你不使用模板化的字段,你就只能限定于普通的文本框,同时还不能进行任何验证。GridView还定义了两个可以在任何列之外使用的模板,即PagerTemplate(允许你自定义分页控件的外观)EmpltyDataTemplate(允许你在GridView绑定到一个空的数据对象时能够设置显示的内容)

 

Visual Studio中编辑模板(Editing Templates in Visual Studio

Visual Studio 2005增强了对模板编辑的支持,使你能够在网页设计器中编辑模板,下面是编辑模板的步骤:

创建至少具有一个模板化列的GridView

选择GridView,然后点击灵巧标签(smart tag,即控件右上角的三角形)中的编辑模板。这样就转换到模板编辑模式。

在灵巧标签中,使用下拉显示列表,选择想要编辑的模板(见图10-12)。你可以选择EmptyDataTemplatePagerTemplate中的任意一个模板来应用到整个GridView。你也可以为单个模板化的列选择特定的模板。

10-12 Visual Studio中编辑模板

在控件中输入内容。你可以输入静态的内容、拖拽控件等。

完成后,在灵巧标签中选择结束编辑。

 

绑定到方法(Binding to a Method

使用模板的一个好处是,它允许你使用数据绑定表达式来扩展格式化和呈现绑定的数据的方法。在很多场合重复出现的一个关键技术是在页面类中使用方法来处理字段值。这消除了简单数据绑定的限制,使你能够将动态信息与条件逻辑结合起来。

例如,你可能会创建一个图标列,在每一行旁显示一个图标。但是你又愿意使用静态图标,你希望根据行中的数据来选择最合适的图像。图10-13显示了一个示例,√标签表明库存充足(大于50个单位),×标签表明库存耗尽。

下面的示例中定义了状态列:

<asp:TemplateField HeaderText="Status">

<ItemTemplate>

<img src='<%# GetStatusPicture(Container.DataItem) %>' />

</ItemTemplate>

</asp:TemplateField>

10-13 根据条件显示的标记行

其中,GetStatusPicture()方法对数据项进行了检查,并且选择正确的图片的URL

protected string GetStatusPicture(object dataItem)

{

int units = Int32.Parse(DataBinder.Eval(dataItem, "UnitsInStock").ToString());

if (units == 0)

return "Cancel.gif";

else if (units > 50)

return "OK.gif";

else

return "blank.gif";

}

这个技术在很多场合出现。例如,你可以使用它来随当前汇率调整价格。或者,你可以使用它来将数据的代码转换为更有意义的文本价格。你甚至可以创建计算列,例如,根据EmployeeDataOfBirth字段值来计算年龄,提供给EmployeeAge列。

注意,如果使用数据绑定表达式来绑定到方法,你不必使用回调(callback)来优化GridView刷新过程。为了防止出现错误,必须将GridView.EnableSortingAndPagingCallbacks设置为true。如果你不想放弃回调功能,你可以在在项第一次出现时,使用GridView.ItemCreated事件来修改项,以获得相似的功能。前面的格式化特定的值formatting-specific values一节已经对此提及。

 

在模板中处理事件(Handling Events in a Template

有时,添加到模板中的列的控件唤起的事件,需要你进行处理。例如,在前面的示例中,假设你不显示静态的图标,而是创建一个可点击的图像链接,这个图像链接是一个ImageButton控件。这是非常容易实现的:

<asp:TemplateField HeaderText="Status">

<ItemTemplate>

<asp:ImageButton ID="ImageButton1" runat="server"

ImageUrl='<%# GetStatusPicture(Container.DataItem) %>' />

</ItemTemplate>

</asp:TemplateField>

问题是,如果你添加控件到模板,GridView就会创建这个控件的多个拷贝,每一个数据项一份拷贝。当点击ImageButton时,你需要一种方法来确定到底点击了哪个图像,它属于哪一列。

解决这个问题的方法是使用GridView里的事件,而不是包含的按钮的事件。RowCommand事件正是为此而来,在任何一个模板中的任何一个按钮被点击时,这个事件都会释放。模板中的控件事件转换成包含的控件里的事件的过程,叫做事件起泡(Event Bubbling)。

当然,你仍需要一种方法来传递信息给RowCommand事件,以便在动作发生时标识行。这个秘密就在所有按钮控件的两个字符串属性中:CommandNameCommandArgument属性。CommandName设置了一个描述性的名字,用于区分GridView中的ImageButton的点击和其它按钮控件的点击。CommandArgument提供了一个基于特定行的数据,可以用来标识被点击的行。你可以使用数据绑定表达式来提供这个信息。

下面是修改后的ImageButton标签:

<asp:TemplateField HeaderText="Status">

<ItemTemplate>

<asp:ImageButton ID="ImageButton1" runat="server"

ImageUrl='<%# GetStatusPicture(Container.DataItem) %>'

CommandName="StatusClick" CommandArgument='<%# Eval("ProductID") %>' />

</ItemTemplate>

</asp:TemplateField>

点击ImageButton后的事件响应代码:

protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)

{

if (e.CommandName == "StatusClick")

lblInfo.Text = "You clicked product #" + e.CommandArgument;

}

这个示例简单地在标签中显示ProductID

提示,记住,你可以使用GridView里内建的选择支持来减少劳动量。只要设置CommandNameSelect,并且处理SelectIndexChanged事件就可以。在本章前面的使用数据字段作为选择按钮(Using a Data Field As a Select Button一节中已有描述。尽管这个方法使你能够方便地访问点击的行,但在你想提供多个按钮来执行不同的任务时,它就无能为力了。

 

使用模板编辑(Editing with a Template

使用模板的最重要的原因是提供更好的编辑体验。在前面一章中,你已看到GridView如何提供自动编辑能力,即你所需要做的仅是设置GridView.EditItemIndex属性来切行换到编辑模式。要做到这一点最简易的方法就是添加CommandField列,并且设置ShowEditButtontrue。然后,用于简单地点击行内的链接就可以进行编辑。此时,在每一列里的标签都被文本框所取代(除非字段为只读)。

标准的编辑支持有几个限制:

使用文本框并不适合编辑所有的值。一些类型的数据需要使用其它控件来处理,如下拉列表,大的字段需要多行的文本框等。

没有验证。限制编辑能力以便现金数字不能被输入负数,类似这样的限制可能会比较好。你不能添加验证器控件到EditItemTemplate中来进行验证。

它比较丑陋。文本框的行横过grid,占据了太多空间,经常看起来专业。

在模板化的列中,你没有这些问题。相反,你可以显性地定义编辑控件,并且使用EditItemTemplate来安排布局。这个过程稍要费点功夫。

下面是编辑模板,允许编辑一个单独的字段—Notes字段:

<EditItemTemplate>

<b>

<%# Eval("EmployeeID") %> -

<%# Eval("TitleOfCourtesy") %> <%# Eval("FirstName") %>

<%# Eval("LastName") %>

</b>

<hr />

<small><i>

<%# Eval("Address") %><br />

<%# Eval("City") %>, <%# Eval("Country") %>,

<%# Eval("PostalCode") %><br />

<%# Eval("HomePhone") %></i>

<br /><br />

<asp:TextBox Text='<%# Bind("Notes") %>' runat="server" id="textBox"

TextMode="MultiLine" Width="413px" />

</small>

</EditItemTemplate>

绑定一个可编辑值到控件时,必须在数据绑定表达式中使用Bind()方法,而不是普通的Eval()方法。只有Bind()方法才能创建双向链接,这样才能保证更新的值被发送回服务器。

需要记住的另一个重要事实是,当GridView提交和更新时,它只提交绑定的、可编辑的参数。在前面的示例中,这意味着GridView会回传Notes字段对应的@Notes参数。这是非常重要的,因为读取参数化的更新命令时(如果使用的是SqlDataSource),则必须使用一个参数,如下所示:

UpdateCommand="UPDATE Employees SET Notes=@Notes WHERE EmployeeID=@EmployeeID"

相似地,使用ObjectDataSource时,必须确保更新方法使用了一个名为Notes的参数

10-14显示了编辑模式下的行

10-14 使用模板进行编辑

 

使用高级控件进行编辑(Editing with Advanced Controls

如果你需要绑定到更多控件,如列表,基于模板的编辑非常有用。例如,你可以对前面的示例进行修改,使TitleOfCourtesy字段通过下拉列表可以编辑。你需要下面的模板,新的细节部分在粗体显示:

<EditItemTemplate>

<b>

<%# Eval("EmployeeID") %> -

<asp:DropDownList runat="server" ID="EditTitle"

SelectedIndex='<%# GetSelectedTitle(Eval("TitleOfCourtesy")) %>'

DataSource='<%# TitlesOfCourtesy %>' />

<%# Eval("TitleOfCourtesy") %> <%# Eval("FirstName") %>

<%# Eval("LastName") %>

</b>

<hr />

<small><i>

<%# Eval("Address") %><br />

<%# Eval("City") %>, <%# Eval("Country") %>,

<%# Eval("PostalCode") %><br />

<%# Eval("HomePhone") %></i>

<br /><br />

<asp:TextBox Text='<%# Bind("Notes") %>' runat="server" id="textBox"

TextMode="MultiLine" Width="413px" />

</small>

</EditItemTemplate>

这个模板允许和户从限制的称谓(title of courtesy)选择项中选取一个。为了创建这个列表,你需要使用一点技巧,使用指向自定义属性的数据绑定表达式来设置DropDownList.DataSource。这个自定义属性能够返回带有可获得称谓的数据源。

下面是在网页类中对TitlesOfCourtesy属性的定义:

protected string[] TitlesOfCourtesy

{

get { return new string[]{"Mr.", "Dr.", "Ms.", "Mrs."}; }

}

称谓的列表并没有完成。这里只列出了Miss,Lord,Lady等,而在实际的应用中,称谓可能来自于数据库表或者配置文件。

这个步骤保证了下拉列表是通用的,但它并没有解决相关的问题,即如何确保在列表中选取正确的title来作为当前值。最好的方法是绑定SelectedIndex到自定义的方法,该方法使用当前title,并且返回那个值的索引。在这个例子中,GetSelectedTitle()方法执行这个任务。它使用title作为输入,然后从TitleOfCourtesy返回的数组里返回各个值的索引。

protectedint GetSelectedTitle(object title)

{

return Array.IndexOf(TitlesOfCourtesy, title.ToString());

}

这段代码使用静态的Array.IndexOf()方法来搜索数组。注意,你必须显性地转换title为字符串。因为DataBinder.Eval()方法返回一个对象,而不是字符串,该对象传递给GetSelectedTitle()方法。

10-15显示了在操作中的下拉列表。

10-15 使用下拉列表值进行编辑

不幸的是,这仍不能完成示例。现在,在编辑模式下,你有一个列表框,列表框中选择了正确的项。但是,如果你改变选择,值并不会发送回数据源。在这个例子中,你可以使用Bind()方法和SelectedValue属性来处理这个问题,因为在控件中的文本对应于你想提交到记录的文本。但是,有时并不能那么称心如意,因为你需要将值转换成不同的数据库呈现。在这种情况下,唯一的选择是处理RowUpdating事件,在当前行中查找列表控件和精确的文本。然后你可以动态地添加附加的参数,如下所示:

protected void gridEmployees_RowUpdating(object sender, GridViewUpdateEventArgs e)

{

// Get the reference to the list control.

DropDownList title = (DropDownList)

(gridEmployees.Rows[e.RowIndex].FindControl("EditTitle"));

// Add it to the parameters.

e.NewValues.Add("TitleOfCourtesy", title.Text);

}

现在就可以成功更新Notes字段和TitleOfCourtesy。你已经看到,可编辑的模板给了你强大的功能,但编码通常要更麻烦一点。

提示,为了使用EditItemTemplate更加有意思,你还可以添加验证器来验证输入的值。验证器的内容在第4章进行了介绍。

 

无命令列的编辑(Editing Without a Command Column

到目前为止,所有的示例都使用了CommandField来自动产生编辑控件。但是,你已经在基于模板的方法上进行了升华,如何添加自己的编辑控件,是值得考虑的事情。

其实这非常简单。你所需要做的就是添加按钮控件到项模板,并且设置CommandNameEdit。这会自动触发编辑过程,释放正确的事件和将行转换为编辑模式。

<ItemTemplate>

<b>

<%# Eval("EmployeeID") %> - <%# Eval("TitleOfCourtesy") %>

<%# Eval("TitleOfCourtesy") %> <%# Eval("FirstName") %>

<%# Eval("LastName") %>

</b>

<hr />

<small><i>

<%# Eval("Address") %><br />

<%# Eval("City") %>, <%# Eval("Country") %>,

<%# Eval("PostalCode") %><br />

<%# Eval("HomePhone") %></i>

<br /><br />

<%# Eval("Notes") %>

<br /><br />

<asp:LinkButton runat="server" Text="Edit"

CommandName="Edit" ID="Linkbutton1" />

</small>

</ItemTemplate>

在编辑模板中,你需要两个按钮,CommandName分别为UpdateCancel

<EditItemTemplate>

<b>

<%# Eval("EmployeeID") %> -

<asp:DropDownList runat="server" ID="EditTitle"

SelectedIndex='<%# GetSelectedTitle(Eval("TitleOfCourtesy")) %>'

DataSource='<%# TitlesOfCourtesy %>' />

<%# Eval("TitleOfCourtesy") %> <%# Eval("FirstName") %>

<%# Eval("LastName") %>

</b>

<hr />

<small><i>

<%# Eval("Address") %><br />

<%# Eval("City") %>, <%# Eval("Country") %>,

<%# Eval("PostalCode") %><br />

<%# Eval("HomePhone") %></i>

<br /><br />

<asp:TextBox Text='<%# Bind("Notes") %>' runat="server" id="textBox"

TextMode="MultiLine" Width="413px" />

<br /><br />

<asp:LinkButton runat="server" Text="Update"

CommandName="Update" ID="Linkbutton1" />

<asp:LinkButton runat="server" Text="Cancel"

CommandName="Cancel" ID="Linkbutton2" />

</small>

</EditItemTemplate>

只要你使用这些名字,GridView编辑事件就会释放,数据源控件就会使用自动产生的编辑控件一样进行响应。

10-16显示了自定义编辑控件。

10-16 自定义编辑控件

DetailsViewFormView(The DetailsView and FormView)

GridView在显示具有很多行信息的大容易表格时颇具优势。但是,有时你需要查看一个记录的细节。尽管你可以在GridView中使用模板列来解决这个问题,但ASP.NET仍为此提供了两个专门的控件,即DetailViewFormView。这两个控件都是每次显示1条记录,但包含了可选的分页按钮来允许你查看其它记录(每页显示1条记录)。DetailsViewFormView的区别是它们所支持的模板不同。DetailsView构建于字段对象之外,正如GridView是构建于列对象之外一样。而FormView是基于模板的,类似GridView模板列的工作方式,这样可以使你获得更多的灵活性。当然,同时会增加一些工作量。

在理解了GridView的特征之后,你就能够快速地学会DetailsViewFromView。因为这两者都借用了GridVeiw模型的一部分。

 

DetailsView控件(The DetailsView

DetailsView用于每次显示1条记录。它将每块信息(一个字段或一个属性)都放置到表格的单独一行。

在第9章中,你已经看到如何创建一个基本的DetailsView来显示当前选择的记录。DetailsView也可以绑定到项集collection of items)。此时,它显示在组中的第一项。它也允许你从通过分页控件,从一条记录跳转到另一条记录。当然,需要将AllowPaging属性设置为true。你可以使用PagngStylePagingSettings属性来配置分页控件,这与GridView配置分页控件是一样的。唯一的区别是DetailsView不支持自定义分页,这意味着整个数据源对象都会被获取。

10-17显示了DetailsView,它绑定到包含了所有雇员信息的雇员记录集。

10-17 具有分页的DetailsView

它试图使用DetailsView分页控件来方便记录的浏览。但不幸的是,这个方法的效率有点低。首先,当用户从一个记录跳转到另一个记录时,都会引发回传(而grid控件却一次能显示多条记录)。每次回传带来的真正缺点是,尽管只显示一条记录,却把所有记录都获取了。如果你在DetailsView中实现一条记录浏览页面,则必须启用缓存来减少数据库的工作(见第11)。

通常情况下,更好的方法是创建自己的分页控件,只使用整个数据的一个子集。例如,你可以创建一个下拉列表,并且绑定到只查询了雇员名字的数据源。从列表选择一个名字时,你可以使用另外一个数据源来仅仅获取那一条记录的详细信息。当然,到底哪种方法最好,有几个因素要考虑,包括所有记录的大小(整个记录比姓和名到底大多少?)、使用模式(通常的用户是浏览一两条记录还是所有记录?)、记录总数有多少(如果只有几十条记录,或许你能够一次性获取,但如果是数千条记录,就需要考虑多次获取了)。

 

定义定段(Defining Fields

DetailsView使用映射来产生显示的字段。也就是说它检查数据对象,并且为每一个字段(在行中)或属性(在自定义对象中)创建一个单独的字段,这与GridView相似。你可以设置AutoGenerateRowsfalse来禁用自动产生字段,然后由你自己来声明字段对象。

有趣的是,你可以用来设计GridView一样使用字段对象来构建DetailsView。例如,来自于数据项的字段通过BoundField标签来表现,使用ButtonField可以创建按钮,等等。对于整个列表,参考表10-1。其中DetailsView不支持的GridView列类型只有TemplateField

下面是DetailsView的一部分字段声明:

<asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False">

<Fields>

<asp:BoundField DataField="EmployeeID" HeaderText="EmployeeID" />

<asp:BoundField DataField="FirstName" HeaderText="FirstName" />

<asp:BoundField DataField="LastName" HeaderText="LastName" />

<asp:BoundField DataField="Title" HeaderText="Title" />

<asp:BoundField DataField="TitleOfCourtesy" HeaderText="TitleOfCourtesy" />

<asp:BoundField DataField="BirthDate" HeaderText="BirthDate" />

...

</Fields>

...

</asp:DetailsView>

你可以使用BoundField标签来设置属性,如标题文本、格式化字符、编辑行为等(见表10-2)。另外,你可以在BoundField中使用相当多的属性,这些属性在GridView中并有任何效果。

DetailsView控件不仅仅只采用GridView的字段模型,它还使用相似的样式设置、相似的事件集和相似的编辑模型。唯一的区别是,没有采用专门的列来编辑控件,而只是需要简单地设置布尔属性,如AutoGenerateDeleteButtonAutoGenerateEditButtonAutoGenerateInsertButton。完成这些任务的链接添加到了DetailsView的底部。当你添加或者编辑一条记录时,DetailsView总是使用标签的文本框(见图10-18)。要获得更多的编辑灵活性,你需要使用FormView控件。

10-18 DetailsView中编辑

 

FormView控件(The FormView

DetailsView支持除了模板化列之外的所有GridView列类型。如果你需要具有模板的完全的灵活性,FormView提供了模板控件来显示和编辑单条记录。

FormView模板模型的美丽之处是,它相当紧密地匹配GridView中的TemplateField模型。这意味着你可以使用如下的模板:

. ItemTemplate

. EditItemTemplate

. InsertItemTemplate

. FooterTemplate

. HeaderTemplate

. EmptyDataTemplate

. PagerTemplate

显然,你可以使用精确的模板内容来放放GridView中的TemplateField和放入FormView。下面是基于早期的模板化的GridView的一个示例:

<asp:FormView ID="FormView1" runat="server" DataSourceID="sourceEmployees">

<ItemTemplate>

<b>

<%# Eval("EmployeeID") %> -

<%# Eval("TitleOfCourtesy") %> <%# Eval("FirstName") %>

<%# Eval("LastName") %>

</b>

<hr />

<small><i>

<%# Eval("Address") %><br />

<%# Eval("City") %>, <%# Eval("Country") %>,

<%# Eval("PostalCode") %><br />

<%# Eval("HomePhone") %></i>

<br /><br />

<%# Eval("Notes") %>

<br /><br />

</small>

</ItemTemplate>

</asp:FormView>

10-19显示了结果。

10-19 具有底脚统计GridView

如果你想支持编辑,你需要添加按钮控件来触发编辑和更新过程,在使用模板进行编辑(Editing with a Template一节进行了描述。

 

高级Grids(Advanced Grids)

在后面的节中,你将学习几种扩展GridView的方法。你将了解如何显示总和,在一个页面上创建一个完整的主控细节报告,显示来自于数据库的图象数据。你还将看到一个示例,它使用了高级并行处理,在更新记录时,警告用户潜在的冲突。

 

GridView中计和(Summaries in the GridView

尽管GridView的一个主要用途是显示记录集,但你仍能添加一些更有意思的信息,如求和数据。第一步是通过设置GridView.ShowFooter属性为true来添加底脚行。这会显示带阴影的底脚行(你可以对其进行自定义),但它没有显示任何数据。为了处理那个任务,你需要向GridView.FooterRow中插入内容。

例如,假设你正在处理一个产品列表。一个简单的总计行能够显示产品的总价格或者平均价格。在下一个示例中,总计行显示仓储内产品的总价值。

第一步是决定什么时候计算这些信息。如果使用手工绑定,你能够获取数据对象,并且在绑定到GridView之前用来执行计算。但是,如果你在使用声明的绑定,你需要另外的技术。你有两个基本选项:在grid绑定前从数据对象获取数据,或者在Grid绑定之后,从它自身获取数据。下面的例子使用了后一种方法,因为这样可以自由地使用计算代码,而不管使用哪个数据源来组装控件。而且还使你在启用分页时,能够合计仅仅在当前页面显示的记录。缺点是计算代码与GridView紧密绑定,因为你需要根据硬编码的列索引号位置来获取信息。

最基本的策略是响应GridView.DataBound事件。这个事件在数据导入GridView后立即释放。此时,你不再具有访问数据源的能力,但你可以在GridView中按行或单元集进行导航。一旦进行了总和计算,它就插入到底脚的行中。

下面是完整的代码:

protected void GridView1_DataBound(object sender, EventArgs e)

{

decimal valueInStock = 0;

// The Rows collection includes rows only on the current page

// (not "virtual" rows).

foreach (GridViewRow row in GridView1.Rows)

{

decimal price = Decimal.Parse(row.Cells[2].Text);

int unitsInStock = Int32.Parse(row.Cells[3].Text);

valueInStock += price * unitsInStock;

}

// Update the footer.

GridViewRow footer = GridView1.FooterRow;

// Set the first cell to span over the entire row.

footer.Cells[0].ColumnSpan = 3;

footer.Cells[0].HorizontalAlign = HorizontalAlign.Center;

// Remove the unneeded cells.

footer.Cells.RemoveAt(2);

footer.Cells.RemoveAt(1);

// Add the text.

footer.Cells[0].Text = "Total value in stock (on this page): " +

valueInStock.ToString("C");

}

总计行与Grid的其它行一样有相同数量的列。因此,如果你想要你的文本在跨越多行显示(如示例中所见),你需要过设置ColumnSpan属性来配置单元跨越(cell spanning。在此例中,第一个单元跨越了3列。图10-20显示了最后的效果。

10-20 具有底脚总计的GridView

 

单表中的父/子视图(A Parent/Child View in a Single Table

本章的前面部分,你已经看到了主控/细节页面,它使用了一个GridView和一个DetailViews。这使你能够灵活地显示选中的父记录对应的子记录。但是,有时你想创建父/子报告来显示来自于子表的所有记录,而且根据父表进行组织。例如,你可以使用这个技术来创建完整的产品列表,产品按分类进行组织。下一个示例演示了如果在一个单独的grid中显示完整的、分组的产品列表。

一个基本技术是为父表创建一个GridView,每一行中再包含一个嵌入的GridView。这些子GridView控件使用TemplateField插入到你GridView中。有一点技巧是,你不能在绑定你GridView的同时绑定子GridView控件,因为父GridView的行还没有被创建。相反,你需要父GridView里的GridView.DataBound事件释放后才能进行子GridView的绑定。

在这个例子中,父GridView定义了两列,它们都是TemplateField类型。第一列合并了分类名和分类描述:

<asp:TemplateField HeaderText="Category">

<ItemStyle VerticalAlign="Top" Width="20%"></ItemStyle>

<ItemTemplate>

<br />

<b><%# Eval("CategoryName") %></b>

<br /><br />

<%# Eval("Description" ) %>

<br />

</ItemTemplate>

</asp:TemplateField>

第二列包含了嵌入的产品的GridView,每个GridView有两个绑定列。下面是摘要的列表,省略了样式相关的属性:

<asp:TemplateField HeaderText="Products">

<ItemStyle VerticalAlign="Top"></ItemStyle>

<ItemTemplate>

<asp:GridView runat="server">

<Columns>

<asp:BoundField DataField="ProductName"

HeaderText="Product Name"></asp:BoundColumn>

<asp:BoundField DataField="UnitPrice"

HeaderText="Unit Price" DataFormatString="{0:C}"></asp:BoundColumn>

</Columns>

</asp:GridView>

</ItemTemplate>

</asp:TemplateField>

现在,你所要做的就是创建两个数据源,一个用于获取分类列表,另一个用于获取特定分类的所有产品。第一个查询填充父GridView,第二个查询被多次调用,用于填充子GridView

你可以直接将第一个grid绑定到数据源:

<asp:GridView id="gridMaster" runat="server" DataKeyNames="CategoryID"

DataSourceID="sourceCategories" OnRowDataBound="gridMaster_RowDataBound" ... >

这部分代码非常典型。绑定到子GridView控件就需要技巧了。如果你忽略了这一步骤,子GridView控件就不会显示。

为了绑定子GridView控件,需要响应GridView.RowDataBound事件,它在每次产生一行并且绑定到父GridView时释放。此时,你可以从第二列中获取子GridView控件,然后在程序中调用数据源的Select()方法来绑定到产品信息。为了确保仅仅显示当前分类中的产品,你必须为当前项获取CategoryID字段,并且作来参数进行传递,如下所示:

protected void gridMaster_RowDataBound(object sender, GridViewRowEventArgs e)

{

// Look for data items.

if (e.Row.RowType == DataControlRowType.DataRow)

{

// Retrieve the GridView control in the second column.

GridView gridChild = (GridView)e.Row.Cells[1].Controls[1];

// Set the CategoryID parameter so you get the products

// in the current category only.

string catID = gridMaster.DataKeys[e.Row.DataItemIndex].Value.ToString();

sourceProducts.SelectParameters[0].DefaultValue = catID;

// Get the data object from the data source.

object data = sourceProducts.Select(DataSourceSelectArguments.Empty);

// Bind the grid.

gridChild.DataSource = data;

gridChild.DataBind();

}

}

10-21显示了结果。

10-21 GridView中嵌入子GridView

 

从数据库获取图像(Serving Images from a Database

本章的数据示例获取了文本、数值和数据数据信息。但是,数据库通常还可能存储二进制数据,如图片。例如,你可能有一个Products表,每一项的二进制字段里都包含了图片。在ASP.NETWeb页面里获取这种数据非常简单,但是要显示出来就不并那么简单了。

基本的问题是为了在HTML页面中显示图像,你需要添加一个图像标签来链接到一个单独的图像文件,在src属性中指定图像文件。如下例:

<img src="myfile.gif" />

不幸的是,这对于你动态显示图像数据没有多少帮助。尽管你可以在代码中设置src属性,但你无法在程序中设置图像的内容。可能够首先存储数据到web服务器硬盘上的图像文件中,但那会非常慢,浪费空间,而且在同时对多个请求服务时,增大了并发错误的可能,因为它们都试图写入相同的文件。

要解决这个问题,有两种方法。一种是将所有图像都存储到单独的文件中,然后在数库记录中仅存储文件名,你可以绑定文件名到服务器端的图像。这是非常合理的解决方法。但在你想存储图像到数据库,以便利用RDBMS来缓存数据、日志使用,并且进行完全备份时,这种方法就不适合了。

在这些情形下,解决办法就是使用分离的asp.net资源来直接返回二进制数据。然后在另外页面的控件里使用二进制数据。为完成这个任务,你还需要放弃数据绑定,编写自定义的ADO.NET代码。后面的节里将一块一块地发展这个解决方法。

提示,作为普通的拇指规则,只要图像文件不太大(如超过 50M ),并且不会被其它应用频繁编辑时,将图像存储在数据库中能够很好地工作。

 

显示二进制数据(Displaying Binary Data

ASP.NET并不仅限于返回HTML内容。事实上,你可以使用Response.BinaryWrite()方法来返回原始的字节,并且完全不用Web页面模型。

下面的页面在pubs数据库的pub_info表中使用了这个技术。它获取了logo字段,该字段包含了二进制图像数据。网页于是直接将这个数据写入到页面中,如下所示:

protected void Page_Load(object sender, System.EventArgs e)

{

string connectionString =

WebConfigurationManager.ConnectionStrings["Pubs"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

string SQL = "SELECT logo FROM pub_info WHERE pub_id='1389'";

SqlCommand cmd = new SqlCommand(SQL, con);

try

{

con.Open();

SqlDataReader r = cmd.ExecuteReader();

if (r.Read())

{

byte[] bytes = (byte[])r["logo"];

Response.BinaryWrite(bytes);

}

r.Close();

}

finally

{

con.Close();

}

}

10-22显示了结果。它并没有显示出多么不寻常(logo数据并不大),但是你可以方便地在自己的数据库中使用相同的技术,包含更加丰富和更大的图像。

10-22 显示从数据库获取的图像

使用BinaryWrite()时,你已经离开了web页面模型。如果你添加另外的控件到页面中,它们不会显示。与此相似,Response.Write()也不会有任何效果,因为你并不是在创建一个HTML网页。相反,你是在返回图像数据。你将在后面的节中看到如何解决这个问题并且优化这个方法。

 

高效地读取二进制数据(Reading Binary Data Efficiently

二进制数据很容易增大尺寸。但是,如果你在处理大的图像文件,前面所见的示例中就会面临非常低的性能。问题根源在于它使用了DataReaderDataReader一次将一条记录读取到内存中。虽然比使用DataSet要好(DataSet会一次加载整个结果集到内存中),但在字段很大时仍然不理想。

没有多少理由说明应当一次性加载整个2MB大小的图片到内存。更好的想法是一块一块地读取,然后用Response.BinaryWrite()将每一块写入到输出流中。幸运的是,DataReader具有连续访问的特征来支持这种设计。为了使用连续访问,你需要提供CommandBehavior.SequentialAccess值给Command.ExecuteDataReader()方法。然后使用DataReader.GetBytes()方法,你能够在行内移动,每次移动一块。

使用连续访问时,你需要记住一些限制。首先,你只能向前读取数据流。一旦你完成对一块数据的读取,则自动移动流的头部,而不能倒回。第二,必须按照查询返回的顺序来读取字段。例如,如果查询返回了三列,第三列是二进制字段,在访问第二列中的二进制数据前,你必须返回第一和第二个字段的值。如果首先访问第三个字段,你就不能访问前面两个字段了。

下面的代码使用连续访问,对前面的页面进行了修改:

protected void Page_Load(object sender, System.EventArgs e)

{

string connectionString =

WebConfigurationManager.ConnectionStrings["Pubs"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

string SQL = "SELECT logo FROM pub_info WHERE pub_id='1389'";

SqlCommand cmd = new SqlCommand(SQL, con);

try

{

con.Open();

SqlDataReader r =

cmd.ExecuteReader(CommandBehavior.SequentialAccess);

if (r.Read())

{

int bufferSize = 100; // Size of the buffer.

byte[] bytes = new byte[bufferSize]; // The buffer of data.

long bytesRead; // The number of bytes read.

long readFrom = 0; // The starting index.

// Read the field 100 bytes at a time.

do

{

bytesRead = r.GetBytes(0, readFrom, bytes, 0, bufferSize);

Response.BinaryWrite(bytes);

readFrom += bufferSize;

} while (bytesRead == bufferSize);

}

r.Close();

}

finally

{

con.Close();

}

}

GetBytes()方法返回一个值,表明获取的字节数。如果需要确定字段中的总的字节数,只需要在调用GetBytes()时,传递一个空的引用替代一个缓冲。

 

整合图像和其它内容(Intergrating Images with other Content

如果你想整合图像数据和其它控件和HTML使用Response.BinaryWrite()方法会面临一个挑战。因为,当你使用BinaryWrite()来返回原始图像数据时,你无法添加额外的HTML内容。

为解决这个问题,你需要创建另一个页面来调用图像产生代码。最好的方法是使用专门的HTTP处理器(产生图像输出)来替换图像产生页面。这样,你保存了整个ASP.NET Web窗体模型的负载。(第5章介绍了HTTP处理器)。

创建HTTP处理器非常简单。你只需要实现IhttpHandler接口和ProcessRequest()方法。HTTP处理器将会获取查询字符串中的记录的ID

下面是完整的HTTP处理器代码:

public class ImageFromDB : IHttpHandler

{

public void ProcessRequest(HttpContext context)

{

string connectionString =

WebConfigurationManager.ConnectionStrings["Pubs"].ConnectionString;

// Get the ID for this request.

string id = context.Request.QueryString["id"];

if (id == null) throw new ApplicationException("Must specify ID.");

// Create a parameterized command for this record.

SqlConnection con = new SqlConnection(connectionString);

string SQL = "SELECT logo FROM pub_info WHERE pub_id=@ID";

SqlCommand cmd = new SqlCommand(SQL, con);

cmd.Parameters.AddWithValue("@ID", id);

try

{

con.Open();

SqlDataReader r =

cmd.ExecuteReader(CommandBehavior.SequentialAccess);

if (r.Read())

{

int bufferSize = 100; // Size of the buffer.

byte[] bytes = new byte[bufferSize]; // The buffer.

long bytesRead; // The # of bytes read.

long readFrom = 0; // The starting index.

// Read the field 100 bytes at a time.

do

{

bytesRead = r.GetBytes(0, readFrom, bytes, 0, bufferSize);

context.Response.BinaryWrite(bytes);

readFrom += bufferSize;

} while (bytesRead == bufferSize);

}

r.Close();

}

finally

{

con.Close();

}

}

public bool IsReusable

{

get { return true; }

}

}

一旦你创建了HTTP处理器,你需要将其在Web.config文件中注册,如下所示:

<httpHandlers>

<add verb="GET" path="ImageFromDB.ashx"

type="ImageFromDB" />

</httpHandlers>

现在,你可以通过请求HTTP处理器URL来获取图像数据。URL中带了行的ID,如下例:

ImageFromDB.ashx?ID=1389

为了在别的页面中显示图像内容,你只需要设置src属性为这个URL

<img src="ImageFromDB.ashx?ID=1389"/>

10-23显示了具有多个控件和logo图像的页面。它在GridView中使用了下面的ItemTemplate:

<ItemTemplate>

<table border='1'><tr><td>

<img src='ImageFromDB.ashx?ID=<%# Eval("pub_id")%>'/>

</td></tr></table>

<b><%# Eval("pub_name") %></b>

<br />

<%# Eval("city") %>,

<%# Eval("state") %>,

<%# Eval("country") %>

<br /><br />

</ItemTemplate>

并且绑定到了这个数据源:

<asp:SqlDataSource ID="sourcePublishers"

ConnectionString="<%$ ConnectionStrings:Pubs %>"

SelectCommand="SELECT * FROM publishers" runat="server"/>

10-23 ASP.NET WEB页面中显示数据库图像

如果想绑定到一个详细页面,页面中显示一条记录的详细信息,这个HTTP处理器方法能够很好工作。例如,你显示一个发布者列表,然后在用户时,显示发表者的对应图像。但是,如果你想一次显示所有发布者的图像,如列表控件一样,这种解决方法效率不高。因为它在获取每个图像时,对HTTP处理器使用了单独的请求(因此使用了独立的数据库连接)。可以在从数据库获取图像数据前,通过创建HTTP处理器来检查缓存中的图像数据,这样可以解决多个连接的问题。在绑定到GridView之前,你应当执行一个查询来返回带有图像数据的所有记录,并且将每个图像加载到缓存中。

 

检测并发冲突(Detecting Concurrency Confilicts

正如在第8章所讨论的那样,如果Web应用允许多个用户实施修改,有可能会出现两个甚至更多编辑重叠。根据这些编辑重叠的方式和所使用的并发策略(详见第8章“并发策略”),有可能在不经意间将过期的值提交给数据库。

为了防止这个问题,开发者通常使用完全匹配和基于时间戳的并发。其思想是UPDATE语句必须与原始记录里的每个值进行匹配,否则不允许进行更新。示例如下:

UPDATE Shippers SET CompanyName=@CompanyName, Phone=@Phone

WHERE ShipperID=@original_ShipperID AND CompanyName=@original_CompanyName

AND Phone=@original_Phone"

SQL ServerShipperID主关键字上使用了索引来查找记录,然后比较其它字段来确认匹配。仅当记录中的值与用户更新时看到的值相匹配时,更新才能成功。

注意,如第8章指出的那样,使用时间显性地比较所有字段能够更好地解决这个问题。但是,这个示例使用了完全匹配方法,因为它是在既存的Northwind数据库上工作的。否则,你需要添加一个新的时间列。

完全匹配策略的问题是,它可能导致编辑失败。换句话说,在用户查询记录和提交更新期间,如果记录发生了改变,,则更新就会失败。事实上,数据绑定控件不会对这个问题给出警告。它们执行UPDATE语句,你看不到任何效果,因为这没有被认为是出错条件。

如果你决定使用完全匹配并发,你至少应当检查丢失的更新。你可以通过处理相应控件的ItemUpdated事件来完成这个任务。这样,你可以检查EventArgs对象的RowsAffected属性。如果属性为0,则不更新记录。这种情况经常会发生,因为别的编辑改变了记录,UPDATE中的WHERE子句就无法进行匹配。(其它的错误,如试图更新时,违背了关键约束,或者提交了无效的数据,都会导致数据源唤起的错误)。

在下面的示例中,在DetailsView控件中检查了失败的更新,然后将问题通知给用户:

protected void DetailsView1_ItemUpdated(object sender,

DetailsViewUpdatedEventArgs e)

{

if (e.AffectedRows == 0)

{

lblStatus.Text = "A conflicting change has already been made to this " +

" record by another user. No records were updated.";

}

}

不幸的是,这对于建立界面友好的Web应用并没有多少帮助。如果记录有几个字段,或者如果字段保留了详细的信息,这反倒成了一个问题,因为这些编辑只是简单地被丢弃,迫使用户重新从头开始。

更好的解决方法是给用户一个选择。理想情况下,页面应当显示记录的当前值(考虑最近的任何改变),并且允许用户应用原始的编辑值,取消更新,或者进行额外优化然后应用更新。

首先,从允许用户编辑单个记录(来自于Nothwind数据库的Shippers表)DetailsView始。(Shippers表使用完全匹配并发策略非常简单,因为它只有3个字段。更大的表使用相等的时间方法会工作得更好)。

下面是DetailsView的一个简短的定义:

<asp:DetailsView ID="detailsEditing" runat="server"

DataKeyNames="ShipperID" AllowPaging="True" AutoGenerateRows="False"

DataSourceID="sourceShippers" OnItemUpdated="DetailsView1_ItemUpdated" ...>

<Fields>

<asp:BoundField DataField="ShipperID" ReadOnly="True" />

<asp:BoundField DataField="CompanyName" />

<asp:BoundField DataField="Phone" />

<asp:CommandField ShowEditButton="True" />

</Fields>

...

</asp:DetailsView>

绑定到DetailsVew的数据源控件使用完全匹配UPDATE表达式来实现严格的并发策略:

<asp:SqlDataSource ID="sourceShippers" runat="server"

ConnectionString="<%$ ConnectionStrings:Northwind %>"

SelectCommand="SELECT * FROM Shippers" UpdateCommand="UPDATE Shippers SET

CompanyName=@CompanyName, Phone=@Phone WHERE ShipperID=@original_ShipperID AND

CompanyName=@original_CompanyName AND Phone=@original_Phone"

ConflictDetection="CompareAllValues">

<UpdateParameters>

<asp:Parameter Name="CompanyName" />

<asp:Parameter Name="Phone" />

<asp:Parameter Name="original_ShipperID" />

<asp:Parameter Name="original_CompanyName" />

<asp:Parameter Name="original_Phone" />

</UpdateParameters>

</asp:SqlDataSource>

你可能已经注意到,SqlDataSource.ConflictDetection属性设置为CompareAllValures这确保原始记录的值作为参数提交(使用OldValueParameterFormatString属性定义的前缀)。

多数代码是为了响应DetailsView.ItemUpdated事件。这里,代码捕获了所有失败的更新,然后在编辑模式中显性地保存DetailsView

protected void DetailsView1_ItemUpdated(object sender,

DetailsViewUpdatedEventArgs e)

{

if (e.AffectedRows == 0)

{

e.KeepInEditMode = true;

...

但是真正需要技巧性的是重新绑定数据控件。这样,DetailsView中的所有原始值被重置来匹配数据库中的值。这意味着更新成功(如果用户重新应用的话)。

...

detailsEditing.DataBind();

...

重新绑定Grid在暗中进行,但仍有更多的事情需要去做。为了维护用户试图应用的值,你应当手工拷贝它们到一个新的数据控件,这虽然简单,但有点繁琐。

...

// Repopulate the DetailsView with the edit values.

TextBox txt;

txt = (TextBox)detailsEditing.Rows[1].Cells[1].Controls[0];

txt.Text = (string)e.NewValues["CompanyName"];

txt = (TextBox)detailsEditing.Rows[2].Cells[1].Controls[0];

txt.Text = (string)e.NewValues["Phone"];

...

此时,你具有了一个检查失败更新的数据控件,并且对其进行重新绑定,然后重新插入用户试图应用的值。这意味着,如果用户第二次点击UPDATE,更新就会成功(假定记录没有被其它用户再次修改)。

但是,这仍然有缺点。此时用户可能没有足够的信息来决定是否应用更新。在进行覆盖之前,他们很可能想到知道发生了哪些改变。处理这个问题的一个方法是在标签或另外的控件中列出当前值。在这个示例中,简单地使包含了另一个DetailsViewPanel可视:

...

ErrorPanel.Visible = true;

}

}

错误面板描述了问题,提供了提示性的错误信息,并且包含了绑定到匹配行的第二个DetailView来显示有问题的记录的当前值。

<asp:Panel ID="ErrorPanel" runat="server" Visible="False" EnableViewState="False">

There is a newer version of this record in the database.<br />

The current record has the values shown below.<br />

<br />

<asp:DetailsView ID="detailsConflicting" runat="server"

AutoGenerateRows="False" DataSourceID="sourceUpdateValues" ...>

<Fields>

<asp:BoundField DataField="ShipperID" />

<asp:BoundField DataField="CompanyName" />

<asp:BoundField DataField="Phone" />

</Fields>

...

</asp:DetailsView>

<br />

* Click <b>Update</b>to override these values with your changes.<br />

* Click <b>Cancel</b>to abandon your edit.</span>&nbsp;

<asp:SqlDataSource ConnectionString="<%$ ConnectionStrings:Northwind %>"

ID="sourceUpdateValues" runat="server"

SelectCommand="SELECT * FROM Shippers WHERE (ShipperID = @ShipperID)"

OnSelecting="sourceUpdateValues_Selecting">

<SelectParameters>

<asp:ControlParameter ControlID="detailsEditing" Name="ShipperID"

PropertyName="SelectedValue" Type="Int32" />

</SelectParameters>

</asp:SqlDataSource>

</asp:Panel>

还有最后一个细节。为了保存负载,在执行的查询中,并没有第二个DetailsView的位置,除非由于发生并发错误,它是完全必要的。为了实现这个逻辑,如果出错面板当前可视,则编写代码响应SqlDataSource.Selecting事件,并且取消查询。

protected void sourceUpdateValues _Selecting(object sender,

SqlDataSourceSelectingEventArgs e)

{

if (!ErrorPanel.Visible) e.Cancel = true;

}

为了试验这个例子,在独立的浏览器窗口中打开页面的两个拷贝,并且使两者的同一行都进入编辑模式。应用第一个改变,然后应用第二个。当你试图应用第二个时,错误面板会出现,并且带有解释信息(如图10-24)。你可以点击Update来继续编辑,或者点击Cancel来放弃编辑。

10-24 检测编辑期间的并发错误

 

总结

本章对富数据绑定页面进行了全面考虑。对GridView进行了深入介绍,并且考虑了它对于格式化、选择、排序、分页、模板和编辑的支持。同时也对DetailsViewFormView有了一定的了解。最后,本章对数据绑定页面的几个常见的高级场合进行了讨论。

 
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值