WEB开发网
开发学院图形图像Flash 使用 Silverlight 2 创建以数据为中心的 Web 应用... 阅读

使用 Silverlight 2 创建以数据为中心的 Web 应用程序

 2009-02-09 11:57:22 来源:WEB开发网   
核心提示:本文示例源代码或素材下载 本文基于 Silverlight 2 和 ADO.NET 数据服务的预发布版撰写而成,文中的所有信息均有可能发生变更,使用 Silverlight 2 创建以数据为中心的 Web 应用程序,本文将介绍以下内容: 将 Silverlight 连接到数据源 使用 ADO.NET 数据服务

本文示例源代码或素材下载

本文基于 Silverlight 2 和 ADO.NET 数据服务的预发布版撰写而成。文中的所有信息均有可能发生变更。

本文将介绍以下内容:

将 Silverlight 连接到数据源

使用 ADO.NET 数据服务

使用 LINQ to SQL

调试服务

本文使用了以下技术:

Silverlight 2 Beta 2、LINQ、ADO.NET 数据服务

使用 Silverlight 2 创建以数据为中心的 Web 应用程序目录

ADO.NET 数据服务

创建服务

查询和更新数据

Silverlight 2.0 客户端库

加载相关实体

变更管理

更新服务

调试服务

总结

业务线和其他以数据为中心的应用程序可以在 Silverlight™ 中加以构建,但在 Silverlight 中使用数据并不总是那样简单。虽然 Silverlight 中包含许多使用数据以及支持 Web 服务和 XML 时所需的工具,但这些工具仅代表了跨防火墙的数据访问基础。

常见的数据访问策略会混合使用 Web 服务和客户端 LINQ,如果您打算利用现有的 Web 服务端点来启动 Silverlight 应用程序,则强烈建议您使用此方法。但是,如果您打算构建新的 Web 服务来专门与 Silverlight 配合使用,则会牵涉不必要的成本。

对于典型的 Web 服务层,您可以在服务器上实现传统的数据访问策略(自定义业务对象、LINQ to SQL、实体框架、Nhibernate 等)并通过 Web 服务公开数据对象。此时 Web 服务仅仅是到底层数据访问策略的通道。

