友情提示:阅读本文大概需要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 订阅

定义一个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        }    }

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);
- 使用SubscriptionTokenDispose方法:
_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实现一个简单的消息组件相关推荐

  1. [UWP]使用Picker实现一个简单的ColorPicker弹窗

    [UWP]使用Picker实现一个简单的ColorPicker弹窗 原文:[UWP]使用Picker实现一个简单的ColorPicker弹窗 在上一篇博文<[UWP]使用Popup构建UWP P ...

  2. ros订阅话题python_ROS入门教程 (写一个简单的消息发布器和订阅器 (Python))

    Note: This tutorial assumes that you have completed the previous tutorials: 创建ROS消息和ROS服务. Descripti ...

  3. C#.net创建一个简单的消息队列程序(MessageQueue)

    MessageQueue:消息队列,现在应用也是越来越广泛,如微信朋友圈,可以概括为仅仅两条消息队列,消息队列常用于处理高并发的问题,当我们在实际应用中发现接收到数据量过多时,可以使用队列. 微软提供 ...

  4. 利用递归组件实现一个简单的树组件(vue3)

    1. 简言 闲来无事不从容,睡觉东窗日已红. 万物静观皆自得,四时佳兴与认同. 最近学习vue3组件的时候实现了一个简单的树组件.话不多说,直接上代码. 2.效果 这个数组件实现了展开.选中和选择三个 ...

  5. 如何自己撸一个简单的开关组件

    之前使用开关的时候你肯定会想到Google原生提供的switch. 最近一直想自己写一个Q弹简单的开关组件,说干就干.经过一小个晚上的奋战,效果如下.这里分别实现了两种Q弹简单的开关组件,一个圆形开关 ...

  6. ROS-写一个简单的消息发布器与订阅器(Python)(roswiki初级教程rospy部分翻译以及程序注释)

    Topic通信机制---编写简单的Publisher & Subscriber http://wiki.ros.org/cn/ROS/Tutorials/WritingPublisherSub ...

  7. ROS学习笔记12(用Python写一个简单的消息发布和消息订阅)

    文章目录 1 写一个消息发布节点 2 写一个消息订阅节点 3 构建节点 1 写一个消息发布节点 $ roscd beginner_tutorials $ mkdir scripts $ cd scri ...

  8. ROS学习笔记11(用C++写一个简单的消息发布和消息订阅)

    文章目录 1 写一个消息发布节点 2 写一个消息订阅节点 3 构建节点 1 写一个消息发布节点 首先切换到工作目录: roscd beginner_tutorials 接着,创建src/talker. ...

  9. ROS学习笔记13(测试一个简单的消息发布和消息订阅)

    文章目录 1 运行消息发布 2 运行消息订阅 1 运行消息发布 $ roscore # In your catkin workspace $ cd ~/catkin_ws $ source ./dev ...

最新文章

  1. python pandas 独热编码
  2. android触摸外部关闭键盘,如何隐藏Android上的软键盘,点击外部EditText?
  3. 下载文件的响应头设置
  4. 2017中国人工智能峰会即将开启,和30位AI大咖一起头脑风暴
  5. 页面加载事件html5,JavaScript页面加载事件实例讲解
  6. 元胞计算机系统,元胞自动机
  7. java 第几周_java获取第几周
  8. 1.maven下仅shiro框架对shiro的测试使用
  9. 起底量化交易的发展之路
  10. 软考高级 真题 2011年下半年 信息系统项目管理师 综合知识
  11. PhotoShop中合并形状颜色会变化的问题
  12. 安卓手机版微信聊天加密软件 悬浮窗版本
  13. 为什么你不想学习?只想玩?人是如何一步一步废掉的
  14. 建筑师妹岛和世为日本西武设计新型旗舰“球形车头”列车
  15. 2021-09-22 WPF上位机 24-动态图形变换
  16. 【科创人】快狗打车CTO沈剑:努力不会背叛,承担社会责任的企业胜算高一点点...
  17. 康托展开与逆康托展开详解
  18. 直逼 Flash 的流畅感:jQuery 运动特效展示
  19. 解决Pycharm装Illuminated Cloud之后无法创建项目的思路_艾孜尔江撰稿
  20. 未来已来?走进元宇宙入口 - 虚拟数字人

热门文章

  1. div 包裹_如何查看到达之前收到的包裹和邮件
  2. [译] 机器学习可以建模简单的数学函数吗?
  3. 第 133 章 FAQ
  4. 书生云王东临:真正的超融合产品要像“机器猫” 开箱即用
  5. 玩一玩微信公众号开发(一) 接入系统
  6. 子商城管理之签到活动
  7. [LeetCode]--20. Valid Parentheses
  8. linux下简单的备份的脚本 2 【转】
  9. 通过电话号码获取姓名 (+86或者飞信)
  10. 使用Windbg内核调试连接调试用户态程序的方法