一、Render UI
1 GridView
GridView 控件用来在表中显示数据源的值。每列表示一个字段,而每行表示一条记录。GridView 控件支持下面的功能:
-
绑定至数据源控件,如 SqlDataSource。
-
内置排序功能。
-
内置更新和删除功能。
-
内置分页功能。
-
内置行选择功能。
-
以编程方式访问 GridView 对象模型以动态设置属性、处理事件等。
-
多个键字段。
-
用于超链接列的多个数据字段。
-
可通过主题和样式进行自定义的外观。
实验一:
将DetailsView和GridView放置在一个页面。然后在DetailsView的SqlDataSource中的查询参数设置为从GridView传递过来。
预期结果:当GridView中的选择不同的行时,作为DataKeyNames属性中的第一个字段au_id应该作为参数传递给DetailsView的SqlDataSource,DetailsView根据数据源Render。
现象:当GridView中的选择不同的行时,DetailsView控件没有展现数据。
分析:
首先怀疑是DetailsView的属性没有设置正确,于是将其数据源的查询参数设置默认值,结果能展现。说明DetailsView和其SqlDataSource的属性设置正确。
然后怀疑是GridView选择行时,其SelectedValue为空。于是在其SelectedIndexChanging事件和SelectedIndexChanged中进行调试,发现第一次选择时GridView的SelectedIndexChanging事件中SelectedValue确实为Null,而SelectedIndexChanged事件中SelectedValue值为预期值。而第二次以及以后的选择时两个事件中SelectedValue的值都为预期值。
那么GridView的SelectedValue为什么是Null呢?我查看一下GridView的属性,EnableViewState设置为false,于是改成True,再试,还是不行。
经过长时间的尝试,发现在如下两种情况下程序结果是对的:
一 :先设置DetailsView使用的SqlDataSource中查询参数的默认值,这样DetailView会显示出来,然后将其切换到Edit模式,发现其各字段的值确实是GridView选中行的值,然后再点击Cancel放弃修改。则DetailView能够正确响应GridView的选择操作。再调试代码,发现GridView的SelectedIndexChanging事件中SelectedValue值不再为空,而是预期值。
二:发现一个更奇特的现象,在SelectedIndexChanging中设置断点,发现程序运行进来,然后什么都不做,跳出事件处理(如下)
...{
}
程序结果也是对的,但是如果不设置断点,直接运行,则结果不对。
又经过很长时间的尝试,没有办法了,只好找同事帮忙。刚开始他也觉得离奇。先跟踪事件发生的顺序,接着发现了一个更离奇的现象:在SelectedIndexChanging事件中设置断点时我添加了对GridView.SelectedValue的监视。虽然在事件响应代码中我什么都没有做,但是这个watch好像起了作用。因为他把我添加的这个监视去掉后,错误现象又重现了。而添加这个监视,或者在代码里这样写
才能正常运行。
最后,同事让我在工程里新建一个页面,SampleCode拷到这个页面,然后运行。以确定不是我的环境有错误。然后把SamlpeCode和我的代码做了对比。把一些不一样的属性去掉。当改到GridView的SqlDataSource的EnableViewSate属性时,“奇迹”出现了。SampleCode的该属性设置为True,而我的是false,我改成True后,程序正常运行。
这时候再去测试SelectedValue的值,发现第一次选择时,在SelectedIndexChanging事件中SelectedValue仍然为null,和错误时的现象没有什么区别。
错误的源头总算找到了,但是SqlDataSource的EnableViewState究竟做了什么呢?GridView的SelectedValue和SqlDataSource有关吗?
实验二:分页排序
虽然 GridView、DetailsView 和 FormView 提供页导航 UI 的默认呈现,但是也可以通过设置 PagerTemplate 属性自定义页导航的呈现。在该模板中,您可以放置 CommandName 属性设置为 Page
并且 CommandArgument 属性设置为 First
、Prev
、 Next
、Last
或 <number>
(其中 <number> 是特定页索引的值)的 Button 控件。
应用分页模板:
<div style="float: right">
<asp:RangeValidator ID="rvTarget" runat="server" ControlToValidate="txtTarget" Type="Integer"
ErrorMessage="页码超出范围" MinimumValue="1" MaximumValue='<%#gvReportForDept.PageCount%>'></asp:RangeValidator>
共<%=gvReportForDept.PageIndex + 1%>/<%=gvReportForDept.PageCount%>页 第<asp:TextBox
runat="server" ID="txtTarget" onkeypress="searchForDept(event);" CssClass="gotopage" MaxLength="10" Width="25px"
Text='<%#gvReportForDept.PageIndex + 1%>'></asp:TextBox>页
<asp:ImageButton runat="server" ID="ibtnGo" CommandName="Page" CommandArgument="-1"
ImageUrl="~/Images/dg_btn_go.gif" />
<asp:ImageButton runat="server" ID="ibtnFirst" CommandName="Page" CommandArgument="First"
ImageUrl="~/Images/dg_btn_le.gif" />
<asp:ImageButton runat="server" ID="ibtnPrev" CommandName="Page" CommandArgument="Prev"
ImageUrl="~/Images/dg_btn_l.gif" />
<asp:ImageButton runat="server" ID="ibtnNext" CommandName="Page" CommandArgument="Next"
ImageUrl="~/Images/dg_btn_r.gif" />
<asp:ImageButton runat="server" ID="ibtnLast" CommandName="Page" CommandArgument="Last"
ImageUrl="~/Images/dg_btn_re.gif" />
</div>
</PagerTemplate>
这将启用默认的分页事件。如果需要在分页中控制逻辑,例如:如果在TextBox中输入页码,然后点击Go按钮,则不仅需要客户端验证输入的范围,还需要在翻页时验证页码的合理性,所以在PageIndexChanging事件中添加如下代码:
...{
GridViewPagingHandler(sender, e, this.gvReportForOrder);
}
/**//// <summary>
/// 处理页面内GrivView的翻页事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <param name="gv">GrivView</param>
public void GridViewPagingHandler(object sender, GridViewPageEventArgs e, GridView gv)
...{
if ((e.NewPageIndex >= 0) && (e.NewPageIndex < gv.PageCount))
...{
gv.PageIndex = e.NewPageIndex;
TextBox target = (TextBox)gv.BottomPagerRow.FindControl("txtTarget");
target.Text = Convert.ToString(gv.PageIndex + 1);
}
else if (e.NewPageIndex == -2) // Go button
...{
TextBox target = (TextBox)gv.BottomPagerRow.FindControl("txtTarget");
int goPage;
if (Int32.TryParse(target.Text, out goPage))
...{
if (goPage >= 1 && goPage <= gv.PageCount)
...{
gv.PageIndex = goPage - 1;
}
else
...{
e.Cancel = true;
return;
}
}
}
else
...{
e.Cancel = true;
return;
}
try
...{
//Bind DataSource To GridView
}
catch
...{
e.Cancel = true;
}
}
2 DetailsView
DetailsView 控件用来在表中显示来自数据源的单条记录,其中记录的每个字段显示在表的一行中。它可与 GridView 控件结合使用,以用于主/详细信息方案。DetailsView 控件支持下面的功能:
-
绑定至数据源控件,如 SqlDataSource。
-
内置插入功能。
-
内置更新和删除功能。
-
内置分页功能。
-
以编程方式访问 DetailsView 对象模型以动态设置属性、处理事件等。
-
可通过主题和样式进行自定义的外观。
实验一:应用DetailsView的模板列
DetailsView除了已经定义的列外,还有模板列,这和FormView的模板类似,但是又有所不同。
在DetailsView'中可以这样应用模板列
Height="50px" Width="125px" AutoGenerateEditButton="True" DataKeyNames="au_id"
AutoGenerateRows="False">
<Fields>
<asp:TemplateField HeaderText="Author">
<ItemTemplate>
.......
</ItemTemplate>
<EditItemTemplate>
.........
</EditItemTemplate>
<InsertItemTemplate>
..........
</InsertItemTemplate>
</asp:TemplateField>
</Fields>
</asp:DetailsView>
而FormView中这样应用模板
<ItemTemplate>
........
</ItemTemplate>
<EditItemTemplate>
.........
</EditItemTemplate>
<InsertItemTemplate>
..........
</InsertItemTemplate>
</asp:FormView>
看起来在ItemTemplate节点下的内容似乎是一样的。但是运行起来发现不一样:
可以看出:DetailsView只适合对一个字段运用模板,而FormView则适合对整条记录运用模板。
结论:
DetailsView和FormView比较:
相同点:
DetailsView和FormView都是对一条记录进行展现。
不同点:虽然可以灵活运用DetailsView对一个字段运用模板的能力使UI看起来好像是对整条记录运用的模板。但是我们没有理由这样做。
也就是说当以表格的形式方方正正的展示一条记录时可以使用DetailsView来展示,并且字段的值可以使用模板来展示的比较漂亮。但是字段的值和字段的Text,必定是在同一行的。而FormView则可以更灵活的来布局。
DetailsView 和 FormView 之间的主要差异在于 DetailsView 具有内置的表格呈现方式,而 FormView 需要用户定义的模板用于呈现。FormView 和 DetailsView 对象模型在其他方面非常类似。
DetailsView和GridView的模板列使用是基本一样的,唯一的区别是一个横向的展示数据,一个纵向的展示数据。
3 FormView
Note:关于数据绑定
Eval 单向绑定:数据是只读的
Bind 双向绑定:数据可以更改,并返回服务器端,服务器可以处理更改后的数据,如存入数据库.
<%#Eval("Field")>或者<%#expression(Eval("Field")) 用来绑定数据, <Eval=expression> 用来计算表达式。
简单属性 | 客户:<%# custID %> |
集合 | 订单:<asp:ListBox id="List1" datasource='<%# myArray %>' runat="server"> |
表达式 | 合同:<%# ( customer.FirstName + " " + customer.LastName ) %> |
方法结果: | 未结余额:<%# GetBalance(custID) %> |
虽然该语法看起来与 Response.Write 的 ASP 快捷方式 <%= %>
相似,但其行为却大不相同。ASP Response.Write 快捷方式语法在页被处理时计算,而 ASP.NET 数据绑定语法仅在 DataBind 方法被调用时计算。
ASP.NET 页框架提供一个静态方法,计算后期绑定的数据绑定表达式并可选择将结果格式化为字符串。DataBinder.Eval 的便利之处在于它消除了开发人员为了将值强制为所需数据类型所必须做的许多显式强制转换。当数据绑定控件在模板化的列表中时,它特别有用,因为通常数据行和数据字段的类型都必须转换。
请考虑下面的示例,其中一个整数将被显示为货币字符串。使用标准的 ASP.NET 数据绑定语法,必须先强制转换数据行的类型才能检索数据字段 IntegerValue
。接着,该值作为参数被传递给 String.Format 方法。
此语法可能很复杂,难于记忆。相反,DataBinder.Eval 只是一个具有三个参数的方法:数据项的命名容器、数据字段名称和格式字符串。在诸如 FormView、GridView、DetailsView、DataList 或 Repeater 这样的模板化控件中,命名容器始终为 Container.DataItem
。Page 是可用于 DataBinder.Eval 的另一个命名容器。正如在前一小节中所讨论的,ASP.NET 2.0 还包含 DataBinder.Eval 的新的简化语法(仅仅是 Eval
),可以在数据绑定控件模板中使用它来自动解析为 Container.DataItem。
<%# Eval("IntegerValue", "{0:c}") %>
格式字符串参数是可选的。如果省略它,则 DataBinder.Eval 返回一个对象类型的值,如下面的示例所示。
要重点注意的是,相对于标准数据绑定语法,DataBinder.Eval 可能导致显著的性能损失,因为它使用后期绑定反射。使用 DataBinder.Eval 时需谨慎,尤其是在不需要字符串格式设置的时候。
与 DetailsView 控件一样,FormView 通过其关联的数据源控件支持自动 Update、Insert 和 Delete 操作。若要定义编辑或插入操作的输入 UI,可在定义 ItemTemplate 的同时定义 EditItemTemplate 或 InsertItemTemplate。在本模板中,您可以对输入控件(如 TextBox、CheckBox 或 DropDownList)进行数据绑定,以绑定到数据源的字段。但是,这些模板中的数据绑定使用“双向”数据绑定语法,从而允许 FormView 从模板中提取输入控件的值,以便传递到数据源。这些数据绑定使用新的 Bind(fieldname)
语法而不是 Eval。
重要事项: 使用 Bind
语法进行数据绑定的控件必须设置有 ID
属性。
FormView 支持使用 DefaultMode 属性指定要显示的默认模板,但在默认情况下,FormView 以 ReadOnly 模式启动并呈现 ItemTemplate。若要启用用于从 ReadOnly 模式转换为 Edit 或 Insert 模式的 UI,可以向模板添加一个 Button 控件,并将其 CommandName 属性设置为 Edit
或 New
。可以在 EditItemTemplate 内添加 CommandName 设置为 Update
或 Cancel
的按钮,以用于提交或中止更新操作。类似地,也可以添加 CommandName 设置为 Insert
或 Cancel
的按钮,以用于提交或中止插入操作。
<EditItemTemplate>
<asp:TextBox ID="CaptionTextBox" Text='<%# Bind("Caption") %>' runat="server"/>
<asp:Button Text="Update" CommandName="Update" runat="server"/>
<asp:Button Text="Cancel" CommandName="Cancel" runat="server"/>
</EditItemTemplate>
<ItemTemplate>
<asp:Label Text='<%# Eval("Caption") %>' runat="server" />
<asp:Button Text="Edit" CommandName="Edit" runat="server"/>
</ItemTemplate>
</asp:FormView>
在对 GridView 或 DetailsView 执行更新或插入操作时,如果该控件的列或字段定义了 BoundField,GridView 或 DetailsView 负责创建 Edit 或 Insert 模式中的输入 UI,以便它能自动提取这些输入值以传递回数据源。由于模板包含任意的用户定义的 UI 控件,因此,需要使用双向数据绑定语法,这样 FormView 等模板化控件才能知道应从模板中提取哪些控件值以用于更新、插入或删除操作。在 EditItemTemplate 中仍然可以使用 Eval 语法进行不传递回数据源的数据绑定。另请注意,FormView 与 DetailsView 和 GridView 一样,支持使用 DataKeyNames 属性保留主键字段(即使这些字段并未呈现)的原始值以传递回更新/插入操作。
实验:Eval VS. Bind
<table>
<tr> <td> ID </td>
<td><%#Eval("au_id")%> </td> </tr>
<tr> <td> au_lname </td>
<td> <asp:TextBox ID="TextBox1" runat="server" Text='<%#Bind("au_lname")%>'></asp:TextBox>
</td></tr>
<!---其他
--->
</table>
<EditItemTemplate>
运行结果和预期结果一致。
4 Repeater
5 DataList
二、DataSource
1 SqlDataSource
SqlDataSource,后者支持用于指定连接字符串、SQL 语句或存储过程的属性,用以查询或修改数据库。虽然这适合大多数小规模的个人或业余站点,但对于较大规模的企业级应用程序,在应用程序的呈现页中直接存储 SQL 语句可能很快就会变得无法维护。这些应用程序通常需要用中间层数据访问层或业务组件构成的封装性更好的数据模型。所幸 ASP.NET 数据源控件模型使用 ObjectDataSource 控件支持这种方法。
实验:通过SqlDataSource将DetailView与GridView结合使用
本实验是在DetailView的练习中的扩展。
当时的场景是通过将 ControlParameter 关联到 GridView 的 SelectedValue 属性来实现主/详细信息方案。SelectedValue 属性返回 DataKeyNames 属性指定的第一个字段的值。
SelectCommand="SELECT [au_id], [au_lname], [au_fname], [phone], [address], [city], [state], [zip], [contract] FROM [authors] WHERE ([au_id] = @au_id)" OnDataBinding="SqlDataSource2_DataBinding" ConnectionString="<%$ ConnectionStrings:Pubs %>"
UpdateCommand="UPDATE [authors] SET [au_id] = @au_id, [au_lname] = @au_lname, [au_fname] = @au_fname, [state] = @state WHERE ([au_id] = @au_id)">
<SelectParameters>
<asp:ControlParameter ControlID="GridView1" Name="au_id" PropertyName="SelectedValue" Type="Object" />
</SelectParameters>
</asp:SqlDataSource>
GridView的DataKeyNames 属性可以指定用多个逗号分隔的字段值,例如,如果需要将多个值传递给主/详细信息方案中的详细信息数据源,便可以这样做。这些附加键字段的值通过 SelectedDataKey 属性公开,该属性返回键字段的名称/值对的 DataKey 对象。ControlParameter 甚至能够通过将 PropertyName 属性设置为一个表达式来引用这些键。例如 SelectedDataKey.Values("au_id"))
。
SelectCommand="SELECT [au_id], [au_lname], [au_fname], [phone], [address], [city], [state], [zip], [contract] FROM [authors] WHERE ([au_id] = @au_id)" OnDataBinding="SqlDataSource2_DataBinding" ConnectionString="<%$ ConnectionStrings:Pubs %>"
UpdateCommand="UPDATE [authors] SET [au_id] = @au_id, [au_lname] = @au_lname, [au_fname] = @au_fname, [state] = @state WHERE ([au_id] = @au_id)">
<SelectParameters>
<asp:ControlParameter ControlID="GridView1" Name="au_id" PropertyName="SelectedDataKey.Values[0]" Type="Object" />
<asp:ControlParameter ControlID="GridView1" Name="au_lname" PropertyName="SelectedDataKey.Values[1]" Type="object" />
</SelectParameters>
</asp:SqlDataSource>
2 ObjectDataSource
ObjectDataSource 控件对象模型类似于 SqlDataSource 控件。ObjectDataSource 公开一个 TypeName 属性(而不是 ConnectionString 属性),该属性指定要实例化来执行数据操作的对象类型(类名)。类似于 SqlDataSource 的命令属性,ObjectDataSource 控件支持诸如 SelectMethod、UpdateMethod、InsertMethod 和 DeleteMethod 的属性,用于指定要调用来执行这些数据操作的关联类型的方法。
ObjectDataSource 要求它能够使用的对象具有非常具体的设计模式。这些限制主要是由执行 Web 应用程序请求的无状态环境施加的。由于为每个请求提供服务通常都要创建和销毁对象,通过对象数据源绑定的对象也需要是无状态的。默认情况下,ObjectDataSource 假定 TypeName 属性所指定的类型具有默认构造函数(无参数),不过,通过处理 ObjectCreating 事件来创建自定义对象实例并将它指定给事件参数的 ObjectInstance 属性,也可以代表 ObjectDataSource 实例化该类型。与 SelectMethod 属性关联的对象方法可以返回任何 Object 或 IEnumerable 列表、集合或数组。在上面的数据访问层示例中,DataView 对象实现了 Ienumerable。正如下一节将要讨论的,这些方法还可能返回强类型集合或对象。
实验一:将ObjectDataSource绑定到对象
步骤:
在App_Code文件夹中建立AuthorDB类
...{
static string connectionString = ConfigurationManager.ConnectionStrings["Pubs"].ConnectionString;
public AuthorsDB()
...{
}
/**//// <summary>
/// 通过ID获得详细情况,用于绑定DetailsView的select方法
/// </summary>
/// <param name="ID"></param>
/// <returns></returns>
public static DataSet GetAuthorsByID(string ID)
...{
IDbConnection dbConnection = new SqlConnection(connectionString);
string queryString = "SELECT [au_id], [au_lname], [au_fname], [phone], [address], [city], [state], [zip], [contract] FROM [authors] WHERE ([au_id] = @au_id)";
IDbCommand cmd = new SqlCommand();
cmd.CommandText = queryString;
cmd.Connection = dbConnection;
IDataParameter param_ID = new SqlParameter("au_id", ID);
cmd.Parameters.Add(param_ID);
IDbDataAdapter adapter = new SqlDataAdapter();
adapter.SelectCommand = cmd;
DataSet dataSet = new DataSet();
adapter.Fill(dataSet);
return dataSet;
}
/**//// <summary>
/// 获得所有ID,用于绑定DropdownList
/// </summary>
/// <returns></returns>
public static DataSet GetIDs()
...{
try
...{
Database db_Pubs = DatabaseFactory.CreateDatabase();
DbCommand cmd = db_Pubs.GetSqlStringCommand(@"SELECT [au_id] FROM [authors]");
DataSet result = db_Pubs.ExecuteDataSet(cmd);
return result;
}
catch (Exception ex)
...{
throw new Exception("无法获取。。。", ex);
}
}
/**//// <summary>
/// 用于绑定DetailView的更新方法
/// </summary>
/// <param name="au_id"></param>
/// <param name="au_lname"></param>
/// <param name="au_fname"></param>
/// <param name="state"></param>
public static void UpdateAuthor(string au_id, string au_lname, string au_fname, string state)
...{
int rowAffected = 0;
Database db_Pubs = DatabaseFactory.CreateDatabase();
DbCommand cmd = db_Pubs.GetSqlStringCommand(@"UPDATE [authors] SET [au_lname]=@au_lname, [au_fname]=@au_fname, [state]=@state WHERE ([authors].[au_id] = @au_id)");
db_Pubs.AddInParameter(cmd, "au_id", DbType.String, au_id);
db_Pubs.AddInParameter(cmd, "au_lname", DbType.String, au_lname);
db_Pubs.AddInParameter(cmd, "au_fname", DbType.String, au_fname);
db_Pubs.AddInParameter(cmd, "state", DbType.String, state);
try
...{
rowAffected = db_Pubs.ExecuteNonQuery(cmd);
}
catch (Exception ex)
...{
throw new Exception("更新失败!", ex);
}
if (rowAffected == 0)
throw new Exception("更新失败!");
}
}
新建DetailsViewDAL.aspx,添加ObjectDataSource,配置数据源和相关方法(碰到ObjectDataSource看不见类文件,重启VS2005可能可以解决问题)。
结果:可以正确显示,但是更新时出错,错误信息为:
ObjectDataSource 'ObjectDataSource2' could not find a non-generic method 'UpdateAuthor' that has parameters: au_id, au_lname, au_fname, state, phone, address, city, zip, contract.
分析:
从异常信息来看,应该是更新时调用的方法签名和AuthorDB中的UpdateAuthor方法的签名不一致。也就是说,虽然给ObjectDataSource指定了更新方法,但是它调用的时候传参是按照Select出来的列构造的。
联想在GridView对数据的绑定,猜想在DetailsView中设置绑定列的ReadOnly属性,应该可以使不需要更改的列不被生成参数(这也更符合实际中的需要,不可编辑的就不需要更新)。这时发现au_id也不会被作为参数传递。于是设置DataKeyNames="au_id",这样ObjectDataSource会根据 OldValuesParameterFormatString="original_{0}“来生成一个参数original_au_id,这样调整UpdateAuthor中的参数即可。
Height="50px" Width="125px" AutoGenerateEditButton="True" DataKeyNames="au_id" AutoGenerateRows="False">
<Fields>
<asp:BoundField HeaderText="ID" DataField="au_id" ReadOnly="true"/>
<asp:BoundField HeaderText="au_lname" DataField="au_lname" />
<asp:BoundField HeaderText="au_fname" DataField="au_fname" />
<asp:BoundField HeaderText="phone" DataField="phone" ReadOnly="True"/>
<asp:BoundField HeaderText="address" DataField="address" ReadOnly="True"/>
<asp:BoundField HeaderText="city" DataField="city" ReadOnly="True"/>
<asp:BoundField HeaderText="state" DataField="state" />
<asp:BoundField HeaderText="zip" DataField="zip" ReadOnly="True"/>
<asp:CheckBoxField HeaderText="contract" DataField="contract" ReadOnly="True"/>
</Fields>
</asp:DetailsView>
结果:
实现了部分数据可以编辑,部分数据只读。
SqlDataSource和ObjectDataSource的比较
ObjectDataSourceStatusEventArgs支持的属性:
AffectedRows | Gets or sets the number of rows that are affected by the data operation. | |
Exception | Gets a wrapper for any exceptions that are thrown by the method that is called by the ObjectDataSource control during a data operation. | |
ExceptionHandled | Gets or sets a value indicating whether an exception that was thrown by the business object has been handled. | |
OutputParameters | Gets a collection that contains business object method parameters and their values. | |
ReturnValue | Gets the return value that is returned by the business object method, if any, as an object. |
SqlDataSourceStatusEventArgs支持的属性:
Name | Description | |
---|---|---|
AffectedRows | Gets the number of rows affected by a database operation. | |
Command | Gets the database command submitted to the database. | |
Exception | Gets a wrapper for any exceptions thrown by the database during a data operation. | |
ExceptionHandled | Gets or sets a value indicating whether an exception thrown by the database has been handled. |
关于AffectedRows:
SqlDataSource在更新后SqlDataSourceStatusEventArgs有AffectedRows来指示相关Command影响的行数,而ObjectDataSource的ObjectDataSourceStatusEventArgs除了AffectedRows属性外,还有ReturnValue来判断相关操作所调用的方法的返回值。
但是不同之处在于,SqlDataSourceStatusEventArgs的AffectedRows属性通常都能正确的反映Command影响的行数,而ObjectDataSourceStatusEventArgs的AffectedRows确总是为-1。
而DetailsView的DetailsViewUpdatedEventArgs的AffectedRows确是在执行没有发生异常或者发生异常但是已经处理时为SqlDataSource、ObjectDataSource的AffectedRows,而发生异常时为0。
所以:如果我们确实需要在DetailsView的事件中拿到正确的AffectedRows的值,并且使用了ObjectDataSource,则需要在Updated/Deleted/Inserted事件后手动设置AffectedRows。
...{
.........
e.AffectedRows = e.ReturnValue;
}
关于参数:
对于参数的方向,缺省情况下都是Input型,即用于将值传入数据源操作。参数也可以是双向的,例如 InputOutput
、Output
和 ReturnValue
参数。可以使用 Parameter 对象的属性 Direction 指定参数的方向性。若要在数据源操作完成之后检索这些参数的值,应处理相应的操作后事件,例如 Selected、Updated、Inserted 或 Deleted 事件,以便从传递给这些事件的事件参数中获得参数值。SqlDataSourceStatusEventArgs 有一个 Command 属性,可以使用该属性获得返回值和输出参数。注意,对于双向参数,在 SqlDataSource 中将 Parameter 对象的 Size 属性设置为适当的值很重要。ObjectDataSourceStatusEventArgs 类型支持 OutputParameters 集合和 ReturnValue 属性。
SqlDataSourceStatusEventArgs 的Output方向的参数一般来自存储过程的output参数。而ObjectDataSource的Output方向的参数一般来自一个相关方法的一个用out声明的参数,例如:
...{
My_ReturnValue=1;
........
return EffactedRows
}
想象如下场景:ObjectDataSource的更新操作需要调用一个存储过程,而该存储过程包含一个output参数,如何返回该参数呢?
首先,ObjectDataSource必须先声明该参数,My_ReturnValue的Direction为Output.
SelectMethod="GetAuthorsByID" TypeName="AuthorsDB" UpdateMethod="UpdateAuthor" OnUpdated="ObjectDataSource2_Updated">
<UpdateParameters>
<asp:Parameter Name="au_id" Type="String" />
<asp:Parameter Name="au_lname" Type="String" />
<asp:Parameter Name="au_fname" Type="String" />
<asp:Parameter Name="state" Type="String" />
<asp:Parameter Direction="Output" Name="My_ReturnValue" Type="Int32" />
</UpdateParameters>
<SelectParameters>
<asp:ControlParameter ControlID="DropDownList1" Name="ID" PropertyName="SelectedValue"
Type="String" />
</SelectParameters>
</asp:ObjectDataSource>
然后,更新方法需要声明out参数,并且给该参数赋值。
...{
My_ReturnValue = -1;
int rowAffected = 0;
Database db_Pubs = DatabaseFactory.CreateDatabase();
DbCommand cmd = db_Pubs.GetSqlStringCommand(@"update [authors] set [au_lname]=@au_lname, [au_fname]=@au_fname, [state]=@state where ([authors].[au_id] = @au_id)
set @my_returnvalue=3");
// DbCommand cmd = db_Pubs.GetStoredProcCommand("MyTest");
db_Pubs.AddInParameter(cmd, "au_id", DbType.String, original_au_id);
db_Pubs.AddInParameter(cmd, "au_lname", DbType.String, au_lname);
db_Pubs.AddInParameter(cmd, "au_fname", DbType.String, au_fname);
db_Pubs.AddInParameter(cmd, "state", DbType.String, state);
db_Pubs.AddOutParameter(cmd, "My_ReturnValue", DbType.Int32,4);
try
...{
rowAffected = db_Pubs.ExecuteNonQuery(cmd);
My_ReturnValue = (int)db_Pubs.GetParameterValue(cmd, "My_ReturnValue");
}
catch (Exception ex)
...{
throw new Exception("更新失败!", ex);
}
if (rowAffected == 0)
throw new Exception("更新失败!");
return rowAffected;
}
这样,在更新之后的事件中
...{
e.ExceptionHandled = true;
e.AffectedRows = 1;
}
可以监视到 e.OutputParameters["My_ReturnValue"] 3 object {int}。
3 XmlDataSource
4 SitemapDataSource
三、Performance and Security
1 缓存数据、页面
2 连接字符串常见处理
3 冲突检测
SqlDataSource和ObjectDataSource都支持ConflictDetection属性。该属性设置为CompareAllValues,则OldValues会被传递,否则不会,而只是传递Key和NewValues。
所以,对于ObjectDataSource,该属性的设置将直接影响到相应方法的签名。所以如果设置错误,将会在编译时就报异常。而SqlDataSource则只会影响到相应Command的正确性,而不会报错误。如
SqlDataSource这样设置
runat="server" SelectCommand="SELECT [OrderID], [OrderDate], [ShipCountry] FROM [Orders]"
UpdateCommand="UPDATE [Orders] SET [OrderDate] = @OrderDate, [ShipCountry] = @ShipCountry WHERE [OrderID] = @original_OrderID AND [OrderDate] = @original_OrderDate AND [ShipCountry] = @original_ShipCountry"
ConflictDetection="OverwriteChanges" OldValuesParameterFormatString="original_{0}"
OnUpdating="SqlDataSource1_Updating">
<UpdateParameters>
<asp:Parameter Name="OrderDate" Type="DateTime" />
<asp:Parameter Name="ShipCountry" Type="String" />
<asp:Parameter Name="original_OrderID" Type="Int32" />
<asp:Parameter Name="original_OrderDate" Type="DateTime" />
<asp:Parameter Name="original_ShipCountry" Type="String" />
</UpdateParameters>
</asp:SqlDataSource>
执行的时候用Sql Server Profiler抓到的语句如下
[OrderDate] = @original_OrderDate AND [ShipCountry] = @original_ShipCountry',N'@OrderDate datetime,@ShipCountry nvarchar(6),@original_OrderID
int,@original_OrderDate datetime,@original_ShipCountry nvarchar(4000)',@OrderDate=''8888-07-05
00:00:00:000'',@ShipCountry=N'France',@original_OrderID=10248,@original_OrderDate=NULL,@original_ShipCountry=NULL
Key和NewValues被传递了过来,但是OldValues则为NULL,所以更新不成功,但是不会出错
而将CompareAllValues属性设置为CompareAllValues后抓到的语句如下:
[OrderDate] = @original_OrderDate AND [ShipCountry] = @original_ShipCountry',N'@OrderDate datetime,@ShipCountry nvarchar(6),@original_OrderID
int,@original_OrderDate datetime,@original_ShipCountry nvarchar(6)',@OrderDate=''7777-07-05
00:00:00:000'',@ShipCountry=N'France',@original_OrderID=10248,@original_OrderDate=''6666-07-05 00:00:00:000'',@original_ShipCountry=N'France'
可以发现OldValues被传递了过来。
不过需要注意的是:不管设置为OverwriteChanges还是CompareAllValues,在SqlDataSourceCommandEventArgs中都能拿到所有值(即OldValues也能拿到)。
另外,ObjectDataSource的DataObjectTypeName可以和CompareAllValues 结合使用。这样ObjectDataSource构造参数时会去调用DataObjectTypeName指定类的构造函数。当更新、删除,插入操作时会调用以该类作为参数,并且正好两个参数的方法。这样可以方便编写代码。
...{
private int _contactID;
public int ContactID
...{
get ...{ return _contactID; }
set ...{ _contactID = value; }
}
private String _contactName;
public String ContactName
...{
get ...{ return _contactName; }
set ...{ _contactName = value; }
}
public Contact()
...{
//
// TODO: Add constructor logic here
//
}
public Contact(int ContactID, String ContactName)
...{
_contactID = ContactID;
_contactName = ContactName;
}
}
...{
HttpContext.Current.Response.Write("UpdateContact(Contact c, Contact original_c)");
return UpdateContact(c.ContactName, original_c.ContactID, original_c.ContactName);
}
SelectMethod="GetContacts" UpdateMethod="UpdateContact" ConflictDetection="CompareAllValues"
OldValuesParameterFormatString="original_{0}" DataObjectTypeName="Contact" OnUpdated="ObjectDataSource1_Updated">
<UpdateParameters>
<asp:Parameter Name="c" />
<asp:Parameter Name="original_c" />
</UpdateParameters>
</asp:ObjectDataSource>
四、其他
1 处理空值
数据控件支持各种处理空数据或缺少数据的方法。首先,GridView、FormView 和 DetailsView 全都支持一个 EmptyDataText 或 EmptyDataTemplate 属性,当数据源没有返回数据行时,可以使用该属性为控件指定一种呈现。只需设置 EmptyDataText 和 EmptyDataTemplate 之一(当同时设置两者时,EmptyDataTemplate 优先)。还可以在 BoundField(以及派生字段类型)、TemplateField 或数据源参数对象上指定一个 ConvertEmptyStringToNull 属性,以指定在调用关联的数据源操作之前,应将从客户端发送的 String.Empty 值转换为空值。ObjectDataSource 还支持一个 ConvertNullToDbNull 属性,当关联的方法需要 DbNull 参数而不是空值时,可将该属性设置为 true(Visual Studio 数据集中的 TableAdapter 类有此要求)。还可以在 BoundField(以及派生字段类型)上指定一个 NullDisplayText 属性,以便在数据源中返回的字段值为空值时,为该字段指定要显示的值。如果该值在编辑模式期间未更改,则它将在一个 Update 操作期间作为空值返回数据源。最后,还可以在数据源参数上指定一个 DefaultValue 属性,以便在传递的参数值为空值时,为参数指定一个默认值。这些属性将按顺序应用;例如如果同时设置 ConvertEmptyStringToNull 和 DefaultValue,则 String.Empty 值首先被转换为空值,随后被转换为默认值。
2 处理事件
数据控件事件旨在为您提供在页面执行生命周期中可插入自己的自定义代码的适当位置。数据控件一般在特定操作发生之前和之后公开事件。在操作之前激发的事件通常使用 -ing 后缀进行命名,而在操作之后激发的事件则使用 -ed 后缀进行命名。例如,GridView 支持的事件包括:
- PageIndexChanging 和 PageIndexChanged -- 在分页操作之前和之后引发
- SelectedIndexChanging 和 SelectedIndexChanged -- 在选择操作之前和之后引发
- Sorting 和 Sorted -- 在排序操作之前和之后引发
- RowEditing 和 RowCancelingEdit -- 在行进入编辑模式之前或在编辑模式被取消之前引发
- RowUpdating 和 RowUpdated -- 在更新操作之前和之后引发
- RowDeleting 和 RowDeleted -- 在删除操作之前和之后引发
- RowDataBound -- 当行与数据绑定时引发
- RowCreated -- 当创建行用于呈现(作为 TableRow)时引发
- RowCommand -- 当从控件中激发按钮命令时引发
数据源控件也公开事件,类似于数据绑定控件事件。SqlDataSource 和 ObjectDataSource 控件都支持下列事件:
- Selecting 和 Selected -- 在选择操作之前和之后引发
- Updating 和 Updated -- 在更新操作之前和之后引发
- Deleting 和 Deleted -- 在删除操作之前和之后引发
- Inserting 和 Inserted -- 在插入操作之前和之后引发
- Filtering -- 在筛选器操作之前引发
ObjectDataSource 控件还在 TypeName 属性所指定的对象被创建或销毁时额外公开一些事件。通过设置所传递的事件参数的 ObjectInstance 属性,可以在 ObjectCreating 事件中实际设置一个自定义对象。
- ObjectCreating 和 ObjectCreated -- 在对象被创建之前和之后引发
- ObjectDisposing -- 在对象被释放之前引发
结论:
通常,在操作发生之前激发的事件用于取消操作(通过将事件参数的 Cancel 属性设置为 true),或用于执行参数验证或对部分数据预处理。在操作之后引发的事件用于编写自定义代码以响应给定的操作,或用于检查操作的成功状态。例如,可以检查 Update、Insert 或 Delete 操作导致的 RowsAffected,或检查 Exception 属性以确定处理期间是否发生了异常。还可以设置事件参数的 ExceptionHandled 属性以防止异常向上冒泡到控件或页。
...{
if (e.Exception.InnerException != null)
...{
Response.Write(string.Format("<script>alert('{0}')</script>", e.Exception.InnerException.Message));
}
else
...{
Response.Write(string.Format("<script>alert('{0}')</script>", e.Exception.InnerException.Message));
}
e.ExceptionHandled = true;
}
处理参数:
如何处理各种数据控件事件以枚举通过事件参数传递的参数集合?注意,与不需要插入数据库的字段 相关联的BoundField 的 InsertVisible 属性应该设置为 false,例如 OrderID 字段是基础数据库中的标识列,不应该将它传递给 Insert 操作(数据库在插入操作发生时自动递增此值)。另请注意,如果OrderID 字段被标记为 DataKeyNames 中的主键,那么此字段的原始值保留在数据绑定控件所传递的 Keys 字典中。用户向输入控件中输入的值在 NewValues 字典中传递,不过标记为 ReadOnly=false
的字段除外。非键字段的原始值也由数据绑定控件保留在一个 OldValues 字典中,以用于传递给数据源。这些参数值由 SqlDataSource 按照 NewValues、Keys 和 OldValues 的顺序追加到命令上,不过在 ConflictDetection 设置为 OverwriteChanges
时,数据源默认不追加 OldValues。有关数据源如何使用 OldValues 的更多信息,请参考QuickStart中的“使用冲突检测”一节。
通过按照自己喜欢的顺序将静态 Parameter 对象添加到数据源操作的参数集合,可以更改 SqlDataSource 向命令追加参数的顺序。SqlDataSource 自动根据这些 Parameter 对象的顺序对数据绑定控件传递的参数重新排序。这在数据源的 ProviderName 属性设置为 System.Data.OleDb
时很有用,因为此设置不支持命名的参数,所以向命令追加参数的顺序必须与命令中的匿名参数占位符(“?”)的顺序相匹配。当使用命名的参数时,参数的顺序无关紧要。可以指定 Parameter 对象的 Type 属性,以便在执行命令或方法之前,强制将数据绑定控件传递的值转换为适当的数据类型。同样,可以设置 Parameter 的 Size 属性,以指示 SqlDataSource 命令中的 DbParameter 的 Size (这是输入/输出、输出和返回值参数所需要的)。
ProviderName="<%$ ConnectionStrings:NorthwindOLEDB.ProviderName %>" runat="server"
SelectCommand="SELECT TOP 10 [OrderID], [OrderDate], [ShipCountry] FROM [Orders]"
UpdateCommand="UPDATE [Orders] SET [OrderDate] = ?, [ShipCountry] = ? WHERE [OrderID] = ?"
OnUpdating="SqlDataSource1_Updating">
<UpdateParameters>
<asp:Parameter Name="OrderDate" Type="DateTime" />
<asp:Parameter Name="ShipCountry" Type="String" />
<asp:Parameter Name="OrderID" Type="Int32" />
</UpdateParameters>
</asp:SqlDataSource>
参数名称的默认约定要求按照数据源 Select 操作所选择的字段对新值进行命名。通过指定 OldValuesParameterFormatString 属性(例如“original_{0}”),可以对来自 Keys 或 OldValues 的参数重命名,以将它们与 NewValues 区别开来。通过处理相应的事件以便在执行数据源操作之前更改参数的值,您还可以自定义参数的名称。例如,如果 SqlDataSource 的更新操作与某个存储过程关联,该存储过程采用的参数名称与匹配默认约定的名称不同,可以在调用该存储过程之前在 SqlDataSource Updating 事件中修改参数名称。
e.Command.Parameters["@id"].Value = e.Command.Parameters["@ContactID"].Value;
e.Command.Parameters["@name"].Value = e.Command.Parameters["@ContactName"].Value;
e.Command.Parameters.Remove(e.Command.Parameters["@ContactID"]);
e.Command.Parameters.Remove(e.Command.Parameters["@ContactName"]);
}
ObjectDataSource 不依赖特定的参数顺序,而只是查找具有匹配的参数名称的方法。注意,为了解析方法重载,ObjectDataSource 没有使用参数的 Type 或 Size。只对参数的名称进行匹配,因此如果业务对象上有两个具有相同名称和参数名称但具有不同参数类型的方法,ObjectDataSource 将无法区分它们。可以在事件中更改 ObjectDataSource 参数的名称和值,类似于上面的 SqlDataSource 示例。但是,如果使用 DataObjectTypeName 指定要传递给 Update、Insert 和 Delete 操作的特定数据对象类型,您将不能修改参数名称 -- 只能修改值。如果需要修改参数名称,则不要使用 DataObjectTypeName,而只需在代码中的数据源事件中手动构造相应的数据对象。