为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 订阅

定义一个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行业真实码农生活
请长按上方二维码关注“程序员在新西兰”

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312943.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mac(OS X)安装、配置并使用MySQL数据库

1、MySQL安装包下载 MySQL Community Server下载地址&#xff1a;http://dev.mysql.com/downloads/mysql/&#xff0c;下载完毕之后安装文件如下所示&#xff1a; 2、MySQL安装 双击安装文件进行安装&#xff0c;出现如下界面&#xff1a; 双击窗体中的“mysql-5.7.10-osx10.9-…

用ASP.NETCore构建可检测的高可用服务

一、前言2019 中国 .NET 开发者峰会&#xff08;.NET Conf China 2019&#xff09;于2019年11月10日完美谢幕&#xff0c;校宝在线作为星牌赞助给予了峰会大力支持&#xff0c;我和项斌等一行十位同事以讲师、志愿者的身份公司参与到峰会的支持工作中&#xff0c;我自己很荣幸能…

Mac(OS X)使用brew安装软件

在Linux下&#xff0c;常用的软件包管理工具有RedHat系的yum和Debian系的apk-get&#xff0c;对于Mac的OS X系统来说&#xff0c;同样有这样的形式的软件包管理工具&#xff0c;即&#xff1a;brew。 brew 又叫Homebrew&#xff0c;是Mac OSX上的软件包管理工具&#xff0c;能…

SonarQube系列二、分析dotnet core/C#代码

来源&#xff1a;https://www.cnblogs.com/7tiny/p/11342902.html【前言】本系列主要讲述sonarqube的安装部署以及如何集成jenkins自动化分析.netcore项目。目录如下&#xff1a;SonarQube系列一、Linux安装与部署SonarQube系列二、分析dotnet core/C#代码SonarQube系列三、如何…

Eclipse使用Maven插件创建Web项目时出错:Could not resolve archetype org.apache.maven.archetypes

问题描述&#xff1a; 在Eclipse下&#xff0c;使用Maven插件创建Web项目时出错&#xff0c;错误提示如下&#xff1a; 错误信息&#xff1a; Could not resolve archetype org.apache.maven.archetypes:maven-archetype-webapp:1.0 from any of the configured repositories. …

ASP.NET Core快速入门(第2章:配置管理)--学习笔记

点击蓝字关注我们课程链接&#xff1a;http://video.jessetalk.cn/course/explore良心课程&#xff0c;大家一起来学习哈&#xff01;任务9&#xff1a;配置介绍命令行配置Json文件配置从配置文件文本到c#对象实例的映射 - Options 与 Bind配置文件热更新框架设计&#xff1a;C…

怎么写技术简历?

作者&#xff1a;zollty&#xff0c;资深程序员和架构师&#xff0c;私底下是个爱折腾的技术极客&#xff0c;架构师社区合伙人&#xff01;纯手打&#xff0c;个人多年经验总结。&#xff08;本人的简历&#xff0c;历年来效果都不错&#xff0c;从校招到高端技术岗位、大厂技…

C# 代码中调用 Javascript 代码段以提高应用程序的配置灵活性(使用 Javascript .NET 与 Jint)...

一般来说&#xff0c;我们需要在开发应用软件的配置文件中&#xff0c;添加一些参数&#xff0c;用于后续用户根据实际情况&#xff0c;自行调整。配置参数&#xff0c;可以放在配置文件中、环境变量中、或数据库表中(如果使用了数据库的话)。通常&#xff0c;配置数据&#xf…

Code Runner for VS Code 突破 1000 万下载量!支持运行超过 40 种语言

记得三年多前&#xff0c;韩老师那时还在写 PHP&#xff08;是的&#xff0c;没错&#xff01;在微软写 PHP&#xff09;&#xff0c;同时需要写 Python 和 Node.js 。所以在那时&#xff0c;支持多种语言的 VS Code 已经是笔者的主力编辑器了。唯一不足的是&#xff0c;韩老师…

Firefox UI已迁移至Web Components

这不是一项一蹴而就的工程&#xff0c;Mozilla 开发者表示团队花费了大约两年的时间&#xff0c;采用“增量更新”的方式才逐渐将 Firefox UI 迁移至使用 Web Components 构建。Mozilla 开发者 Brian Grinstead 在博客宣布&#xff0c;经过多年的努力&#xff0c;他已在几个星期…

