Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Architecture for a Service with Multiple Clients using DispatcherObject

0.00/5 (No votes)
4 Dec 2016 2  
Showing an architecture for a service to handle multiple clients on their own thread using the DispatcherObject.

Introduction

Let's say you have a service (not WCF or Windows service) that needs to update its clients about changes in the service and you don't want to use the GUI thread to avoid blocking of the user interface, then what do you do?

You could just create a thread and update the clients, but when something goes wrong, it can be hard to debug so that's why this architecture is going to be more debugging friendly since each client will have its own thread named by the Id of the client and a timestamp for when it was created. (That part is really not necessary but is used in this example to show that it's really a new thread on register and unregister from the service.)

The architecture that I'm presenting is inspired by a colleague of mine.

Download the code here.

Presentation of the Architecture

First of all, each client needs to implement an interface which in this example is called IClient.

public interface IClient : IClientCallbackEvents
    {
        /// <summary>
        /// Get the name of the client
        /// </summary>
        string Name { get; }
        /// <summary>
        /// Get the client Id 
        /// 
        /// For internal use
        /// </summary>
        string Id { get; }
    }  

    public interface IClientCallbackEvents
    {
        /// <summary>
        /// Fired when you are registered
        /// </summary>
        void OnRegistred();
        /// <summary>
        /// Fired when you are unregistered
        /// </summary>
        void OnUnRegistred();
        /// <summary>
        /// Fired when the time changes
        /// </summary>
        /// <param name="time"></param>
        void OnTimeChanged(DateTime time);
    }

As can be seen, the IClient interface inherits the IClientCallbackEvents interface that in this example has the OnTimeChanged notification event to be raised on the clients own threads when the time changes. In this example, it will be called for each client each second. The two other events are called when the client registers and unregisters.

Let's take a look at how the ClientDipatcher class is implemented. It's the class that is responsible for providing the separate thread for each client using the DispatcherObject class.

class ClientDispatcher : DispatcherObject, IClientCallbackEvents, IDisposable
{
    #region Fields
    private IClient m_Client;
    private bool m_Disposed = false;
    #endregion

    #region Constructors
    public ClientDispatcher(IClient client)
    {
        m_Client = client;
    }
    #endregion

    #region Properties
    #endregion

    #region Public Methods
    public void Dispose()
    {
        if (m_Disposed)
            return;

        if (!Dispatcher.HasShutdownStarted)
            Dispatcher.BeginInvokeShutdown(DispatcherPriority.Normal);

        m_Disposed = true;
    }
    #endregion

    #region Helper Methods
    #endregion

    #region Event Handling
    public void OnRegistred()
    {
        if (!CheckAccess())
        {
            Action action = () => OnRegistred();
            Dispatcher.BeginInvoke(action);
        }
        else
            m_Client.OnRegistred();
    }

    public void OnTimeChanged(DateTime time)
    {
        if (!CheckAccess())
        {
            Action<DateTime> action = (dt) => OnTimeChanged(dt);
            Dispatcher.BeginInvoke(action, time);
        }
        else
            m_Client.OnTimeChanged(time);
    }

    public void OnUnRegistred()
    {
        if (!CheckAccess())
        {
            Action action = () => OnUnRegistred();
            Dispatcher.BeginInvoke(action);
        }
        else
            m_Client.OnUnRegistred();
    }
    #endregion
}

The ClientDispatcher takes the client as parameter in the constructor and has the methods that will call the client on the right Dispatcher. The CheckAccess method makes sure we invoke the notification on the right thread.

To manage all the clients, the ClientManager class is there. It's responsible for registering, unregistering and holding an instance of each client and its ClientDispatcher. It's implemented like this:

public static class ClientManager
{
    #region Fields
    private static Dictionary<IClient,
       ClientDispatcher> m_ClientsDict = new Dictionary<IClient, ClientDispatcher>();
    private static System.Timers.Timer m_Timer = new System.Timers.Timer(1000); //Tick each second
    #endregion

    #region Constructors
    static ClientManager()
    {
        Application.Current.Exit += OnApplication_Exit;
        m_Timer.Elapsed += OnTimer_Elapsed;
        m_Timer.Start();
    }
    #endregion

    #region Properties
    #endregion

    #region Public Methods
    public static void Register(IClient client)
    {
        if (client == null)
            return;

        if (m_ClientsDict.ContainsKey(client))
            return;

        Action action = () =>
        {
            ClientDispatcher dispatcher = new ClientDispatcher(client);
            m_ClientsDict.Add(client, dispatcher);
            Thread.CurrentThread.Name = $"{client.Id} - {DateTime.Now.ToString("HH:mm:ss")}";
           //Notify the client
            Task.Factory.StartNew(() => dispatcher.OnRegistred());

            //Keep the dispatcher alive.
            System.Windows.Threading.Dispatcher.Run();
        };

        Thread thread = new Thread(new ThreadStart(action));
        thread.Start();
    }

    public static void UnRegister(IClient client)
    {
        if (client == null)
            return;

        if (!m_ClientsDict.ContainsKey(client))
            return;

        ClientDispatcher dispatcher = m_ClientsDict[client];
        m_ClientsDict.Remove(client);

        //Notify the client
        dispatcher.OnUnRegistred();

        dispatcher.Dispose();
    }
    #endregion

    #region Helper Methods
    private static void NotifyOnTimeChanged(DateTime time)
    {
        foreach (ClientDispatcher dispatcher in m_ClientsDict.Values.ToList())
            dispatcher.OnTimeChanged(time);
    }
    #endregion

    #region Event Handling
    private static void OnTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        //stop the timer while notifying clients
        m_Timer.Stop();

        //Notify the clients on their own thread
        NotifyOnTimeChanged(DateTime.Now);

        m_Timer.Start();
    }

    private static void OnApplication_Exit(object sender, ExitEventArgs e)
    {
        foreach (IClient client in m_ClientsDict.Keys.ToList())
            UnRegister(client);
    }
    #endregion
}

The most interesting part here is when a client registers, a new Thread is created and named using the client.Id and a timestamp. Then, the thread dispatcher is kept alive with the call to:

System.Windows.Threading.Dispatcher.Run();

That's why Dispose() on the ClientDispatcher is important since it will shutdown the Dispatcher when unregister is called.

The rest is just GUI code to show how each client really is notified on its own thread. The user interface looks like this and has two buttons for each client. A register and unregister button that is using a delegatecommand for the click. It will then call the ClientManager according to what should be done and the ClientDispatcher will raise the events according to what is requested. As soon as the clients are registered, they will be called each second with the OnTimeChanged event. Note this is just for demonstrating the architecture. In a real world application, you would have more events specific for your need.

Regarding formatting of strings, I'm using the new syntax:

CurrentTime = $"{time.ToString("HH:mm:ss")}
ThreadName = $"Thread: '{Thread.CurrentThread.Name}'";

Take a look at the source code and hopefully, enjoy this "new" pattern.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here