Tray Notify - Part III (WCF Service)

1. Introduction

This is the third part of the Tray Notify series which illustrates how a Windows client app can communicate with a Window Service.

The sample application monitors file change events from the Windows service and uses a WCF service to send the change events to a task bar tray application.

This article will walk through the creation of the WCF service and how to host it inside the Windows Service that was created in Part II.

  • Part I - Covers the basic architecture and the creation of the common class library.
  • Part II - Covers the creation of the Windows Service and service installer using C#.
  • Part III - Covers the creation of the WCF service and how it's hosted inside the Windows Service.
  • Part IV- Covers the WPF tray application and its communication with the WCF service.
  • 2. WCF Overview

    A complete overview of WCF Services beyond the scope of this article but essentially WCF Services is Microsoft's newer approach to creating Web Services. Earlier asmx Web Services (.net 2.0 and before) relied on IIS for hosting. With WCF services introduced in .Net 3.0, Microsoft has removed the IIS hosting restriction and has enabled WCF services to be hosted in any application including IIS, a Windows Service, or a .Net console, Winforms or WPF application.

    In addition, WCF offers bindings that allow different communication protocols such as http, TCP/IP and msmq to be used. WCF offers additional security options as well.

    For further information on WCF Service, see the Reference section at the end of this article.

    3. Why use a WCF service as the communication layer?

    A WCF service was chosen as the communication layer between the Windows Service and the client Tray application because it offers two-way communication and it can be hosted from a Windows Service application. The client application calls a WCF service method to register itself with the service, and the service uses the dual binding to send notifications back to the client. In addition, WCF services are not session dependent so they are able to provide the communication layer between the Windows service running in session 0 and an interactive process running in a logged on user session (n).

    4. Build out the WCF Service

    Starting with the source code built in Part II, we are going to create a couple of WCF service interfaces as well as the code that monitors the file change events. The first interface will be the interface used by the client applications to registering themselves with the service on startup. This lets the WCF know where to send the file change notification events. The second interface is the callback event interface that passes the file change notification events to the clients.

    4.1 Create the TrayNotify Interface

    4.1.1 Remove the default Service files

    Remove the Service1 files from the CG.TrayNotify.WCF.Service project. By default, Visual Studio always adds this interface to a WCF Service project. Since we'll be adding our own interface, we can delete the files related to this interface.

    1. Load the CG.TrayNotify solution created in Part II
    2. Expand the CG.TrayNotify.WCF.Service project in the solution explorer.
    3. Delete the Service1.cs file
    4. Delete the IService1.cs file
    5. Open the App.config file and remove the entries between the <system.ServiceModel/> nodes.
    6. Since we'll be hosting this WCF Service in the Windows Service application, you can remove the <system.web/> nodes.

    You should be left with the following app.config entries:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <system.serviceModel>
      </system.serviceModel>
    </configuration>
    

    1.1.2 Create a ITrayNotify Interface

    The ITrayNotify interface is used by the client application to register itself with the service.

    1. Right click on the CG.TrayNotify.WCF.Service project in the Solution Explorer.
    2. Choose, "New Item"
    3. Under "Templates:", choose "WCF Service"
    4. Change the name to "TrayNotify.cs"
    5. Click "Add"

    1.1.3 Modify the app.config file

    The app.config file will contain the new ITrayNotify service config entries. Modify the app.config file as follows:

    1. Within the "serviceBehaviors/behavior" node, rename the "CG.TrayNotify.Wcf.Service.TrayNotifyBehavior" name attribute to "TrayNotifyBehavior".
    2. In the "services/service" node, do the following:
      • Change the behaviorConfiguration entry to "TrayNotifyBehavior".
      • Change the name attribute from "CG.TrayNotify.Wcf.Service.TrayNotify" to "CG.TrayNotify.Wcf.Service:CG.TrayNotify.Wcf.TrayNotify".

        Notice how the name contains a ':'. The left side of the colon contains the assembly name of the class contained on the right side. This isn't a standard WCF services naming convention; however, this format will be used by the WcfServiceHost class which was added to the CG.TrayNotify.Common library in Part I.

    3. In the "services/service/endpoint" node, do the following:
      • Change the binding attribute to "wsDualHttpBinding". Dual binding will enable the WCF Service to make calls to the client application. More on this later in Part IV.
      • Change the contract attribute to "CG.TrayNotify.Common.Interface.ITrayNotify".

        Note: at present the ITrayNotify interface is not located in the CG.TrayNotify.Common class library; however, we will move it to the common library later.

    4. In the "services/service/endpoint/host" node, change the baseAddress attribute to "http://localhost:8071/TrayNotify"

    The completed app.config file should resemble:

    <configuration>
      <system.serviceModel>
        <behaviors>
          <serviceBehaviors>
            <behavior name="TrayNotifyBehavior">
              <serviceMetadata httpGetEnabled="true" />
              <serviceDebug includeExceptionDetailInFaults="false" />
            </behavior>
          </serviceBehaviors>
        </behaviors>
        <services>
          <service behaviorConfiguration="TrayNotifyBehavior"
           name="CG.TrayNotify.Wcf.Service:CG.TrayNotify.Wcf.Service.TrayNotifyEndpoint">
            <endpoint address=""
              binding="wsDualHttpBinding"
              contract="CG.TrayNotify.Common.Interface.ITrayNotify">
              <identity>
                <dns value="localhost" />
              </identity>
            </endpoint>
            <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
            <host>
              <baseAddresses>
                <add baseAddress="http://localhost:8071/TrayNotify/" />
              </baseAddresses>
            </host>
          </service>
        </services>
      </system.serviceModel>
    </configuration>
    

    1.1.4 Add the interface methods and callback interface

    Open the ITrayNotify.cs file and modify the default interface.

    1. Add attribute parameters to the ServiceContract attribute
      • Add the SessionMode = SessionMode.Required
      • Add the CallbackContract = typeof( ITrayNotifyCallback )
    2. Add the Register, Unregister, Start and Stop methods
    3. Create the ITrayNotifyCallback interface and method

    The complete ITrayNotify and ITrayNotifyCallback interfaces should look like:

        [ServiceContract( SessionMode = SessionMode.Required, CallbackContract = typeof( ITrayNotifyCallback ) )]
        public interface ITrayNotify
        {
            [OperationContract]
            void Register( Guid instanceId );
    
            [OperationContract]
            void UnRegister( Guid instanceId );
    
            [OperationContract]
            void Start( Guid instanceId, string folderToMonitor );
    
            [OperationContract]
            void Stop( Guid instanceId, string folderToMonitor );
        }
    
        [ServiceContract]
        public interface ITrayNotifyCallback
        {
            [OperationContract]
            void OnFileChangeEvent( FileEventArgs e );
        }
    

    WCF Service comment

    Setting the SessionMode parameter to Required forces WCF to retain session state between service calls. This is necessary in order to retain the connection to the ITrayNotifyCallback interface which gets registered when the client first connects to the server.

    Tray Notify - Part III (WCF Service)

    4.1.5 Modify the TrayNotify implementation

    Open the TrayNotify.cs file and modify the TrayNotify class

    1. Add the InstanceContextMode = InstanceContextMode.Single param to the ServiceBehavior attribute.
    2. Add the ConfigurationName = "CG.TrayNotify.Wcf.Service:CG.TrayNotify.Wcf.Service.TrayNotifyEndpoint" param to the ServiceBehavior attribute.
    3. Rename the TrayNotify class to TrayNotifyEndpoint.
    4. Add the Register, Unregister, Start and Stop method stubs. The method stubs will be filled in later after the Folder and Monitor classes have been created.

    The stubbed out TrayNotifyEndpoint class should look like:

    [ServiceBehavior( ConfigurationName = "CG.TrayNotify.Wcf.Service:CG.TrayNotify.Wcf.Service.TrayNotifyEndpoint", InstanceContextMode = InstanceContextMode.Single )]
    public class TrayNotifyEndpoint : ITrayNotify
    {
        public void Register( Guid instanceId ) {}
        public void UnRegister( Guid instanceId ) { }
        public void Start( Guid instanceId, string folderToMonitor ) { }
        public void Stop( Guid instanceId, string folderToMonitor ) { }
    }
    

    WCF Service comment

    The InstanceContextMode determines the lifetime of the TrayNotify object. Setting the InstanceContextMode parameter to Single essentially creates turns the TrayNotify class into a singleton instance. In other words, a TrayNotify instance is created during the very first service call and that instance is reused for as long as the service remains hosted. Since we are using a Windows Service application to host the WCF service, the TrayNotify instance will be kept alive until the Windows service is stopped.

    The ConfigurationName param must match the service name param in the AppConfig file.

    4.1.6 Move the ITrayNotify interface file into the common library

    Typically client applications talk to web services via a web proxy. Since we are writing both the webservice and the client, we can use a 'proxy-less' approach and connect the client to the web service without using a proxy. We'll see how to do this in Part IV, but for now we need to move the ITrayNotify interface into the CG.TrayNotify.Common class library.

    1. Add a new "Interface" folder to the CG.TrayNotify.Common project
    2. Right click on the ITrayNotify.cs file and choose, "Cut"
    3. Select the "Interface" folder in the CG.TrayNotify.Common project
    4. Right click and choose, "Paste".
    5. Open the ITrayNotify.cs file and change the namespace to CG.TrayNotify.Common.Interface.
    6. Open the TrayNotify.cs file and add a CG.TrayNotify.Common.Interface using statement.

    4.2 Create the FileEventArgs contract class

    The FileEventArgs contract is the class sent to the client application(s) whenever a file is changed in the 'My Documents' folder. This class needs to be available in the both the WCF and client projects, so we'll add the class to the common class library.

    4.2.1 Add the new class

    1. Add a new "Contract" folder to the CG.TrayNotify.Common project
    2. Right click on the "Contract" folder and choose "Add class"
    3. Enter FileEventArgs.cs as the name and press enter.
    4. Replace the contents of the file with the following listing.
    namespace CG.TrayNotify.Common.Contract
    {
        #region Using Directives
    
        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Text;
        using System.Runtime.Serialization;
        using System.IO;
    
        #endregion Using Directives
    
        /// 
        /// Delegate for the file event
        /// 
        public delegate void FileEventHandler( object sender, FileEventArgs e );
    
        [Serializable]
        [DataContract]
        public class FileEventArgs : EventArgs
        {
            public static FileEventArgs Create( FileSystemEventArgs args, string folder )
            {
                return new FileEventArgs( )
                    { Folder = folder
                        , Date = DateTime.Now
                        , FileName = args.Name
                        , ChangeType = args.ChangeType
                        , Id = Guid.NewGuid( )
                    };            
            }
    
            public static FileEventArgs Create( RenamedEventArgs args, string folder )
            {
                return new FileEventArgs( )
                    { Folder = folder
                        , Date = DateTime.Now
                        , FileName = args.Name
                        , ChangeType = args.ChangeType
                        , Id = Guid.NewGuid( )
                    };
            }
    
            [DataMember] public WatcherChangeTypes ChangeType { get; private set; }
            [DataMember] public DateTime Date { get; private set; }
            [DataMember] public string FileName { get; private set; }
            [DataMember] public string Folder { get; private set; }
            [DataMember] public Guid Id { get; private set; }
        }
    }
    

    4.2.2 Compile the project

    Rebuild the solution and fix any errors.

    4.3 Create the Folder class

    The Folder class implements an instance of the FileSystemWatcher class and performs the watching of file system change events on the specified folder. Each client that registers its "My Documents" folder is represented by an instance of this class.

    4.3.1 Add the new class

    1. Add a new class called "Folder" to the CG.TrayNotify.Wcf.Service project.
    2. Replace the contents of the file with the following listing.
    namespace CG.TrayNotify.Wcf.Service
    {
        #region Using Directives
    
        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Text;
        using System.IO;
        using CG.TrayNotify.Common.Contract;
    
        #endregion Using Directives
    
        internal class Folder
        {
            public static Folder Create( Monitor monitor, string folderToMonitor )
            {
                return new Folder( monitor, folderToMonitor );
            }
    
            public void Start( )
            {
                _watcher.Changed += new FileSystemEventHandler( OnFileEvent );
                _watcher.Created += new FileSystemEventHandler( OnFileEvent );
                _watcher.Deleted += new FileSystemEventHandler( OnFileEvent );
                _watcher.Renamed += new RenamedEventHandler( OnRenameEvent );
    
                _watcher.EnableRaisingEvents = true;
            }
    
            public void Stop( )
            {
                _watcher.EnableRaisingEvents = false; 
                
                _watcher.Changed -= new FileSystemEventHandler( OnFileEvent );
                _watcher.Created -= new FileSystemEventHandler( OnFileEvent );
                _watcher.Deleted -= new FileSystemEventHandler( OnFileEvent );
                _watcher.Renamed -= new RenamedEventHandler( OnRenameEvent );
            }
            public void OnFileEvent( object source, FileSystemEventArgs e )
            {
                _monitor.AddQueueItem( FileEventArgs.Create( e, _folder ) );
            }
    
            public void OnRenameEvent( Object source, RenamedEventArgs e )
            {
                _monitor.AddQueueItem( FileEventArgs.Create( e, _folder ) );
            }
    
            private Folder( Monitor monitor, string folderToMonitor )
            {
                _monitor = monitor;
                _folder = folderToMonitor;
    
                _watcher.Path = folderToMonitor;
                _watcher.NotifyFilter = NotifyFilters.FileName |
                                       NotifyFilters.Attributes |
                                       NotifyFilters.LastAccess |
                                       NotifyFilters.LastWrite |
                                       NotifyFilters.Security |
                                       NotifyFilters.Size;
    
            }
    
            private string _folder = String.Empty;
            private Monitor _monitor;
            private FileSystemWatcher _watcher = new FileSystemWatcher( );
        }
    }
    

    4.3.2 Compile the project

    Rebuild the solution and fix any errors.

    Tray Notify - Part III (WCF Service)

    4.4 Create the Monitor class

    The monitor class keeps a dictionary of register folders and creates a Folder instance for each folder. Each Folder instance monitors the folder for file change events and forwards them to the Monitor class.

    4.4.1 Add the new class

    1. Add a new class called "Monitor" to the CG.TrayNotify.Wcf.Service project.
    2. Replace the contents of the file with the following listing.
    namespace CG.TrayNotify.Wcf.Service
    {
        #region Using Directives
    
        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Text;
        using CG.TrayNotify.Common.Threading;
        using CG.TrayNotify.Common.Contract;
        using System.Threading;
    
        #endregion Using Directives
    
        /// 
        /// Maintains an dictionary of Folder instances (which represent a file folder).
        /// Each folder monitors file change events and sends them to this class
        /// via the file queue. This class then forwards the events through the
        /// FileEvent event.
        /// 
        internal class Monitor
        {
            /// 
            /// Class factory
            /// 
            /// 
            public static Monitor Create( )
            {
                return new Monitor( );
            }
    
            /// 
            /// Occurs when one or more of the monitor folders has a file changed event
            /// Used by the TrayNotify service to dispatch events to the clients
            /// 
            public event FileEventHandler FileEvent
            {
                remove { _fileEvent -= value; }
                add { _fileEvent += value; }
            }
    
            #region Public Methods
    
            /// 
            /// Adds a folder to monitor
            /// 
            /// 
            public void Add( string folderToMonitor )
            {
                if ( !_folderDictionary.ContainsKey( folderToMonitor ) )
                {
                    _folderDictionary.Add( folderToMonitor, Folder.Create( this, folderToMonitor ) );
                }
            }
    
            /// 
            /// Removes a folder from monitoring
            /// 
            /// 
            public void Remove( string folderToMonitor )
            {
                if ( _folderDictionary.ContainsKey( folderToMonitor ) )
                {
                    _folderDictionary.Remove( folderToMonitor );
                }
            }
    
            /// 
            /// Inserts a FileEventArgs object into the fileevent queue.
            /// Called by each Folder instance when a file change event
            /// has occurred for that folder item.
            /// 
            /// 
            public void AddQueueItem( FileEventArgs e )
            {
                _fileEventQueue.Enqueue( e );
    
                // Signal the queue service thread
                // that a queue item is available
                _queueEvent.Set( );
            }
    
            /// 
            /// Starts monitoring of all folders
            /// 
            public void Start( )
            {
                if ( null == _waitHandles )
                {
                    _waitHandles = new WaitHandle [ ] { _stopEvent, _queueEvent };
                }
    
                if ( null == _queueServiceThread )
                {
                    _queueServiceThread = new Thread( new ThreadStart( QueueServiceThreadProc ) );
    
                    _queueServiceThread.Priority = ThreadPriority.AboveNormal;
                    _queueServiceThread.IsBackground = true;
    
                    _queueServiceThread.Start( );
                }
    
                foreach ( Folder folder in _folderDictionary.Values )
                {
                    folder.Start( );
                }
            }
    
            /// 
            /// Stops all monitoring of all folders
            /// 
            public void Stop( )
            {
                if ( null != _queueServiceThread )
                {
                    // Signal the thread to exit
                    _stopEvent.Set( );
    
                    // Close the thread
                    _queueServiceThread = null;
    
                    foreach ( Folder folder in _folderDictionary.Values )
                    {
                        folder.Stop( );
                    }
                }
            }
    
            #endregion Public Methods
    
            #region Private Methods
    
            /// 
            /// Thread proc wakes up on the specified interval and calls the
            /// ServiceSafe method to retrieve the safe det file
            /// 
            private void QueueServiceThreadProc( )
            {
                try
                {
                    while ( true )
                    {
                        switch ( WaitHandle.WaitAny( _waitHandles, Timeout.Infinite, false ) )
                        {
                        // Stop Event: exit thread
                        case StopEvent:
                            return;
    
                        // Queue event: retrieve queue data
                        case QueueEvent:
                            FileEventArgs args = null;
    
                            if ( null != ( args = _fileEventQueue.GetNextItem( true ) ) )
                            {
                                FireFileEvent( args );
                                _fileEventQueue.Dequeue( );
                            }
                            break;
                        }
                    }
                }
                catch ( Exception )
                {
                    // TODO: report error
                }
            }
    
            /// 
            /// Fires a FileEvent
            /// 
            /// 
            private void FireFileEvent( FileEventArgs args )
            {
                if ( _fileEvent != null )
                {
                    _fileEvent( this, args );
                }
            }
    
            #endregion Private Methods
    
            #region Private Ctor
    
            /// 
            /// Use class factory
            /// 
            private Monitor( ) { }
    
            #endregion Private Ctor
    
            #region Private Fields
    
            /// 
            /// Dictionary to hold the list of folders to monitor
            /// 
            private DictionarySync< ReaderWriterAutoLock, string, Folder > _folderDictionary 
                = new DictionarySync< ReaderWriterAutoLock, string, Folder >( );
     
            /// 
            /// Each folder in the above dictionary sends file change events via this queue
            /// 
            private QueueSync< CriticalSectionAutoLock, FileEventArgs > _fileEventQueue
                = new QueueSync< CriticalSectionAutoLock, FileEventArgs >( );
    
            /// 
            /// FileEvent handler
            /// 
            private event FileEventHandler _fileEvent;
    
            /// 
            /// Thread to service FileEvents from the file event queue
            /// 
            private Thread _queueServiceThread = null;
    
            /// 
            /// WaitHandle array (will contain StopEvent and QueueEvent events)
            /// 
            private WaitHandle [ ] _waitHandles = null;
    
            /// 
            /// Stop Event. 
            /// 
            private AutoResetEvent _stopEvent = new AutoResetEvent( false );
    
            /// 
            /// Queue event.
            /// 
            private AutoResetEvent _queueEvent = new AutoResetEvent( false );
    
            /// 
            /// StopEvent waithandle array index
            /// 
            private const int StopEvent = 0;
            /// 
            /// QueueEvent waithandle array index
            /// 
            private const int QueueEvent = 1;
    
            #endregion Private Fields
        }
    }
    

    1.4.2 Compile the project

    Rebuild the solution and fix any errors.

    Tray Notify - Part III (WCF Service)

    4.5 Fill out the TrayNotifyEndpoint method stubs

    In section 1.1.4 the TrayNotifyEndpoint class methods were stub out. Now that the Folder and Monitor classes have been created, the method stubs can be replaced with the final code.

    Open the TrayNotify.cs file and replace its contents with:

    namespace CG.TrayNotify.Wcf.Service
    {
        #region Using Directives
    
        using System;
        using System.Collections.Generic;
        using System.ServiceModel;
        using System.Threading;
        using Common.Contract;
        using Common.Interface;
        using Common.Threading;
    
        #endregion Using Directives
    
        [ServiceBehavior( ConfigurationName = "CG.TrayNotify.Wcf.Service:CG.TrayNotify.Wcf.Service.TrayNotifyEndpoint"
            , InstanceContextMode = InstanceContextMode.Single )]
        public class TrayNotifyEndpoint : ITrayNotify
        {
            /// 
            /// Default constructor
            /// 
            public TrayNotifyEndpoint( )
            {
                _monitor.FileEvent += new FileEventHandler( OnFileEvent );
            }
    
            #region Public Client Methods
    
            /// 
            /// Called by the client app to register itself with the service
            /// 
            /// Unique app instance guid
            public void Register( Guid instanceId )
            {
                ITrayNotifyCallback caller = OperationContext.Current.GetCallbackChannel< ITrayNotifyCallback >( );
    
                if ( caller != null )
                {
                    if ( !_clients.ContainsKey( instanceId ) )
                    {
                        _clients.Add( instanceId, Client.Create( instanceId, caller ) );
                    }
                }
            }
    
            /// 
            /// Called by a client app to no longer receive file change event notifications
            /// 
            /// 
            public void UnRegister( Guid instanceId )
            {
                if ( _clients.ContainsKey( instanceId ) )
                {
                    _clients.Remove( instanceId );
                }
            }
    
            /// 
            /// Called by a client app. Adds a folder to be monitored and starts monitoring
            /// 
            /// Guid to client app instance
            /// Folder containing files to monitor
            public void Start( Guid instanceId, string folderToMonitor )
            {
                _monitor.Add( folderToMonitor );
    
                _monitor.Start( );
            }
    
            /// 
            /// Called  by a client app. Removes a folder to monitor and stops monitoring
            /// 
            /// 
            /// 
            public void Stop( Guid instanceId, string folderToMonitor )
            {
                _monitor.Stop( );
    
                _monitor.Remove( folderToMonitor );
            }
    
            #endregion Public Client Methods
    
            #region Private Methods
            
            /// 
            /// FileEvent handler - called when there are file change events for any
            /// of the monitored folders.  Forward the change events to the clients.
            /// 
            /// 
            /// 
            private void OnFileEvent( object sender, FileEventArgs e )
            {
                RemoveInvalidClients( );
    
                // Although the dictionary is synchronized with a
                // ReaderWriterLock, enumerations of the dictionary
                // are not synchronized. So lock the dictionary for
                // the enumeration.
                using ( AutoLock.LockToRead( _clients.Lock, 5000 ) )
                {
                    // Walk through the client list and notify each client of a file event
                    foreach ( Client client in _clients.Values )
                    {
                        // Since client callbacks are a synchronous operation and the client may not be
                        // there any longer (or is otherwise unresponsive), use a secondary thread to
                        // do the notification
                        ThreadPool.QueueUserWorkItem( NotifyThreadProc, NotifyThreadStateInfo.Create( client, e ) );
                    }
                }
            }
    
            /// 
            /// Thread which performs the client callback notification.
            /// 
            private void NotifyThreadProc( object state )
            {
                NotifyThreadStateInfo stateInfo = state as NotifyThreadStateInfo;
    
                if ( stateInfo == null ) return;
    
                try
                {
                    stateInfo.Client.Callback.OnFileChangeEvent( stateInfo.Args );
                }
                catch( TimeoutException )
                {
                    // An error occurred while sending the client event
                    // Invalidate the client so it's removed from the list
                    stateInfo.Client.Invalidate( );
                }
            }
    
            /// 
            /// Removes any clients marked invalid from the clients dictionary 
            /// 
            private void RemoveInvalidClients( )
            {
                List removeClientList = new List( );
    
                // Although the dictionary is synchronized with a
                // ReaderWriterLock, enumerations of the dictionary
                // are not synchronized. So lock the dictionary for
                // the enumeration.
                using( AutoLock.LockToRead( _clients.Lock, 5000 ) )
                {
                    // Get a list of invalid client Id's
                    foreach ( Client client in _clients.Values )
                    {
                        if ( !client.IsValid )
                        {
                            removeClientList.Add( client.Id );
                        }
                    }
                }
    
                // Cycle through the list and remove each invalid
                // client from the dictionary
                foreach ( Guid id in removeClientList )
                {
                    if ( _clients.ContainsKey( id ) )
                    {
                        _clients.Remove( id );
                    }
                }
            }
    
    
            #endregion Private Methods
    
            #region Private Fields
    
            /// 
            /// Thread safe dictionary used to map client instances to their callbacks
            /// 
            private DictionarySync< ReaderWriterAutoLock, Guid, Client > _clients = new DictionarySync< ReaderWriterAutoLock, Guid, Client >( );
    
            /// 
            /// Monitor object - Monitors the registered folders for file change events
            /// 
            private Monitor _monitor = Monitor.Create( );
    
            #endregion Private Fields
        }
    }
    

    Tray Notify - Part III (WCF Service)

    4.6 Create the Client class

    The Client class represents each registered TrayNotify client user instance.

    4.6.1 Add the new class

    1. Add a new class called "Client" to the CG.TrayNotify.Wcf.Service project.
    2. Replace the contents of the file with the following listing.
    namespace CG.TrayNotify.Wcf.Service
    {
        #region Using Directives
    
        using System;
        using CG.TrayNotify.Common.Interface;
    
        #endregion Using Directives
    
        /// 
        /// Represents a TrayNotifyClient
        /// 
        internal class Client
        {
            public static Client Create( Guid id, ITrayNotifyCallback callback )
            {
                return new Client( ) { Id = id, Callback = callback, IsValid = true };
            }
    
            public Guid Id { get; private set; }
            public ITrayNotifyCallback Callback { get; private set; }
            public bool IsValid { get; private set; }
    
            public void Invalidate( ) { IsValid = false; }
    
        }
    }
    

    4.7 Create the NotifyThreadStateInfo class

    The NotifyThreadStateInfo class passes a state data to the secondary thread that performs the client callback operation.

    4.7.1 Add the new class

    1. Add a new class called "NotifyThreadStateInfo" to the CG.TrayNotify.Wcf.Service project.
    2. Replace the contents of the file with the following listing.
    namespace CG.TrayNotify.Wcf.Service
    {
        #region Using Directives
    
        using CG.TrayNotify.Common.Contract;
    
        #endregion Using Directives
    
        /// 
        /// Class that passes state info to the NotifyThreadProc
        /// 
        internal class NotifyThreadStateInfo
        {
            public static NotifyThreadStateInfo Create( Client client, FileEventArgs e )
            {
                return new NotifyThreadStateInfo( ) { Client = client, Args = e };
            }
    
            public Client Client { get; private set; }
            public FileEventArgs Args { get; private set; }
        }
    }

    4.8 Compile the project

    Rebuild the solution and fix any errors.

    5. Service Operation Explained

    The following table describes the TrayNotify class members and operation.

    MemberTypeVisibility Description
    RegisterMethodPublicCalled by the client app to register itself with the service. The client app passes in an instance Id guid. A dictionary is used to store the guid and its corresponding client callback for each client.
    UnregisterMethodPublicCalled by the client app to unregister itself from the service. The instance id is removed from the client dictionary.
    StartMethodPublicCalled by the client to start monitoring on a specified folder.
    StopMethodPublicCalled by the client to stop monitoring the sepecified folder.
    OnFileEventMethodPrivateMonitor.FileEvent event handler. Method that gets called by the monitor class when ever a file change event has occurred. This handler walks the client list and uses QueueUserWorkItem to create a new thread for each client in order to send the notification. Note: Clients are not guaranteed to be present at the time a file event occurs. As such, sending a notification to each client would result in blocking the main thread while attempting to call each client callback. Since walking the client list and calling each client callback is a synchronous operating, non-responsive clients would needlessly delay active clients from receiving notifications. To solve this problem, each client notification is sent via a thread. Rather than manually going through the chore of creating/destroying threads, the ThreadPool.QueueUserWorkItem method is leveraged.
    NotifyThreadProcMethodPrivateThis procedure is called for each client notification. It simply calls the client callback in a try/catch block. If a timeout exception is caught, the client entry is marked invalid.
    RemoveInvalidClientsMethodPrivateWalks the client dictionary list and removes any clients marked as invalid.
    _clientsFieldPrivateA dictionary mapping of client instance guids and callbacks.
    _monitorFieldPrivateThe monitor class contains the list of Folder objects which describe folders to monitor for file change events.

    6. Modify the Windows Service to host the WCF Service

    As mentioned earlier, WCF services can be hosted in a variety of applications and hosting a service is quite simple. At the most basic level, only the following code is required:

    var host = new ServiceHost( typeof( TrayNotify ) );
    host.Open( );
    

    However, instead of manually hosting the services we're going to dynamically load the services by reading the entries in the config file and create a ServiceHost instance for each service.

    6.1 WcfServiceHost and WcfServices classes

    In Part I, these classes were copied over from the Support Files folder into the CG.TrayNotify.Common class library. The WcfServices class is a serialization class that reads the system.serviceModel service entries in the config file. The WcfServiceHost class uses these classes to create the appropriate ServiceHost class based on the binding type of the service entry.

    6.2 Using the WcfServiceHost class

    Replace the existing TrayNotifyService code with the following:

    /// 
    /// Called by the SCM when the service starts
    /// 
    protected override void OnStart( string [ ] args )
    {
      OnStop( );
    
      if ( null == _wcfServiceHost )
      {
        _wcfServiceHost = WcfServiceHost.Create( EventLog );
      }
    
      _wcfServiceHost.OnStart( args );
    }
    /// 
    /// Called by the SCM when the service is shutdown
    /// 
    protected override void OnShutdown( )
    {
      OnStop( );
    
      _wcfServiceHost = null;
    
    }
    /// 
    /// Called by the SCM when the service is stopped
    /// 
    protected override void OnStop( )
    {
      if ( null == _wcfServiceHost ) return;
    
      _wcfServiceHost.OnStop( );
    }
    /// 
    /// WcfServiceHost class - does all the service hosting work
    /// 
    WcfServiceHost _wcfServiceHost;
    }
    

    Since the WcfServiceHost does all the work, the code above is all that's necessary to host a WCF service in the windows service application.

    7. Running the Windows Service in Debug Mode

    We discussed how the Windows Service app hosts the WCF Service in Part II. Although the Host.Service was registered to start under the SCM as a Windows Service, it's better to make sure that it will be able to properly host the WCF TrayNotify service. To this end, we'll first start the service in debug mode:

    1. Right Click on the CG.TrayNotify.Host.Service project and choose "Set as Startup Project".
    2. Next, right Click on the same project and Choose "Properties".
    3. Click on the Debug tab
    4. In the "Command line arguments:" text box, enter "/Debug".
    5. Close the properties window
    6. Rebuild the project.
    7. Press F5 to start debugging


    8. If the project build and runs, you should see the following console window:

      [HostServiceRunning.jpg]

      Note: Vista and Win7 Users

      On Vista and Windows 7, you may get an error when trying to host the WCF Service. You'll need to open up the port by running the following command from an administrator cmd window

      netsh http add urlacl url=http://+:8071/TrayNotify user=

      For more info, see: Configuring HTTP and HTTPS


      8. Summary

      This article built out the WCF service TrayNotify class and other classes which tracks file change events and send notifications to clients. It also showed how to host a WCF service in a Windows Service application. Part IV, the last article in the series, will cover the creation of task bar tray application client code - the app that creates a task bar icon, registers itself with the service, and displays notification windows to the user.

      9. About the Author

      Arjay Hawco is an application developer/architect and Microsoft MVP who works with the latest WxF .Net technologies. He is also cofounder of Iridyn, Inc, a software consulting firm.

      10. References

      Introduction to building Windows Communication Foundation Services
      Windows Communication Foundation
      Configuring HTTP and HTTPS




    Comments

    • There are no comments yet. Be the first to comment!

    Leave a Comment
    • Your email address will not be published. All fields are required.

    Top White Papers and Webcasts

    • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

    • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

    Most Popular Programming Stories

    More for Developers

    Latest Developer Headlines

    RSS Feeds