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

使用 WCF 服务与 Silverlight

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (15投票s)

2011 年 9 月 30 日

CPOL

11分钟阅读

viewsIcon

97031

downloadIcon

4588

本文展示了如何使用 WCF 服务与 Silverlight。

引言

在我们的一款产品中,我们必须在 Silverlight 中使用 WCF 服务。在开发 Silverlight 和 WCF 时,我发现了一些非常有趣的事情,我觉得这些值得分享。每当我们想使用 Silverlight 并需要某种服务通信时,我们都会遇到这些常见问题。在本文中,我们将讨论关于 WCF 和 Silverlight 桥接的一些有趣发现。

为了演示目的,我们将使用一个带有简单类的演示应用程序。假设我们有一个费用管理应用程序,并且我们有一个用 Silverlight 构建的客户端。客户端通过 WCF 与服务器通信。让我们以此为例来解释我们的过程。

在 Silverlight 中使用 WCF

在本节中,我们将了解如何在 Silverlight 中使用 WCF 服务。我确信每位读者都对 WCF 是什么以及如何在 Web 应用程序中使用它有很好的理解。有些内容会重复和再次讨论,可能听起来很熟悉。但是由于我们需要其中的一些部分作为子集,所以我再次讨论它。首先,我们将了解基础知识,然后我们将看到一些其他相关的技巧和信息。

WCF 基础

WCF 有三个基本构建块。它们被称为 WCF 的 A、B、C。A 代表地址(Address),B 代表绑定(Binding),C 代表契约(Contract)。在后面的部分中,我们将了解使用 WCF 所需的最常见知识,当然是在 Silverlight 上下文中的通信。WCF 可以通过多种方式托管。最常见的如下:

  • 在 Internet Information Services 中托管
  • 在 Windows Process Activation Service 中托管
  • 在 Windows 服务应用程序中托管
  • 在托管应用程序中托管

本文不打算讨论上述托管选项,因为它们需要进一步研究,并且假设不是本文的重点领域。我建议您花一些时间在 MSDN 或 Google 上了解更多关于 WCF 托管过程的信息。

服务契约和操作契约

我确信我们都知道服务契约是什么,但我仍然讨论它,因为这是一篇初学者文章。我们通过添加一个简单的类属性“[ServiceContract]”使任何类成为服务契约,但最好先声明一个接口,然后将类属性应用于该接口,然后在一个派生类中实现该类。现在,我们希望作为服务一部分公开的方法需要用名为“[OperationContract]”的方法属性进行修饰。

下面我们有一个代码块,其中有一个被声明为服务契约的接口,然后我们从该接口实现了一个派生类来定义将通过客户端调用的方法。

服务合同

[ServiceContract]
public interface IMoneyService
{
    [OperationContract]
    ServiceResponse AddExpense(Expense expense);
    [OperationContract]
    ServiceResponse UpdateExpense(Expense expense);
    [OperationContract]
    ServiceResponse DeleteExpense(Expense expense);
    [OperationContract]
    ServiceResponse GetExpenseByID(int expenseId);
    [OperationContract]
    ServiceResponse AddCategory(Category category);
    [OperationContract]
    ServiceResponse UpdateCategory(Category category);
    [OperationContract]
    ServiceResponse DeleteCategory(Category category);
    [OperationContract]
    ServiceResponse GetCategoryByID(int categoryId);
}

契约的实现

