为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 == )
{
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将使其更加复杂。
为WPF, UWP 及 Xamarin实现一个简单的消息组件的更多相关文章
- Java编程的逻辑 (61) - 内存映射文件及其应用 - 实现一个简单的消息队列
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- 手写一个简单的starter组件
spring-boot中有很多第三方包,都封装成starter组件,在maven中引用后,启动springBoot项目时会自动装配到spring ioc容器中. 思考: 为什么我们springBoot ...
- 编写一个简单的COM组件
参考网站:编写一个简单的COM组件_a ray of sunshine-CSDN博客 (1) 用MIDL编写.idl文件 //将以下代码保存成 IXIYIZ.idl 文件 //在命令行上进行编译,编译 ...
- [UWP]使用Picker实现一个简单的ColorPicker弹窗
在上一篇博文<[UWP]使用Popup构建UWP Picker>中我们简单讲述了一下使用Popup构建适用于MVVM框架下的弹窗层组件Picker的过程.但是没有应用实例的话可能体现不出P ...
- 哪种缓存效果高?开源一个简单的缓存组件j2cache
背景 现在的web系统已经越来越多的应用缓存技术,而且缓存技术确实是能实足的增强系统性能的.我在项目中也开始接触一些缓存的需求. 开始简单的就用jvm(java托管内存)来做缓存,这样对于单个应用服务 ...
- 一个简单的消息提示jquery插件
最近在工作中写了一个jquery插件,效果如下: 就是一个简单的提示消息的一个东西,支持最大化.最小化.关闭.自定义速度.自定义点击事件,数据有ajax请求和本地数据两种形式.还有不完善的地方,只做了 ...
- setbuffer和freopen做一个简单的日志组件
目标场景是这样的: 多线程的应用程序要频繁打一些小字节的日志,也不想引用很重的日志库. 设想了一个极其简单的日志组件,main线程中重定向stdout到文件,同时setbuffer设置一个10k的缓冲 ...
- C#TCPClient应用-一个简单的消息发送和接收
TcpSend窗口用于发送消息,另外写一个用于接收消息的应用程序,消息接受到以后,必须要关闭接收消息的窗口,才能在接收新的消息,不知道怎么能解决这个问题. 源代码: 发送消息的窗口代码 using S ...
- python安装及写一个简单的验证码组件(配合node)
1.安装Python 到官网下载响应系统的版本(这里以windows为例):https://www.python.org/downloads/windows/ 然后就是不断地"下一步&quo ...
随机推荐
- SpringSide 3 中的安全框架
在SpringSide 3的官方文档中,说安全框架使用的是Spring Security 2.0.乍一看,吓了我一跳,以为Acegi这么快就被淘汰了呢.上搜索引擎一搜,发现原来Spring Secur ...
- window 系统下修改`CMD`的编码格式的方法,`CHCP` 的 使用
CHCP的使用 CHCP是一个计算机指令,能够显示或设置活动代码页编号. 一般上是在命令提示框中使用,用来查询和修改命令提示框的编码格式 具体使用方法 查看活动代码页编号 方式1: >>& ...
- Roslyn 使用 WriteLinesToFile 解决参数过长无法传入
在写 Roslyn 的时候,经常需要辅助编译的工具,而这些工具需要传入一些参数,在项目很大的时候,会发现自己传入的参数比微软限制控制台可以传入的参数大很多,这时就无法传入了参数 本文告诉大家如何使用 ...
- 【t050】方程求解
Time Limit: 1 second Memory Limit: 128 MB [问题描述] 要求Xi(i = 1,2,3,4)是一个[-T..T]中的整数,满足方程AX1 + BX2 + CX3 ...
- Educational Codeforces Round 61
Educational Codeforces Round 61 今早刚刚说我适合打pikmike出的EDU 然后我就挂了 A 不管 B 不管 C 这道题到快结束了才调出来 大概就是\(n^2\)枚举不 ...
- HDU - 3671 Boonie and Clyde (图的割点)
As two icons of the Great Depression, Bonnie and Clyde represent the ultimate criminal couple. Stori ...
- Servlet 的面试题
Servlet运行在Servlet容器中,其生命周期由容器来管理.Servlet的生命周期通过javax.servlet.Servlet接口中的init().service()和destroy()方法 ...
- web.config修改文件修改上传大小
老是要修改上传文件大小的限制,先记在这里. <httpRuntime maxRequestLength= "1048576 " //最大长度 executionTimeout ...
- 百度DMA+小度App的蓝牙语音解决方案入局
前记 人机交互经历了三个阶段键鼠.触屏和语音交互.在国外,谷歌.亚马逊.苹果等巨头的竞争已经到达白热化状态:在国内,百度的DuerOS凭借着入局早,投入大,已经成为国内语音互交的一面旗帜.无论是从 ...
- 聊聊最近撸Spring源码感悟
一.前言 最近一段时间撸了Spring IOC.AOP.Transactional源码,这篇文章聊聊我写了哪些小玩意,这可能就是阅读源码以后最大收获.希望大家在里面能学习一些什么东西吧: 二. ...