1. 程式人生 > >為WPF, UWP 及 Xamarin實現一個簡單的訊息元件

為WPF, UWP 及 Xamarin實現一個簡單的訊息元件

原文地址:Implementing a simple messenger component for WPF, UWP and Xamarin


歡迎大家關注我的公眾號:程式設計師在紐西蘭

瞭解紐西蘭IT行業真實碼農生活
請長按上方二維碼關注“程式設計師在紐西蘭”

 

最初的需求是我需要開發一個實現Socket傳送/接收的WPF應用程式。 首先,我用MVVM模式建立了一個基本的WPF應用程式。 然後,我做了一個專案來完成所有與Socket通訊有關的工作。 接下來,我必須將Socket專案整合到ViewModel專案中,以操作Socket連線。
顯然,我們可以為此使用event

。 例如,我們可以有一個名為“ SocketServer”的類,該類具有一個事件來接收Socket資料包,然後在ViewModel層中對其進行訂閱。 但這意味著我們必須建立“ SocketServer”類的例項,該類將ViewModel層與套接字專案耦合在一起。 我希望建立一箇中間件以解耦它們。 因此,釋出者和訂閱者不需要彼此瞭解。

在我使用 MvvmCross 作為MVVM框架時,我發現MvvmCross提供了一個名為 Messenger 的外掛以在ViewModel之間進行通訊。 但是它依賴於某些MvvmCross庫,這意味著如果我想在其他專案中使用此外掛,則必須引用MvvmCross。 這對我當前的情況而言並不理想,因為實際上,套接字專案沒有要求引用MvvmCross。 因此,我做了一個專注於釋出/訂閱模式的專案,並刪除了對MvvmCross的依賴。 現在,我可以在任何WPF,UWP和Xamarin專案中重複使用它。 該專案位於此處:https://github.com/yanxiaodi/CoreMessenger 。讓我們深入瞭解更多細節。

資訊

Message是在此係統中表示訊息的抽象類:

    public abstract class Message
    {
        public object Sender { get; private set; }
        protected Message(object sender)
        {
            Sender = sender ?? throw new ArgumentNullException(nameof(sender));
        }
    }


我們應該建立從該抽象類派生的不同訊息的例項。 它有一個名為“sender”的引數,因此訂閱者可以獲取傳送者的例項。 但這不是強制性的。

訂閱

BaseSubscription是訂閱的基類。 程式碼如下:

    public abstract class BaseSubscription
    {
        public Guid Id { get; private set; }
        public SubscriptionPriority Priority { get; private set; }
        public string Tag { get; private set; }
        public abstract Task<bool> Invoke(object message);
        protected BaseSubscription(SubscriptionPriority priority, string tag)
        {
            Id = Guid.NewGuid();
            Priority = priority;
            Tag = tag;
        }
    }


它具有一個“ Id”屬性和一個“ tag”屬性,因此您可以放置一些標籤來區分或分組訂閱例項。 “ Priority”屬性是一個列舉型別,用於指示訂閱的優先順序,因此將按預期順序呼叫訂閱。
訂閱有兩種型別。 一是“StrongSubscription”:

    public class StrongSubscription<TMessage> : BaseSubscription where TMessage : Message
    {
        private readonly Action<TMessage> _action;

        public StrongSubscription(Action<TMessage> action,
            SubscriptionPriority priority, string tag): base(priority, tag)
        {
            _action = action;
        }
        public override async Task<bool> Invoke(object message)
        {
            var typedMessage = message as TMessage;
            if (typedMessage == null)
            {
                throw new Exception($"Unexpected message {message.ToString()}");
            }
            await Task.Run(() => _action?.Invoke(typedMessage));
            return true;
        }
    }


它繼承了BaseSubscription並覆蓋了Invoke()方法。 基本上,它具有一個名為“ _action”的欄位,該欄位在建立例項時定義。 當我們釋出訊息時,訂閱將呼叫Invoke()方法來執行操作。 我們使用Task來包裝動作,以便可以利用非同步操作的優勢。


這是名為“ WeakSubscription”的另一種“ Subscription”:

    public class WeakSubscription<TMessage> : BaseSubscription where TMessage : Message
    {
        private readonly WeakReference<Action<TMessage>> _weakReference;

        public WeakSubscription(Action<TMessage> action,
            SubscriptionPriority priority, string tag) : base(priority, tag)
        {
            _weakReference = new WeakReference<Action<TMessage>>(action);
        }

        public override async Task<bool> Invoke(object message)
        {
            var typedMessage = message as TMessage;
            if (typedMessage == null)
            {
                throw new Exception($"Unexpected message {message.ToString()}");
            }
            Action<TMessage> action;
            if (!_weakReference.TryGetTarget(out action))
            {
                return false;
            }
            await Task.Run(() => action?.Invoke(typedMessage));
            return true;
        }
    }


