数据绑定(数据源控件 -- SqlDataSource)

       数据源控件包括那些所有实现 IDataSource 接口的控件。.NET Framework 包含以下数据源控件:

  • SqlDataSource:连接到任意 ADO.NET 数据提供程序的数据源。
  • ObjectDataSource:连接到自定义的数据访问类。(这是大型专业 Web 应用程序倾向使用的数据源控件)
  • AccessDataSource:连接到 Access 数据库文件。用于小型网站,更好的小范围数据解决方案是使用免费的 SQL Server Express。
  • XmlDataSource:连接到 XML 文件。
  • SiteMapDataSource:连接到描述站点导航信息的 web.sitemap 文件。

 

数据绑定页面的生命周期

       数据绑定页面可以完成两类任务:

  • 从数据源中读取数据并为关联的控件提供数据
  • 在关联的控件编辑数据后,它们可以更新数据源

       理解页面的声明周期是非常重要的,本质上,数据绑定任务按下列顺序发生:

  1. 页面对象被创建(基于 .aspx 文件)
  2. 页面生命周期开始,Page.Init 和 Page.Load 事件发生
  3. 产生其他所有控件事件
  4. 数据源控件执行所有更新。某行被更新触发 Updating 和 Updated 事件。新增某行触发 Inserting 和 Inserted 事件。删除某行触发 Deleting 和 Deleted 事件。
  5. Page.PreRender 事件发生
  6. 数据源控件执行所有查询并将获得的数据插入到关联的控件,此时触发 Selecting 和 Selected 事件。
  7. 页面被呈现和释放

 

SqlDataSource

       SqlDataSource 代表一个使用 ADO.NET 提供程序的数据库连接,它需要一个通用的方法创建它所需要的 Connection、Command、DataReader 对象。使其唯一可行的办法是有一个数据提供程序工厂来负责创建这些对象。

       .NET 和以下 4 个提供程序工厂一起发行:

  • System.Data.SqlClient
  • System.Data.OracleClient
  • System.Data.OleDb
  • System.Data.Odbc

       这些工厂已经在 machine.config 文件中注册,所以可用它们中的任何一个配合SqlDataSource 一起使用,通过设置提供程序的名字来选择数据源:

<asp:SqlDataSource ProviderName="System.Data.SqlClient" ID="SqlDataSource1" runat="server"></asp:SqlDataSource>

       下一步是提供连接字符串(不需要硬编码),应从 web.config 中读取(使用表达式构造器的方式):

<asp:SqlDataSource ProviderName="System.Data.SqlClient" ConnectionString="<%$ ConnectionStrings:Northwind %>" 
ID="SqlDataSource1" runat="server"></asp:SqlDataSource>

       SqlDataSource 命令逻辑由 4 个属性提供:SelectCommand、InsertCommand、UpdateCommand、DeleteCommand,它们都接收一个字符串(SQL 语句或存储过程名称);与之相应的 SelectCommandType、InsertCommandType、UpdateCommandType、DeleteCommandType 也要设置为 Text 或 StoredProcedure (Text 是默认值)。

       下面是一个完整的 SqlDataSource ,它定义了从 Employees 表中读取记录的 SELECT 命令:

<asp:SqlDataSource ID="sourceEmployees" runat="server" 
  ProviderName="System.Data.SqlClient"
  ConnectionString="<%$ ConnectionStrings:Northwind %>"
  SelectCommand="select EmployeeID,FirstName,LastName,Title,City from Employees">
</asp:SqlDataSource>

       创建完数据源后,我们可以在设计时绑定控件,而不必在 Page.Load 事件中编写逻辑了,看图:

 image image

       智能标签的刷新架构可以促使数据源控件连接数据库并读取查询的信息。

       创建一些控件并进行数据绑定,看下效果:

image

 

数据绑定探源

       我们知道可以用 DataReader 或 DataView 来绑定,那么 SqlDataSource 使用的是哪一种呢?其实这决定于 DataSourceMode 的设置。

