为WPF, UWP 及 Xamarin实现一个简单的消息组件
生活随笔
收集整理的這篇文章主要介紹了
为WPF, UWP 及 Xamarin实现一个简单的消息组件
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
友情提示:閱讀本文大概需要8分鐘。
歡迎大家點擊上方公眾號鏈接關注我,了解新西蘭碼農生活
本文目錄:
1. 介紹
2. Message - 消息
3. Subscription - 訂閱
4. MessageHub - 消息總線
4.1 Subscribe - 訂閱
4.2 Unsubscribe - 取消訂閱
4.3 Publish - 發布
5. 用法
5.1 從NuGet安裝
5.2 創建Message類
5.3 訂閱
5.4 發布Message
5.5 參數
5.6 取消訂閱
6. 與MvvmCross.Messenger的差異
1. 介紹
Sub-Pub模式是一種常用的設計模式,用來在系統的不同組件中傳遞消息。發送消息的稱為Publisher,接收消息的稱為Subscriber。雙方一般不需要知道對方的存在,由一個代理負責消息的傳遞。其結構如圖所示:最初的需求是我需要開發一個實現Socket發送/接收的WPF應用程序。首先,我應用MVVM模式創建了一個基本的WPF應用程序。然后,我創建了另一個項目來完成所有與Socket通信有關的工作。接下來,我必須將Socket項目集成到ViewModel項目中,以操作Socket連接。顯然,我們可以為此使用Event。例如,我們可以有一個名為?SocketServer的類,該類具有一個事件來接收Socket數據包,然后在ViewModel層中對其進行訂閱。但這意味著我們必須在ViewModel層中創建?SocketServer類的實例,該類將ViewModel層與Socket項目耦合在一起。我希望創建一個中間件以解耦它們。?這樣,發布者和訂閱者就不需要知道對方的存在了。MvvmCross提供了一個名為?Messenger?的插件以在ViewModel之間進行通信。但它依賴于某些MvvmCross組件,這意味著如果我想在其他項目中使用此插件,則必須引用MvvmCross。這對我當前的情況而言并不理想,因為實際上,Socket項目沒有必要引用MvvmCross。因此,我做了一個專注于發布/訂閱模式的項目,并刪除了對MvvmCross的依賴。現在,可以在任何WPF,UWP和Xamarin項目中重復使用它。我已將其發布到GitHub上:https://github.com/yanxiaodi/CoreMessenger ,并發布了NuGet包:https://www.nuget.org/packages/FunCoding.CoreMessenger/。本文僅介紹該組件的實現細節,后面會再寫一篇文章介紹如何使用Azure DevOps實現CI/CD。下面讓我們了解一下Sub-Pub模式的一種實現方式。
2. Message - 消息
Message是在此系統中表示消息的抽象類:
public abstract class Message{ public object Sender { get; private set; } protected Message(object sender) { Sender = sender ?? throw new ArgumentNullException(nameof(sender)); }}我們需要從該抽象類派生不同消息的實例。它有一個名為sender的參數,因此訂閱者可以獲取發送者的實例。但這并不是強制性的。3. Subscription - 訂閱
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()方法來執行該_action。我們使用Task來包裝動作,以便可以利用異步操作的優勢。
這是名為?WeakSubscription”的另一種訂閱: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,當對象超出范圍時,會自動刪除訂閱。
4. 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在其內部維護一個ConcurrentDictionary來管理訂閱的實例,如下所示:private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Guid, BaseSubscription>> _subscriptions = new ConcurrentDictionary<Type, ConcurrentDictionary<Guid, BaseSubscription>>();該ConcurrentDictionary的Key是Message的類型,Value是一個ConcurrentDictionary,其中包含該特定Message的一組訂閱。顯然,一種類型可能具有多個訂閱。4.1 Subscribe - 訂閱
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方法時,它將首先取消訂閱。
4.2 Unsubscribe - 取消訂閱
取消訂閱消息的方法如下所示: 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); } } }這段代碼很容易理解。當我們取消訂閱消息時,訂閱將從字典中刪除。4.3 Publish - 發布
我們已經訂閱了消息,并創建了存儲在字典中的訂閱實例。現在可以發布消息了。發布消息的方法如下所示: 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將查詢字典以檢索該消息的訂閱列表,然后循環執行操作。需要注意的另一件事是,由于某些訂閱可能是弱引用,因此需要檢查執行結果。如果引用已經被GC收集,則執行結果會返回false,這時候需要將該訂閱從訂閱列表中刪除。
5. 用法
5.1 從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)
5.2 創建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");5.3 訂閱
private void OnTestMessageReceived(TestMessage message) {#if DEBUG System.Diagnostics.Debug.WriteLine($"Received messages of type {message.GetType().ToString()}. Content: {message.Content}");#endif } }
5.4 發布Message
在組件A中發布消息:public async Task PublishMessage(){ await MessengerHub.Instance.Publish(new TestMessage(this, $"Hello World!"));}就是這么簡單。5.5 參數
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。為訂閱指定一個標簽,是可選的。5.6 取消訂閱
您可以使用以下方法取消訂閱:- 使用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}
6. 與MvvmCross.Messenger的差異
如果您已經使用MvvmCross開發應用程序,并無需在ViewModel層之外傳遞消息,請直接使用MvvmCross.Messenger。我僅實現了一些主要方法,沒有提供UI線程調度的功能,并刪除了對MvvmCross組件的依賴,因此只要您的項目目標.NET Standard 2.0以上,就可以在任何WPF,UWP和Xamarin項目中使用。另外,Publish方法始終在后臺運行,以避免阻塞UI。但是您應該知道何時需要返回UI線程,尤其是當您需要與UI控件進行交互時。另一個區別是無需使用DI來創建MessageHub實例,該實例是所有應用程序域中的單例實例。如果解決方案包含需要相互通信的多個組件,則單例模式會比較簡單,DI將使其更加復雜。請點擊閱讀原文查看GitHub鏈接。如果覺得有用歡迎加星????了解新西蘭IT行業真實碼農生活
請長按上方二維碼關注“程序員在新西蘭”
總結
以上是生活随笔為你收集整理的为WPF, UWP 及 Xamarin实现一个简单的消息组件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Facebook押注VS Code
- 下一篇: 用ASP.NETCore构建可检测的高可