它與強訂閱的區別在於action儲存在“ WeakReference”欄位中。 您可以在這裡瞭解更多資訊:WeakReference 類。 它用於表示型別化的弱引用,該弱引用引用一個物件,同時仍允許該物件被垃圾回收回收。 在使用它之前,我們需要使用TryGetTarget(T)方法檢查目標是否已由GC收集。 如果此方法返回false,則表示該引用已被GC收集。
如果使用StrongSubscription,Messenger將保留對回撥方法的強引用,並且Garbage Collection將不會破壞訂閱。 在這種情況下,您需要明確取消訂閱,以避免記憶體洩漏。 否則,可以使用WeakSubscription,當物件超出範圍時,會自動刪除訂閱。

MessengerHub

MessengerHub是整個應用程式域中的一個單例例項。 我們不需要使用“依賴注入”來建立例項,因為它的目的是明確的,我們只有一個例項。 這是實現單例模式的簡單方法:

public class MessengerHub
{
        private static readonly Lazy<MessengerHub> lazy = new Lazy<MessengerHub>(() => new MessengerHub());
        private MessengerHub() { }
        public static MessengerHub Instance
        {
            get
            {
                return lazy.Value;
            }
        }
}

MessengerHub維護一個Dictionary來維護訂閱的例項,如下所示:

private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Guid, BaseSubscription>> _subscriptions =
            new ConcurrentDictionary<Type, ConcurrentDictionary<Guid, BaseSubscription>>();

該Dictionary的Key是“Message”的型別,Value是一個Dictionary,其中包含該特定“Message”的一組訂閱。 顯然,一種型別可能具有多個訂閱。

訂閱

MessageHub公開了幾種重要的方法來訂閱/取消訂閱/釋出訊息。
Subscribe()方法如下所示:

        public SubscriptionToken Subscribe<TMessage>(Action<TMessage> action,
            ReferenceType referenceType = ReferenceType.Weak,
            SubscriptionPriority priority = SubscriptionPriority.Normal, string tag = null) where TMessage : Message
        {
            if (action == null)
            {
                throw new ArgumentNullException(nameof(action));
            }
            BaseSubscription subscription = BuildSubscription(action, referenceType, priority, tag);
            return SubscribeInternal(action, subscription);
        }

        private SubscriptionToken SubscribeInternal<TMessage>(Action<TMessage> action, BaseSubscription subscription)
            where TMessage : Message
        {
            if (!_subscriptions.TryGetValue(typeof(TMessage), out var messageSubscriptions))
            {
                messageSubscriptions = new ConcurrentDictionary<Guid, BaseSubscription>();
                _subscriptions[typeof(TMessage)] = messageSubscriptions;
            }
            messageSubscriptions[subscription.Id] = subscription;
            return new SubscriptionToken(subscription.Id, async () => await UnsubscribeInternal<TMessage>(subscription.Id), action);
        }


當我們訂閱訊息時,我們建立Subscription的例項並將其新增到字典中。 根據您的選擇,它可能是強引用或者弱引用。 然後它將建立一個SubscriptionToken,這是一個實現IDisposable介面來管理訂閱的類:

    public sealed class SubscriptionToken : IDisposable
    {
        public Guid Id { get; private set; }
        private readonly Action _disposeMe;
        private readonly object _dependentObject;

        public SubscriptionToken(Guid id, Action disposeMe, object dependentObject)
        {
            Id = id;
            _disposeMe = disposeMe;
            _dependentObject = dependentObject;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool isDisposing)
        {
            if (isDisposing)
            {
                _disposeMe();
            }
        }
    }


當我們建立SubscriptionToken的例項時,實際上我們傳遞了一個方法來銷燬自己-因此,當呼叫Dispose方法時,它將首先取消訂閱。

退訂

取消訂閱訊息的方法如下所示:

        public async Task Unsubscribe<TMessage>(SubscriptionToken subscriptionToken) where TMessage : Message
        {
            await UnsubscribeInternal<TMessage>(subscriptionToken.Id);
        }
        private async Task UnsubscribeInternal<TMessage>(Guid subscriptionId) where TMessage : Message
        {
            if (_subscriptions.TryGetValue(typeof(TMessage), out var messageSubscriptions))
            {
                if (messageSubscriptions.ContainsKey(subscriptionId))
                {
                    var result = messageSubscriptions.TryRemove(subscriptionId, out BaseSubscription value);
                }
            }
        }


這很簡單。 當我們取消訂閱訊息時,訂閱將從字典中刪除。

釋出

好了,我們已經訂閱了該訊息,並建立了儲存在字典中的訂閱例項。 我們現在可以釋出訊息。 釋出訊息的方法如下所示:

        public async Task Publish<TMessage>(TMessage message) where TMessage : Message
        {
            if (message == null)
            {
                throw new ArgumentNullException(nameof(message));
            }
            List<BaseSubscription> toPublish = null;
            Type messageType = message.GetType();

            if (_subscriptions.TryGetValue(messageType, out var messageSubscriptions))
            {
                toPublish = messageSubscriptions.Values.OrderByDescending(x => x.Priority).ToList();
            }

            if (toPublish == null || toPublish.Count == 0)
            {
                return;
            }

            List<Guid> deadSubscriptionIds = new List<Guid>();
            foreach (var subscription in toPublish)
            {
                // Execute the action for this message.
                var result = await subscription.Invoke(message);
                if (!result)
                {
                    deadSubscriptionIds.Add(subscription.Id);
                }
            }

            if (deadSubscriptionIds.Any())
            {
                await PurgeDeadSubscriptions(messageType, deadSubscriptionIds);
            }
        }  