image

       DataSet 模式几乎总会更好一些。因为它支持 排序、过滤、缓存。

 

       数据绑定在页面处理结束时处理,就是在页面呈现之前(看前面介绍的生命周期)。数据绑定在每次回发时都会发生。如果你希望编写代码处理数据绑定完成后的活动,可以重写 Page.OnPreRenderComplete()方法,该方法在 PreRender 状态之后且在视图状态被序列化以及真实的 HTML 呈现之前发生

 

参数化命令

       在前面的查询中,完整的查询语句都是硬编码的。通常你不会有这样的灵活性。你可能需要根据情况来展示部分的数据。下面的示例创建一个 主/从 表单。需要2个数据源,第一个提供城市表,并设置自动回发为 True:

<asp:SqlDataSource ID="sourceEmployeeCities" runat="server"
 ProviderName="System.Data.SqlClient"
 ConnectionString="<%$ ConnectionStrings:Northwind %>"
 SelectCommand="select distinct City from Employees">
</asp:SqlDataSource>
<asp:DropDownList ID="ddlCities" runat="server" AutoPostBack="True" 
    DataSourceID="sourceEmployeeCities" DataTextField="City">
</asp:DropDownList>

       选择一个城市后,第二个数据源获取该城市的全部雇员:

<asp:SqlDataSource ID="sourceEmployees" runat="server"
 ProviderName="System.Data.SqlClient"
 ConnectionString="<%$ ConnectionStrings:Northwind %>"
 SelectCommand="select EmployeeID,FirstName,LastName,Title,City From Employees where City = @City">
 <SelectParameters>
    <asp:ControlParameter ControlID="ddlCities" Name="City"
     PropertyName="SelectedValue" />
 </SelectParameters>
</asp:SqlDataSource>
<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceEmployees">
</asp:GridView>

image

       使用参数编写查询。用符号 @ 表示参数,你可以定义任意多个参数,但是必须一一映射到某个值。在这个示例中,@City参数的值从 ddlCities.SelectedValue 属性获得。

 

1. 存储过程查询

       若是现在改用存储过程来进行查询也是非常简单的,只需修改 SqlDataSource 2个地方:

SelectCommand="GetEmployeesByCity" SelectCommandType="StoredProcedure"

       这样不仅可以获得存储过程的全部优点,而且通过删除实际的 SQL 查询,页面的 .aspx 部分也将显得很流畅,而在实际应用中这些查询语句通常非常长!

2. 更多参数类型

       参数值不一定要从其他控件获取,看下表:

控件属性 <asp:ControlParameter> 页面控件的属性
查询字符串的值 <asp:QueryStringParameter> 当前查询字符串的值
会话状态的值 <asp:SessionParameter> 当前用户会话中的值
cookie 的值 <asp:CookieParameter> 来自附加在当前请求上 cookie 的值
用户配置的值 <asp:ProfileParameter> 来自当前用户配置的值
表单变量 <asp:FormParameter> 某个输入控件发送到页面的值。通常可以使用某个控件的属性获取值。
但在禁用了相应控件的视图状态时,就需要直接从 Forms 集合中抓取。
路由值 <asp:RouteParameter> 路由 URL 的值。
通过编程设置 <asp:Parameter> 所有其他参数继承的基类。从来不需要自动设置,因为它在使用代码手工设置参数时才有意义。

       数据源控件的 SelectQuery 属性界面中也可以调出窗口界面来设定参数

 

       改用查询字符串的方式来实现一下2个页面中的切换:

City 页面:

<asp:SqlDataSource ID="sourceEmployeeCities" runat="server" ProviderName="System.Data.SqlClient"
    ConnectionString="<%$ ConnectionStrings:Northwind %>" SelectCommand="select distinct City from Employees">
</asp:SqlDataSource>
<asp:ListBox ID="ListBox1" runat="server" DataSourceID="sourceEmployeeCities" 
    DataTextField="City" Rows="7" Width="136px"></asp:ListBox>
<asp:Button ID="Button1" runat="server" Text="Button" onclick="Button1_Click" />
protected void Button1_Click(object sender, EventArgs e)
{
    Response.Redirect("QueryEmployees.aspx?city=" + ListBox1.SelectedValue);
}

Employees 页面:

