65.9K
CodeProject 正在变化。 阅读更多。
Home

ADO.NET数据服务/RIA服务简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (47投票s)

2010年2月23日

CPOL

26分钟阅读

viewsIcon

189723

downloadIcon

2503

ADO.NET数据服务/RIA服务简介。

目录

引言

距离我上次写文章已经有一段时间了,我只能为此道歉;你看,我一直在搬家,新家甚至还没有互联网(事实上,我甚至还没有拆包),所以我在火车上用我那台值得信赖的笔记本电脑,这里做一点那里做一点,进展缓慢。

总之,长话短说,这是一篇新文章。这篇文章并不复杂,也不会以任何形式提供任何可重用的代码,但它希望能为您提供一个温和的入门,了解如何开始使用微软的两项较新的数据访问服务

  • WCF数据服务,以前称为ADO.NET数据服务,再以前称为Astoria。
  • RIA数据服务(新来者)。

我对这两种技术并不专业;正如许多人所知,我的主要领域是WPF/LINQ/WCF/多线程,但我自从我写了这篇文章以来就没有怎么接触过Silverlight,所以我只是在探索Silverlight现在能做什么,并且选择研究数据访问方面,并写一些我在这个过程中发现的东西。

正如我所说,这篇不是为那些可能已经使用这两种数据访问领域一段时间的人准备的高级文章;相反,它是一个关于这两种不同数据访问技术可以做什么以及如何使用它们执行一些简单的CRUD操作的概述。

我还应该指出,尽管附带的演示代码使用了Silverlight,但为了简洁起见,我选择避免使用MVVM模式来使阅读体验复杂化。尽管我喜欢这种模式,但这并不是本文试图传达的重点,所以本文的演示代码完全是代码隐藏或XAML。请原谅我。

必备组件

我使用Visual Studio 2010 BETA 2 / Silverlight 4 / RIA Services Preview编写了附带的演示代码,因此以下所有内容都被视为先决条件

由于ADO.NET数据服务代码和RIA服务代码都使用了SQL数据,因此SQL Server也是一个先决条件。

数据库设置

事实上,当我们谈论SQL时,让我解释一下您需要如何设置SQL Server数据库才能运行演示应用程序。

如果您打开附带的解决方案,您将看到一个解决方案文件夹,其中包含几个允许您创建演示数据库的设置脚本

您需要在自己的SQL Server安装上运行这些SQL脚本,运行这些SQL脚本的顺序如下

  1. CreateDatabase.sql
  2. CreateTables.sql
  3. Add Some Dummy Data.sql

数据库连接

创建演示应用程序数据库后,您必须记住更改以下两个_Web.Config_中的连接字符串设置,以指向您的SQL Server安装。您可能需要更改连接字符串的DataSource/User ID和Password以匹配您的SQL Server安装。

  1. _RIADataServicesDemoApp.Web_项目,_Web.Config_,DemoEntities连接字符串部分
  2. _WCFDataServicesDemoApp.Web_项目,_Web.Config_,DemoEntities连接字符串部分

关于演示应用程序

附带的演示应用程序绝不是一个真实世界的示例,这可能不会让一些人满意,但我不想创建一个庞大而过于复杂的怪物应用程序,因为基本的CRUD原则可以通过一个非常简单的数据库设置来演示。为此,演示应用程序的数据库包含两个简单的表;事实上,演示代码实际上只使用了这两个表中的一个,但你知道吗?这足以演示ADO.NET数据服务和RIA数据服务的基本CRUD操作。

整个数据库看起来像这样

ADO.NET数据服务

这一切是关于什么

ADO.NET数据服务以前称为“Astoria”,简而言之,它们用于使用REST将数据公开为资源(JSON/ATOM)。因此,数据作为资源公开,可以使用标准HTTP URL获取。还可以使用REST删除、更新和插入新数据。RESTful资源通过WCF在幕后公开,尽管查看典型的ADO.NET数据服务,您可能不会这样认为。

由于WCF用作实际的服务实现,因此从数据库获取数据的所有核心逻辑都在应用程序服务器上运行,因此可以位于防火墙后面,这有助于维护我们可能都习惯的物理安全隔离的三层。

我认为这张图很好地解释了这一点,您可以想象数据库在一个盒子中,数据访问层(ADO.NET数据服务)在另一个物理盒子中,而客户端应用程序(例如Silverlight)可能在另一个不同的盒子中运行(基本上是客户端的PC)。

通过使用REST公开关系数据,我们能够使用标准HTTP请求并利用各种URL来访问资源。我们还可以使用标准HTTP动词(如PUT/POST)来更新资源,这最终可能转化为更新关系数据库中的一行关系数据(如果您最初确实使用关系数据库来公开数据,而您可能没有)。

但在我们深入探讨之前,我们需要了解如何创建ADO.NET数据服务。那么,我们来讨论一下,好吗?

步骤 1

是为数据定义一个实体模型。这很容易(对于演示应用程序来说是这样),只需创建一个新的实体模型,并按照向导完成,使用您自己的SQL Server数据库连接字符串,并指向您使用上面提供和讨论的设置脚本设置的WCFDataServicesDemoApp数据库。

第二步

一旦我们有了实体模型,我们就可以创建一个新的ADO.NET数据服务来将关系数据公开为资源。同样,这很容易;我们只需要添加一个新项,如下所示

