數據源控件包括那些所有實現 IDataSource 接口的控件。.NET Framework 包含以下數據源控件:
- SqlDataSource:連接到任意 ADO.NET 數據提供程序的數據源。
- ObjectDataSource:連接到自定義的數據訪問類。(這是大型專業 Web 應用程序傾向使用的數據源控件)
- AccessDataSource:連接到 Access 數據庫文件。用於小型網站,更好的小范圍數據解決方案是使用免費的 SQL Server Express。
- XmlDataSource:連接到 XML 文件。
- SiteMapDataSource:連接到描述站點導航信息的 web.sitemap 文件。
數據綁定頁面的生命周期
數據綁定頁面可以完成兩類任務:
- 從數據源中讀取數據並為關聯的控件提供數據
- 在關聯的控件編輯數據后,它們可以更新數據源
理解頁面的聲明周期是非常重要的,本質上,數據綁定任務按下列順序發生:
- 頁面對象被創建(基於 .aspx 文件)
- 頁面生命周期開始,Page.Init 和 Page.Load 事件發生
- 產生其他所有控件事件
- 數據源控件執行所有更新。某行被更新觸發 Updating 和 Updated 事件。新增某行觸發 Inserting 和 Inserted 事件。刪除某行觸發 Deleting 和 Deleted 事件。
- Page.PreRender 事件發生
- 數據源控件執行所有查詢並將獲得的數據插入到關聯的控件,此時觸發 Selecting 和 Selected 事件。
- 頁面被呈現和釋放
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 事件中編寫邏輯了,看圖:
智能標簽的刷新架構可以促使數據源控件連接數據庫並讀取查詢的信息。
創建一些控件並進行數據綁定,看下效果:
數據綁定探源
我們知道可以用 DataReader 或 DataView 來綁定,那么 SqlDataSource 使用的是哪一種呢?其實這決定於 DataSourceMode 的設置。
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>
使用參數編寫查詢。用符號 @ 表示參數,你可以定義任意多個參數,但是必須一一映射到某個值。在這個示例中,@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 的左邊:
“更新”鏈接會把值傳送到 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 幾乎相同的功能,包括設計時的數據綁定和無需在網頁中編寫代碼。