<asp:SqlDataSource ID="sourceEmployees" runat="server" ProviderName="System.Data.SqlClient"
    ConnectionString="<%$ ConnectionStrings:Northwind %>" SelectCommand="select EmployeeID,FirstName,LastName,Title,City from employees where city=@city">
    <SelectParameters>
        <asp:QueryStringParameter Name="city" QueryStringField="city" />
    </SelectParameters>
</asp:SqlDataSource>
<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceEmployees">
</asp:GridView>

 

       有时候可能会在需要使用参数值前对其进行修改,这种情况下,你需要在数据库操作发生前通过代码设置参数值,SqlDataSource 有一些为实现这一目的而设计的事件(Selecting、Updating、Inserting、Deleting):

protected void sourceEmployees_Selecting(object sender, SqlDataSourceSelectingEventArgs e)
{
    e.Command.Parameters["@city"].Value = Request.QueryString["city"].Substring(0, 3);
}

       请注意,在 Parameters 集合中查找参数时,必须在参数名称前加入 @ 字符!!!

 

错误处理

       如果有错误发生,你可以依赖 SqlDataSource 正确释放所有的资源(如连接)。不过,底层的异常并没有得到处理,它会一直向上传递到你的页面,直到破坏整个进程。和其他未处理的异常一样,用户将会看到一些私密的错误信息或出错页面,这种设计时不可容忍的。

       在网页中处理错误并显示更为合适的信息是一个不错的注意。为了达到这一目的,你需要处理错误发生后立即产生的数据源事件(Selected、Inserted、Updated、Deleted)。

       看一个示例:

protected void sourceEmployees_Selected(object sender, SqlDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        Label1.Text = "An exception occurred performing the query.";
 
        // 阻止异常向上传递
        e.ExceptionHandled = true;
    }
}

 

更新记录

       ASP.NET的部分控件还支持编辑、更新。看下面示例:

<asp:SqlDataSource ID="SqlDataSource1" runat="server" ProviderName="System.Data.SqlClient"
 ConnectionString="<%$ ConnectionStrings:Northwind %>"
 SelectCommand="select EmployeeID,FirstName,LastName,Title,City from Employees"
 UpdateCommand="update Employees set FirstName=@FirstName,LastName=@LastName,
 Title=@Title,City=@City where EmployeeID=@EmployeeID">
</asp:SqlDataSource>

       在这个示例中,参数名不是随意起的!只要每个参数的名字和它影响的字段名一样,且在前面有@符号,这些参数就不需要定义。因为 ASP.NET 数据控件在触发更新前自动提交含有新值的参数集合,其中每个参数都使用这样的命名方法。

       可以对此做个试验。创建一个 GridView 并绑定到 SqlDataSource ,设置 GridView 的AutoGenerateEditButton 属性为 true ,一个新列出现在 GridView 的左边:

image

       “更新”链接会把值传送到 SqlDataSource.UpdateParameters 集合(使用字段名)并触发 SqlDataSource.Update()方法来更新数据库,此时,你不需要编写任何代码!

 

1. 严格并发检查

       上面的示例更新使用 ID 匹配记录,这样的做法问题在于更新命令会不加选择的更新所有字段,带来的后果是可能会消除其他用户的更新,如果他们的更新介于你请求的页面和更新的页面之间。

       为了防止出现这类问题,可以强制使用更严格的并发检查。一个办法是使用更精确的 where 子句创建只有当所有字段都完全匹配时才执行的更新命令,这个命令如下所示:

UpdateCommand="update Employees set FirstName=@FirstName,LastName=@LastName,
Title=@Title,City=@City where EmployeeID=@original_EmployeeID 
and FirstName=@original_FirstName and LastName=@original_LastName
and Title=@original_Title and City=@original_City" 
   onupdating="sourceEmployees_Updating">

       这个命令里有一个重要的变化。where 子句并不会试图匹配名为 @FirstName、@LastName 等参数,因为这些参数反映了当前的值(它们可能不是原始值)。相反,它们使用名为 @original_FirstName、@original_LastName 等参数。这引发了一个问题,这些参数的值来自哪里呢?为了能够访问这些原始值,必须执行一些最初的步骤。

       首先,把 SqlDataSource.ConflictDetection 属性设置为 ConflictOptions.CompareAllValues 而不是 ConflictOptions.OverwriteChanges(默认值)来告诉 SqlDataSource 你需要访问原始值。