完成此操作后,您的解决方案中将有一个新的_<YOUR_SERVICE_NAME>.svc_项,其中包含以下样板代码

using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Web;
using System.Web;

namespace WCFDataServicesDemoApp.Web
{
    public class WebDataService1 : 
        DataService< /* TODO: put your data source class name here */ >
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(DataServiceConfiguration config)
        {
            // TODO: set rules to indicate which entity sets and service 
            // operations are visible, updatable, etc.
            // Examples:
            // config.SetEntitySetAccessRule("MyEntityset", 
            //    EntitySetRights.AllRead);
            // config.SetServiceOperationAccessRule("MyServiceOperation", 
            //    ServiceOperationRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = 
                DataServiceProtocolVersion.V2;
        }
    }
}

信不信由你,你已经接近创建一个基本的ADO.NET服务了。您所要做的就是添加泛型参数,即您之前创建的实体模型的类型,并决定您要公开哪些实体集以及您希望它们拥有什么权限。因此,对于演示应用程序(这是一个极其微不足道的示例),它看起来像这样

using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Web;
using System.Web;

namespace WCFDataServicesDemoApp.Web
{
    public class WCFDataService : DataService<DemoAppEntities>
    {
        // This method is called only once to 
        // initialize service-wide policies.
        public static void InitializeService(
        DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", 
                   EntitySetRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = 
                   DataServiceProtocolVersion.V2;
        }


        // Define a change interceptor for the Address entity set.
        [ChangeInterceptor("Addresses")]
        public void OnChangeAddresses(Address address, 
            UpdateOperations operations)
        {
            if (operations == UpdateOperations.Add ||
               operations == UpdateOperations.Change)
            {

                if (String.IsNullOrEmpty(address.City) ||
                    String.IsNullOrEmpty(address.Line1) ||
                    String.IsNullOrEmpty(address.PostCode))
                {
                    throw new DataServiceException(400,
                       "Can not save Address with empty City/Line1/PostCode");
                }
            }
        }

    }
}

不必太担心`OnChangeAddresses()`方法,我稍后会讲到。有了这个,我们应该能够在浏览器中测试这个服务。我们可以使用许多URL来做到这一点;让我们看一些:_https://:1183/WCFDataService.svc/_会产生以下结果

<?xml version="1.0" encoding="utf-8" standalone="yes" ?> 
 <service xml:base="https://:1183/WCFDataService.svc/" 
   xmlns:atom="http://www.w3.org/2005/Atom" 
   xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">
 <workspace>
  <atom:title>Default</atom:title> 
 <collection href="Addresses">
  <atom:title>Addresses</atom:title> 
  </collection>
 <collection href="Customers">
  <atom:title>Customers</atom:title> 
  </collection>
  </workspace>
  </service>

我们可以看到ADO.NET服务运行正常。请注意其中的两个`collection`节点:我们有一个用于地址的,一个用于客户的。想知道当我们尝试这些URL中的一个时会发生什么吗?我们试一个好吗?

这个怎么样:_https://:1183/WCFDataService.svc/Addresses/_?看看我们如何获得所有地址的列表

很酷,是吧?但我们还能做什么?实际上,我们能做的事情更多;例如,尝试一些这些URL

  • _https://:1183/WCFDataService.svc/Addresses(6)/_:获取AddressId = 6的地址
  • _https://:1183/WCFDataService.svc/Addresses?$top=2_:获取前两个地址
  • _https://:1183/WCFDataService.svc/Addresses?$orderby=City%20asc_:获取按城市升序排列的所有地址

有关这些查询字符串的更多信息,我发现以下文章非常有用:http://msdn.microsoft.com/en-us/library/cc907912.aspx

这向您展示了从服务角度发生的事情,但是您作为客户端如何使用它呢?幸运的是,有一个很棒的Silverlight客户端API可以使用,这就是我们接下来要讨论的。

在此之前,需要注意的是,由于资源是使用JSON/ATOM公开的,因此非Microsoft客户端可以轻松使用它们。JSON显然意味着任何JavaScript都可以轻松使用ADO.NET数据服务,我认为这是其主要卖点之一。它是一种简单、不花哨的方式,可以轻松地使用WCF公开数据,而无需深入WCF的细节。如果您想看一篇真正的RESTful WCF文章,我很久以前在以下URL发布了一篇,您可能会想比较一下我当时为了使用RESTful WCF扩展进行CRUD操作而需要进行多少基础设施工作:https://codeproject.org.cn/KB/smart/GeoPlaces.aspx

创建数据

好了,现在是时候看看我们如何在客户端应用程序中实际使用一些REST公开的资源了。显然,由于我从事Microsoft技术,我将使用Silverlight。演示应用程序使用Silverlight 4.0。Silverlight如何利用ADO.NET数据服务公开的RESTful资源呢?正如我前面提到的,有一个非常好的客户端API,当您下载并安装ADO.NET服务安装程序时(我在文章开头提供了一个链接)就可以获得。客户端API是一个名为_Microsoft.Data.Services.Client.dll_的单个DLL,可以在以下位置找到(假设您使用默认设置下载并安装了ADO.NET服务安装程序):_C:\Program Files\ADO.NET Data Services V1.5 CTP1\sl_bin_。

总之,一旦您的Silverlight应用程序安装并引用了_Microsoft.Data.Services.Client.dll_,您就可以开始了。让我们开始查看一些数据,好吗?但在此之前,我想向您介绍一下演示应用程序的外观和工作原理,以便您知道如何操作它。我不得不说它并不漂亮,但它确实完成了任务。

