Silverlight 中无缝的 WCF 服务消耗:第 1 部分






4.71/5 (12投票s)
在 Silverlight 中消耗 WCF 服务,无需静态生成服务代理代码。
引言
本文将向您展示如何在 Silverlight 中消耗纯 WCF 服务,而无需静态生成服务代理的代码(即 Visual Studio 的“添加服务引用”向导)。
背景
虽然 Microsoft 推荐在 Silverlight 中与服务器端交互的方式是使用 WCF RIA Services,但我发现它非常有限。事实上,它与我们的 EF 实体装饰方式配合得不好。我们为本地化支持引入的一些额外代码属性引用了本身不是实体的类型,因此代码生成工具(由于存在 WCF RIA Services 链接而被调用)只是忽略了它们,导致我们的代码无法编译。此外,对 DMZ 部署场景的支持不佳,迫使我们寻找其他方法来解决问题。我决定不“与工具斗争”,而是走大家熟知的路线,直接使用纯 WCF 服务。这个决定反过来又带来了一些摩擦,我将在本文系列中消除这些摩擦。
示例
设想我们有以下服务定义
[ServiceContract]
public class CalculatorService
{
[OperationContract]
public Number Add(Number fist, Number second)
{
return new Number {Value = fist.Value + second.Value};
}
}
[DataContract]
public class Number
{
[DataMember]
public int Value
{
get; set;
}
}
在 Silverlight 中消耗它非常容易——只需在 Visual Studio 中使用“添加服务引用”向导,它就会为您生成所有必需的连接代码。我不会列举使用这种简单方法的每一种可能的特点和遇到的问题(互联网上已经有很多讨论了);对我来说,最显著的是:生成的代理难以测试(不易模拟),增加了 XAP 文件的大小,每次更改都需要刷新引用(很烦人),不支持 TDD。此外,这种方法仍然没有解决我的自定义属性问题,因为为客户端生成的类与服务器端的类不同。我真正想要的是在双方之间共享相同的类定义。
重用服务器端代码
要重用(共享)类定义,需要引入一个服务接口,并将其(以及所有相关类)放在一个独立程序集中,该程序集对客户端和服务器代码都可见。但是,Silverlight 不支持这样做。在 Silverlight 中,您不能直接引用 CLR 程序集。因此,我们需要创建一个单独的 Silverlight 类库,并通过链接源文件来包含类定义。
[ServiceContract]
public interface ICalculatorService
{
[OperationContract]
Number Add(Number fist, Number second);
}
public class CalculatorService : ICalculatorService
{
public Number Add(Number fist, Number second)
{
return new Number {Value = fist.Value + second.Value};
}
}
现在,如果您重新生成代理(在 VS 中更新服务引用),代码生成工具将能够重用我们之前链接的类定义。
因此,能够完全重用服务器端接口后,代码生成的要求似乎就变得多余了。我们通过它获得的只是围绕 WCF IChannel
接口的简单包装器的生成。WCF 通道的创建和配置可以在运行时动态完成——在这种简单情况下,不需要使用 ClientBase<T>
类。
消除摩擦
第一次尝试
在运行时创建和配置 WCF 通道非常容易
var factory = new ChannelFactory<ICalculatorService>(
new BasicHttpBinding(),
new EndpointAddress("https:///SampleSilverlightWCF"));
ICalculatorService service = factory.CreateChannel();
这就是我第一次尝试的内容,一运行,我就遇到了以下情况
令人惊讶的是,Silverlight 中的 WCF 运行时强制您 **只** 使用异步接口进行服务调用。您不能直接在 Silverlight 中使用同步服务接口,您需要要么实现服务的异步模式,要么依赖服务代理生成(它将为您的服务生成一个异步接口)。这似乎是一个“一票否决”的问题。
第二次尝试
我知道应该有一个解决方案。我不想以异步方式实现我的服务操作,也不想使用代码生成。等等,为什么服务代理的生成能解决这个问题,而不强制我将服务操作实现为异步!?好吧,如果您查看生成的代码,您会发现它并没有重用同步服务接口;相反,它创建了一个异步副本。然后使用此接口以异步方式与同步服务通信。真是个巧妙的技巧!
所以,我最终创建了两个接口:一个纯粹的同步接口——用于在服务器端实现;以及它的异步副本——用于在客户端使用。
[ServiceContract]
public interface ICalculatorService
{
[OperationContract]
Number Add(Number fist, Number second);
}
[ServiceContract(Name="ICalculatorService")]
public interface ICalculatorServiceAsync
{
[OperationContract(AsyncPattern = true)]
IAsyncResult BeginAdd(Number fist, Number second,
AsyncCallback callback, object state);
void EndAdd(IAsyncResult result);
}
现在,为了能够成功地与服务交互,您需要在创建 WCF 通道时使用异步接口
ICalculatorServiceAsync service;
private void UserControl_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
var factory = new ChannelFactory<ICalculatorServiceAsync>(
new BasicHttpBinding(),
new EndpointAddress("https:///SampleSilverlightWCF/" +
"Services/CalculatorService.svc"));
service = factory.CreateChannel();
service.BeginAdd(
new Number { Value = 2 },
new Number { Value = 2 },
OnAddCompleted, null
);
}
void OnAddCompleted(IAsyncResult ar)
{
Number result = service.EndAdd(ar);
Debug.Assert(result.Value == 4);
}
效果极佳!
结论
您不需要使用服务代理的静态代码生成,也不需要为了在 Silverlight 中消耗 WCF 服务而牺牲其接口和实现。您需要的是
- 通过源文件链接在 Silverlight 中共享您想要重用的代码定义
- 创建一个次要的异步接口,仅在 Silverlight 中使用(您不需要为其实现)
次要异步接口应遵循以下规则
- 接口应通过
[ServiceContract[Name="OriginalInterfaceName"]]
声明一个指向原始同步接口的别名 - 对于同步接口中的每个操作,都应该有一对
BeginXXX/EndXXX
方法 - 每个
BeginXXX
方法都应标记[OperationContract(AsyncPattern=true)]
属性 - 每个
BeginXXX
方法应具有与原始同步对应方法相同的参数(顺序相同),再加上两个特殊参数(AsyncCallback callback, object state
),并返回IAsyncResult
- 每个
EndXXX
方法应返回原始方法的类型(或void
),并且只接受一个类型为iAsyncResult
的参数
后续文章
虽然我们已经消除了“一些”摩擦,但仍有改进的空间。在接下来的系列文章中,我将展示如何进一步简化解决方案(消除对次要接口的需求),如何简化 Silverlight 中异步服务调用的处理,以及如何使 Silverlight 中的 WCF 消耗对 TDD 更友好。敬请期待。
请参阅本系列的 第二部分:解决“一切皆异步”问题。