namespace System.Web.UI
{
    // 摘要:
    //     确定 ASP.NET 数据源控件在更新或删除数据时如何处理数据冲突。
    public enum ConflictOptions
    {
        // 摘要:
        //     数据源控件使用数据行自己的值覆盖该行中的所有值。
        OverwriteChanges = 0,
        //
        // 摘要:
        //     数据源控件使用 Update 和 Delete 方法的 oldValues 集合来确定数据是否已被其他进程更改。
        CompareAllValues = 1,
    }
}

       第二步是告诉 SqlDataSource 如何命名保持原始值的参数。默认情况下,原始值被赋予和变化了的值相同的参数名称。事实上,它们覆盖了原始参数值。为了避免这一行为,你需要设置 SqlDataSource.OldValuesParameterFormatString 属性。这个属性接收一个 {0} 作为占位符的字符串,其中 {0} 指定原始参数名称。例如,如果把 OldValuesParameterFormatString 属性设置为 original_{0}(这是一般的习惯),那么具有原始值的参数就会被赋予前缀 original_ 。

       理解了这些细节后,就能编写实现这一技术的完全配置的 SqlDataSource 了:

<asp:SqlDataSource ID="sourceEmployees" runat="server" ProviderName="System.Data.SqlClient"
    ConnectionString="<%$ ConnectionStrings:Northwind %>" SelectCommand="select EmployeeID,FirstName,LastName,Title,City from Employees"
    ConflictDetection="CompareAllValues" OldValuesParameterFormatString="original_{0}"
    UpdateCommand="update Employees set FirstName=@FirstName,LastName=@LastName,
 Title=@Title,City=@City where EmployeeID=@original_EmployeeID 
 and FirstName=@original_FirstName and LastName=@original_LastName
 and Title=@original_Title and City=@original_City" OnUpdating="sourceEmployees_Updating">
</asp:SqlDataSource>

 

2. 使用存储过程执行更新

       此时,只要略微修改下:

UpdateCommand="UpdateEmployee" UpdateCommandType="StoredProcedure"

       不过,这里有一个缺点。你已经知道,参数的名字是基于字段名的。如果存储过程使用和参数一样的名字,更新毫无问题。但是如果不一样,更新将失败。(参数的顺序不重要,参数的大小写也不重要,只有参数的名称是重要的)。

       例如,有这样一个存储过程:

create proc UpdateEmployee
@EmployeeID            int,
@First            varchar(10),
@Last            varchar(20),
@TitleOfCourtesy    varchar(25)
as
。。。。。。

       可以看出,存储过程使用的参数名和 SqlDataSource 中的参数是对应不上的。遗憾的是,并没有声明性的方法来修正这个参数映射的问题。你需要定义新的参数并编写一些自定义代码

       第一步,向 SqlDataSource.UpdateParameters 集合添加两个参数,遗憾的是不能在进行更新时添加,而是需要添加到 SqlDataSource 标签

<asp:SqlDataSource ID="sourceEmployees" runat="server" ProviderName="System.Data.SqlClient"
    ConnectionString="<%$ ConnectionStrings:Northwind %>" 
    SelectCommand="select EmployeeID,FirstName,LastName,Title,City from Employees"
    ConflictDetection="CompareAllValues" OldValuesParameterFormatString="original_{0}"
    UpdateCommand="UpdateEmployee" UpdateCommandType="StoredProcedure" 
    OnUpdating="sourceEmployees_Updating">
    <UpdateParameters>
        <asp:Parameter Name="First" Type="String" />
        <asp:Parameter Name="Last" Type="String" />
    </UpdateParameters>
</asp:SqlDataSource>

       注意,在 SqlDataSource 标签内定义参数时,参数名称不包括 @ 符号

       第二步,响应 SqlDataSource 的 Updating 事件,它在更新前发生:

