Introduction
This article discusses about some features of the service model of WCF which is part of .NET 3.0. It demonstrates how to host multiple instances of the same service and contract in a Windows Forms application. Those services are then consumed by a client form belonging to the same application and by the same client form but created from another application. It demonstrates how to create singleton services and consume them without using any configuration file.
Background
As I was abroad and I couldn't carry with me my model train control station, I decided to write a simple simulator so I could continue to work on the bigger software I'm designing and that drives the control station.
First of all, I had to create a simple simulation of the locomotives that run on the track and that the control station can pilot. As I recently attended a WCF training, I thought that it would be a good practice to write this part using the new WCF service model. Years ago, I would have written some COM servers to simulate the locomotives, but technology has evolved, and the new service model developed for .NET 3.0 is far more powerful than was COM.
In fact, what I want to simulate is the locomotive DCC decoder. This little piece of hardware is used to control a locomotive on a track referenced by its address. The control station sends commands to change the speed, the direction, and switch the lights. It also can read the status of the device by its address. It can easily be simulated by a singleton service. There is no problem of scalability as the locomotives are going to run on a single machine and their number won't be big, neither the connections to a given locomotive. First, I wanted to use a Sharable
instance for the service, but this instance mode has disappeared in the latest release of WCF…
Using the code: Service contract
Here is how the interface to simulate a simple locomotive decoder looks like:
Collapse
[ServiceContract]
public interface IDCCLocomotiveContract
{
[OperationContract(IsOneWay=false)]
Direction GetDirection();
[OperationContract]
void ChangeDirection(Direction direction);
[OperationContract]
void SetSpeed(byte speed);
[OperationContract(IsOneWay=false)]
byte GetSpeed();
[OperationContract]
void SwitchLight(bool state);
[OperationContract(IsOneWay=false)]
bool GetLightState();
}
Service implementation
WCF allows managing the instance behavior just by using an attribute parameter for the class that implements the service. There are three different instance modes. When using the PerCall
mode, every time you call the service, you get a fresh instance. That means that all data are lost between calls. The PerSession
mode maintains a session between each call until the proxy is released. The PerSession
is the default mode so you don't have to specify it. Finally, the Single
mode keeps the same instance of the service until the server itself is shutdown. Take note that in earlier versions of WCF (May CTP and before), the PerCall
was the default mode and that there was a Sharable
mode that doesn't exist anymore.
Here is how the implementation of this simple service looks like:
Collapse
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
class DCCLocomotiveService : IDCCLocomotiveContract, IDisposable
{
const byte
MaxSpeed = 28,
MinSpeed = 0;
protected byte m_speed = 0;
protected bool m_light = false;
protected Direction m_direction = Direction.Forward;
public Direction GetDirection()
{
return m_direction;
}
public void ChangeDirection(Direction direction)
{
m_direction = direction;
}
public void SetSpeed(byte speed)
{
if (speed >= MinSpeed && speed <= MaxSpeed)
m_speed = speed;
}
public byte GetSpeed()
{
return m_speed;
}
public void SwitchLight(bool state)
{
m_light = state;
}
public bool GetLightState()
{
return m_light;
}
}
Demo applications
I created two simple applications. The "locomotive factory" behaves like when you put the locomotive on the track, and the "Locomotives monitor" allows watching the parameters of the locomotives. The locomotive factory is a window application that starts one server per locomotive. Each server gets an address related to the address of the locomotive. It has one endpoint for the locomotive contract. I chose this implementation because I wanted to be able to stop the server like when you remove a locomotive from the track. For testing purposes, I made it possible to the factory to control the locomotives from a control dialog box. First, I wanted to use one service and several endpoints for the locomotives, but it was not possible because all the endpoints must be created before the service host is started.
I use an XML file that contains the list of the available locomotives. Basically, I use the address and the name. A checkbox list is created from that file. When you check an element, it starts the locomotive server and creates the endpoint for it. When you uncheck the element, it stops the server.
The main user interface of the "Locomotive factory" is shown at the beginning of the article.
A button allows sending commands to the given locomotive from a dialog box. This is where I found a strange behavior. In my first draft, I was creating the ServiceHost
instance in the same thread as the application. When I was calling a method on the IDCCLocomotiveContract
instance I got from a ChannelFactory
in the dialog box, it was failing with a timeout. However, the same call from the same ChannelFactory
from another application was working.
Hosting the Service
I designed a simple class that creates the ServiceHost
instance in a different thread than the one of the application. It seems that a Windows Forms application introduces restrictions due to the window messaging, and I suppose that there is some interference between this messaging and the one used by WCF. This class creates a Thread
that starts the ServiceHost
for my contracts and waits until the application stops it. It uses a simple pattern to stop it, with a boolean that triggers the end of the thread method. A thread must not be stopped using the Abort
method, because in our case, it must close the ServiceHost
. A call to Abort
would let the ServiceHost
open.
Collapse
class ThreadedServiceHost<TService, TContract>
{
const int SleepTime = 100;
private ServiceHost m_serviceHost = null;
private Thread m_thread;
private string
m_serviceAddress,
m_endpointAddress;
private bool m_running;
private Binding m_binding;
public ThreadedServiceHost(
string serviceAddress,
string endpointAddress, Binding binding)
{
m_binding = binding;
m_serviceAddress = serviceAddress;
m_endpointAddress = endpointAddress;
m_thread = new Thread(new ThreadStart(ThreadMethod));
m_thread.Start();
}
void ThreadMethod()
{
try
{
m_running = true;
m_serviceHost = new ServiceHost(
typeof(TService),
new Uri(m_serviceAddress));
m_serviceHost.AddServiceEndpoint(
typeof(TContract),
m_binding,
m_endpointAddress);
m_serviceHost.Open();
while (m_running)
{
Thread.Sleep(SleepTime);
}
m_serviceHost.Close();
}
catch (Exception)
{
if (m_serviceHost != null)
{
m_serviceHost.Close();
}
}
}
public void Stop()
{
lock (this)
{
m_running = false;
}
}
}
Proxy for the Service
An interesting issue of my application is that I don't know by advance the number of locomotives and their addresses, so I don't use any App.Config file in my construction. I don't use any proxy as well but the ChannelFactory
that allows creating a proxy on the fly. I use a NetTcpBinding
which is OK for a local service and can eventually be used in Windows distributed environment. The ChannelFactory
takes an interface derived from the contract of your service and the IChannel
interface. It then dynamically creates a proxy to your service that you can use as if you where using the interface of your contract.
Below is the code that I use to create the proxy with my service:
EndpointAddress endPoint = new EndpointAddress(
new Uri(string.Format(Constants.LocoServerBaseAddress,
address) + address));
System.ServiceModel.Channels.Binding binding = new NetTcpBinding();
m_dccLocoFactory = new ChannelFactory(binding, endPoint);
m_dccLocoFactory.Open();
m_locoChannel = m_dccLocoFactory.CreateChannel();
m_locoChannel.Open();
The interface to create this proxy using the ChannelFactory
is as follows:
interface IDCCLocomotiveChannel : IDCCLocomotiveContract, IClientChannel
{
}
The ChannelFactory
is used in a dialog box that can be configured either to send commands to the locomotive service or to watch the different parameters.
Here is a copy of the two versions of that dialog box which are in fact implemented by the same code:s
Locomotive control is a child window form of the Locomotive Factory while the Locomotive Monitor is a child window form of the Locomotives Monitor, a simple application that connects to a locomotive service, given its address.
Like for a real locomotive decoder, the locomotive service doesn't provide any notification. A locomotive decoder is a passive device that cannot send information to the control station by its own. In order to reflect changes in the different clients, each client must manage its own pooling.
Points of Interest
You can dig into the complete code of this simple application, and I hope it will give you some ideas of what you could do with the new service model of WCF. Of course, this is just a glimpse at this very powerful framework for Service Oriented Application (SOA). WCF is a very complete framework to develop SOA based applications in the Microsoft world. However, as it is based on open standards like WS-*, applications can be designed to interoperate with other applications that comply to those standards regardless of the technology that implements it.
Even if it is a simple demo of what can be done with WCF, this should help those who are discovering this new service model. Doing it, I discovered as few interesting points such as hosting the services in a Forms application which requires to use a specific thread for the service. I also found out that if you need to start and stop individual instances of the same service, you cannot use multiple endpoints, but must use a host for each of your instance.
Another interesting point is the creation of dynamic hosts and proxies as their addresses depend on a list and is unknown by advance.
Olivier ROUIT
|
Click here to view Olivier ROUIT's online profile.
|