屏幕分为四个区域,功能如下

  • 左上角:显示所有地址。
  • 左下角:显示所有城市以“B”开头的地址。
  • 右上角:允许用户更新/删除项目。用户必须从左上角地址列表中选择要更新/删除的项目。
  • 右下角:允许用户创建新地址。为此,用户必须点击“创建新项目”,然后填写数据,然后点击“插入”按钮。

在我向您展示如何开始使用涉及基本CRUD操作的编程方法之前,我应该提一下,我确实遇到了一个小的跨域访问问题,我通过在ADO.NET数据服务Web应用程序和RIA服务Web应用程序中都包含以下文件来解决这个问题。我不是自己编写的这些文件,而是在互联网上找到的,它们对我有用。

ClientAccessPolicy.xml
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <domain uri="*"/>
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>
CrossDomain.xml
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy 
   SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>

好的,现在您知道演示应用程序能做什么以及如何处理跨域问题了,那么我们开始吧?在我们的Silverlight应用程序中,我们首先需要做的就是创建一个指向我们的ADO.NET数据服务的指针。这可以通过以下方式完成

//a link to our WCF data service
DemoAppEntities proxy = new DemoAppEntities(
    new Uri("https://:1183/WCFDataService.svc", UriKind.Absolute));

从那里,我们可以像这样创建一些关系数据(请记住,这实际上将执行POST HTTP请求)

private void Insert_Click(object sender, RoutedEventArgs e)
{
    Insert(insertAddress);
    btnInsert.IsEnabled = false;
}

private void Insert(Address currentAddress)
{
    proxy.AddToAddresses(currentAddress);

    proxy.BeginSaveChanges(new AsyncCallback(r =>
    {
        try
        {
            proxy.EndSaveChanges(r);
            FetchAllAddresses();
            FetchAllBAddresses();
        }
        catch (Exception ex)
        {
            MessageBox.Show(String.Format("Error Inserting\r\n{1}",  
                ReadException(ex.InnerException.Message.ToString())));
        }
    }), null);
}

private string ReadException(String error)
{
    XDocument xdoc = XDocument.Parse(error);
    return ((XElement)xdoc.DescendantNodes().First()).Value;
}

正如您所看到的,客户端API允许我们通过使用我们之前创建的代理简单地添加一个新地址。当然,由于这是Silverlight,我们需要异步执行此操作。唯一需要注意的另一件事是,我们正在捕获一个异常,然后使用一些XLINQ对其进行解析。这是怎么回事?

还记得我在演示应用程序的实际ADO.NET服务定义中说过我会回过头来解释的那段代码吗

// Define a change interceptor for the Address entity set.
[ChangeInterceptor("Addresses")]
public void OnChangeAddresses(Address address, 
    UpdateOperations operations)
{
    if (operations == UpdateOperations.Add ||
       operations == UpdateOperations.Change)
    {

        if (String.IsNullOrEmpty(address.City) ||
            String.IsNullOrEmpty(address.Line1) ||
            String.IsNullOrEmpty(address.PostCode))
        {
            throw new DataServiceException(400,
               "Can not save Address with empty City/Line1/PostCode");
        }
    }
}

那么,这就是异常的来源。但是,如果没有XLINQ进行的少量解析,我们将得到一大块XML。XML(因为它很容易通过HTTP发送)。让我向您展示在解析以提取实际异常消息之前它将包含的完整内容

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <code></code>
  <message xml:lang="en-GB">Can not save Address with empty City/Line1/PostCode</message>
</error>

所以这就是XLINQ的全部意义,但是实际的ADO.NET服务实现中`ChangeInterceptorAttribute`的用法呢?好吧,以下是MSDN关于使用拦截器的一般说法

ADO.NET数据服务允许应用程序拦截请求消息,以便您可以向操作添加自定义逻辑。您可以使用此自定义逻辑来验证传入消息中的数据。您还可以使用它来进一步限制查询请求的范围,例如在每个请求的基础上插入自定义授权策略。

拦截是通过数据服务中具有特殊属性的方法执行的。这些方法由ADO.NET数据服务在消息处理的适当点调用。拦截器是按实体集定义的,拦截器方法不能像服务操作一样接受请求参数。查询拦截器方法在处理HTTP GET请求时调用,必须返回一个lambda表达式,该表达式确定查询结果是否应返回拦截器实体集的实例。数据服务使用此表达式进一步优化请求的操作。以下是查询拦截器的示例定义。

http://msdn.microsoft.com/zh-cn/library/dd744842.aspx

如果您想在演示应用程序中看到`ChangeInterceptor`的实际操作,只需尝试插入一个缺少数据的新地址,然后看看会发生什么。您应该会看到一个类似于以下内容的MessageBox

检索数据

检索数据实际上非常酷,因为借助精巧的客户端API,我们可以使用LINQ的子集来根据我们想要获取的数据定制查询。演示应用程序有两个查询:一个获取所有地址,一个只获取城市以“B”开头的地址。让我们看看这些,好吗?

所有地址

这里是很标准的东西;简单地获取所有地址(同样,必须是异步的,因为我们正在使用Silverlight)