但是为了启用完全数据连接,您必须将四种数据操作(创建、读取、更新和删除)映射到 Web 服务方法。例如,一个支持 Product 类的简单服务约定可能如图 1 所示(请注意,我在整篇文章中使用的都是 C#,但代码下载也包括 Visual Basic® 代码)。

使用 Silverlight 2 创建以数据为中心的 Web 应用程序图 1 简单 Product Web 服务的服务约定

[ServiceContract]
public interface ICustomerService
{
 [OperationContract]
 List<Product> GetAllProducts();
 [OperationContract]
 Product GetProduct(int productID);
 [OperationContract]
 List<Product> GetAllProductsWithCategories();
 [OperationContract]
 Product SaveProduct(Product productToSave);
 [OperationContract]
 void DeleteProduct(Product productToDelete);
}

创建一组与应用程序的整个数据模型配合使用的服务可能会相当耗时。而且,正如本例所示,特定于功能的操作可能会导致 Web 服务变得臃肿。换句话说,随着时间的推移,Web 服务将增加新的要求和操作(包括实际上并不属于核心业务领域的操作)。

例如在图 1 中,您会看到 GetAllProducts­WithCategories 操作,默认情况下它被用来检索 Product 并包括该类别。在这样一个简单的示例中添加排序、筛选甚至分页机制也不足为奇。如果有一种简单的方法既可以支持数据操作(如查询、排序、筛选)又不必不断地手动构建所有这些机制该有多好。ADO.NET 数据服务的出现解决了这一问题。

ADO.NET 数据服务

ADO.NET 数据服务的目标是为数据模型提供允许 Web 访问的端点。这些端点提供了对服务器中的数据进行筛选、排序、整理和分页的方法,无需开发人员再针对这些操作自定义构建功能。实质上,每个端点都是 LINQ 查询的起点。通过该端点,您可以查询您要实际搜索的数据。

但请不要认为 ADO.NET 数据服务只是另一种数据访问策略。ADO.NET 数据服务并不直接执行任何数据访问。实际上,它位于支持序列化、查询和更新等功能的数据访问技术的顶层。查询和更新数据的工作由位于底层的数据访问层来执行。图 2 显示了典型的应用程序体系结构中的 ADO.NET 数据服务及其位置。

使用 Silverlight 2 创建以数据为中心的 Web 应用程序

图 2 ADO.NET 数据服务层

由于 ADO.NET 数据服务依赖于数据访问工具来执行实际的数据访问工作,因此必须要有一种方法能够指定此工作的外观。在 ADO.NET 数据服务中,每项服务都必须有启用了 LINQ 的提供程序的支持。实际上,每个端点其实只是一个 IQueryable 端点。因此,ADO.NET 数据服务可以支持任何支持 Iqueryable 的对象。

创建服务

当您将 ADO.NET 数据服务添加到项目中时,将会创建一个新的 .svc 文件作为代表该服务的类。与 Web 服务不同,您实际上并不是亲自实施服务操作,而是让 DataService 类来处理最繁重的工作。要运行此服务,必须要先完成几项小任务。首先,DataService 类需要一个被称为上下文对象的类参数。此上下文对象是一个类,用来描述要作为服务公开的数据。如果您的服务公开了关系数据库中的数据,则此类通常会从 Entity­Framework 的 ObjectContext 或 LINQ to SQL 的 DataContext 派生:

// Use my NorthwindEntities context object
// as the source of the Service's Data
public class Products : DataService<NorthwindEntities>

此上下文对象没有基类要求。实际上,只要类属性实现 IQueryable 接口,您就可以创建自己的类;ADO.NET 数据服务将以端点形式公开这些属性:

public class StateContext
{
 StateList _states = new StateList();
 public IQueryable<State> States
 {
  get { return _states.AsQueryable<State>(); }
 }
}

在 InitializeService 调用中,您将处理 IDataServiceConfiguration 对象,它可以用来指定服务中允许的权限种类。ADO.NET 数据服务使用名词和动词的抽象来指定权限,如图 3 所示。

使用 Silverlight 2 创建以数据为中心的 Web 应用程序图 3 设置访问规则

// This method is called only once to initialize service-wide policies.
public static void InitializeService(IDataServiceConfiguration config)
{
 // Only allow us to read or update Products Entities
 // not Delete or Create
 config.SetEntitySetAccessRule("Products",
                EntitySetRights.AllRead |
                EntitySetRights.WriteUpdate);
 // Only Allow Reading of Category and Supplier Entities
 config.SetEntitySetAccessRule("Categories", EntitySetRights.AllRead);
 config.SetEntitySetAccessRule("Suppliers", EntitySetRights.AllRead);
}

完成此操作后,您可以直接浏览到该服务,它将显示有关各端点的 Atom 源信息。若要调试 ADO.NET 数据服务,我建议您在 Internet Explorer® 中禁用 RSS 源视图,或使用其他浏览器来查看 XML 格式的服务。

查询和更新数据

ADO.NET 数据服务将服务公开为基于具像状态传输 (REST) 的服务,它并不是基于 SOAP 的服务。这意味着来自服务的响应负载(而非 SOAP 信封)仅包含数据,而不是请求的元数据。所有请求都是使用 HTTP 动词(GET、PUT、POST 等)和请求 URI 的组合进行描述的。假设您有一个描述 Products、Categories 和 Suppliers 的模型,如图 4 所示。在所生成的服务(即 ADO.NET 数据服务)中可能有三个端点,每个都对应模型中的一个实体集。用于寻址模型中某个实体集的 URI 只是该服务的地址和端点的名称:http://localhost/{服务名称}/{端点名称} 或 http://localhost/Product.svc/Products。

使用 Silverlight 2 创建以数据为中心的 Web 应用程序

图 4 数据模型示例

此 URI 语法支持许多不同的功能,包括检索特定实体、排序、筛选、分页和整理结果等。

ADO.NET 数据服务使用这些 URL 样式的查询将数据返回到服务的使用者。目前支持以下两种序列化格式(但未来版本很可能会加以扩展):JavaScript Object Notation (JSON) 和基于 Atom 的 XML。JSON 是一种方便客户端 Web 代码使用的格式,而 Atom 是一种以 XML 为基础的格式,它在随后需要 XML 分析器的帮助。

ADO.NET 数据服务使用标准的 HTTP accept 标头来确定使用哪种格式返回给客户端,而不要求在查询中指定序列化格式。如果您从可以使用 XML 的客户端(如浏览器)发出请求,但却未通过 Accept 标头指定首选的格式类型,则 Atom 将作为返回数据的默认格式。

查询数据只是此解决方案的一部分。最终,解决方案必须要支持查询和更新。为支持所有这些要求,ADO.NET 数据服务将四种基本的数据访问操作映射到四个基本的 HTTP 动词(请参见图 5)。

使用 Silverlight 2 创建以数据为中心的 Web 应用程序图 5 数据访问动词与 HTTP 动词

数据访问动词HTTP 动词
CreatePOST
ReadGET
UpdatePUT
DeleteDELETE

通过使用这些动词,ADO.NET 数据服务可以让服务的使用者充分利用所有类型的数据操作,而不必针对不同的类型创建特殊的端点。使数据更新在 ADO.NET 数据服务内正常工作的唯一要求是底层数据访问技术要支持 IUpdatable 接口。此接口是定义如何将更新从 ADO.NET 数据服务传播到数据源的约定。

目前,实体框架是唯一支持此接口的数据访问技术。在今后,我预计大多数层(包括 LINQ to SQL、LLBGenPro 和 Nhibernate)也都会引入对它的支持。现在您已经掌握了 ADO.NET 数据服务的知识,可以开始在 Silverlight 2 中使用它了。

Silverlight 2.0 客户端库

如果要使用 ADO.NET 数据服务发出查询、通过 URI 语法更新数据以及直接操作 XML,您虽然可以获得所需的功能,但却需要大量构建工作。为此引入了 ADO.NET 数据服务 Silverlight 客户端库。此库(与命令行工具结合使用)允许您直接在 Silverlight 应用程序中发出 LINQ 查询(由客户端库转换为 HTTP 查询)或者更新对数据服务的请求。

在开始之前,首先您需要生成一些代码。此代码生成将读取 ADO.NET 数据服务的元数据,并为服务的实体生成仅限数据的类以及一个代表服务本身的类。

要生成此代码,您需要使用位于 Microsoft® .NET Framework 3.5 文件夹(通常为 c:­windowsMicrosoft.NETFrameworkv3.5)下的 DataSvcUtil.exe tool 工具。您可以运行此工具并指定服务的 URI、输出代码文件以及要用于构建代码的语言,如下所示:

DataSvcUtil.exe –uri:http://localhost/Product.svc –out:data.cs 
 –lang:CSharp

这将构建一个新文件,其中包含各端点的数据约定类和 DataServiceContext 派生类。DataService­Context 类将被用作服务的入口点(并公开可查询的服务和端点)。如果将此类包括在您的 Silverlight 项目中并添加对 System.Data.Services.Client.dll(Silverlight 2 Beta 2 SDK 的一部分)的引用,您即拥有了使用 ADO.NET 数据服务所需的全部代码。

Silverlight 客户端代码与您可能已在以 .NET 为目标的代码中编写的其他基于 LINQ 的查询类似。您将创建 Data­ServiceContext 派生类并针对其发出 LINQ 查询。它类似于下面所示:

// Create the Service class specifying the
// location of the ADO.NET Data Services
NorthwindEntities ctx =
 new NorthwindEntities(new Uri("Products.svc", UriKind.Relative));
// Create a LINQ Query to be issued to the service
var qry = from p in ctx.Products
        orderby p.ProductName
        select p;

执行此查询时,它将发出一个 Web 请求以获取所需的数据。但是,此处的 Silverlight 代码与标准 LINQ 查询明显不同,因为在 LINQ 查询中 Silverlight 不允许同步 Web 请求。因此,要执行异步操作,必须先将查询转换为 DataServiceQuery<T> 对象,然后再显式调用 Begin­Execute 来启动异步执行,如下所示:

// Cast to a DataServiceQuery<Product>
// (since the query is returning Products)
DataServiceQuery<Product> productQuery =
 (DataServiceQuery<Product>)qry;
 // Execute the Query Asynchronously specifying
 // a callback method
 productQuery.BeginExecute(new
  AsyncCallback(OnLoadComplete),
  productQuery);

查询执行完毕后(无论操作成功与否),都将执行在 AsyncCallback 中指定的方法。此时您可以枚举 LINQ 查询以处理实际结果。通常,您可以将原始查询包括在 AsyncCallback 中,这样您就可以在回调方法中检索它(或者将其作为类的一部分加以保存),如图 6 所示。

使用 Silverlight 2 创建以数据为中心的 Web 应用程序图 6 将结果添加到集合中

void OnLoadComplete(IAsyncResult result)
{
 // Get a reference to the Query
 DataServiceQuery<Product> productQuery =
  (DataServiceQuery<Product>)result.AsyncState;
 try
 {
  // Get the results and add them to the collection
  List<Product> products = productQuery.EndExecute(result).ToList();
 }
 catch (Exception ex)
 {
  if (HtmlPage.IsEnabled)
  {
   HtmlPage.Window.Alert("Failed to retrieve data: " + ex.ToString());
  }
 }
}

如果您以前从未处理过 LINQ,则此模式对您而言将非常不合适。目前,除了在异步数据包(如 ThreadPool 和 BackgroundWorker)中执行 LINQ 以外,没有任何比较适合异步 LINQ 的模式。Silverlight 要求所有请求都是异步的,因此在使用 ADO.NET 数据服务客户端库时需要使用此模式。

加载相关实体

ADO.NET 数据服务还允许您选择要如何加载相关实体。在前面给出的示例中,我都是从服务器来加载产品的。每种产品都与该产品的供应商以及该产品的类别存在某种关系。

使用前面的 LINQ 查询,我只能检索到产品。如果我想显示供应商或类别信息,将没有办法立即访问到它们。我不得不在需要的时候加载此信息,或者在对服务器执行原始查询时显式检索。两种方法都有各自的好处,但一般来说,如果您确定将会用到每个对象的信息,则显式加载会更有效。如果预计仅加载几个实体的数据,则最好使用按需检索。

默认情况下,当存在关系属性(如 Product.Supplier)时,该属性为 null(如果未显式加载该属性)。为了方便按需加载,DataService­Context 类有一个 BeginLoadProperty 方法(遵循同一个异步模型),您可以在其中指定源实体、属性名称以及回调:

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

调用了 EndLoadProperty 后,该属性便与相关实体一同被完全加载。在许多情形下,您都需要在原始查询中显式加载它们。为帮助实现这一点,LINQ 提供程序提供了对 Expand 扩展方法的支持。此方法允许您指定要在执行查询时加载的属性路径的名称。在 LINQ 查询的 From 子句中,Expand 扩展方法被用来告知提供程序应尝试加载这些相关实体。例如,如果将原始查询更改为对 Category 和 Supplier 使用 Expand 方法,则我们的对象会在原始查询执行期间加载这些相关的实体:

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

如果您只是从 ADO.NET 数据服务读取数据,则您已经完成了任务。了解如何创建查询、运行查询以及加载相关实体是您需要执行的全部操作。如果您需要实际修改数据,还会涉及一些更多工作。将您的新数据绑定到 Silverlight 控件就算万事大吉!

变更管理

ADO.NET 数据服务客户端库不支持对象的自动变更监视。这意味着当对象、集合和关系发生变化时,必须要由开发人员告诉 Data­ServiceContext 对象这些变更。用于通知 DataServiceContext 对象的 API 相当简单,如图 7 所示。

使用 Silverlight 2 创建以数据为中心的 Web 应用程序图 7 DataServiceContext 变更 API

方法说明
AddObject添加新创建的对象。
UpdateObject将对象标记为已变更。
DeleteObject标记要删除的对象。
AddLink在两个对象之间添加链接。
UpdateLink更新两个对象之间的链接。
DeleteLink删除两个对象之间的链接。

这意味着您将需要监视对象的变更并使用您自己的代码来通知 DataServiceContext 对象。从表面上看,没有自动变更管理功能似乎有些不尽如人意,但这可以使库更加有效、更加小巧。

您可能想知道如何监视对象的变更。答案就在生成的代码中。在每个生成的数据约定类中,随着类中数据的变更,都需要调用一些局部方法。(有关局部方法的详细信息,请参阅 go.microsoft.com/fwlink/?LinkId=122979。)如果从未实现过这些方法,那么类不会因为此变更通知而产生任何额外开销。您可以对任何支持变更以实现变更通知的数据约定使用局部方法机制。只需在局部方法中调用 DataService­Contract 即可;不需要耦合 DataServiceContract 类。

幸运的是,Silverlight 已经支持用于变更通知的接口 (INotifyPropertyChange)。在您的实现中,利用此接口可以通知对您的数据变更感兴趣的任何人。例如,您可以在数据约定类(在我们的示例中为 Product 类)中实现 INotify­PropertyChange,以定义可以随对象的变更而触发的事件。如下所示:

public partial class Product : INotifyPropertyChanged
{
 public event PropertyChangedEventHandler PropertyChanged;
}

实现这一点您便可以在任何属性变更时引发某个事件。通过实现生成的数据约定类所调用的局部方法,您可以确定何时引发此事件。例如,要在 ProductName 属性变更时通知订阅者,您只需实现 OnProductNameChanged 局部方法并触发 PropertyChanged 事件,然后传入 ProductName 以向事件订阅者标识发生变更的属性。以下是相关代码:

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

通过对所有可写属性实现这些局部方法,您可以用一种简单的方法来监视对象的变更。然后,您只需注册 PropertyChanged 事件并在对象发生变更时通知 DataServiceContext 对象即可:

// In the OnLoadComplete method
// Get the results and add them to the collection
List<Product> products = productQuery.EndExecute(result).ToList();
foreach (Product product in products)
{
 // Wireup Change Notification
 product.PropertyChanged +=
  new PropertyChangedEventHandler(product_PropertyChanged);
}

最后,您可以实现 product_PropertyChanged 方法以通知 DataServiceContext 对象:

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

同样,创建或删除对象时,您也需要通知 DataServiceContext,如图 8 所示。

使用 Silverlight 2 创建以数据为中心的 Web 应用程序图 8 创建和删除通知

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 的已标记枚举。此枚举允许您指定两个选项:是否使用批处理以及是否在一些对象无法保存的情况下继续。通常,我始终建议使用批处理。实际上,如果服务器拥有特定的引用完整性约束,则需要进行批处理以使某些类型的父/子关系能够正确更新。

保存完毕后,将执行回调。有两种机制可为您传播错误信息。首先,如果在执行保存期间有异常抛出,那么当您在回调中调用 "EndSaveChanges"­ 时也会抛出此异常。有鉴于此,您需要使用 try/catch 块来捕获灾难性错误。此外,EndSaveChanges 的返回类型是 DataServiceResponse 对象。DataService­Response 拥有一个 HasErrors 属性(请参见图 9),但它在 Silverlight 2 Beta 2 版本的库中是不可靠的。

使用 Silverlight 2 创建以数据为中心的 Web 应用程序图 9 BeginSaveChanges 回调

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 属性。

调试服务

在调试服务时,有三个重要的任务需要执行:查看 DataService­Context 对象中的数据状态、查看由 ADO.NET 数据服务发出的请求,最后是捕获服务器错误。

首先,让我们来处理 DataService­Context 对象中的实体的状态。DataServiceContext 类提供了两个有用的集合:实体和链接。这些集合是被 DataServiceContext 跟踪的实体之间的实体和链接的只读集合。在调试时,无论您是否将对象标记为已变更,在调试器中查看这些集合都非常有助于确定您的变更跟踪代码是否工作正常。

请注意,查看由 Silverlight 2 应用程序发出的实际服务器请求也至关重要。最佳的做法是使用某种类型的网络代理。我使用 Fiddler2 (fiddler2.com) 来实现此目的。可能您对 Fiddler2 不是很熟悉,它是一款观察 Web 通讯流量的基本工具,您通过它可以了解到线路上究竟发生了什么。

对于 ADO.NET 数据服务,您需要查看通过线路的实际通信情况以了解进出 Silverlight 应用程序的具体内容。有关详细信息,请参阅我的博客 (wildermuth.com/2008/06/07/Debugging_ADO_NET_Data_Services_with_Fiddler2)。

最后,在最新的 .NET Framework 3.5 SP1 中,服务器端错误并没有传播到客户端。实际上,服务器上的大多数错误都被服务器所抑制。调试服务器错误的最佳策略是使用“调试”菜单中的“异常”选项(“调试”->“异常”…),并将调试器配置为遇到所有 .NET 异常时都停止。选择了此选项后,您即可看到由服务抛出的异常(但您可能需要单击“继续”才能转到其他的首现异常)。

总结

本文的目标是介绍 ADO.NET 数据服务如何在 Silverlight 2 和基于服务器的模型之间建立联系。您现在应了解如何使用 ADO.NET 数据服务来从服务器读写数据而不必依赖手动设计的 Web 服务。如您所见,Silverlight、ADO.NET 数据服务和 LINQ 的组合允许您充分发挥 Web 2.0 技术的所有优点来构建强大的数据驱动的 Web 应用程序。

Tags:使用 Silverlight 创建

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接