玄铁剑

成功的途径:抄,创造,研究,发明...
posts - 128, comments - 42, trackbacks - 0, articles - 174

Hosting WCF services in a Windows Forms Application

Posted on 2007-01-03 17:58 玄铁剑 阅读(454) 评论(0)  编辑 收藏 引用 所属分类: Service

Sample Image

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
{    
    /// Gets the running direction of that locomotive/// </summary>/// <returns></returns>
    [OperationContract(IsOneWay=false)]
    Direction GetDirection();
    /// <summary>/// Changes the direction of that locomotive/// </summary>/// <param name="direction">New direction</param>
    [OperationContract]
    void ChangeDirection(Direction direction);
    /// <summary>/// Sets the new speed value/// </summary>/// <param name="speed">Speed value (0 - 28)</param>
    [OperationContract]
    void SetSpeed(byte speed);

    /// <summary>/// Gets the current locomotive speed/// </summary>/// <returns>Speed value</returns>
    [OperationContract(IsOneWay=false)]
    byte GetSpeed();

    /// <summary>/// Switch the main light/// </summary>/// <param name="state">ON if true, OFF if false</param>
    [OperationContract]
    void SwitchLight(bool state);

    /// <summary>/// Gets the main light status/// </summary>/// <returns>true if ON, false otherwise</returns>
    [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
				/// <summary>
				/// Implements the IDCCLocomotiveContract
				/// </summary>
[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;
            // Start the host
            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)
            {
                // Wait until thread is stopped
                Thread.Sleep(SleepTime);
            }

            // Stop the host
            m_serviceHost.Close();
        }
        catch (Exception)
        {
            if (m_serviceHost != null)
            {
                m_serviceHost.Close();
            }
        }
    }

    /// <summary>/// Request the end of the thread method./// </summary>
    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:

				// Creates the corresponding endpoint
EndpointAddress endPoint = new EndpointAddress(    
    new Uri(string.Format(Constants.LocoServerBaseAddress, 
    address) + address));

// Creates the proper binding
System.ServiceModel.Channels.Binding binding = new NetTcpBinding();

// Creates channel factory with the binding and endpoint
m_dccLocoFactory = new ChannelFactory(binding, endPoint);
m_dccLocoFactory.Open();

// Creates the channel and opens it to work with the service
m_locoChannel = m_dccLocoFactory.CreateChannel();
m_locoChannel.Open();

The interface to create this proxy using the ChannelFactory is as follows:

				///<summary>
				/// Interface used to create the Proxy for IDCCLocomotiveContract
				///</summary>
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.

只有注册用户登录后才能发表评论。