private void FetchAllAddresses()
{
    // Create the query using LINQ
    var qry = proxy.Addresses;

    // Execute the Query
    qry.BeginExecute(new AsyncCallback(r =>
    {
        try
        {
            lstAllAddresses.ItemsSource = qry.EndExecute(r);
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error FetchAllBAddresses\r\n" +
                ReadException(ex.InnerException.Message.ToString()));
        }
    }), null);
}

B 地址

这次,看看我们能做什么。我们可以使用一些LINQ来完全定制我们想要获取的内容(同样,必须是异步的,因为我们正在使用Silverlight)

private void FetchAllBAddresses()
{
    //use some projections that were added in ADO .NET DataServices v1.5
    var qry = (from a in proxy.Addresses
               where a.City.ToLower().StartsWith("b")
               orderby a.City
               select a) as DataServiceQuery<Address>;

    // Execute the projected Query
    qry.BeginExecute(new AsyncCallback(r =>
    {
        try
        {
            lstCityBAddresses.ItemsSource = qry.EndExecute(r);
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error FetchAllBAddresses\r\n" + 
                ReadException(ex.InnerException.Message.ToString()));
        }
    }), null);
}

更新数据

这与插入的工作方式非常相似;事实上,在演示应用程序的代码中,插入/更新/删除都使用一个通用方法,因为唯一不同的是操作类型(插入/更新/删除)。我只在这里展示这个通用方法,但删除也使用这个通用方法。

private void Update_Click(object sender, RoutedEventArgs e)
{
    DeleteOrUpdateOrInsert(OperationType.Update, 
      (Address)lstAllAddresses.SelectedItem);
}

private void Delete_Click(object sender, RoutedEventArgs e)
{
    DeleteOrUpdateOrInsert(OperationType.Delete, 
      (Address)lstAllAddresses.SelectedItem);
}


private void DeleteOrUpdateOrInsert(OperationType opType,
             Address currentAddress)
{

    switch (opType)
    {
        case OperationType.Delete:
        case OperationType.Update:
            if (currentAddress == null)
            {
                MessageBox.Show(
                  "You need to pick an address from All Addresses 1st");
                return;
            }
            break;
    }

    String msgType = String.Empty;
    switch (opType)
    {
        case OperationType.Delete:
            proxy.DeleteObject(currentAddress);
            msgType = "Deleting";
            break;
        case OperationType.Update:
            proxy.UpdateObject(currentAddress);
            msgType = "Updating";
            break;
        case OperationType.Insert:
            proxy.AddToAddresses(currentAddress);
            msgType = "Inserting";
            break;
    }

    proxy.BeginSaveChanges(new AsyncCallback(r =>
    {
        try
        {
            proxy.EndSaveChanges(r);
            FetchAllAddresses();
            FetchAllBAddresses();
        }
        catch (Exception ex)
        {
            MessageBox.Show(String.Format("Error {0}\r\n{1}", msgType, 
                ReadException(ex.InnerException.Message.ToString())));
        }
    }), null);
}

private string ReadException(String error)
{
    XDocument xdoc = XDocument.Parse(error);
    return ((XElement)xdoc.DescendantNodes().First()).Value;
}

删除数据

使用与更新相同的方法,但删除当前对象而不是更新它,请参阅演示应用程序的代码。

验证

虽然目前还没有明确的验证,但Shawn Wildermuth已经启动了一个开源项目,旨在为ADO.NET数据服务带来RIA风格的验证;这是一个非常有趣的项目,涉及生成代码库和创建代理。请查看以下URL:http://niagara.codeplex.com/

这是一篇非常有趣的读物,也是我见过的第一个正确使用OSLO的例子。向Shawn致敬。

RIA数据服务

这一切是关于什么

RIA数据服务可以说是新来的。它们也提供了在WCF服务背后抽象实体模型的能力。那么为什么不使用ADO.NET数据服务呢?好吧,RIA数据服务也有一些绝招。正如我们之前看到的,ADO.NET数据服务中内置验证支持的唯一形式似乎是使用拦截器,这看起来似乎只适用于那些可以轻松解析异常中包含的XML的客户端应用程序。我不知道你们怎么看,但我不太喜欢解析一小段XML来获取异常消息。也许我愚蠢或错过了什么,谁知道呢。

另一方面,RIA使用了一些巧妙的动态数据启发式技巧,允许EntityModel实体标记额外的验证属性(System.ComponentModel.DataAnnotations命名空间),这些属性实际上在Silverlight客户端应用程序上工作。通过使用这些验证属性,您可以为部分元数据类添加元数据,这些元数据将用于在客户端验证业务对象。

如果您曾经做过WPF工作,请考虑`IDataErrorInfo`。

DomainService实际上只是一个WCF服务,它会根据您的终结点格式化数据。Silverlight使用二进制终结点(即常规WCF二进制),但RIA服务也提供SOAP终结点和JSON终结点。数据服务和RIA服务之间的真正区别在于,数据服务发送对象图(即,在客户端,您会得到ObjectA.ObjectB.ObjectC),而RIA服务发送单个对象(ObjectA、ObjectB、ObjectC)并期望客户端通过匹配外键将对象重新组合起来。(即,ObjectB.ObjectAId = ObjectA.Id)。这意味着使用RIA服务,客户端必须提前知道对象的结构才能将事物重新组合起来,而使用数据服务,数据的结构可以在客户端运行时推断出来。