當我們釋出一條訊息時,MessageHub將查詢字典以檢索該訊息的訂閱,然後迴圈執行操作。
我們需要注意的另一件事是,由於某些訂閱可能是弱引用,因此我們需要檢查執行結果。 如果失敗,我們需要將其從訂閱中刪除。

用法

從NuGet安裝:

PM> Install-Package FunCoding.CoreMessenger

在整個應用程式域中,將“ MessengerHub.Instance”用作單例模式。它提供了以下方法:
-釋出:

public async Task Publish<TMessage>(TMessage message)

-訂閱:

public SubscriptionToken Subscribe<TMessage>(Action<TMessage> action, ReferenceType referenceType = ReferenceType.Weak, SubscriptionPriority priority = SubscriptionPriority.Normal, string tag = null)

-取消訂閱:

public async Task Unsubscribe<TMessage>(SubscriptionToken subscriptionToken)

建立Message

首先,定義不同元件之間從Message繼承的Message類,如下所示:

public class TestMessage : Message
{
    public string ExtraContent { get; private set; }
    public TestMessage(object sender, string content) : base(sender)
    {
        ExtraContent = content;
    }
}

然後在元件A中建立Message的例項,如下所示:

var message = new TestMessage(this, "Test Content");

訂閱

定義一個SubscriptionToken例項來儲存訂閱。在元件B中訂閱“訊息”,如下所示:

public class HomeViewModel
    {
        private readonly SubscriptionToken _subscriptionTokenForTestMessage;
        public HomeViewModel()
        {
            _subscriptionTokenForTestMessage = 
                MessengerHub.Instance.Subscribe<TestMessage>(OnTestMessageReceived,
                ReferenceType.Weak, SubscriptionPriority.Normal);
        }

        private void OnTestMessageReceived(TestMessage message)
        {
#if DEBUG
            System.Diagnostics.Debug.WriteLine($"Received messages of type {message.GetType().ToString()}. Content: {message.Content}");
#endif
        }
    }

釋出Message

在元件A中釋出“訊息”:

public async Task PublishMessage()
{
    await MessengerHub.Instance.Publish(new TestMessage(this, $"Hello World!"));
}

就是這麼簡單。

引數

Subscribe方法的完整簽名為:

public SubscriptionToken Subscribe<TMessage>(Action<TMessage> action,
            ReferenceType referenceType = ReferenceType.Weak,
            SubscriptionPriority priority = SubscriptionPriority.Normal, string tag = null) where TMessage : Message

您可以指定以下引數:
-ReferenceType。預設值為“ ReferenceType.Weak”,因此您不必擔心記憶體洩漏。一旦SubscriptionToken例項超出範圍,GC便可以自動收集它(但不確定何時)。如果需要保留強引用,請將引數指定為ReferenceType.Strong,以使GC無法收集它。
-SubscriptionPriority。預設值為SubscriptionPriority.Normal。有時需要控制一個“訊息”的訂閱的執行順序。在這種情況下,請為訂閱指定不同的優先順序以控制執行順序。注意,該引數不適用於不同的Message
-Tag。檢查訂閱的當前狀態,是可選的。

退訂

您可以使用以下方法取消訂閱:
-使用“Unsubscribe”方法,如下所示:

await MessengerHub.Instance.Unsubscribe<TestMessage>(_subscriptionTokenForTestMessage);

-使用SubscriptionToken的Dispose方法:

_subscriptionTokenForTestMessage.Dispose();

在許多情況下,您不會直接呼叫這些方法。如果使用強訂閱型別,則可能會導致記憶體洩漏問題。因此,建議使用“ ReferenceType.Weak”。請注意,如果令牌未儲存在上下文中,則GC可能會立即收集它。例如:

public void MayNotEverReceiveAMessage()
{
    var token = MessengerHub.Instance.Subscribe<TestMessage>((message) => {
        // Do something here
    });
    // token goes out of scope now
    // - so will be garbage collected *at some point*
    // - so the action may never get called
}

與MvvmCross.Messenger的差異

如果您使用MvvmCross開發應用程式,請直接使用MvvmCross.Messenger。我提取了一些主要方法並刪除了對“ MvvmCross”元件的依賴,因此它可以在沒有“ MvvmCross”的任何WPF,UWP和Xamarin專案中使用。另外,Publish方法始終在後臺執行,以避免阻塞UI。但是您應該知道何時需要返回UI執行緒,尤其是當您需要與UI控制元件進行互動時。另一個區別是無需使用DI來建立MessageHub例項,該例項是所有應用程式域中的單例例項。如果解決方案包含需要相互通訊的多個元件,則將很有用。 DI將使其更加複雜。