protected void sourceEmployees_Updating(object sender, SqlDataSourceCommandEventArgs e)
{
    e.Command.Parameters["@First"].Value = e.Command.Parameters["@FirstName"].Value;
    e.Command.Parameters["@Last"].Value = e.Command.Parameters["@LastName"].Value;
    e.Command.Parameters.Remove(e.Command.Parameters["@FirstName"]);
    e.Command.Parameters.Remove(e.Command.Parameters["@LastName"]);
}

       上面的情形是无代码的数据绑定无法工作的典型情形。总体而言,如果你可以设计存储过程和类使它们和数据源控件一起工作,这将避免编写大量的代码。另一方面,如果让数据源控件和现有的具有固定数据库架构或数据库组件的应用程序一起使用,将会须要大量额外的代码才能让它们融合在一起。

 

删除记录

       与更新相似:

<asp:SqlDataSource ID="sourceEmployees" runat="server" ProviderName="System.Data.SqlClient"
    ConnectionString="<%$ ConnectionStrings:Northwind %>" 
    SelectCommand="select EmployeeID,FirstName,LastName,Title,City from Employees"
    DeleteCommand="delete from Employees where EmployeeID=@EmployeeID"> 
</asp:SqlDataSource>

       如果使用了标准的 ConflictOptions(ConflictOptions.OverwriteChanges),那么还要把 GridView.DataKeyNames 设置为代表主键的用逗号分隔的字段名称列表。如果忘记执行这一步骤,GridView 就不会把这些参数传给 SqlDataSource,SqlDataSource 也就不能够找到它要删除的记录。

       这是创建一个使用 SqlDataSource 从而允许删除记录的 GridView 所需的最小标记:

<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceEmployees" 
 AutoGenerateDeleteButton="True" DataKeyNames="EmployeeID">
</asp:GridView>

 

插入记录

       GridView 支持编辑和删除记录,不支持插入记录。不过,DetailsView 和 FormView 确实支持插入记录,基本的过程也相同:

<asp:SqlDataSource ID="sourceEmployees" runat="server" ProviderName="System.Data.SqlClient"
    ConnectionString="<%$ ConnectionStrings:Northwind %>" 
    SelectCommand="select EmployeeID,FirstName,LastName,Title,City from Employees"
    InsertCommand="insert into Employees(FirstName,LastName) values(@FirstName,@LastName)"> 
</asp:SqlDataSource>
<asp:DetailsView ID="DetailsView1" runat="server" DataSourceID="sourceEmployees"
 AutoGenerateInsertButton="true">
</asp:DetailsView>

       此外,也可以把 DetailsView 的 DefaultMode 属性设置为 Insert 以便让 DetailsView 以插入模式启动。当把 GridView 和 DetailsView 组合到同一个页面时,这很有用

 

SqlDataSource 的不足

       使用 SqlDataSource 通常可以节省大量的数据访问代码,但同时也牺牲了很多的灵活性。这里列出最明显的不足:

  • 数据访问逻辑嵌在页面内:需要在网页上硬编码 SQL 语句,不修改网页就不能调整查询,在企业级应用中,这样的限制是不可容忍的。因为在应用程序发布后,考虑到用户配置、建立索引、预期负载的需要,常常会调整查询。
  • 在大型应用程序中的维护:每个访问数据库的页面都需要一个自己的 SqlDataSource 控件。在不同页面相同的查询(它们每一个都需要一个重复的 SqlDataSource 实例),这将是维护的噩梦。在一个基于组件的应用程序中,你可以使用更高层次的模型。网页将和数据访问类库通信,它们包含全部的数据库细节
  • 缺乏灵活性:每个数据访问任务都需要一个 SqlDataSource ,如果要向用户提供查看查询数据的多种方式,你的页面将会陷入数据源对象 SqlDataSource 的泥沼!
  • 和其他数据任务不兼容:SqlDataSource 不能完成某些类型的任务。比如,将发货请求插入到管道中或者记录事件日志。

 

       为了消除这些限制,应该考虑使用 ObjectDataSource 。ObjectDataSource 允许把页面绑定到自定义数据访问组件上。最妙的是,你可以得到和 SqlDataSource 几乎相同的功能,包括设计时的数据绑定和无需在网页中编写代码。

posted on 2012-07-25 17:16  SkySoot  阅读(24887)  评论(3编辑  收藏  举报

导航