Shawn Wildermuth有一篇关于ADO.NET数据服务的优秀博客文章,我强烈建议大家阅读,他在其中很好地阐述了每种数据访问方法的优点:http://wildermuth.com/Tag/ADO.NET%20Data%20Services

为了给您提供与我谈论ADO.NET数据服务时相同的视角,我认为这张图很好地说明了正在发生的事情

shameless 摘自Nikhil的博客。

所以我想你想看一些代码,对吧?

但在我们深入探讨之前,我们需要了解如何创建RIA数据服务。那么,我们来讨论一下,好吗?

步骤 1

是为数据定义一个实体模型。这很容易(对于演示应用程序来说是这样),只需创建一个新的实体模型,并按照向导完成,使用您自己的SQL Server数据库连接字符串,并指向您使用上面提供和讨论的设置脚本设置的WCFDataServicesDemoApp数据库。

第二步

一旦我们有了实体模型,我们需要编译代码,以便RIA服务向导会看到确实有一个实体模型可以通过RIA服务公开。所以一旦您构建了代码,就真的只是添加一个RIA服务的问题了,这可以使用以下向导完成

然后会询问您是否要将此RIA服务实例附加到实体模型,您可以再次选择演示应用程序的数据库(步骤1),并选择您希望在RIA服务实例中授予实体模型何种访问权限。

完成此操作后,您的解决方案中将有一个新的_<YOUR_SERVICE_NAME>.cs_项,其中包含以下样板代码。您可以自由修改此样板代码以添加您认为合适的新方法。

我应该指出,我正在使用SL4 BusinessApplication VS2010模板,它为您提供了Web项目和一个Silverlight 4 RIA项目,其中SL4应用程序还具有所有必要的导航组件。这非常省时,因此非常值得使用。唯一的问题是其中可能有很多您不需要的东西;为了这个演示应用程序的缘故,我删除了许多不需要的东西,只保留了运行演示代码所需的内容。

总之,随附的演示代码中的RIA服务如下所示(我使用与上面所示图中相同的实体模型选项生成并修改了它)

namespace RIADataServicesDemoApp.Web.CustomRIAServices
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Data;
    using System.Linq;
    using System.Web.DomainServices;
    using System.Web.DomainServices.Providers;
    using System.Web.Ria;
    using System.Web.Ria.Services;
    using RIADataServicesDemoApp.Web.Model;

    [EnableClientAccess()]
    public class RIADataService : 
           LinqToEntitiesDomainService<RIADemoAppEntities>
    {

        public IQueryable<Address> GetAddresses()
        {
            return this.ObjectContext.Addresses.OrderBy(x => x.AddressId);
        }

        public void InsertAddress(Address address)
        {
            if ((address.EntityState != EntityState.Added))
            {
                if ((address.EntityState != EntityState.Detached))
                {
                    this.ObjectContext.ObjectStateManager.
                        ChangeObjectState(address, EntityState.Added);
                }
                else
                {
                    this.ObjectContext.AddToAddresses(address);
                }
            }
        }

        public void UpdateAddress(Address currentAddress)
        {
            if ((currentAddress.EntityState == EntityState.Detached))
            {
                this.ObjectContext.AttachAsModified(currentAddress, 
                    this.ChangeSet.GetOriginal(currentAddress));
            }
        }

        public void DeleteAddress(Address address)
        {
            if ((address.EntityState == EntityState.Detached))
            {
                this.ObjectContext.Attach(address);
            }
            this.ObjectContext.DeleteObject(address);
        }

        public IQueryable<Customer> GetCustomers()
        {
            return this.ObjectContext.Customers;
        }

        public void InsertCustomer(Customer customer)
        {
            if ((customer.EntityState != EntityState.Added))
            {
                if ((customer.EntityState != EntityState.Detached))
                {
                    this.ObjectContext.ObjectStateManager.
                        ChangeObjectState(customer, EntityState.Added);
                }
                else
                {
                    this.ObjectContext.AddToCustomers(customer);
                }
            }
        }

        public void UpdateCustomer(Customer currentCustomer)
        {
            if ((currentCustomer.EntityState == EntityState.Detached))
            {
                this.ObjectContext.AttachAsModified(currentCustomer, 
                    this.ChangeSet.GetOriginal(currentCustomer));
            }
        }

        public void DeleteCustomer(Customer customer)
        {
            if ((customer.EntityState == EntityState.Detached))
            {
                this.ObjectContext.Attach(customer);
            }
            this.ObjectContext.DeleteObject(customer);
        }
    }
}

再次,我们离创建一个基本的、可工作的RIA服务已经不远了,尽管它远非一个真实的例子,但对于基本的CRUD操作来说,它已经足够了。

但在此之前,我想向您介绍一下演示应用程序的外观和工作原理,以便您知道如何操作它。我不得不说,它不漂亮,但它确实完成了任务。我现在应该指出,RIA演示应用程序与之前演示的ADO.NET数据服务应用程序执行相同的工作,正如我在本文中一直强调的,它并不令人惊叹或真实世界,但应该能够很好地演示简单的CRUD操作。

屏幕分为四个区域,功能如下

  • 左上角:显示所有地址。
  • 左下角:显示所有城市以“B”开头的地址。
  • 右上角:允许用户更新/删除项目。用户必须从左上角地址列表中的所有地址列表中选择要更新/删除的项目。
  • 右下角:允许用户创建新地址。为此,用户必须点击“创建新项目”,然后填写数据,然后点击“插入”按钮。

