【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技术的所有有点。
【编辑推荐】