使用LINQ和ADO.NET創建Silverlight程序
原創【51CTO.com獨家特稿】在Silverlight中可以創建行業和其它以數據為中心的應用系統,但在Silverlight中處理數據不是一件容易的事情,由于Silverlight包括許多處理數據和支持Web Service及XML的工具,但這些工具僅代表跨過防火墻進行數據訪問的最基礎的部分。
常見的數據訪問策略是使用Web Service和客戶端LINQ共同實現的,如果你正在修改現有Web Service端點強化你的Silverlight應用程序,那么我推薦你使用這個方法。但如果你在使用Silverlight創建一個新的Web Service,就沒有必要這么做了。
對一個典型的Web Service層而言,你在服務器上實現一個傳統的數據訪問策略(自定義業務對象、LINQ to SQL、實體框架、Nhibernate等)通過Web Service暴露數據對象,Web Service僅僅是數據訪問策略下面的網關。
但為了開啟完整的數據連通性,你必須要映射四個數據操作(創建、讀取、更新和刪除)到Web Service方法,下面是一個簡單的支持Product類的service contract,注意我在本文中使用的都是C#。
例1 Product Web Service的Service Contract
[ServiceContract] public interface ICustomerService { [OperationContract] List |
創建一套服務來處理應用程序完整的數據模型可能是相當費時的,正如這個例子中顯示的,特殊特性的操作可能導致Web Service非常臃腫,換句話說,Web Service將有新的要求和操作要增加,甚至包括不屬于核心業務域的操作。
在例1中你看到GetAllProductsWithCategories操作默認用于檢索Product和分類。即使添加排序、過濾和分頁機制到這個簡單的例子你也不要感到驚訝,如果有一個簡單的方法支持數據操作(如查詢、排序、過濾等)不用每次都手動構建這些機制那將是非常吸引人的,ADO.NET Data Service就正是為此而生的。
ADO.NET Data Service
ADO.NET Data Service的目標是為數據模型提供Web訪問端點,這些端點提供了數據排序、過濾、調整和分頁功能,因此開發人員就不需要在自己去編寫這部分代碼了,實際上,每個端點都是LINQ查詢的起點,就從這個端點上你就可以查詢你想要查找的數據。
但不要認為ADO.NET Data Service是另一個數據訪問策略,實際上,ADO.NET Data Service不執行任何直接的數據訪問操作,它位于數據訪問的上層,圖1顯示了ADO.NET Data Service和它在一個應用程序架構中的位置。
圖 1 ADO.NET Data Service層
由于ADO.NET Data Service依賴于數據訪問程序完成真實的數據訪問工作,你必須指定這個方法該如何做,在ADO.NET Data Service中,每個服務(Service)必須回到開啟LINQ的提供程序的后面,實際上,每個端點就是一個Iqueryable端點,因此ADO.NET Data Service支持任何支持Iqueryable的對象。
創建服務(Service)
當你將ADO.NET Data Service添加到你的項目中時,會創建一個新的.svc文件,代表一個服務的類,和Web Service不同,你不需要自己親自實現服務的操作,但要允許DataService類處理這些工作,為了運行這些服務,有兩個小任務必須執行。首先,DataService類需要一個類型參數叫做上下文對象,它是將數據作為服務暴露的類,當你的服務從關系數據庫暴露數據時,這個類是從實體框架(EntityFramework)的ObjectContext或LINQ to SQL的DataContext衍生而來的。
//使用我的NorthwindEntities上下文對象(context object)作為服務(Service)的數據源
public class Products : DataService
上下文對象沒有基數類要求,實際上,你可以創建你自己的上下文對象,只要它的屬性實現了Iqueryable接口,ADO.NET Data Service將會以端點形式暴露這些屬性:
public class StateContext { StateList _states = new StateList(); public IQueryable |
InitializeService調用中,你可以使用IdataServiceConfiguration對象指定什么類型的許可允許進入服務,ADO.NET Data Service使用名詞和動詞具體指定許可,如例2所示:
例2 設置訪問規則
//這個方法只被調用一次初始化服務端策略 public static void InitializeService(IDataServiceConfiguration config) { //只允許我們讀取和更新Products實體,不允許刪除和創建 config.SetEntitySetAccessRule("Products", EntitySetRights.AllRead | EntitySetRights.WriteUpdate); //只允許讀取Category和Suppliers實體 config.SetEntitySetAccessRule("Categories", EntitySetRights.AllRead); config.SetEntitySetAccessRule("Suppliers", EntitySetRights.AllRead); } |
完成這個之后,你可以直接瀏覽服務了,它將會顯示每個端點的原子反饋信息,為了調試ADO.NET Data Service,我建議你禁用Internet Explorer的RSS 反饋視圖,或使用另一個瀏覽器查看服務的XML格式。
#p#
查詢和更新數據
ADO.NET Data Service將服務作為具有代表性的狀態轉換器(Representational State Transfer (REST))暴露,它是一個基礎服務,不是基于SOAP的服務,這意味著要替換掉SOAP封包,服務響應的有效負載只包括數據,不包括原數據(metadata),所有請求都使用HTTP動詞(GET,PUT,POST等)和請求URI描述,假定你有一個如圖2所示的模型描述Products,Categories和Suppliers,ADO.NET Data Service服務將會產生三個端點,每個實體集一個,URI為了確定一個模型中的實體集,只需要使用服務的地址和端點的名字就可以了:http://localhost/{服務名}/{端點名}或http://localhost/Product.svc/Products。
圖 2 數據模型示例
URI語法支持許多不同的特性,包括檢索特殊的實體,對結果進行排序、過濾、分頁和調整。
ADO.NET Data Service使用這些URI風格的查詢將數據返回給服務的用戶,目前支持兩個序列化格式(將來的版本很可能會進行擴展):JavaScript對象標記(JavaScript Object Notation即JSON)和基于原子的XML(Atom-based XML)。JSON對于客戶端Web代碼非常有吸引力,而Atom是基于XML的格式,因此需要借助XML解析器。
ADO.NET Data Service在查詢中使用標準的HTTP訪問頭來確定向客戶端返回什么格式,如果你從客戶端(如一個瀏覽器)發出一個請求可以破壞XML,如果你不通過Accept頭指定一個優先選用的格式,默認將使用Atom作為返回的格式。
查詢數據只是解決方案的一部分,我們的最終目標是同時支持查詢和更新,為了支持這些要求,ADO.NET Data Service映射了四個最基本的數據訪問操作到基本的HTTP動詞(如表1所示):
數據訪問動詞 | HTTP動詞 |
Create | POST |
Read | GET |
Update | PUT |
Delete | DELETE |
表 1 數據訪問動詞 vs HTTP動詞
通過使用這些動詞,ADO.NET Data Service讓服務的用戶可以利用所有的數據操作類型,而不用為不同類型創建專門的端點。使用ADO.NET Data Service更新數據的唯一要求是在數據訪問技術下支持Iupdatable接口,這個接口定義了如何從ADO.NET Data Service更新和傳播到數據源。
Silverlight 2.0客戶端庫
如果你使用ADO.NET Data Service通過URI語法和操作XML來進行查詢和更新數據,你可能會得到你想要的許多功能,但你仍然要自行構建一些管道,ADO.NET Data Service客戶端庫的引入就是要解決這個問題,這個庫允許你直接在Silverlight程序中進行LINQ查詢,由客戶端庫將LINQ查詢翻譯成HTTP查詢或更新請求。
首先,你需要生成一些代碼,這些代碼讀取ADO.NET Data Service服務的元數據,并為服務的實體生成數據類。
為了生成這些代碼,你需要在你的項目(Project)中添加Service Reference,你可以在項目資源管理器(Project Explorer)中Silverlight項目上點擊右鍵,然后選擇‘添加服務引用(即Add Service Reference)’,在彈出的對話框中點擊‘查找(Discover)’按鈕,顯示你項目中的服務(包括ADO.NET Data Service),選擇ADO.NET Data Service端點,點擊確定按鈕。這樣會創建一個新的文件,包含了每個端點對應的data contract類和一個DataServiceContext衍生類,DataServiceContext類用作服務接入點(暴露可查詢的服務端點),這樣會在你的Silverlight項目中包含這些類,并在System.Data.Services.Client.dll(Silverlight 2 SDK的一部分)中添加一個引用。Silverlight客戶端代碼和其它使用.NET的代碼基于LINQ的查詢非常相似,下面是示例代碼:
// 創建服務類指定ADO.NET Data Service的位置 NorthwindEntities ctx = new NorthwindEntities(new Uri("Products.svc", UriKind.Relative)); //創建LINQ查詢 var qry = from p in ctx.Products orderby p.ProductName select p; |
當你執行這個查詢時,它會直接向目標數據發送一個Web請求,但這里的Silverlight代碼和標準的LINQ查詢不同,在Silverlight中不允許同步Web請求,因此,如果要執行異步,你首先需要將查詢轉換成DataServiceQuery
// 創建一個DataServiceQuery |
當這些查詢執行完后,無論操作是否成功,在AsyncCallback中指定的方法都會執行,通常你會在AsyncCallback中包含原始查詢,因此可以在callback方法中檢索它,你也可以將其保存為類的一部分,正如你在例3中看到的:
例3 將結果添加到集合中
void OnLoadComplete(IAsyncResult result) { //為查詢獲取一個引用 DataServiceQueryproductQuery = (DataServiceQuery )result.AsyncState; try { //獲得結果并將其添加到集合中 List products = productQuery.EndExecute(result).ToList(); } catch (Exception ex) { if (HtmlPage.IsEnabled) { HtmlPage.Window.Alert("Failed to retrieve data: " + ex.ToString()); } } }
如果你以前還沒有處理過LINQ,理解這些模型可能就非常困難,在寫本文的時候,除了在異步包中執行LINQ(如ThreadPool和BackgroundWorker)外,還沒有關于異步LINQ很好的模型,Silverlight需要所有的請求都是異步的,因此在使用ADO.NET Data Service客戶端庫時需要使用這個模型。
載入相關實體
ADO.NET Data Service也允許你選擇如何載入相關的實體,在前面的例子中,我是從服務器中載入Products(產品)的,每個產品與供應商都有一個關系。使用前面的LINQ查詢,我們只檢索了產品,如果我還想顯示供應商和分類信息,我們可以按需載入相關信息,也可以在原始查詢中明確地從服務器去檢索,這兩種技術各有各的優勢,但如果你清楚地知道需要顯示哪些信息,明確地載入可能更有效,如果你只想為一些實體載入數據,使用按需檢索可能會更好。
默認情況下,如果你沒有明確地載入屬性,關系屬性(如產品供應商)就是空的,為便于按需載入,DataServiceContext類有一個BeginLoadProperty方法(遵循相同的異步模式)可以指定源實體,屬性名和callback。
|
調用EndLoadProperty后,屬性和相關的實體就被正確地載入,在許多情況下,你可能想在原始查詢中明確地載入它們,因為如此,LINQ提供者支持Expand擴展方法,這個方法允許你指定屬性的名稱路徑便于查詢執行時載入它們,Expand方法在LINQ查詢的from子句中使用,它告訴提供者視圖載入這些相關實體,例如,如果你使用Expand方法改變了Category 和 Supplier原始查詢,在原始查詢執行期間,我們的對象將會載入這些相關實體:
|
如果你使用ADO.NET Data Service讀取數據,知道如何創建一個查詢,運行它,載入你想要的相關實體。如果你需要真實地修改數據,只需要將你的新數據綁定到你的Silverlight控制器即可。
#p#
變化管理
ADO.NET Data Service客戶端庫不支持對象的自動變更監視,這意味著當對象,集合和關系發生變化時,需要開發人員告訴DataServiceContext這些變化,通知DataServiceContext對象的API相當簡單,如例4所示:
例4 DataServiceContext變更API
方法 | 描述 |
AddObject | 添加一個新創建的對象 |
UpdateObject | 標記一個已經變化的對象 |
DeleteObject | 標記一個刪除的對象 |
AddLink | 在兩個對象之間添加一個鏈接 |
UpdateLink | 更新兩個對象之間的鏈接 |
DeleteLink | 刪除兩個對象之間的鏈接 |
這意味著你要監視對象的變化,并在你自己的代碼中通知DataServiceContext對象,表面上看起來這樣讓人很失望,因為沒有實現自動化的變化管理,但這樣可以讓庫變得更有效也更mini。
你可能會對如何監視對象的變化感到奇怪,答案就是生成的代碼中,在每個生成的data contract類中,當類中的數據變化時partial方法被調用,如果這些方法從來沒有使用過,它們本身不會造成任何資源消耗,你可以在任何支持變化通知的data contracts上使用partial方法機制,只需要在partial方法中調用DataServiceContract即可,不用連接DataServiceContract整個類。
幸運的是,Silverlight已經有一個接口支持變化通知了(INotifyPropertyChange),通過這個接口可以在你的實現中將任何變化通知給感興趣的人,例如你可以在你的data contract類(在我們的例子中是Product類)中調用InotifyPropertyChange定義一個事件,當數據發生變化時可以激活它,下面就是具體的示例:
public partial class Product : INotifyPropertyChanged |
這樣當任何屬性發生變化時都可以觸發一個事件,你可以通過partial方法決定什么時候觸發這個事件,例如,當ProductName發生變化時要通知預定人,只需要調用OnProductNameChanged方法,然后觸發PropertyChanged事件,傳遞ProductName通知變化的屬性給事件預定人,下面是代碼:
|
通過在這些可寫的屬性上調用這些partial方法,監視你對象的變化就很簡單了,當對象發生變化時,你可以注冊PropertyChanged事件然后通知DataServiceContext對象:
|
最后你可以調用product_PropertyChanged方法通知DataServiceContext對象:
void product_PropertyChanged(object sender, PropertyChangedEventArgs e) |
同樣,在創建對象或刪除對象時也需要通知DataServiceContext,如:
void addNewButton_Click(object sender, RoutedEventArgs e) { Product theProduct = new Product(); // ... TheContext.AddObject(theProduct); } void deleteButton_Click(object sender, RoutedEventArgs e) { Product theProduct = (Product)theList.SelectItem; TheContext.DeleteObject(theProduct); theCollection.Remove(theProduct); } |
在這些代碼中,你可以在你的Silverlight UI中修改這些對象,讓數據綁定和變化通知代碼確保讓DataServiceContext知道所有變化都會引發什么后果,但你如何對這些服務執行真實的更新呢?
通過服務更新
現在你的DataServiceContext對象已經知道數據的變化,但還需要一個方法通知給服務器,為了解決這個問題,DataServiceContext類提供了一個BeginSaveChanges方法,它和本文前面描述的查詢都使用了相同的異步方法,BeginSaveChanges方法將所有變化都吸收進DataServiceContext,并將它們發送給服務器:
|
調用BeginSaveChanges時,有一個標志枚舉調用SaveChangesOptions,這個枚舉允許你指定兩個選項:是否使用批處理,是否繼續,即使某些變化保存失敗。通常,我建議使用批處理,實際上,在某些父/子關系類型上批處理是必須的,因為父子之間可能使用了引用完整性約束,這樣更新才能保證父子之間的一致性。
保存完畢時,將會執行callback,有兩個機制可以傳播錯誤消息給你,首先,如果在執行保存時出現了異常,當你在調用EndSaveChanges時,會拋出異常,因為如此,你可能想要使用try/catch來捕獲災難性的錯誤;另外,EndSaveChanges返回的類型是一個DataServiceResponse對象,DataServiceResponse有一個HasErrors屬性,但在Silverlight 2 Beta 2版本庫中它還不夠安全:
void OnSaveAllComplete(IAsyncResult result) { bool succeeded = true; try { DataServiceResponse response = (DataServiceResponse)TheContext.EndSaveChanges(result); foreach (OperationResponse opResponse in response) { if (opResponse.HasErrors) { succeeded = false; } } } catch (Exception ex) { succeeded = false; } // Alert the User }
你可以重復使用OperationResponse對象來查看是否出現了錯誤,DataServiceResponse是OperationResponse對象的一個集合,在以后的版本中,你應該可以依賴于DataServiceResponse類自身的HasErrors屬性了。
服務調式
在調試服務時,你要執行三個重要的任務:查看DataServiceContext對象中數據的狀態,查看ADO.NET Data Services產生的請求,以及捕獲服務器錯誤。
首先我們處理DataServiceContext對象中的實體狀態,DataServiceContext類暴露了兩個有用的集合:Entities和Links,這些集合是只讀的,由DataServiceContext進行跟蹤,在調式時,不管你是將對象標記為已變化還是未變化,在調試器中查看這些集合是非常有用的,可以幫助你確定跟蹤思路是不是正確的。
注意對你而言,查看你的Silverlight 2程序對服務器的真實請求也是很重要的,最好的方法是使用網絡代理,我個人使用的是Fiddler2,如果你對Fiddler2不熟悉,也可以使用Web traffic之類的工具來捕獲數據包,查看真正發生了什么。
對于ADO.NET Data Service而言,你可能想查看你在線上傳來傳去的都是什么,即Silverlight程序發出的數據和接收到的數據,可以去我的博客(http://wildermuth.com/2008/06/07/Debugging_ADO_NET_Data_Services_with_Fiddler)轉轉。
最后,.NET Framework 3.5 SP1不會將服務端錯誤傳遞給客戶端了,實際上,服務器上的大部分錯誤都是服務器吞下去的,調試服務端錯誤的最好辦法是在調試菜單(Debug->Exceptions…)中使用Exception選項,配置調試器停止一切.NET異常,如果你選擇了這個選項,你可以通過服務看到拋出的異常。
我在本文的目標是展示ADO.NET Data Service是如何在Silverlight 2和基于服務的模塊之間建立起連接的,現在你應該已經知道如何使用ADO.NET Data Service從服務器讀取數據和往服務器寫數據了,再也不用自己動手設計Web Service了,正如你所看到的,Silverlight、ADO.NET Data Service和LINQ三者的組合讓你可以創建強大的基于數據驅動的Web應用程序,具有Web 2.0技術的所有有點。
【編輯推薦】