如前所述,我使用的是SL BusinessApplication模板,其中包含许多文件,但演示应用程序中您感兴趣的所有内容都在_Views\Home.xaml_和_Views\Home.xaml.cs_文件中。

检索数据

如果您曾经使用过LINQ to SQL或LINQ to Entities,您会熟悉DataContext对象的概念,该对象具有某种实体和实体集/关系,并将这些实体和实体集/关系公开为某种中心上下文对象的属性和集合,并且具有跟踪更改的内置能力,还允许用户通过调用Add()和SubmitChanges()等方法将更改提交回数据库。

如果这一切对您来说似乎很熟悉,那么您会很高兴地知道RIA服务具有相同的客户端API,您将在接下来的部分中看到。

那么我们从数据检索开始吧,好吗?

我们可以像使用数据服务那样做,就像这样

//has LINQ extension methods for EntityQuery<T>
using System.Windows.Ria; 

//This is how you can also bind stuff using code behind for RIA services
try
{
    RIADataContext context = new RIADataContext();
    var qry = context.GetAddressesQuery().Where(a => 
    a.City.ToLower().StartsWith("b"));

    // Bind the data
    lstCityBAddresses.ItemsSource = 
    context.Load<Address>(qry).AllEntities;
}
catch (Exception ex)
{
}

但有了RIA,事情变得更有趣了。我们实际上可以在XAML中直接使用RIA自带的一些非常丰富的数据控件来做很多事情。我们来看看其中的一些,好吗?

附带的RIA演示应用程序与ADO.NET数据服务应用程序的功能完全相同,因此我们继续。

所有地址列表

这是使用RIA控件完成的,它在XAML中定义如下

<riaControls:DomainDataSource 
    x:Name="allAddressesDataSource"
    QueryName="GetAddressesQuery" 
    AutoLoad="True"/>

<ListBox x:Name="lstAllAddresses" 
         Grid.Row="1" 
         ItemTemplate="{StaticResource addTemplate}"
         ItemsSource="{Binding Data, 
            ElementName=allAddressesDataSource}"/>

其中`allAddressesDataSource` `DomainDataSource`的`Context`在代码隐藏中设置,如下所示

private RIADataContext context = new RIADataContext();

public Home()
{
  InitializeComponent();
  this.allAddressesDataSource.DomainContext = context;
}

另请注意,`allAddressesDataSource` `DomainDataSource`有一个`QueryName`属性,其设置为“`GetAddressesQuery`”;这从何而来?

嗯,如果我们回到最初的RIA服务并查看,有一个`IEnumerable

`;这就是RIA `DomainDataSource`控件正在使用的数据源。

[EnableClientAccess()]
public class RIADataService : LinqToEntitiesDomainService<RIADemoAppEntities>
{

    public IQueryable<Address> GetAddresses()
    {
        return this.ObjectContext.Addresses.OrderBy(x => x.AddressId);
    }
}

B 地址列表

这是使用RIA控件完成的,它在XAML中定义如下

<riaControls:DomainDataSource 
    x:Name="allBStartingAddressesDataSource"
        QueryName="GetAddressesQuery" AutoLoad="True">

    <riaControls:DomainDataSource.FilterDescriptors>
        <riaData:FilterDescriptorCollection 
        x:Name="DataSourceFilters" 
        LogicalOperator="And">
            <riaData:FilterDescriptor 
            PropertyPath="City" 
            Operator="StartsWith">
                <!--
                NOTE : There appears to be an issue with the 
               controlParameter, having a direct
                       Value, so have been forced into using a 
                       control to bind to for ControlParameter
                       Value
                -->
                <riaControls:ControlParameter  
            ControlName="txtCityFilter" 
                        PropertyName="Text" />
            </riaData:FilterDescriptor>
        </riaData:FilterDescriptorCollection>
    </riaControls:DomainDataSource.FilterDescriptors>

    <riaControls:DomainDataSource.SortDescriptors>
        <riaData:SortDescriptor PropertyPath="City" 
        Direction="Ascending" />
    </riaControls:DomainDataSource.SortDescriptors>
    
</riaControls:DomainDataSource>

<ListBox x:Name="lstCityBAddresses" Grid.Row="4" 
         ItemsSource="{Binding Data, 
        ElementName=allBStartingAddressesDataSource}"
         ItemTemplate="{StaticResource addTemplate}"/>

这次请注意,事情变得更多了;`DomainDataSource`控件变得更加复杂。我们实际上直接在XAML中设置了一些`FilterDescriptors`和`SortDescriptors`。这是RIA提供而ADO.NET数据服务不提供的一个功能,一套丰富的控件。然而,因此,RIA中的耦合比ADO.NET数据服务更紧密。

总之,从上面的XAML可以看出,我们正在声明一个`FilterDescriptor`,它正在过滤一些对象的列表,其中`City`属性`StartsWith`某个值。该值绑定到一个名为“`txtCityFilter`”的控件,在演示应用程序中,它简单地设置为字符串“B”。`DomainDataSource`控件的上下文与之前来自RIA服务的`IEnumerable`相同;只是这次,我们正在使用这些丰富的RIA控件在客户端过滤此列表。

注意:我认为当前RIA版本中存在一个小错误,即我无法在XAML中直接为`ControlParameter`的`Value`属性设置字符串文字,例如“B”。我不得不使用绑定到另一个控件。我预计这个缺陷不会持续太久。

