| | |
Chapter XI | | Chapter XIII |
The series
WCF by Example is a series of articles that describe how to design and develop a WPF client using WCF for communication and NHibernate for persistence purposes. The series introduction describes the scope of the articles and discusses the architect solution at a high level. The source code for the series is found at CodePlex.
Chapter overview
For the first time, besides the given name to the series, we will cover the WCF implementation. As indicated previously, we have provided a development environment for RAD practices where the business domain and UI are streamlined so feedback from the product owners is obtained in a quick fashion; we have discussed in previous chapters that development of the full back-end infrastructure is not required at this stage in the project. Development on the persistence or communication layers should be postponed until a stable domain is available. Although, it is a good practice to design services that will prove not to be problematic later when the WCF implementation takes place; the eDirectory solution, with the inclusion of DTOs, resolves the majority of those problems at the later stage.
It was originally envisaged that this section of the series will be covered in three different chapters, instead only one is to be produced. As a result of this decision, the chapter will be one of the largest so far in the series, we just hope this is not an issue for the readers. The main sections for the chapter are the following:
- In-proc WCF testing
- WCF services implementation and programmatic dynamic client proxies
- WCF request context implementation
- Client async commands
The section about in-proc WCF testing demonstrates how easy it is to execute the services using WCF; it is a good example of how to implement unit testing on WCF. Also, we will demonstrate how we can enhance our existing tests so they can also run using WCF. The purpose is to validate that the services running on WCF can send our communication objects on the "wire" and that the serialization is not failing. In fact, the design is so flexible that you could execute services using WCF with the in-memory or NHibernate mode. Isn't that nice?.
At a later stage in this chapter, we discuss what modifications are required to split our components between the client and the server. In summary, we will create a WCF Service Web Site project, discuss what changes are required in the client side, and which modifications are required at the client and the server in configuration terms. The section finishes describing how to run the server and the client running in different process for the first time in the series.
The chapter is then finished with the implementation of a customised WCF request context which handles the creation of service instances for each request processed in the server side.
In-proc WCF tests
We are going to see how it is relatively easy to create a test executing WCF services within one single process, which simplifies the creation and execution of unit tests. What we need is to execute the WCF server and client in the same process, which by the way, as many of you may know, is not the standard WCF configuration. As mentioned previously, the reason for having tests invoking WCF validates the serialization of the communication objects, which is probably one of the most important aspects when working with WCF.
As explained in previous chapters, in the eDirectory solution, we don't rely on the standard WCF infrastructure to communicate business warnings and exceptions; all communication objects are required to inherit from a base class that provides holders for warnings and exceptions. We should create tests for this communication infrastructure before we cover the business services. In the test project, we have declared a new service for this purpose, it exposes methods to ensure that we correctly handle business exceptions and warnings and application exceptions:
namespace eDirectory.UnitTests.WCF
{
[ServiceContract(Namespace = "http://eDirectory/testservices/")]
public interface ITestService
: IContract
{
[OperationContract]
DtoResponse MethodThrowsBusinessException();
[OperationContract]
DtoResponse MethodReturnsBusinessWarning();
[OperationContract]
DtoResponse MethodReturnsApplicationException();
}
}
The implementation of the test service is very straightforward, so please look at the source code for the complete implementation of the TestService
class:
namespace eDirectory.UnitTests.WCF
{
public class TestService
: ServiceBase, ITestService
{
#region Implementation of ITestService
...
#endregion
}
}
Our tests will require to create WCF Services and client channels, the WcfServiceHost
provides a set of static helpers for this purpose. The StartService
method is used for the creation of service instances; a net pipe endpoint is created using the interface type name:
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://localhost/" + 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);
}
...
}
In the above code snippet, in Line (01), we use the passed interface to create a unique URI. In Lines (02, 03 and 04), a NetNamedPipe endpoint is created. Line (05) adds the endpoint to a static service hashmap so we can get a reference to the instance at a later stage.
If we need to stop the above endpoint, the following method can be used:
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);
}
...
}
For the client side, two methods are provided, the InvokeService
is the key method in this implementation. The combination of generics, service interfaces, and action delegates provides an easy way to execute services on WCF. Let's see how this is achieved:
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://localhost/" + type.Name));
06 var factory = new ChannelFactory<T>(netPipeService);
ChannelFactories.Add(type, factory);
return factory;
}
}
For clarity purposes, an example of the execution of one of the tests may help to explain how it works:
[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"));
}
...
}
Let's follow the test execution, it may prove to be easier:
- Line (10) invokes the
WcfServiceHost.InvokeService
indicating the action to execute and the service interface. This implies that the ServiceReturnsWarningCommand
expects a reference to an instance that implements the service interface, this will be our WCF client proxy. InvokeService
at Line (01) retrieves an instance of a WCF channel factory calling the GetFactory
method.- At Line (02), a client proxy is created using the factory.
- At Line (03), the passed delegate, the
ServiceReturnsWarningCommand
is called passing the client proxy. - The proxy (
service
) is used to call the server method at Line (11).
This pattern where a combination of delegates and generics are used is constantly used in the eDirectory solution; it proves to be very powerful, but it may take a little bit of time for new comers to fully understand. If that is your case, you may want to spend a little bit of time debugging the tests until you are fully comfortable with this approach.
Line (05) needs to match to the server endpoint definition, this is why we use the interface type name in the URI definition.
We can now define a sort of template that we need to follow when writing WCF tests:
- Initialise the WCF server endpoint before the test starts
- At the end of the test, we close the server endpoint
So for InfrastructureTests
, we can leverage the test framework capabilities to achieve the mentioned template:
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();
}
...
}
}
Before we finish this section, we are going to demonstrate how little effort is required to "upgrade" our tests so they execute using 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();
}
);
}
}
}
So that is all, we need to inherit from the original test class (line (01)) so we can initialise (line (02)) the endpoint as discussed. The test class provides a helper method named ExecuteBaseMethod
which delegates to the already mentioned WcfServiceHost.InvokeService
helper method. You also want to notice that we have changed the original class' methods to be declared virtual so they can be overridden on the WCF test implementation. Line (05) demonstrates how we can leverage our tests reusing the logic in the original test class. Not too bad.
WCF services implementation
It is worth noting that in the previous section, we discussed how to test our services on WCF without having to implement a WCF Server project or doing any changes on the client side; this approach permits the upfront testing of our services, postponing the full implementation for a later stage.
But at some stage, we will hit the wall and we will have to provide a server using WCF for our clients. This section discusses what is required to build a WCF web site exposing the server methods. It also discusses which changes are required on the client side, so WCF proxies are automatically generated without the use of a Web reference and Visual Studio generated proxies. In the final stage, we will briefly cover the server and client configuration settings. Before we continue, it is important to clarify some points:
- The client has a reference to the the eDirectory.Common assembly
- We don't use VS generated proxies on the client side, instead we create our WCF proxies programmatically
- We will declare the methods on the server side, so for each request, a WCF instance is created; that is, we use
PerCall
methods
The purpose is to have two applications, the Client and a WCF Server running as a web server. The client is relatively simple:
The server is where most components are found:
We need to declare a new project; this is going to be a WCF Web Site project that eventually will be deployed on IIS. The implementation is relatively easy:
- Each server service requires an SVC file
- In the code-behind of the service, we indicate that the class inherits from the service class in the
eDirectory.Domain
assembly - Each server service is configured to invoke a factory helper class:
ServiceFactory
And that is all, this is going to be a project with relatively little code, it is more about configuration which is probably the way it is supposed to be. The most important aspect is ServiceFactory
, its main responsibility is to ensure the start-up of any infrastructure component like the Dependency Injection container. The implementation is as follows:
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)
{
if (IsInitialised) return;
InitialiseDependecy();
IsInitialised = true;
}
}
private void InitialiseDependecy()
{
string spring = @"file://" + HttpRuntime.AppDomainAppPath +
ConfigurationManager.AppSettings.Get("SpringConfigFile");
DiContext.AppContext = new XmlApplicationContext(spring);
}
}
}
So at Line (01), when the service host is about to be created, the InitialiseService
method is called which creates the Spring.Net container. There is not much more story in this project, the service classes are linked to this factory class using the service declaration statement:
<%@ ServiceHost Language="C#"
Debug="true"
Service="eDirectory.WcfService.CustomerWcfService"
CodeBehind="CustomerWcfService.svc.cs"
Factory="eDirectory.WcfService.ServiceFactory" %>
And the code-behind could not be simpler:
namespace eDirectory.WcfService
{
public class CustomerWcfService
: CustomerService
{
}
}
The last aspect is the WCF configuration settings: as with any other WCF solution, there is a little bit of effort in the setting of the configuration files. For the server, we have:
="1.0"
<configuration>
<appSettings>
01 <add key="SpringConfigFile" value="ServerInMemoryConfiguration.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">
</binding>
</basicHttpBinding>
</bindings>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
</modules>
</system.webServer>
</configuration>
There is a little noise in the above configuration file but it is not so bad. At Line(01), we indicate that we want to use the in-memory repositories in the server side instead of the NHibernate ones. Line (02) declares the server endpoint for the Customer services; in this example, basicHttp
is used, but you could use anything else you want. The client also needs to be modified:
="1.0"="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="http://localhost:40003/CustomerWcfService.svc/CustomerServices"
binding="basicHttpBinding"
contract="eDirectory.Common.ServiceContract.ICustomerService"
name="BasicHttpBinding_ICustomerService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
</client>
</system.serviceModel>
</configuration>
At Line (02), we declare the endpoint address that is defined by default on the eDirectory.WcfService web project; this is one value that you will need to change when the server components are deployed to a proper IIS environment. Line (01) indicates that the client is running in WCF mode; we need to have a quick look at this implementation as this is the production one:
="1.0"="utf-8"
<objects xmlns="http://www.springframework.net">
<object
id="ClientServiceLocatorRef"
type="eDirectory.WPF.Services.ClientServiceLocator, eDirectory.WPF"
factory-method="Instance"
singleton="true">
<property name="ContractLocator" ref="ClientContractLocatorRef" />
</object>
<object
id="ClientContractLocatorRef"
01 type="eDirectory.WPF.Services.ClientContractLocator, eDirectory.WPF">
<property name="NextAdapterLocator" ref="ContractLocatorRef" />
</object>
<object
id="ContractLocatorRef"
02 type="eDirectory.WPF.Services.Wcf.WcfContractLocator, eDirectory.WPF" />
</objects>
This is probably a small and tidy Spring.Net configuration file when we compare it to the NHibernate or the in-memory ones; the good news is that this is the production version. In this implementation, we don't require any server assembly deployed on the client side any longer. At Line (02) is indicated that WcfContractLocator
is used in this application mode. This class has not been implemented yet, and is the key for calling the WCF services. The client does not rely on WCF proxies auto generated by Visual Studio from a web reference; in fact, the client does not have such a reference at all. For some people, that could be surprising. Instead, we build the proxies dynamically; the only code required is in a base class named 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;
}
}
}
}
This is another implementation where an Execute
method is exposed. The signature of the method at this stage should be familiar, so we will not discuss it. The most important aspect here is Line (04), where a client proxy is generated using the generic private WcfProxy
class. At Line (05), we invoke the command
delegate passing a reference to the private Channel
of the WcfProxy
instance, which is the main reason for the WcfProxy
class to be created.
For each service, we need to create a class that inherits from this class and implements the contract interface; for our client services, we have:
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
}
}
There is not so much going on in the above code. As it can be seen, the only purpose for the CustomerServiceProxy
class is to delegate the execution to the ExecuteCommand
method. The last piece of code is the WcfContractLocator
, which is also straightforward:
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;
}
}
}
}
At this stage, we could try to execute the client against the server. Check the configuration for the client so it is set to use the WCF mode. Set eDirectory.WcfService as the "start-up" project and press Shift+F5, then change the eDirectory.WPF client to be the "start-up" project and press F5. If the client works, and hopefully that is the case, it is doing so using WCF. That is pretty much your application executing on production mode. Not too bad.
Request implementation
At the beginning of the series, we discussed the need for a Global and Request context at the server side; most of our services are global, but when there is a need to store state for each request, we are dealing with a service that needs to be handled by the Request context. What it needs to take place is that each time a request is processed on the server side, any service intended to be used on the Request context needs to be created. WCF extensibility provides an excellent framework for enhancements of this sort; in our case, we will implement our own instance context extension. The following components are required when implementing this type of solution:
- Implementation of
IExtension
; in our case, we'll call it: InstanceCreationExtension
- Implementation of
IInstanceContextInitializer
; we'll name this one: InstanceCreationInitializer
- Create an attribute implementing
IContractBehavior
; in our case, the attribute is called InstanceCreationAttribute
In terms of responsibility:
InstanceCreationExtension
is the key component as it holds an instance of the services that need to be created for the request. In our case, it does create a BusinessNotifier
instance.InstanceCreationInitializer
is responsible for declaring which extensions need to be added to the instance context when a request is processed on the server side.InstanceCreationAttribute
is the "glue" between our service implementations and our InstanceCreationInitializer
.
Let us see the implementations. The extension is simple, besides of the constructor; the implementation subscribes to InstanceContext.Closed
so some basic cleansing can be done when the request finishes:
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)
{
owner.Closed += (sender, args) => Detach((InstanceContext)sender);
}
public void Detach(InstanceContext owner)
{
Notifier = null;
}
#endregion
}
}
There is not too much to say about the InstanceCreationInitializer
implementation:
namespace eDirectory.Domain.AppServices.WcfRequestContext
{
public class InstanceCreationInitializer : IInstanceContextInitializer
{
#region IInstanceContextInitializer Members
public void Initialize(InstanceContext instanceContext, Message message)
{
instanceContext.Extensions.Add(new InstanceCreationExtension(DateTime.Now));
}
#endregion
}
}
Then the attribute class is also simple:
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
}
}
We also need to define a new implementation of IRequestContext
so our business code can resolve the service for the given request. It is worth noting that this should only be used when running WCF services, otherwise you may find some issues:
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;
}
}
}
}
The last aspect is to modify our services so we indicate that the customised extension needs to be used:
namespace eDirectory.Domain.Services
{
[ServiceBehavior(InstanceContextMode =
InstanceContextMode.PerCall)]
[InstanceCreation]
public class CustomerService
:ServiceBase, ICustomerService
{
...
}
}
Now when a WCF client calls any customer service method, the customised extension creates a new BusinessNotifier
instance so we can store state at request level only, isolating requests from each other. You may find this pattern very useful for security, auditing, or other tasks that need to be invoked at the start of each request.
Client Command Dispatcher - Async commands
There is one client aspect that may become more obvious once we start dealing with WCF. When the client executes a service proxy, it is doing so using the UI thread, something that is not a really good idea because it freezes the UI, and if the request takes a few seconds, the user may not be a happy one for too long. As a result, a re-factor of how the services are executed is needed. The ServiceAdapterBase
class is a good candidate for adding the new code:
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);
}
}
}
After the re-factor, we can see that two new things are taking place: the mouse pointer changes when a method is executed and the command is executed in an asynchronous manner. The pointer is nicely handled in Lines (01 and 02). The new DispatchCommand
method executes the command and waits until it finishes, Line (04) being an ugly solution, it does the trick. (Can anyone advise a better approach?)
In the WPF classes, when this new behavior is applied, a new problem arises; when an action is executed, there is nothing stopping the user from clicking on something else; this might not be the intended behavior, so we need to handle some sort of state and then change the UI controls accordingly. For this purpose, a new field is added to the CustomerModel
named IsEnabled
. If this field is false
(the Save button changes this state), then the "New Customer" groupbox and the "Refresh" button are disabled. We use XAML bindings instead of any code-behind; code-behind on the View must be always the last resource:
<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>
...
Chapter summary
This chapter sets a major milestone in the series. Besides some minor aspects, we have covered the bulk of the components that a standard enterprise application requires. The series does not intent to be the best example for a WPF solution, nor the best example for an NHibernate integration; however, it demonstrates how three main enterprise frameworks can be integrated following best practices for RAD, TDD, and DDD.
In the next chapter, we will re-factor the business domain so a parent-child relationship is created; this implies a more complex UI, a new service, and a few changes in the domain.
We would appreciate feedback where we should conduct the series in the next future; we could cover aspects like validation, implementation of business warnings, and exceptions on the client side, maybe client report services using DTOs. But all those may not be that relevant for most people, maybe, instead, we could look into a Silverlight client.