.Net线程同步技术解读

C#开发者(面试者)都会遇到lock(Monitor)&#xff0c;Mutex&#xff0c;Semaphore&#xff0c;SemaphoreSlim这四个与锁相关的C#类型&#xff0c;本文期望以最简洁明了的方式阐述四种对象的区别。什么是线程安全教条式理解如果代码在多线程环境中运行的结果与单线程运行结果一样…

.NET使用VS2010开发Web Service

Web Service是一个面向服务的架构&#xff08;SOA&#xff09;&#xff0c;不依赖于语言&#xff0c;不依赖于平台&#xff0c;可以实现不同的语言间的相互调用&#xff0c;通过Internet进行基于Http协议的网络应用间的交互。基于.NET平台&#xff0c;可以使用Visual Studio来开…

Java 开发Web Service的几种方式

webservice的应用已经越来越广泛了&#xff0c;下面介绍几种在Java体系中开发webservice的方式&#xff0c;相当于做个记录。 1.Axis2 Axis是apache下一个开源的webservice开发组件&#xff0c;出现的算是比较早了&#xff0c;也比较成熟。这里主要介绍Axiseclipse开发webservi…

动手造轮子:实现简单的 EventQueue

动手造轮子&#xff1a;实现简单的 EventQueueIntro最近项目里有遇到一些并发的问题&#xff0c;想实现一个队列来将并发的请求一个一个串行处理&#xff0c;可以理解为使用消息队列处理并发问题&#xff0c;之前实现过一个简单的 EventBus&#xff0c;于是想在 EventBus 的基础…

【.NET Core 跨平台 GUI 开发】第二篇:Gtk# 布局入门,初识HBox 和 VBox

这是 Gtk# 系列博文的第二篇。在上一篇博文《编写你的第一个 Gtk# 应用》中&#xff0c;我们提到“一个 Gtk.Window 只能直接包含一个部件”。这意味着&#xff0c;在不做其他额外操作的情况下&#xff0c;如果你向一个 GtkWindow 中添加了一个 GtkLabel &#xff08;就像上一篇…

Java开发Web Service的几种解决方案

转自&#xff1a;http://blog.csdn.net/zolalad/article/details/25158995 Java开发中经常使用到的几种WebService技术实现方案 随着异构系统互联需求的不断增加&#xff0c;WebService的重要性也日益彰显出来。凭借webservice&#xff0c;我们可以实现基于不同程序语言的项目的…

【.NET Core 跨平台 GUI 开发】第一篇:编写你的第一个 Gtk# 应用

本文是【.NET Core 跨平台 GUI 开发】系列博文的第一篇。该系列博文是一个关于 Gtk# 跨平台应用开发的初级随笔集合。该随笔集合介绍了 GTK 和 Gtk# 的基本信息以及开发方法&#xff0c;并展示了如何使用 .NET Core 技术栈开发基于 Gtk# 的跨平台 GUI 程序。博文假设你已经对 C…

ASP.NET Core快速入门(第4章:ASP.NET Core HTTP介绍)--学习笔记

点击蓝字关注我们课程链接&#xff1a;http://video.jessetalk.cn/course/explore良心课程&#xff0c;大家一起来学习哈&#xff01;任务22&#xff1a;课程介绍1.HTTP 处理过程2.WebHost 的配置与启动3.Middleware 与管道4.Routing MiddleWare 介绍任务23&#xff1a;Http请求…

Java使用JWS API开发Web Service

JAX-WS&#xff0c;即Java API for XML Web Service&#xff0c;是Java开发基于SOAP协议的Web Service的标准。使用JWS API就可以直接开发简单的Web Service应用。 一、创建Web Service 打开Eclipse&#xff0c;新建一个Java Project&#xff0c;如下图所示&#xff1a; 新建了…

ASP.NET Core快速入门(第3章:依赖注入)--学习笔记

点击蓝字关注我们课程链接&#xff1a;http://video.jessetalk.cn/course/explore良心课程&#xff0c;大家一起来学习哈&#xff01;任务16&#xff1a;介绍1、依赖注入概念详解从UML和软件建模来理解从单元测试来理解2、ASP.NET Core 源码解析任务17&#xff1a;从UML角度来理…