创建数据

创建数据非常容易;只需将新项添加到相关上下文`EntitySet`即可。这是添加新地址的代码

context.Addresses.Add(newAddress);
context.SubmitChanges();

更新数据

更新数据也相当容易。与ADO.NET数据服务演示应用程序一样,我在代码隐藏中有一个组合的辅助方法,用于处理删除或更新操作,这是那个简单的辅助方法,从中可以看出,首先发生的是检查在所有地址列表中是否有当前选定的项目。

如果操作类型是删除,则从上下文中(并因此从数据库中)删除选定的地址。如果操作类型是更新,则下一步是验证用于显示当前选定地址的`DataForm`,并且只有在有效的情况下,操作才会完成。

private void Delete_Click(object sender, RoutedEventArgs e)
{
    DeleteOrUpdate(OperationType.Delete, 
    (Address)editAddressDataForm.CurrentItem);
}

private void Update_Click(object sender, RoutedEventArgs e)
{
    DeleteOrUpdate(OperationType.Update, 
    (Address)editAddressDataForm.CurrentItem);
}

private void DeleteOrUpdate(OperationType opType, 
    Address currentAddress)
{
    switch (opType)
    {
        case OperationType.Delete:
        case OperationType.Update:
            if (currentAddress == null)
            {
                MessageBox.Show(
            "You need to pick an address from All Addresses 1st");
                return;
            }
            break;
    }


    switch (opType)
    {
        case OperationType.Delete:
            context.Addresses.Remove(currentAddress);
            context.SubmitChanges();
            break;
        case OperationType.Update:
            if (editAddressDataForm.ValidateItem())
            {
                //Could not get the XAML Parser to 
        //understand {x:Type dataForm:DataForm.ValidationSummary} 
                //for a XAML based Style
                if (editAddressDataForm.ValidationSummary != null)
                    editAddressDataForm.ValidationSummary.Foreground 
                       = new SolidColorBrush(Colors.Red);

                if (editAddressDataForm.IsItemValid)
                {
                    editAddressDataForm.CommitEdit();
                    if (context.HasChanges)
                        context.SubmitChanges();
                }
            }
            break;
    }
}

其中XAML看起来像这样

<dataForm:DataForm Grid.Row="0" x:Name="editAddressDataForm"   
                   CurrentItem="{Binding ElementName=lstAllAddresses, Path=SelectedItem}"
                   AutoGenerateFields="False" AutoCommit="True" 
                   AutoEdit="True" CommandButtonsVisibility="None" Margin="0,0,0,5" >
    <dataForm:DataForm.EditTemplate>
        <DataTemplate>
            <StackPanel>
                <dataForm:DataField Label="AddressId">
                    <TextBlock Text="{Binding AddressId, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="City">
                    <TextBox Text="{Binding City, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="Line1">
                    <TextBox Text="{Binding Line1, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="Line2">
                    <TextBox Text="{Binding Line2, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="PostCode">
                    <TextBox Text="{Binding PostCode, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>

            </StackPanel>
        </DataTemplate>
    </dataForm:DataForm.EditTemplate>
</dataForm:DataForm>

删除数据

这与更新的工作方式非常相似,只是对Context的`EntitySet`的调用在调用`SubmitChanges()`之前针对删除操作有一个Remove。这是执行删除的代码片段

private RIADataContext context = new RIADataContext();
...
...
context.Addresses.Remove(currentAddress);
context.SubmitChanges();

RIA验证

RIA验证是一把双刃剑,它在客户端对实体模型对象提供验证支持确实很酷,这很好很棒,但代价是什么?例如,考虑以下问题

  • 在我看来,EntityModel对象应该是POCO对象,而当前RIA验证的工作方式依赖于使用_System.ComponentModel.DataAnnotations.dll_中的属性,这使得它更不像POCO,也更不容易移植。
  • 启用验证的方式是使用自定义部分类,这些类使用进行验证所需的属性。如果这可以来自其他来源,例如XML、数据库,那会很好,但目前看来,手工创建的部分类,或代码生成类,或T4生成类是唯一的方法,因为_System.ComponentModel.DataAnnotations.dll_属性是使Silverlight客户端中的验证工作所必需的。
  • `MetadataTypeAttribute`感觉像是一个很大的漏洞。它有效,但天哪,它不漂亮,是吗?

无论如何,说了这么多,我在这篇文章中的工作是展示RIA服务领域中验证的当前状态,这正是我现在要做的。

那么RIA服务验证是如何工作的

好吧,正如我们上面看到的,我们有一个实际的RIA服务,它有一个上下文对象,这是它所封装的实体模型。谜题的下一部分是,我们有一个包含模型部分类(与实体模型的模型对象匹配)的代码文件,它位于RIA服务旁边。这个额外的元数据文件可能包含一个或多个模型部分类(与实体模型的模型对象匹配),每个类都应标记有`MetadataTypeAttribute`。此属性用于提供验证信息,Silverlight客户端使用该信息来确定哪个类持有当前序列化的RIA `DataContext`对象的当前验证属性(我们稍后会讲到)。

我们现在知道有一个元数据文件包含一个或多个具有`MetadataTypeAttribute`的部分模型类,那么这些部分类还有什么呢?这还不够,是吗?嗯,不,不够!

