在 Silverlight 3 中抽象 WCF 服务调用






3.40/5 (3投票s)
一种在 Silverlight 中抽象 WCF 服务调用以方便重用和服务重新定位的方法。
虽然在 Silverlight 中使用 WCF 是一种福音,但由于 Silverlight 管理服务引用的方式,它也带来了挑战。通常需要额外的步骤来确保服务与 Silverlight 兼容,然后还需要考虑使服务在 Silverlight 端可用的额外因素,并确保满足安全方面的考虑等等。本文的目的是提供一个相对简单轻量级的框架来抽象应用程序中的服务,为使用它们提供更坚实的基础。
服务从 Web 服务器端开始。Silverlight 提供了额外的模板,包括“启用 Silverlight 的 WCF 服务”。使用此模板添加新的服务引用。这是乐趣开始的地方。
尽管名字听起来很友好,但实际上我曾让设计器添加了一个服务,然后在 web.config 中声明了自定义绑定,模式为二进制。Silverlight 无法识别这种模式。在收到含糊不清的“未找到”错误,然后深入挖掘发现我的服务在 Silverlight 客户端中导致了“415 不支持的媒体类型”错误后,我意识到我的服务生成不正确。
第一步是深入 web.config 文件,找到我服务的服务行为。“basicHttpBinding
”是 Silverlight 兼容的绑定,自定义或二进制绑定则不行。此 web.config 片段演示了在服务器端正确配置服务的一些基本要素。
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<behaviors>
<serviceBehaviors>
<behavior name="Silverlight.Service.CustomServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="Silverlight.Service.CustomServiceBehavior"
name="Silverlight.Service.CustomService">
<endpoint address="" binding="basicHttpBinding"
contract="Silverlight.Service.ICustomService"/>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
</system.serviceModel>
基本要素:aspNetCompatibilityEnabled
、basicHttpBinding
和用于元数据交换的“mex”绑定。现在我们已经准备好,可以将服务发布到某个地方,供 Silverlight 客户端引用。
在客户端,事情变得更有趣了。如果您知道服务发布的地点,可以硬编码终结点,但我需要更灵活的东西。我所知道的是,客户端总会位于相对于 Web 主机和服务的文件夹中,因此通过提取我的本地 URI,我可以即时重新构造服务 URI。令我烦恼的是每次都要为每个服务重复此操作,因此我最终创建了一个通用的 BaseService
类。
该类基于客户端和服务通道。通过这个,它知道如何构造 URL。因为我的名为“CustomService
”的服务实际上位于“Silverlight/Service/CustomService.svc”,所以只需解析主机并更改终结点即可。
这是类
public abstract class BaseService<TClient,TChannel> :
ClientBase<TChannel> where TClient: ClientBase<TChannel>,
new() where TChannel: class
{
private const string BASE_SERVICE = "Silverlight/Service/{0}.svc";
private static readonly string _baseUri;
static BaseService()
{
_baseUri = System.Windows.Browser.HtmlPage.Document.DocumentUri.AbsoluteUri;
int lastSlash = _baseUri.LastIndexOf("/");
_baseUri = _baseUri.Substring(0, lastSlash+1);
}
private readonly TClient _channel;
protected BaseService()
{
if (_baseUri.Contains("http"))
{
Binding binding = new BasicHttpBinding();
EndpointAddress endPoint =
new EndpointAddress(string.Format("{0}{1}", _baseUri,
string.Format(BASE_SERVICE, typeof (TChannel).Name.Substring(1))));
_channel = (TClient)Activator.CreateInstance(typeof (TClient),
new object[] {binding, endPoint});
}
else
{
_channel = Activator.CreateInstance();
}
}
protected TClient _GetClientChannel()
{
return _channel;
}
}
基本上,该服务封装了创建客户端通道以与服务通信的过程。静态构造函数接收 Silverlight 应用程序的路径,然后使用字符串操作来查找托管它的虚拟目录(请注意,如果您在子文件夹中运行客户端,则需要修剪更多的斜杠才能回到应用程序根目录)。我们使用子字符串是因为如果我们使用接口,我们会得到 ICustomService
,但终结点位于 CustomService
。
注意使用 Activator
来实例化类型为我们所需通道/客户端的新对象。第一个参数是一个 object[]
数组,这会导致 Activator 找到最符合参数的构造函数,在本例中我们基本上是这样做:
_channel = new CustomServiceClient(binding, endpoint);
在构造服务时,它会检查基本 URI 是否包含“http”。我们这样做的原因是因为在调试时,URI 实际上位于文件系统(file:
)中。我们假设您默认的终结点设置已足够用于调试,并且只是实例化客户端而无需任何参数。但是,如果您在 Web 上下文中运行,终结点将被重新构造的路径覆盖。请注意,我们获取通道的名称然后将其格式化到路径中,以便名为 CustomService 的通道映射到 Silverlight/Service/CustomService.svc
。
然后,我创建了一个特殊的事件参数来处理服务调用的完成。它将返回服务引用的实体以及错误对象,以便客户端可以按需处理。
public class ServiceCompleteArgs<T> : EventArgs where T: class
{
public T Entity { get; set; }
public Exception Error { get; set; }
public ServiceCompleteArgs(T entity, Exception e)
{
Entity = entity;
Error = e;
}
}
现在,我们可以从基类继承来实现实际的服务调用。假设 CustomService
返回一个整数。我的自定义服务助手看起来像这样:
public class CustomServiceClientHelper :
BaseService<CustomServiceClient, CustomService>
{
public event EventHandler<ServiceCompleteArgs<int>> CustomServiceCompleted;
public CustomServiceClientHelper()
{
_GetClientChannel().GetIntegerCompleted +=
_CustomServiceClientHelperGetIntegerCompleted;
}
public void GetInteger()
{
_GetClientChannel().GetIntegerAsync();
}
void _CustomServiceClientHelperGetIntegerCompleted
(object sender, GetIntegerCompletedEventArgs e)
{
if (CustomServiceCompleted != null)
{
int result = e.Error == null ? e.Result : -1;
CustomServiceCompleted(this, new ServiceCompleteArgs(result,e.Error));
}
}
}
现在,我很容易在其他地方重用该服务,而无需了解终结点或数据是如何绑定的——在我的代码中,将结果放在 TextBlock
中就像这样简单:
public void Init()
{
CustomServiceClientHelper client = new CustomServiceClientHelper();
client.CustomServiceCompleted += _ClientCustomServiceCompleted;
client.GetInteger();
}
void _ClientCustomServiceCompleted(object sender, ServiceCompleteArgs<int> e)
{
if (e.Error == null)
{
DisplayTextBlock.Text = e.Entity.ToString();
}
else
{
HandleError(e.Error);
}
}
现在,您拥有了一个简单的框架,用于将服务插入客户端并消耗它们,这些服务会根据安装位置更新其引用。当然,您还可以做更多的事情,我建议阅读以下文章以获取进一步的参考:
CodeProject