数据绑定(数据源控件 -- ObjectDataSource)
数据绑定(数据源控件 -- ObjectDataSource)
ObjectDataSource 在网页控件和数据访问组件间建立一个声明性的链接。ObjectDataSource 非常灵活,并可以和多种类型的组件一起工作。
要使用它,你的数据访问类必须遵守以下几个规则:
- 所有逻辑必须包含在单个类中(如果使用不同的类选择和更新数据,那么必须把它们封装在一个更高层的类中)
- 调用单个方法后,它必须提供查询结果
- 查询结果必须是几条记录的组合,可以表现为集合、数组、DataSet、或实现 IEnumerable 的列表对象。每个记录由一个自定义对象通过公用属性公开它所有的数据
- 可以使用实例方法或静态方法。不过,如果使用实例方法,类必须有一个默认的无参构造函数,以便 ObjectDataSource 可以创建所需的实例
- 必须是无状态的。因为如果正在使用实例方法,那么 ObjectDataSource只在需要时才创建对象实例,并在每次请求结束后销毁它。
通过处理 ObjectDataSource 事件以及编写自定义代码可以忽略上面很多条规则。但如果你希望你的数据访问类能够无需额外的工作就无缝插入数据绑定模型,那么应该遵守这些规则。
选择记录
例如有这样一个自定义数据访问组件:
public class EmployeeDB
{
private string conStr;
public EmployeeDB()
{
conStr = WebConfigurationManager.ConnectionStrings["NorthWind"].ConnectionString;
}
public EmployeeDB(string connectionString)
{
this.conStr = connectionString;
}
// 一些增删改查的方法,调用数据库相应的存储过程
public int InsertEmployee(EmployeeDetails emp) { ...}
public EmployeeDetails GetEmployee(int employeeID) { ...}
public List<EmployeeDetails> GetEmployees() { ...}
public int InsertEmployee(EmployeeDetails emp){...}
public void DeleteEmployee(int employeeID) { ...}
// 省略......
}
在页面中,使用这个类的第一步是定义 ObjectDataSource 并指定包含数据访问类的名称,可以通过 TypeName 属性提供类的全名来指定:
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" TypeName="Model.EmployeeDB">
</asp:ObjectDataSource>
下一步就是指向用于增删改查的那些方法(ObjectDataSource 定义了 SelectMethod、InsertMethod、UpdateMethod、DeleteMethod 四个属性):
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
TypeName="Model.EmployeeDB" SelectMethod="GetEmployees">
</asp:ObjectDataSource>
然后可以绑定网页控件了:
<asp:ObjectDataSource ID="sourceEmployees" runat="server" TypeName="Model.EmployeeDB"
SelectMethod="GetEmployees"></asp:ObjectDataSource>
<asp:ListBox ID="ListBox1" runat="server" DataSourceID="sourceEmployees" DataTextField="FirstName"
DataValueField="EmployeeID"></asp:ListBox>
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataSourceID="sourceEmployees">
</asp:GridView>
默认情况下,ObjectDataSource 在页面上列的显示顺序是 EmployeeDetails 类中属性的声明顺序。而 SqlDataSource 在页面上列的显示顺序是查询中的顺序。外观的相似掩盖了幕后的真相,实际上场景是不同的。在这个示例中,网页不需要硬编码任何 SQL 语句,所有的工作都在 EmployeeDB 类中完成。
EmployeeDB 类使用错误处理块以确保发生错误时连接被正确关闭,但它并没有捕获异常。这是正确的,因为最佳的设计实践是让异常通知到网页,由页面决定如何来更好的通知用户。那么在页面中,我们一样可以处理 ObjectDataSource 的 Selected、Inserted、Updated、Deleted 事件,检查异常,最后将异常标记为已处理。
1. 使用参数化构造函数
ObjectDataSource 默认只能创建没有参数的构造函数的自定义访问类,不过你可以响应 ObjectDataSource .ObjectCreating 事件让 ObjectDataSource 和不符合这个条件的数据访问类一起工作。
现在的 EmployeeDB 类直接从 web.config 文件中读取数据库连接字符串,像下面这样:
private string conStr;
public EmployeeDB() { conStr = WebConfigurationManager.ConnectionStrings["NorthWind"].ConnectionString; }
然而你可能再添加一个构造函数,作为让网页提供特定字符串的另外一种方法:
public EmployeeDB(string connectionString) { this.conStr = connectionString; }
为了强制 ObjectDataSource 使用这个带参的构造函数,需要处理 ObjectCreating 事件:
protected void sourceEmployees_ObjectCreating(object sender, ObjectDataSourceEventArgs e)
{
e.ObjectInstance = new Model.EmployeeDB("......");
}
很显然,这个事件中还可以执行更复杂的初始化工作。(例如,可以调用一个初始化方法来选择创建它的某个子类等等)。
2. 使用方法参数
ListBox 提供所有雇员的 ID,并设置了自动回发:
<asp:ObjectDataSource ID="sourceEmployees" runat="server" TypeName="Model.EmployeeDB"
SelectMethod="GetEmployees"></asp:ObjectDataSource>
<asp:ListBox ID="ListBox1" runat="server" DataSourceID="sourceEmployees" DataTextField="EmployeeID"
DataValueField="EmployeeID" AutoPostBack="true"></asp:ListBox>
DetailsView 根据 ID 来获取单个用户的信息,因此要接收 ListBox 的选中记录的值:
<asp:ObjectDataSource ID="sourceEmployee" runat="server" TypeName="Model.EmployeeDB"
SelectMethod="GetEmployee" onselecting="sourceEmployee_Selecting">
<SelectParameters>
<asp:ControlParameter ControlID="ListBox1" Name="employeeID" PropertyName="SelectedValue" />
</SelectParameters>
</asp:ObjectDataSource>
<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceEmployee">
</asp:GridView>
ObjectDataSource 中定义的参数名必须要和将要调用的方法中的参数名完全一致。ObjectDataSource 调用方法时会利用反射检查方法,参数以匹配。ObjectDataSource 支持方法的重载。
第一次请求页面时,ListBox 控件不会有任何选定的值。但 DetailsView 仍然试图执行绑定。employeeID 参数值为空值,但因为整形不可以为空值,所有真实参数值是 0 ,GetEmployee()方法执行查询时找不到 ID 为 0 的记录,这是一个错误的条件,因此会抛出一个异常。
可以修改 GetEmployee()方法来返回空值解决这个问题,但更有意义的做法是捕获试图绑定的事件并取消它的执行:
protected void sourceEmployee_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
{
if (e.InputParameters["employeeID"] == null)
{
e.Cancel = true;
}
}
更新记录
ObjectDataSource 提供和 SqlDataSource 相同的更新数据绑定的支持。
<asp:ObjectDataSource ID="sourceEmployees" runat="server" TypeName="Model.EmployeeDB"
SelectMethod="GetEmployees" UpdateMethod="UpdateEmployee"></asp:ObjectDataSource>
保证 UpdateMethod 有正确的签名很有挑战性。更新、删除、新增操作自动从链接的数据库控件中获得参数的集合,这些参数具有和相应类属性相同的名字。
假设创建了一个网格并启用了编辑,用户提交更新时,GridView 为 EmployeeDetails 类的每个属性创建一个参数(EmployeeID、FirstName、LastName、TitleOfCourtesy)并加入到 ObjectDataSource.UpdateParameters 集合。接着 ObjectDataSource 在 EmployeeDB 类中查找 UpdateEmployee()的方法,这个方法必须包含具有相同名字的相同参数。
也就是说,下面这个是匹配的:
public void UpdateEmployee(int employeeID,string firstName,string lastName,string titleOfCourtesy)
而这个不匹配,名字不完全相同:
public void UpdateEmployee(int id,string first,string last,string titleOfCourtesy)
这个也不匹配,有额外的参数:
public void UpdateEmployee(int employeeID,string firstName,string lastName,string titleOfCourtesy,bool useOpt)
提示:
- 方法匹配算法不区分大小写
- 不考虑参数顺序和数据类型
- 只寻找正确参数个数和相同参数名称的方法,只要有这样的方法,更新就会自动提交,无需任何自定义代码。
使用数据对象执行更新
先前示例中的 UpdateEmployee()方法有一个问题,方法签名略显笨拙!既然已经定义了 EmployeeDetails 类,使用它的实例作为参数才是有意义的:
public void UpdateEmployee(EmployeeDetails emp)
ObjectDataSource 支持这一的方式,但必须设置 DataObjectTypeName 为要使用的类的全名:
<asp:ObjectDataSource ID="sourceEmployees" runat="server" TypeName="Model.EmployeeDB"
DataObjectTypeName="Model.EmployeeDetails"></asp:ObjectDataSource>
此外,你的数据对象必须遵守下面的规则:
- 必须有一个默认的无参构造函数
- 对于每个参数,必须有一个同名的属性
- 所有属性均为公有并可写
1. 处理非标准的方法签名
有时会碰到数据访问类(例如 EmployeeDetails)的属性名称和更新方法(例如 EmployeeDB.UpdateEmployee())的参数名称不完全匹配的问题。要解决这样的问题,仍然是新建参数,其次捕获更新事件,替换参数。示例如下:
<asp:ObjectDataSource ID="sourceEmployees" runat="server" SelectMethod="GetEmployees"
UpdateMethod="UpdateEmployee" TypeName="Model.EmployeeDB"
DataObjectTypeName="Model.EmployeeDetails"
onupdating="sourceEmployees_Updating">
<UpdateParameters>
<asp:Parameter Name="id" Type="int32" />
</UpdateParameters>
</asp:ObjectDataSource>
protected void sourceEmployees_Updating(object sender, ObjectDataSourceMethodEventArgs e)
{
e.InputParameters["id"] = e.InputParameters["EmployeeID"];
e.InputParameters.Remove("EmployeeID");
}
执行新增和删除操作时一样,差别在于处理事件要相应变化,也可以用类似的方法添加额外的参数并在事件中为它赋值,甚至在事件中还可以将 ObjectDataSource 指向同一个类中不同的更新方法:
sourceEmployees.UpdateMethod = "UpdateEmployeeStrict";
2. 在新增操作中处理标识值
下面这个示例,上部分是 DetailsView 允许用户添加记录,GridView 显示当前存在的所有记录,并允许用户删除它们。
<asp:DetailsView ID="detailsInsertEmployee" runat="server" DataSourceID="sourceEmployees"
DefaultMode="Insert" AutoGenerateInsertButton="true">
</asp:DetailsView>
<asp:Label ID="lblConfirmation" runat="server" EnableViewState="false"></asp:Label><br />
<br />
<asp:GridView ID="gridEmployeeList" runat="server" DataSourceID="sourceEmployees"
AutoGenerateDeleteButton="true">
</asp:GridView>
<asp:ObjectDataSource ID="sourceEmployees" runat="server" TypeName="Model.EmployeeDB"
SelectMethod="GetEmployees" DeleteMethod="DeleteEmployee" InsertMethod="InsertEmployee"
DataObjectTypeName="Model.EmployeeDetails"
oninserted="sourceEmployees_Inserted">
<InsertParameters>
<asp:Parameter Name="EmployeeID" Type="Int32" Direction="ReturnValue" />
</InsertParameters>
</asp:ObjectDataSource>
出于其他原因你可能希望使用标识值,如显示确认信息等。因此在 ObjectDataSource 中定义一个参数来接收Insert方法的返回值。接着可以响应 Inserted 事件来获取参数:
protected void sourceEmployees_Inserted(object sender, ObjectDataSourceStatusEventArgs e)
{
if (e.Exception!=null)
{
lblConfirmation.Text = "Inserted record " + e.ReturnValue;
}
else
{
lblConfirmation.Text = e.Exception.Message;
e.ExceptionHandled = true;
}
}
效果:
使用特性标识数据访问类
使用智能标签可以启动数据源配置向导,向导会引导你完成一系列步骤,提示你选取数据访问类并选择用于 增删改查 的方法。
以下技巧可以让你的数据库组件能和数据源配置向导这类工具很好的工作。(但不是必须的)
using System.ComponentModel; // 引入命名空间
// 将某一类型标识为适合绑定到 System.Web.UI.WebControls.ObjectDataSource 对象的对象。
[DataObject] // 添加特性
public class EmployeeDB
如果在向导的对话框启用了只显示数据组件复选框,就会只看见使用了 DataObject 特性的类,这是快速访问解决方案中数据访问类的好方法。
[DataObjectMethod(DataObjectMethodType.Select,true)]
public List<EmployeeDetails> GetEmployees()
DataObjectMethod 特性标识了该方法所执行的操作类型以及该方法是否是默认的数据方法。DataObjectMethodType 枚举标识该方法所执行的数据操作类型(增删改查及填充 DataSet)。
第2个布尔参数表明同一操作类型的多个方法中的默认方法。(比如有多个 select 方法,默认方法应该为未经过滤的所有记录的集合)
数据源控件的限制
整体而言,对于 ASP.NET 开发者来说,数据绑定是一项非凡的创新,不过你还是会碰到绑定所不能及的状况,甚至要完全放弃绑定。
问题
一个下拉表显示城市,一个网格显示对应的雇员。你可能希望在下拉列表中添加两个选项(选择城市、选择所有城市),怎么做呢?
数据绑定的一个弱点就是你从来都不能显式的处理或创建用户绑定控件的数据对象。它产生的后果是,你没有机会添加额外的项。
这里的两个难点在于:1. 如何添加新增项? 2. 新增项被选中时如何替换自动产生的逻辑?
添加其他项
这个问题有几个解决办法,但没有一个是完美的。
- 重写查询:select '(Choose a City)' AS City union select distinct city from Employees ;采用这种方法的问题在于你不得不把表现层的细节问题加入到数据层。
- 编程解决:在正常操作中,数据源控件在相关联的控件需要数据或者更新提交完成时被自动调用。很少有人知道,其实还可以通过编程调用 Select()、Insert()、Delete()、Update()这些方法来接管数据源控件,此时将完全由你来决定返回的结果。为了使这些成为现实,首先要删除 DropDownList.DataSourceID属性,而在页面第一次加载时绑定控件。
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
ddlCities.DataSource = sourceEmployees.Select(DataSourceSelectArguments.Empty);
ddlCities.DataBind();
ddlCities.Items.Insert(0, "(Choose a City)");
ddlCities.Items.Insert(1, "(All City)");
ddlCities.SelectedIndex = 0;
}
}
使用 SqlDataSource 处理其他选项
下一个挑战是如何阻止前面两个项目的单击事件。可以通过数据源的 Selecting 事件来实现:
protected void SqlDataSource1_Selecting(object sender, SqlDataSourceSelectingEventArgs e)
{
if (e.Command.Parameters["@city"].Value == "(Choose a City)")
{
e.Cancel = true;
}
else
{
e.Command.CommandText = "select * from Employees";
}
}
这是一种硬编码的暴力解决方式,很难看。
使用 ObjectDataSource 处理其他选项
ObjectDataSource 可以把这个问题解决的好一点,因为它可以讲命令重定向到其他方法:
protected void sourceEmployees_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
{
if (e.InputParameters["city"].ToString() == "(Choose a City)")
{
e.Cancel = true;
}
else if(e.InputParameters["city"].ToString() == "(All City)")
{
sourceEmployees.SelectMethod = "GetAllEmployees";
// 必须移除参数,ObjectDataSource 是根据参数去匹配数据组件中的方法的
e.InputParameters.Remove("city");
}
}