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
{
string Name { get; }
string Id { get; }
}
public interface IClientCallbackEvents
{
void OnRegistred();
void OnUnRegistred();
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); #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")}";
Task.Factory.StartNew(() => dispatcher.OnRegistred());
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);
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)
{
m_Timer.Stop();
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 string
s, 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.