WCF 示例 – 第十二章 – WCF 实现






4.86/5 (20投票s)
进程内 WCF 测试、动态客户端代理、WCF 请求上下文扩展以及 WCF 客户端异步命令。
![]() |
![]() |
|
第十一章 | 第十三章 |
系列文章
《WCF 实例教程》系列文章介绍如何使用 WCF 进行通信、使用 NHibernate 进行持久化来设计和开发 WPF 客户端。《系列简介》描述了文章的范围,并从宏观层面讨论了架构解决方案。该系列的源代码可在 CodePlex 上找到。
章节概述
这是我们第一次在本系列中,除了文章的标题外,真正地去讨论 WCF 的实现。如前所述,我们为快速应用开发(RAD)实践提供了一个开发环境,其中业务领域和 UI 得到简化,以便快速获得产品负责人的反馈;我们在前几章中讨论过,在项目的这个阶段不需要开发完整的后端基础架构。持久层或通信层的开发应推迟到领域模型稳定之后。不过,设计服务时最好能确保在后续 WCF 实现阶段不会出现问题;eDirectory 解决方案通过引入 DTO,解决了后期的大部分问题。
最初设想本系列这部分内容将分三章来介绍,但最终决定只写一章。因此,本章将是系列中篇幅最长的一章之一,希望读者不会觉得有问题。本章主要内容如下:
- 进程内 WCF 测试
- WCF 服务实现和动态客户端代理编程
- WCF 请求上下文实现
- 客户端异步命令
关于进程内 WCF 测试的部分,演示了使用 WCF 执行服务是多么容易;这是如何在 WCF 上实现单元测试的一个很好的例子。此外,我们还将演示如何增强现有测试,使其也能使用 WCF 运行。目的是验证在 WCF 上运行的服务能够通过“网络”发送我们的通信对象,并且序列化不会失败。事实上,该设计非常灵活,您甚至可以使用 WCF 运行内存模式或 NHibernate 模式的服务。这不是很棒吗?
在本章的后面部分,我们将讨论需要进行哪些修改,以便在客户端和服务器之间拆分组件。总的来说,我们将创建一个 WCF 服务网站项目,讨论客户端需要做哪些更改,以及客户端和服务器在配置方面需要进行哪些修改。本节最后将介绍如何在本系列中首次实现服务器和客户端在不同进程中运行。
本章最后将实现一个自定义的 WCF 请求上下文,用于处理服务器端为每个请求创建服务实例的过程。
进程内 WCF 测试
我们将看到,在单个进程内创建一个执行 WCF 服务的测试是相对容易的,这简化了单元测试的创建和执行。我们需要做的是在同一进程中执行 WCF 服务器和客户端,顺便说一下,这并非标准的 WCF 配置,想必很多人都知道。如前所述,进行调用 WCF 的测试是为了验证通信对象的序列化,这在使用 WCF 时可能是最重要的方面之一。
正如前几章所解释的,在 eDirectory 解决方案中,我们不依赖标准的 WCF 基础架构来传达业务警告和异常;所有通信对象都要求继承自一个基类,该基类提供了用于存放警告和异常的容器。在讨论业务服务之前,我们应该为这个通信基础架构创建测试。在测试项目中,我们为此声明了一个新服务,它公开了一些方法,以确保我们能正确处理业务异常、警告以及应用程序异常。
namespace eDirectory.UnitTests.WCF
{
[ServiceContract(Namespace = "http://eDirectory/testservices/")]
public interface ITestService
: IContract
{
[OperationContract]
DtoResponse MethodThrowsBusinessException();
[OperationContract]
DtoResponse MethodReturnsBusinessWarning();
[OperationContract]
DtoResponse MethodReturnsApplicationException();
}
}
测试服务的实现非常直接,请查看源代码以了解 TestService
类的完整实现。
namespace eDirectory.UnitTests.WCF
{
public class TestService
: ServiceBase, ITestService
{
#region Implementation of ITestService
...
#endregion
}
}
我们的测试将需要创建 WCF 服务和客户端通道,WcfServiceHost
为此提供了一组静态辅助方法。StartService
方法用于创建服务实例;一个 net pipe 端点是使用接口类型名称创建的。
public class WcfServiceHost
{
private static readonly IDictionary<Type, ServiceHost>
ServiceHosts = new Dictionary<Type, ServiceHost>();
private static readonly IDictionary<Type, ChannelFactory>
ChannelFactories = new Dictionary<Type, ChannelFactory>();
public static void StartService<TService, TInterface>()
where TInterface : IContract
{
var serviceType = typeof(TService);
var interfaceType = typeof(TInterface);
if (ServiceHosts.ContainsKey(serviceType)) return;
01 var strUri = @"net.pipe:///" + interfaceType.Name;
02 var instance = new ServiceHost(serviceType, new Uri(strUri));
03 instance.AddServiceEndpoint(interfaceType, new NetNamedPipeBinding(), strUri);
04 instance.Open();
05 ServiceHosts.Add(interfaceType, instance);
}
...
}
在上面的代码片段中,第 (01) 行,我们使用传入的接口来创建一个唯一的 URI。在第 (02)、(03) 和 (04) 行,创建了一个 NetNamedPipe 端点。第 (05) 行将该端点添加到一个静态服务哈希表中,以便我们稍后可以获取该实例的引用。
如果我们需要停止上述端点,可以使用以下方法:
public class WcfServiceHost
{
private static readonly IDictionary<Type, ServiceHost>
ServiceHosts = new Dictionary<Type, ServiceHost>();
private static readonly IDictionary<Type, ChannelFactory>
ChannelFactories = new Dictionary<Type, ChannelFactory>();
...
public static void StopService<TInterface>()
{
var type = typeof(TInterface);
if (!ServiceHosts.ContainsKey(type)) return;
var instance = ServiceHosts[type];
StopChannel<TInterface>();
if (instance.State != CommunicationState.Closed) instance.Close();
ServiceHosts.Remove(type);
}
...
}
对于客户端,提供了两个方法,InvokeService
是此实现中的关键方法。泛型、服务接口和 Action 委托的结合提供了一种在 WCF 上轻松执行服务的方法。让我们看看这是如何实现的:
public class WcfServiceHost
{
private static readonly IDictionary<Type, ServiceHost>
ServiceHosts = new Dictionary<Type, ServiceHost>();
private static readonly IDictionary<Type, ChannelFactory>
ChannelFactories = new Dictionary<Type, ChannelFactory>();
...
public static void InvokeService<T>(Action<T> action) where T : IContract
{
01 var factory = GetFactory(action);
02 var client = factory.CreateChannel();
03 action.Invoke(client);
}
public static ChannelFactory<T> GetFactory<T>(Action<T> action)
where T : IContract
{
var type = typeof (T);
04 if (ChannelFactories.ContainsKey(type))
return ChannelFactories[type] as ChannelFactory<T>;
05 var netPipeService = new ServiceEndpoint(
ContractDescription.GetContract(type),
new NetNamedPipeBinding(),
new EndpointAddress("net.pipe:///" + type.Name));
06 var factory = new ChannelFactory<T>(netPipeService);
ChannelFactories.Add(type, factory);
return factory;
}
}
为了清晰起见,一个测试执行的例子可能有助于解释其工作原理:
[TestClass]
public class InfrastructureTests
:eDirectoryTestBase
{
...
[TestMethod]
public void ServiceReturnsWarning()
{
10 WcfServiceHost.InvokeService<ITestService>(ServiceReturnsWarningCommand);
}
private void ServiceReturnsWarningCommand(ITestService service)
{
11 var result = service.MethodReturnsBusinessWarning();
Assert.IsTrue(result.Response.HasWarning, "A warning was expected");
Assert.IsTrue(result.Response.BusinessWarnings.Count() == 1,
"Only one warning was expected");
Assert.IsTrue(result.Response.BusinessWarnings.First().Message.Equals(
"Warning was added"));
}
...
}
让我们跟随测试的执行过程,这样可能会更容易理解:
- 第 (10) 行调用
WcfServiceHost.InvokeService
,指明要执行的动作和服务接口。这意味着ServiceReturnsWarningCommand
需要一个实现了该服务接口的实例引用,这将是我们的 WCF 客户端代理。 InvokeService
在第 (01) 行通过调用GetFactory
方法获取一个 WCF 通道工厂的实例。- 在第 (02) 行,使用该工厂创建了一个客户端代理。
- 在第 (03) 行,传入的委托,即
ServiceReturnsWarningCommand
,被调用,并传入了客户端代理。 - 代理 (
service
) 在第 (11) 行被用来调用服务器方法。
这种结合使用委托和泛型的模式在 eDirectory 解决方案中被频繁使用;它被证明非常强大,但对于新手来说可能需要一些时间来完全理解。如果您是这种情况,您可能需要花一些时间调试测试,直到您完全适应这种方法。
第 (05) 行需要与服务器端点定义匹配,这就是为什么我们在 URI 定义中使用接口类型名称的原因。
我们现在可以定义一个在编写 WCF 测试时需要遵循的模板:
- 在测试开始前初始化 WCF 服务器端点
- 测试结束时,我们关闭服务器端点
因此,对于 InfrastructureTests
,我们可以利用测试框架的功能来实现上述模板:
namespace eDirectory.UnitTests.WCF
{
[TestClass]
public class InfrastructureTests
:eDirectoryTestBase
{
[TestInitialize]
public override void TestsInitialize()
{
base.TestsInitialize();
WcfServiceHost.StartService<TestService, ITestService>();
}
[TestCleanup]
public override void TestCleanUp()
{
WcfServiceHost.StopService<ITestService>();
base.TestCleanUp();
}
...
}
}
在本节结束之前,我们将演示“升级”我们的测试以使用 WCF 执行需要多小的努力:
namespace eDirectory.UnitTests.WCF
{
[TestClass]
public class CustomerServiceWcfTests
01 : CustomerServiceTests
{
[TestInitialize]
02 public override void TestsInitialize()
{
base.TestsInitialize();
WcfServiceHost.StartService<CustomerService, ICustomerService>();
}
03 [TestCleanup]
public override void TestCleanUp()
{
WcfServiceHost.StopService<ICustomerService>();
base.TestCleanUp();
}
[TestMethod]
04 public override void CreateCustomer()
{
05 ExecuteBaseMethod(base.CreateCustomer);
}
[TestMethod]
public override void FindAll()
{
ExecuteBaseMethod(base.FindAll);
}
[TestMethod]
public override void CheckFindAllNotification()
{
ExecuteBaseMethod(base.CheckFindAllNotification);
}
[TestMethod]
public override void UpdateCustomer()
{
ExecuteBaseMethod(base.UpdateCustomer);
}
private void ExecuteBaseMethod(Action action)
{
06 WcfServiceHost.InvokeService<ICustomerService>
(
service =>
{
this.Service = service;
action.Invoke();
}
);
}
}
}
就是这样,我们需要继承自原始测试类(第 (01) 行),以便我们可以如前所述初始化端点(第 (02) 行)。该测试类提供了一个名为 ExecuteBaseMethod
的辅助方法,它将调用委托给前面提到的 WcfServiceHost.InvokeService
辅助方法。你可能还注意到,我们已经将原始类的方法声明为 virtual,以便它们可以在 WCF 测试实现中被重写。第 (05) 行演示了我们如何利用原始测试类中的逻辑来复用我们的测试。还不错吧。
WCF 服务实现
值得注意的是,在上一节中,我们讨论了如何在 WCF 上测试我们的服务,而无需实现 WCF 服务器项目或对客户端进行任何更改;这种方法允许我们预先测试服务,将完整的实现推迟到后期阶段。
但总有一天,我们会遇到瓶颈,必须为我们的客户端提供一个使用 WCF 的服务器。本节讨论了构建一个公开服务器方法的 WCF 网站需要什么。它还讨论了客户端需要进行哪些更改,以便自动生成 WCF 代理,而无需使用 Web 引用和 Visual Studio 生成的代理。在最后阶段,我们将简要介绍服务器和客户端的配置设置。在继续之前,有几点需要澄清:
- 客户端引用了 eDirectory.Common 程序集。
- 我们不在客户端使用 VS 生成的代理,而是通过编程方式创建我们的 WCF 代理。
- 我们将在服务器端声明方法,以便为每个请求创建一个 WCF 实例;也就是说,我们使用
PerCall
方法。
目的是拥有两个应用程序,即客户端和一个作为 Web 服务器运行的 WCF 服务器。客户端相对简单:
服务器是大部分组件所在的地方:
我们需要声明一个新项目;这将是一个 WCF 网站项目,最终将部署在 IIS 上。实现相对简单:
- 每个服务器服务都需要一个 SVC 文件。
- 在服务的代码隐藏文件中,我们指出该类继承自
eDirectory.Domain
程序集中的服务类。 - 每个服务器服务都被配置为调用一个工厂辅助类:
ServiceFactory
就是这样,这将是一个代码量相对较少的项目,更多的是关于配置,这可能也是它应该有的样子。最重要的方面是 ServiceFactory
,它的主要职责是确保任何基础架构组件(如依赖注入容器)的启动。实现如下:
namespace eDirectory.WcfService
{
public class ServiceFactory : ServiceHostFactory
{
static readonly Object ServiceLock = new object();
static bool IsInitialised;
protected override ServiceHost CreateServiceHost(
Type serviceType, Uri[] baseAddresses)
{
01 if (!IsInitialised) InitialiseService();
return base.CreateServiceHost(serviceType, baseAddresses);
}
private void InitialiseService()
{
lock (ServiceLock)
{
// check again ... cover for a race scenario
if (IsInitialised) return;
InitialiseDependecy();
IsInitialised = true;
}
}
private void InitialiseDependecy()
{
string spring = @"file://" + HttpRuntime.AppDomainAppPath +
ConfigurationManager.AppSettings.Get("SpringConfigFile");
DiContext.AppContext = new XmlApplicationContext(spring);
}
}
}
因此,在第 (01) 行,当服务主机即将被创建时,会调用 InitialiseService
方法,该方法会创建 Spring.Net 容器。这个项目中没有太多别的内容了,服务类通过服务声明语句链接到这个工厂类:
<%@ ServiceHost Language="C#"
Debug="true"
Service="eDirectory.WcfService.CustomerWcfService"
CodeBehind="CustomerWcfService.svc.cs"
Factory="eDirectory.WcfService.ServiceFactory" %>
而代码隐藏文件再简单不过了:
namespace eDirectory.WcfService
{
public class CustomerWcfService
: CustomerService
{
}
}
最后一个方面是 WCF 配置设置:与任何其他 WCF 解决方案一样,在设置配置文件方面需要一些努力。对于服务器,我们有:
<?xml version="1.0"?>
<configuration>
<appSettings>
<!-- Use the following configuration file to execute the client with memory entities-->
01 <add key="SpringConfigFile" value="ServerInMemoryConfiguration.xml"/>
<!--<add key="SpringConfigFile" value="NhClientConfiguration.xml"/>-->
</appSettings>
<system.web>
...
</system.web>
<system.serviceModel>
<services>
02 <service name="eDirectory.WcfService.CustomerWcfService"
behaviorConfiguration="eDirectory.WcfServiceBehaviour">
<endpoint address="CustomerServices" binding="basicHttpBinding"
bindingConfiguration="eDirectoryBasicHttpEndpointBinding"
contract="eDirectory.Common.ServiceContract.ICustomerService" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="eDirectory.WcfServiceBehaviour">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<basicHttpBinding>
<binding name="eDirectoryBasicHttpEndpointBinding">
<!--<security mode="TransportCredentialOnly">
<transport clientCredentialType="Windows"/>
</security>-->
</binding>
</basicHttpBinding>
</bindings>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
</modules>
</system.webServer>
</configuration>
上面的配置文件中有一些干扰信息,但还不算太糟。在第 (01) 行,我们指示希望在服务器端使用内存中的仓储,而不是 NHibernate 的。第 (02) 行声明了 Customer 服务的服务器端点;在这个例子中,使用了 basicHttp
,但你也可以使用任何你想要的其他方式。客户端也需要进行修改:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
01 <add key="SpringConfigFile" value="file://WcfConfiguration.xml" />
</appSettings>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="eDirectoryBasicHttpBinding" />
</basicHttpBinding>
</bindings>
<client>
02 <endpoint address="https://:40003/CustomerWcfService.svc/CustomerServices"
binding="basicHttpBinding"
contract="eDirectory.Common.ServiceContract.ICustomerService"
name="BasicHttpBinding_ICustomerService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
</client>
</system.serviceModel>
</configuration>
在第 (02) 行,我们声明了 eDirectory.WcfService Web 项目中默认定义的端点地址;当服务器组件部署到正式的 IIS 环境时,您需要更改这个值。第 (01) 行表示客户端正在 WCF 模式下运行;我们需要快速看一下这个实现,因为这是生产环境的实现。
<?xml version="1.0" encoding="utf-8" ?>
<!--
WCF Configuration
=================
-->
<objects xmlns="http://www.springframework.net">
<!-- CLIENT SERVICE LOCATOR -->
<object
id="ClientServiceLocatorRef"
type="eDirectory.WPF.Services.ClientServiceLocator, eDirectory.WPF"
factory-method="Instance"
singleton="true">
<property name="ContractLocator" ref="ClientContractLocatorRef" />
</object>
<!-- Client Contract Locator -->
<object
id="ClientContractLocatorRef"
01 type="eDirectory.WPF.Services.ClientContractLocator, eDirectory.WPF">
<property name="NextAdapterLocator" ref="ContractLocatorRef" />
</object>
<!-- Next Adapter Locator -->
<object
id="ContractLocatorRef"
02 type="eDirectory.WPF.Services.Wcf.WcfContractLocator, eDirectory.WPF" />
</objects>
与 NHibernate 或内存模式的配置文件相比,这可能是一个小而整洁的 Spring.Net 配置文件;好消息是这是生产版本。在此实现中,我们不再需要在客户端部署任何服务器程序集。第 (02) 行指明在此应用程序模式下使用 WcfContractLocator
。该类尚未实现,它是调用 WCF 服务的关键。客户端不依赖于 Visual Studio 从 Web 引用自动生成的 WCF 代理;事实上,客户端根本没有这样的引用。对某些人来说,这可能令人惊讶。相反,我们动态地构建代理;唯一需要的代码位于一个名为 WcfAdapterBase
的基类中。
namespace eDirectory.WPF.Services.Wcf
{
public class WcfAdapterBase<TContract> where TContract: class, IContract
{
01 private class WcfProxy<TService>:
ClientBase<TService> where TService : class, IContract
{
public TService WcfChannel
{
get
{
02 return this.Channel;
}
}
}
03 protected TResult ExecuteCommand<TResult>(Func<TContract, TResult> command)
where TResult : IDtoResponseEnvelop
{
04 var proxy = new WcfProxy<TContract>();
try
{
05 var result = command.Invoke(proxy.WcfChannel);
proxy.Close();
return result;
}
catch (Exception)
{
proxy.Abort();
throw;
}
}
}
}
这是另一个公开 Execute
方法的实现。该方法的签名此时应该已经很熟悉了,所以我们不再讨论它。这里最重要的方面是第 (04) 行,这里使用泛型私有类 WcfProxy
生成了一个客户端代理。在第 (05) 行,我们调用 command
委托,并传入一个指向 WcfProxy
实例的私有 Channel
的引用,这也是创建 WcfProxy
类的主要原因。
对于每个服务,我们需要创建一个继承自该类并实现契约接口的类;对于我们的客户端服务,我们有:
namespace eDirectory.WPF.Services.Wcf
{
public class CustomerServiceProxy
:WcfAdapterBase<ICustomerService>, ICustomerService
{
#region Implementation of ICustomerService
public CustomerDto CreateNewCustomer(CustomerDto customer)
{
return ExecuteCommand(proxy => proxy.CreateNewCustomer(customer));
}
public CustomerDto GetById(long id)
{
return ExecuteCommand(proxy => proxy.GetById(id));
}
public CustomerDto UpdateCustomer(CustomerDto customer)
{
return ExecuteCommand(proxy => proxy.UpdateCustomer(customer));
}
public CustomerDtos FindAll()
{
return ExecuteCommand(proxy => proxy.FindAll());
}
#endregion
}
}
上面的代码没什么特别的。可以看到,CustomerServiceProxy
类的唯一目的就是将执行委托给 ExecuteCommand
方法。最后一部分代码是 WcfContractLocator
,它也很直接:
namespace eDirectory.WPF.Services.Wcf
{
public class WcfContractLocator
:IContractLocator
{
private ICustomerService CustomerServiceProxyInstance;
public ICustomerService CustomerServices
{
get
{
if (CustomerServiceProxyInstance != null)
return CustomerServiceProxyInstance;
CustomerServiceProxyInstance = new CustomerServiceProxy();
return CustomerServiceProxyInstance;
}
}
}
}
到此阶段,我们可以尝试让客户端连接服务器运行。检查客户端的配置,确保它设置为使用 WCF 模式。将 eDirectory.WcfService 设置为“启动项目”并按 Shift+F5,然后将 eDirectory.WPF 客户端更改为“启动项目”并按 F5。如果客户端正常工作(希望如此),那么它就是通过 WCF 运行的。这基本上就是您的应用程序在生产模式下运行的样子。还不错。
请求实现
在本系列开始时,我们讨论了在服务器端需要全局上下文和请求上下文;我们的大多数服务是全局的,但当需要为每个请求存储状态时,我们处理的是需要由请求上下文处理的服务。需要发生的是,每次在服务器端处理请求时,都需要创建旨在用于请求上下文的任何服务。WCF 的可扩展性为这类增强功能提供了一个出色的框架;在我们的例子中,我们将实现自己的实例上下文扩展。实现这类解决方案时需要以下组件:
- 实现
IExtension
;在我们的例子中,我们称之为:InstanceCreationExtension
- 实现
IInstanceContextInitializer
;我们将其命名为:InstanceCreationInitializer
- 创建一个实现
IContractBehavior
的特性(attribute);在我们的例子中,该特性名为InstanceCreationAttribute
在职责方面:
InstanceCreationExtension
是关键组件,因为它持有一个需要在请求中创建的服务实例。在我们的例子中,它创建了一个BusinessNotifier
实例。InstanceCreationInitializer
负责声明在服务器端处理请求时需要向实例上下文添加哪些扩展。InstanceCreationAttribute
是我们的服务实现和InstanceCreationInitializer
之间的“粘合剂”。
让我们看看实现。这个扩展很简单,除了构造函数;该实现订阅了 InstanceContext.Closed
事件,以便在请求结束时可以进行一些基本的清理工作。
namespace eDirectory.Domain.AppServices.WcfRequestContext
{
public class InstanceCreationExtension : IExtension<InstanceContext>
{
public DateTime InstanceCreated { get; private set; }
public BusinessNotifier Notifier { get; private set; }
public InstanceCreationExtension(DateTime instanceCreated)
{
InstanceCreated = instanceCreated;
Notifier = new BusinessNotifier();
}
#region IExtension<InstanceContext> Members
public void Attach(InstanceContext owner)
{
// Make sure we detach when the owner is closed
owner.Closed += (sender, args) => Detach((InstanceContext)sender);
}
public void Detach(InstanceContext owner)
{
Notifier = null;
}
#endregion
}
}
关于 InstanceCreationInitializer
的实现,没什么太多可说的。
namespace eDirectory.Domain.AppServices.WcfRequestContext
{
public class InstanceCreationInitializer : IInstanceContextInitializer
{
#region IInstanceContextInitializer Members
public void Initialize(InstanceContext instanceContext, Message message)
{
// Add extension which contains the new instance creation index
instanceContext.Extensions.Add(new InstanceCreationExtension(DateTime.Now));
}
#endregion
}
}
然后,特性类也很简单:
namespace eDirectory.Domain.AppServices.WcfRequestContext
{
public class InstanceCreationAttribute : Attribute, IContractBehavior
{
#region IContractBehavior Members
public void AddBindingParameters(ContractDescription contractDescription,
ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
}
public void ApplyDispatchBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
{
dispatchRuntime.InstanceContextInitializers.Add(
new InstanceCreationInitializer());
}
public void Validate(ContractDescription contractDescription,
ServiceEndpoint endpoint)
{
}
#endregion
}
}
我们还需要为 IRequestContext
定义一个新的实现,以便我们的业务代码可以为给定的请求解析服务。值得注意的是,这只应在运行 WCF 服务时使用,否则您可能会遇到一些问题。
namespace eDirectory.Domain.AppServices.WcfRequestContext
{
public class RequestContext
: IRequestContext
{
public IBusinessNotifier Notifier
{
get
{
InstanceContext ic = OperationContext.Current.InstanceContext;
InstanceCreationExtension extension =
ic.Extensions.Find<InstanceCreationExtension>();
return extension.Notifier;
}
}
}
}
最后一个方面是修改我们的服务,以便我们指明需要使用自定义的扩展:
namespace eDirectory.Domain.Services
{
[ServiceBehavior(InstanceContextMode =
InstanceContextMode.PerCall)]
[InstanceCreation]
public class CustomerService
:ServiceBase, ICustomerService
{
...
}
}
现在,当 WCF 客户端调用任何客户服务方法时,自定义扩展会创建一个新的 BusinessNotifier
实例,这样我们就可以只在请求级别存储状态,从而将各个请求隔离开来。您可能会发现这种模式对于安全性、审计或其他需要在每个请求开始时调用的任务非常有用。
客户端命令分发器 - 异步命令
一旦我们开始处理 WCF,有一个客户端方面的问题可能会变得更加明显。当客户端执行服务代理时,它是在 UI 线程上执行的,这并不是一个好主意,因为它会冻结 UI,如果请求需要几秒钟,用户可能很快就会不高兴。因此,需要重构服务的执行方式。ServiceAdapterBase
类是添加新代码的理想候选者。
namespace eDirectory.WPF.Services
{
abstract class ServiceAdapterBase<TService> where TService: IContract
{
protected TService Service;
protected ServiceAdapterBase(TService service)
{
Service = service;
}
public TResult ExecuteCommand<TResult>(Func<TResult> command)
where TResult : IDtoResponseEnvelop
{
try
{
01 Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
TResult result = DispatchCommand(command);
...
return result;
}
finally
{
02 Mouse.OverrideCursor = null;
}
}
private static TResult DispatchCommand<TResult>(Func<TResult> command)
{
03 var asynchResult = command.BeginInvoke(null, null);
while (!asynchResult.IsCompleted)
{
04 Application.DoEvents();
Thread.CurrentThread.Join(50);
}
return command.EndInvoke(asynchResult);
}
}
}
重构后,我们可以看到发生了两件新事情:执行方法时鼠标指针会改变,并且命令以异步方式执行。指针在第 (01) 和 (02) 行得到了很好的处理。新的 DispatchCommand
方法执行命令并等待其完成,第 (04) 行是一个丑陋的解决方案,但它确实起作用了。(有谁能建议一个更好的方法吗?)
在 WPF 类中,当应用这种新行为时,会出现一个新问题;当一个操作被执行时,没有任何东西阻止用户点击其他东西;这可能不是预期的行为,所以我们需要处理某种状态,然后相应地更改 UI 控件。为此,我们在 CustomerModel
中添加了一个名为 IsEnabled
的新字段。如果该字段为 false
(保存按钮会改变此状态),则“新客户”分组框和“刷新”按钮将被禁用。我们使用 XAML 绑定而不是任何代码隐藏;视图中的代码隐藏应始终是最后的手段。
<Window x:Class="eDirectory.WPF.Customer.View.CustomerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="Customer View" Height="400"
Width="400" WindowStartupLocation="CenterScreen">
...
<GroupBox Header="New Customer" Margin="5"
Padding="5" Grid.Row="0"
IsEnabled="{Binding Path=Model.IsEnabled}">
...
<Button Grid.Row="1" Padding="5"
Margin="5" Command="{Binding RefreshCommand}"
IsEnabled="{Binding Path=Model.IsEnabled}">Refresh</Button>
...
章节总结
本章为本系列树立了一个重要的里程碑。除了一些次要方面,我们已经涵盖了标准企业应用程序所需的大部分组件。本系列不打算成为 WPF 解决方案的最佳范例,也不是 NHibernate 集成的最佳范例;然而,它展示了如何遵循 RAD、TDD 和 DDD 的最佳实践来集成三个主要的企业框架。
在下一章中,我们将重构业务领域,以创建一个父子关系;这意味着更复杂的 UI、一个新的服务以及领域中的一些变化。
我们希望得到关于未来系列内容的反馈;我们可以涵盖诸如验证、在客户端实现业务警告和异常,或者使用 DTO 的客户端报表服务等方面。但这些可能对大多数人来说并不那么重要,或许,我们可以转而研究 Silverlight 客户端。