[AspNetCompatibilityRequirements
	(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class MoneyService : IMoneyService
{
    public ServiceResponse AddExpense(Expense expense)
    {
        var response = new ServiceResponse();
        using (var manager = new ExpenseManager())
        {
            try
            {
                response.Result = manager.AddExpense(expense);
                response.IsSuccess = true;
            }
            catch (Exception exception)
            {
                response.ServiceException = exception;
                response.IsSuccess = false;
                response.ErrorMessage = "Unable to add Expense";
            }
        }
        return response;
    }
    
    ///Other method's implementation.... goes bellow
    ///....
}    

在上面的例子中,我们没有将所有实现都放在一个方法中来演示这个想法。

数据契约

如果我们要通过 WCF 从服务器向客户端传输自定义数据,我们必须在自定义数据类型上应用“[DataContract]”类属性。此外,所有原始数据类型都可以用作可传输数据。下面我们添加了一个简单的类,我们将其用作自定义类型来从服务向客户端传输数据。

如果您的数据契约中包含嵌套的自定义类型,您可能需要使用 KnownType 属性。

[KnownType(<span style="COLOR: blue">typeof</span>(your-custom-type))]

下面给出了一个示例数据契约实现。

[DataContract]
public class ServiceResponse
{
    private string _errorMessage;
    [DataMember]
    public string ErrorMessage
    {
        get { return _errorMessage; }
        set { _errorMessage = value; }
    }
    private object _result;
    [DataMember]
    public object Result
    {
        get { return _result; }
        set { _result = value; }
    }
    private bool _isSuccess;
    [DataMember]
    public bool IsSuccess
    {
        get { return _isSuccess; }
        set { _isSuccess = value; }
    }
    private Exception _serviceException;
    [DataMember]
    public Exception ServiceException
    {
        get { return _serviceException; }
        set { _serviceException = value; }
    }
}

ASP.NET 兼容性

如果我们将 WCF 服务托管在 IIS 环境中,这种特定场景非常有用。其思想是在服务方法和内部方法中共享相同的 HttpContext,以便我们可以访问公开 WCF 服务的 Web 应用程序的会话和应用程序数据。

var context = HttpContext.Current;
var path = context.Server.MapPath("~/MyPics");

if (!Directory.Exists(path))
{
    Directory.CreateDirectory(path);
}

var userFolder = string.Format("{0}\\{1}", path, userId);

if (!Directory.Exists(userFolder))
{
    Directory.CreateDirectory(userFolder);
}    

在上面的例子中,我们想找到一个名为“MyPics”的文件夹,特定用户的图片将保存在该文件夹中。因此,我们需要服务运行的 httpContext

下面给出了服务配置。关键部分是标签“serviceHostingEnvironment”,我们必须将属性 aspNetCompatibilityEnabled="true" 设置为 true。

    <system.serviceModel>
    <behaviors>
      <endpointBehaviors>
        <behavior name="PEM.MoneyTrackingServiceAspNetAjaxBehavior">
          <enableWebScript />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"
      multipleSiteBindingsEnabled="true" />
    <services>
      <service name="PEM.MoneyTrackingService">
        <endpoint address="" 
	behaviorConfiguration="PEM.MoneyTrackingServiceAspNetAjaxBehavior"
          binding="webHttpBinding" contract="PEM.MoneyTrackingService" />
      </service>
    </services>
   </system.serviceModel>

之后,我们必须将 AspNetCompatibilityRequirements 类属性(其值设置为 RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)添加到所需的服务契约实现中。请注意,此属性不能添加到作为契约的接口之前。我们必须将其添加到服务契约实现之前。

[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements
	(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class MoneyTrackingService
{
    [OperationContract]
    public void DoWork()
    {
        return;
    }
}

向 Silverlight 添加服务引用

好的。我们已经掌握了基础知识,现在是时候让服务工作了。以下是我们必须执行的步骤。

  • 第 1 步:定义我们希望通过服务传输的数据契约。向类添加“[DataContract]”类属性,向属性添加“[DataMember]”属性。如果 DataContract 中有任何 enum,我们必须应用 EnumMember 属性。
  • 第 2 步:为带有方法的 ServiceContract 定义接口,添加服务契约属性。将我们希望公开的每个方法标记为 OperationContract
  • 第 3 步:为 ServiceContract 实现定义一个派生类。如果需要 ASP.NET 兼容性,则添加必要的类属性。
    在我们的案例中,我们在一个名为 MoneyTracking.Service 的单独项目中定义了这些类和接口。
  • 第 4 步:向我们定义类的项目添加引用。如果我们在 Web 项目中定义了所有类和接口,那么就不用担心。
  • 第 5 步:添加一个 svc 文件。删除“cs”文件。修改 .svc 文件的 XML。填入正确的服务名称。
     <%@ ServiceHost Language="C#" Debug="true" 
    	Service="full Qualified name goes here" %>
  • 第 6 步:在 web.config 的 service model 标签中定义适当的服务定义。我们已经在上一节中定义了 XML,也可以从那里复制。请注意,我们还应该添加 mex 绑定。
  • 第 7 步:浏览服务以查看一切是否正常工作。
  • 第 8 步:转到我们想要使用服务的 Silverlight 项目,右键单击项目的引用节点并选择添加服务引用,这将调出配置添加服务引用向导。向导中其余的工作是不言自明的。

完成添加服务后,您将有一个服务引用节点,服务将在此处添加。Visual Studio 还在幕后添加了许多代码,还有一个 ServiceReferences.ClientConfig 文件。我们将在后面的部分中看到其用途。

ServiceReferences.ClientConfig

<configuration>
<system.serviceModel>
    <bindings>
        <basicHttpBinding>
            <binding name="BasicHttpBinding_IMoneyService" maxBufferSize="2147483647"
                maxReceivedMessageSize="2147483647">
                <security mode="None" />
            </binding>
        </basicHttpBinding>
    </bindings>
    <client>
        <endpoint address="https:///MoneyTrackingService.svc"
            binding="basicHttpBinding" 
		bindingConfiguration="BasicHttpBinding_IMoneyService"
            contract="MoneyTrackingServiceReference.IMoneyService"
            name="BasicHttpBinding_IMoneyService" />
    </client>
</system.serviceModel>
</configuration>    

使用通道工厂

到目前为止,我们有三种在 Silverlight 中使用 WCF 服务的方法

  • 使用服务引用
  • 使用通道工厂
  • 使用客户端基类

下面,我们解释了如何在 Silverlight 端使用通道工厂来定义 WCF 服务。请注意,在这种情况下,我们无需为服务使用任何形式的引用。但是如何实现呢?Silverlight 只允许服务方法调用的异步模型,因此我们不能使用相同的 OperationContract。我们可以将所有作为 DataContract 的类作为链接复制并在 Silverlight 项目中使用,对于服务契约,我们必须定义一个带有 ServiceContract 属性的新类。请注意,方法需要具有 [OperationContract(AsyncPattern = true)] 方法属性。对于服务器中 ServiceContract 的每个方法,我们必须定义两个带有前缀“Begin”和“End”的方法,因此如果服务器中有一个名为“AddExpense”的方法,在客户端类中我们必须定义两个名为“BeginAddExpense”和“EndAddExpense”的方法。下面,给出了客户端类 IMoneyService 的完整代码。

异步模式契约示例

namespace MoneyTracking.Service
{
    [ServiceContract]
    public interface IMoneyService
    {
        [OperationContract(AsyncPattern = true)]
        IAsyncResult BeginAddExpense
		(Expense expense, AsyncCallback callback, object state);
        
        [OperationContract(AsyncPattern = true)]
        IAsyncResult BeginUpdateExpense
		(Expense expense, AsyncCallback callback, object state);
        
        [OperationContract(AsyncPattern = true)]
        IAsyncResult BeginDeleteExpense
		(Expense expense, AsyncCallback callback, object state);
        
        [OperationContract(AsyncPattern = true)]
        IAsyncResult BeginGetExpenseByID
		(int expenseId, AsyncCallback callback, object state);
        
        [OperationContract(AsyncPattern = true)]
        IAsyncResult BeginAddCategory
		(Category category, AsyncCallback callback, object state);
        
        [OperationContract(AsyncPattern = true)]
        IAsyncResult BeginUpdateCategory
		(Category category, AsyncCallback callback, object state);
        
        [OperationContract(AsyncPattern = true)]
        IAsyncResult BeginDeleteCategory
		(Category category, AsyncCallback callback, object state);

        [OperationContract(AsyncPattern = true)]
        IAsyncResult BeginGetCategoryByID
		(int categoryId, AsyncCallback callback, object state);

        ServiceResponse EndAddExpense(IAsyncResult result);

        ServiceResponse EndUpdateExpense(IAsyncResult result);

        ServiceResponse EndDeleteExpense(IAsyncResult result);

        ServiceResponse EndGetExpenseByID(IAsyncResult result);

        ServiceResponse EndAddCategory(IAsyncResult result);

        ServiceResponse EndUpdateCategory(IAsyncResult result);

        ServiceResponse EndDeleteCategory(IAsyncResult result);

        ServiceResponse EndGetCategoryByID(IAsyncResult result);
    }
}

通道工厂示例

现在我们已经定义了接口。是时候使用 ChanelFactory 构建服务了。下面给出了一个简单的代码来演示如何创建服务客户端并调用方法。

private void UsingChanelFactory(object sender, RoutedEventArgs e)
{
    var basicHttpBinding = new BasicHttpBinding();
    var endpointAddress = new EndpointAddress
    ("https:///MoneyTrackingWeb/MoneyTrackingService.svc");
    var moneyService = new ChannelFactory
    <moneytracking.service.imoneyservice>
    (basicHttpBinding, endpointAddress).CreateChannel();
    moneyService.BeginAddCategory
    (new MoneyTracking.Common.Category(), ASyncronousCallBack, moneyService);
}

private void ASyncronousCallBack(IAsyncResult ar)
{
    if(ar.IsCompleted)
    {
        MoneyTracking.Common.ServiceResponse serviceResponse = 
        ((MoneyTracking.Service.IMoneyService)ar.AsyncState).EndAddCategory(ar);
        if(serviceResponse.IsSuccess)
        {
            //do your work here
        }
    }
}  

进行同步调用

默认情况下,我们在 Silverlight 中的服务客户端中的方法是异步方法调用。下面给出了一个简单的代码来演示这个想法。客户端已启动,然后订阅了一个完成事件,然后调用异步方法。

异步调用示例

public void AddExpense()
{
    var client = new MoneyServiceClient();
    var category = new Category();
    //fill category attributes here            
    client.AddCategoryCompleted += ClientAddCategoryCompleted;
    client.AddCategoryAsync(category);
}

void ClientAddCategoryCompleted(object sender, AddCategoryCompletedEventArgs e)
{
    ServiceResponse serviceResponse = e.Result;
    if(serviceResponse.IsSuccess)
        Categories.Add((Category) serviceResponse.Result);
}

同步调用示例

在某些情况下,我们需要进行同步调用,由于 Silverlight 不允许同步调用,我们总是可以通过使用相同系统的方式来绕过它,从而实现同步调用,其思想是停止当前代码执行,直到完成事件被触发。

我们定义了一个自定义类 AsyncCallStatus<T>,这是一个自定义状态,将通过 Async 方法传递。但是使这一切成为可能的魔术类是“AutoResetEvent”,我们使用 _autoResetEvent.WaitOne() 冻结了当前执行。当在完成事件中,我们得到结果后,我们只需设置 _autoResetEvent.Set();,这会再次恢复进程。

请注意,在 Silverlight 4 中,我们必须使用 ThreadPool.QueueUserWorkItem(MethodNameGoesHere); 来启动进程,否则 AutoResetEvent.WaitOne() 将停止当前线程执行,并且完成事件根本不会触发,实际上服务器永远不会被调用。下面,我们列出了同步调用的方法的完整代码。

private void SyncronousCall(object sender, RoutedEventArgs e)
{
    ThreadPool.QueueUserWorkItem(AddExpenseInServer);
    
}

private void AddExpenseInServer(object state)
{
    Expense addedExpense = AddExpense();
    Dispatcher.BeginInvoke(() =>
                               {
                                   StatusMessage.Content = "Expense is been Added";
                                   Expenses.Add(addedExpense);
                               });
}

private Expense AddExpense()
{
    var asyncCallStatus = new AsyncCallStatus<AddExpenseCompletedEventArgs>();
    var client = new MoneyServiceClient();
    client.AddExpenseCompleted += ClientAddExpenseCompleted;
    client.AddExpenseAsync(new Expense(),asyncCallStatus);
    _autoResetEvent.WaitOne();
    if (asyncCallStatus.CompletedEventArgs.Error != null)
    {
        throw asyncCallStatus.CompletedEventArgs.Error;
    }
    var serviceResponse = asyncCallStatus.CompletedEventArgs.Result;
    if (serviceResponse.IsSuccess)
    {
        return serviceResponse.Result as Expense;
    }
    else
        return null;
}

void ClientAddExpenseCompleted(object sender, AddExpenseCompletedEventArgs e)
{
    var status = e.UserState as AsyncCallStatus<AddExpenseCompletedEventArgs>;
    if (status != null) status.CompletedEventArgs = e;
    _autoResetEvent.Set();
}

private readonly AutoResetEvent _autoResetEvent = new AutoResetEvent(false);

public class AsyncCallStatus<T>
{
    public T CompletedEventArgs { get; set; }
}    

跨域服务调用

默认情况下,Silverlight 可以从发起方站点调用任何方法。因此,如果 Silverlight 组件是从“http://mytestside.com/silverlightTestPage.aspx”调用的,则该组件可以调用托管在“http://mytestside.com/testservice.svc”中的任何 WCF 服务。但是假设我们想调用托管在“http://myotherside.com/otherservice.svc”中的 WCF 服务,我们可能会遇到类似以下的错误:

在 Silverlight 中实现跨域服务调用的方法实际上非常简单。我们必须在服务根文件夹中放置两个名为“clientaccesspolicy.xml”和“crossdomain.xml”的 XML 文件。因此,如果我们的服务名为“http://mytestserver.com/myservice.svc”,我们需要将这两个文件放置在“http://mytestserver.com/clientaccesspolicy.xml”中。这两个文件及其内容如下所示。

clientaccesspolicy.xml

<?xml version="1.0" encoding="utf-8" ?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="SOAPAction">
        <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="SOAPAction,Content-Type"/>
</cross-domain-policy>

您也可以在本地环境中模拟 crossdomain 类。为此,请修改主机文件。

127.0.0.1 testserver.com 
127.0.0.1 testclient.com

现在你可以从 testclient 运行你的项目,服务将从 testserver 消费。

传输大量数据

在本节中,我们将看到如何自定义 WCF 服务,以便传输大量数据。我们经常在通信时遇到错误,例如数组大小的最大限制已超出,或者终结点未找到的异常,这仅仅是因为可能存在大小问题。请注意,终结点未找到异常可能由多种原因引起。

为了克服此限制,我们需要自定义 basicHttpBinding。我们必须在 <system.servicemodel> 下定义一个 bindings 部分,并在其中自定义 basicHttpBinding。为此,我们必须在 binding 下创建一个名为“<basicHttpBinding>”的子部分。在这里我们可以放置多个绑定,当然我们也可以设置所有高级属性。

下面,我们列出了客户端和服务器的配置。

服务器上的服务配置

<system.serviceModel>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" 
	aspNetCompatibilityEnabled="true"/>
    <bindings>
      <basicHttpBinding>
        <binding name="MoneyTrackingServiceBinding" maxBufferPoolSize="2147483647"
                 maxBufferSize="2147483647" maxReceivedMessageSize="2147483647">
          <readerQuotas  maxDepth="2147483647" maxStringContentLength="2147483647" 
                         maxArrayLength="2147483647" maxBytesPerRead="2147483647" 
                         maxNameTableCharCount="2147483647">
          </readerQuotas>
        </binding>
      </basicHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MoneyTrackingServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="false" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service name="MoneyTracking.Service.MoneyService" 
               behaviorConfiguration="MoneyTrackingServiceBehavior">
        <endpoint address="" binding="basicHttpBinding"
                  bindingConfiguration="MoneyTrackingServiceBinding" 
                  contract="MoneyTracking.Service.IMoneyService" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
</system.serviceModel>    

在这里,在 service 下,我们已将 behaviorConfiguration 添加到 service 中的"",并在其下的 endpoint 部分中,我们还将"bindingConfiguration"设置为我们所需的配置名称。

客户端配置

<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="MoneyTrackingServiceBinding" maxBufferSize="2147483647"
                    maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647" />
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="https:///MoneyTrackingWeb/MoneyTrackingService.svc"
                binding="basicHttpBinding" 
		bindingConfiguration="MoneyTrackingServiceBinding"
                contract="MoneyTrackingServiceReference.IMoneyService"
                name="BasicHttpBinding_IMoneyService" />
        </client>
    </system.serviceModel>
</configuration>    

上面的服务配置部分是针对客户端的,请注意服务器和客户端绑定配置名称应匹配,并且值也应匹配。在客户端服务配置中,我们不必定义“readerQuotas”,实际上那会抛出异常。而且我们不需要行为配置。你们也可以将上述配置复制粘贴到你们的上下文中,在这样做时,你们必须修复契约名称和服务名称。

摘要

在这篇短文中,我们已经看到了 WCF 服务的基础知识。我们只讨论了构建一个可供 Silverlight 客户端使用的简单 WCF 服务所必需的部分。然后我们看到了两种为 Silverlight 创建客户端代理的方法。我们可以添加服务引用,也可以使用通道工厂来构建客户端。通道工厂对客户端代理提供了更多的控制。

之后,我们看到了如何使用一些小型策略 XML 文件来扩展服务,以便可以通过在不同域中运行的 Silverlight 组件来使用它。最后,我们看到了如何自定义绑定以支持大量数据传输。在现实世界的 WCF 和 Silverlight 开发中存在许多挑战。我希望在未来的文章中提供更多内容。

参考文献

历史

  • 2011 年 9 月 29 日:初始版本
© . All rights reserved.