谜题的下一部分是,我们需要引用_System.ComponentModel.DataAnnotations.dll_,以便用我们想要的所有验证规则来注释元数据。通过这样做,我们现在与_System.ComponentModel.DataAnnotations.dll_紧密耦合,但没关系。

_System.ComponentModel.DataAnnotations.dll_确实包含一些非常有用的属性,例如

  • [Required]
  • [RegularExpression("[A-Z][A-Za-z0-9]*")]
  • [StringLength(32)]

您应该能看出它的发展方向,所以我在这里提供一个部分类的示例,该类可以与演示应用程序的实体模型/RIA服务中的Address模型一起使用

#pragma warning disable 649
// disable compiler warnings about unassigned fields

namespace RIADataServicesDemoApp.Web.Model
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Data.Objects.DataClasses;
    using System.Linq;
    using System.Web.DomainServices;
    using System.Web.Ria;
    using System.Web.Ria.Services;

    // The MetadataTypeAttribute identifies AddressMetadata as the class
    // that carries additional metadata for the Address class.
    [MetadataTypeAttribute(typeof(Address.AddressMetadata))]
    public partial class Address
    {

        // This class allows you to attach custom attributes to properties
        // of the Address class.
        internal sealed class AddressMetadata
        {

            // Metadata classes are not meant to be instantiated.
            private AddressMetadata()
            {
            }

            public int AddressId;

            [Required]
            [StringLength(20)]
            public string City;

            public EntityCollection<Customer> Customers;

            public EntityCollection<Customer> Customers1;

            [Required]
            [StringLength(20)]
            public string Line1;

            public string Line2;

            [Required]
            [StringLength(20)]
            public string PostCode;
        }
    }
}

#pragma warning restore 649    
// re-enable compiler warnings about unassigned fields

谜题的最后一部分非常简单;这只是一个约定问题。您只需要在Silverlight端正确设置绑定,一切就都解决了。下面是一个示例,我正在使用`DataForm`并将其绑定到正在添加到RIA的地址

<dataForm:DataForm Grid.Row="0" x:Name="addAddressDataForm"   
                   AutoGenerateFields="False" AutoCommit="True" 
                   AutoEdit="True" CommandButtonsVisibility="None" 
                   Margin="0,0,0,5" >
    <dataForm:DataForm.EditTemplate>
        <DataTemplate>
            <StackPanel>
                <dataForm:DataField Label="City">
                    <TextBox Text="{Binding City, 
                    Mode=TwoWay,
                    NotifyOnValidationError=True,  
                    ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="Line1">
                    <TextBox Text="{Binding Line1, 
                    Mode=TwoWay,
                    NotifyOnValidationError=True,  
                    ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="Line2">
                    <TextBox Text="{Binding Line2, 
                    Mode=TwoWay,
                    NotifyOnValidationError=True,  
                    ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="PostCode">
                    <TextBox Text="{Binding PostCode, 
                    Mode=TwoWay,
                    NotifyOnValidationError=True,  
                    ValidatesOnExceptions=True}" />
                </dataForm:DataField>

            </StackPanel>
        </DataTemplate>
    </dataForm:DataForm.EditTemplate>
</dataForm:DataForm>

这其中重要的部分是

  • _Mode=TwoWay_:这是至关重要的,因为验证实际上是通过访问服务器(网站应用程序)来完成的,然后带着任何验证错误返回到Silverlight客户端。
  • _NotifyOnValidationError=True_:这告诉`DataForms`字段在发生错误(即验证错误)时进行验证;这就是`Annotations`属性的使用之处。
  • _ValidatesOnExceptions=True_:这告诉`DataForms`字段在发生异常时进行验证(最后的希望,例如尝试将字符串“Tree”解析为绑定的Int32字段)。

为了触发验证,我是这样处理`DataForm`的

if (editAddressDataForm.ValidateItem())
{
    //Could not get the XAML Parser to 
    //understand {x:Type dataForm:DataForm.ValidationSummary} 
    //for a XAML based Style
    if (editAddressDataForm.ValidationSummary != null)
        editAddressDataForm.ValidationSummary.Foreground 
        = new SolidColorBrush(Colors.Red);

    if (editAddressDataForm.IsItemValid)
    {
        editAddressDataForm.CommitEdit();

        switch (opType)
        {
            case OperationType.Delete:
                context.Addresses.Remove(currentAddress);
                context.SubmitChanges();
                break;
            case OperationType.Update:
                if (context.HasChanges)
                    context.SubmitChanges();
                break;
        }
    }
}

有一点:当然,最好在XAML中设置`DataForm.ValidationSummary`的样式,但这对我不起作用,所以我不得不使用这段代码隐藏来应用红色前景色画笔。同样,我认为是我做错了什么,我预计这个问题很快就会修复。

以下是尝试使用RIA验证插入不正确地址的结果

就这样了

各位,我暂时就说这么多了。正如我在本文中多次强调的,这绝不是一个真实的应用程序,也不会让您成为WCF数据服务或RIA服务的真正专家,但我认为它向您展示了这两种技术的基础知识,也许让您尝到了这两种不同数据堆栈的可能性,以及目前Silverlight开发方面的选择。

另外说一句,我即将第一次做爸爸(是个小男孩,嗯……我是说未来的程序员),所以这可能是我在一段时间内的最后一篇文章(拭目以待),但无论如何,我会努力让Barberous代码机器全速运转,尽管可能会很困难。

一如既往,欢迎投票/评论。

© . All rights reserved.