65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 DispatcherObject 的具有多个客户端的服务架构

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2016年12月4日

CPOL

3分钟阅读

viewsIcon

11272

downloadIcon

239

展示一种服务架构,该架构使用DispatcherObject在各自的线程上处理多个客户端。

引言

假设您有一个服务(不是WCF或Windows服务),它需要向其客户端更新服务中的更改,并且您不想使用GUI线程来避免阻塞用户界面,那么您该怎么做呢?

您可以创建一个线程并更新客户端,但是当出现问题时,调试起来可能会很困难,这就是为什么这种架构更易于调试的原因,因为每个客户端都将拥有自己的线程,该线程以客户端的ID和创建时的时间戳命名。(这部分实际上并非必要,但在本例中用于演示在向服务注册和注销时,它确实是一个新的线程。)

我提出的架构受到了我一位同事的启发。

在此处下载代码 此处

架构介绍

首先,每个客户端都需要实现一个接口,在本例中称为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);
    }

如您所见,IClient接口继承了IClientCallbackEvents接口,在本例中,该接口具有OnTimeChanged通知事件,当时间更改时,将在客户端自己的线程上触发。在本例中,它将每秒为每个客户端调用一次。当客户端注册和注销时,将调用另外两个事件。

让我们看看ClientDipatcher类的实现方式。它是负责使用DispatcherObject类为每个客户端提供单独线程的类。

    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
    }    

ClientDispatcher在构造函数中将客户端作为参数,并具有在正确的Dispatcher上调用客户端的方法。CheckAccess方法确保我们在正确的线程上调用通知。

为了管理所有客户端,ClientManager类存在。它负责注册、注销以及保存每个客户端及其ClientDispatcher的实例。它的实现如下所示:

    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 
    }

这里最有趣的部分是,当客户端注册时,将创建一个新Thread,并使用client.Idtimestamp命名。然后,通过调用以下内容使线程调度程序保持活动状态:

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

这就是为什么ClientDispatcher上的Dispose()很重要,因为它会在调用注销时关闭Dispatcher

其余的只是GUI代码,用于演示每个客户端如何在自己的线程上真正收到通知。用户界面如下所示,每个客户端都有两个按钮。一个注册按钮和一个注销按钮,它们使用delegatecommand进行点击。然后,它将根据应该执行的操作调用ClientManager,而ClientDispatcher将根据请求触发事件。一旦客户端注册,它们将每秒钟使用OnTimeChanged事件被调用。请注意,这只是为了演示架构。在实际应用程序中,您将有更多针对您需求的事件。

关于string的格式化,我使用的是新的语法。

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

查看源代码,希望您喜欢这种“新”模式。

© . All rights reserved.