使用LINQ和ADO.NET创建Silverlight程序

原创
开发 后端
以数据为中心的Web应用程序变得越来越重要,使用Silverlight 2、LINQ和ADO.NET很容易创建富的、以数据为驱动的Web应用程序,本文向你展示创建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 GetAllProducts();
  [OperationContract]
  Product GetProduct(int productID);
  [OperationContract]
  List GetAllProductsWithCategories();
  [OperationContract]
  Product SaveProduct(Product productToSave);
  [OperationContract]
  void DeleteProduct(Product productToDelete);
}

创建一套服务来处理应用程序完整的数据模型可能是相当费时的,正如这个例子中显示的,特殊特性的操作可能导致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 States 
  {
    get { return _states.AsQueryable(); } 
  }
}

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对象,然后再调用BeginExecute启动异步执行:

// 创建一个DataServiceQuery,因为查询返回的是Products
DataServiceQuery productQuery =
  (DataServiceQuery)qry;
//指定一个callback函数执行异步查询
  productQuery.BeginExecute(new 
    AsyncCallback(OnLoadComplete),
    productQuery);

当这些查询执行完后,无论操作是否成功,在AsyncCallback中指定的方法都会执行,通常你会在AsyncCallback中包含原始查询,因此可以在callback方法中检索它,你也可以将其保存为类的一部分,正如你在例3中看到的:

例3 将结果添加到集合中

void OnLoadComplete(IAsyncResult result)
{
  //为查询获取一个引用
  DataServiceQuery productQuery =
    (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。

public void LoadSupplierAsync(Product theProduct)
{
  TheContext.BeginLoadProperty(theProduct,
                               "Supplier",
                               new AsyncCallback(SupplierLoadComplete),
                               null);
  }

  public void SupplierLoadComplete(IAsyncResult result)
  {
    TheContext.EndLoadProperty(result);
  }

调用EndLoadProperty后,属性和相关的实体就被正确地载入,在许多情况下,你可能想在原始查询中明确地载入它们,因为如此,LINQ提供者支持Expand扩展方法,这个方法允许你指定属性的名称路径便于查询执行时载入它们,Expand方法在LINQ查询的from子句中使用,它告诉提供者视图载入这些相关实体,例如,如果你使用Expand方法改变了Category 和 Supplier原始查询,在原始查询执行期间,我们的对象将会载入这些相关实体:

var qry =
  from p in TheContext.Products.Expand("Supplier").Expand("Category")
          orderby p.ProductName
          select p;

如果你使用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
{
  public event PropertyChangedEventHandler PropertyChanged;
}

这样当任何属性发生变化时都可以触发一个事件,你可以通过partial方法决定什么时候触发这个事件,例如,当ProductName发生变化时要通知预定人,只需要调用OnProductNameChanged方法,然后触发PropertyChanged事件,传递ProductName通知变化的属性给事件预定人,下面是代码:

partial void OnProductNameChanged()
{
  if (PropertyChanged != null)
  {
    PropertyChanged(this, new PropertyChangedEventArgs("ProductName"));
  } 
}

通过在这些可写的属性上调用这些partial方法,监视你对象的变化就很简单了,当对象发生变化时,你可以注册PropertyChanged事件然后通知DataServiceContext对象:

//在OnLoadComplete方法中,获取结果然后将它们添加到集合中
List products = productQuery.EndExecute(result).ToList();
foreach (Product product in products)
{
  //触发变化通知
  product.PropertyChanged +=
    new PropertyChangedEventHandler(product_PropertyChanged);
}

 

最后你可以调用product_PropertyChanged方法通知DataServiceContext对象:

void product_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
  Product product = (Product)sender;
  TheContext.UpdateObject(product);
}

同样,在创建对象或删除对象时也需要通知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,并将它们发送给服务器:

TheContext.BeginSaveChanges(SaveChangesOptions.None,

new AsyncCallback(OnSaveAllComplete),

null);

调用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技术的所有有点。

 

【编辑推荐】

  1. LINQ to SQL实现数据访问通用基类
  2. SQL连接中加条件查询的LINQ表达式写法
  3. 图解Silverlight 3的7个新功能
责任编辑:彭凡 来源: 51CTO
相关推荐

2009-09-14 13:37:25

LINQ ADO.NE

2009-08-13 11:22:59

LINQ和ADO.NE

2009-12-29 16:12:25

ADO程序员

2009-12-28 15:11:36

ADO.NET专家

2009-12-25 10:25:59

2009-12-29 14:09:17

ADO.NET通用接口

2009-12-21 17:35:24

ADO.NET对象

2009-12-23 17:30:54

ADO.NET应用程序

2009-12-24 14:06:22

ADO.NET 应用程

2009-12-25 17:05:32

ADO.NET数据库

2009-12-24 15:18:42

创建ADO.NET

2009-11-12 13:26:56

使用ADO.NET参数

2009-11-13 09:45:54

ADO.NET程序集

2009-12-22 15:03:51

ADO.NET使用

2011-05-20 17:05:59

ADO.NET

2009-12-21 16:45:41

ADO.NET程序

2009-12-21 17:23:56

ADO.Net程序

2009-12-24 14:45:32

ADO.NET程序

2009-11-13 14:22:11

ADO.NET Dat

2009-11-13 10:10:07

点赞
收藏

51CTO技术栈公众号