15°

为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;
    </span><span style="color: #0000ff;">public</span> StrongSubscription(Action&lt;TMessage&gt;<span style="color: #000000;"> action,
        SubscriptionPriority priority, </span><span style="color: #0000ff;">string</span> tag): <span style="color: #0000ff;">base</span><span style="color: #000000;">(priority, tag)
    {
        _action </span>=<span style="color: #000000;"> action;
    }
    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">override</span> <span style="color: #0000ff;">async</span> Task&lt;<span style="color: #0000ff;">bool</span>&gt; Invoke(<span style="color: #0000ff;">object</span><span style="color: #000000;"> message)
    {
        </span><span style="color: #0000ff;">var</span> typedMessage = message <span style="color: #0000ff;">as</span><span style="color: #000000;"> TMessage;
        </span><span style="color: #0000ff;">if</span> (typedMessage == <span style="color: #0000ff;">null</span><span style="color: #000000;">)
        {
            </span><span style="color: #0000ff;">throw</span> <span style="color: #0000ff;">new</span> Exception($<span style="color: #800000;">"</span><span style="color: #800000;">Unexpected message {message.ToString()}</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        }
        </span><span style="color: #0000ff;">await</span> Task.Run(() =&gt; _action?<span style="color: #000000;">.Invoke(typedMessage));
        </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">true</span><span style="color: #000000;">;
    }
}</span></pre> 


它继承了BaseSubscription并覆盖了Invoke()方法。 基本上,它具有一个名为“ _action”的字段,该字段在创建实例时定义。 当我们发布消息时,订阅将调用Invoke()方法来执行操作。 我们使用Task来包装动作,以便可以利用异步操作的优势。


这是名为“ WeakSubscription”的另一种“ Subscription”:

    public class WeakSubscription<TMessage> : BaseSubscription where TMessage : Message
    {
        private readonly WeakReference<Action<TMessage>> _weakReference;
    </span><span style="color: #0000ff;">public</span> WeakSubscription(Action&lt;TMessage&gt;<span style="color: #000000;"> action,
        SubscriptionPriority priority, </span><span style="color: #0000ff;">string</span> tag) : <span style="color: #0000ff;">base</span><span style="color: #000000;">(priority, tag)
    {
        _weakReference </span>= <span style="color: #0000ff;">new</span> WeakReference&lt;Action&lt;TMessage&gt;&gt;<span style="color: #000000;">(action);
    }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">override</span> <span style="color: #0000ff;">async</span> Task&lt;<span style="color: #0000ff;">bool</span>&gt; Invoke(<span style="color: #0000ff;">object</span><span style="color: #000000;"> message)
    {
        </span><span style="color: #0000ff;">var</span> typedMessage = message <span style="color: #0000ff;">as</span><span style="color: #000000;"> TMessage;
        </span><span style="color: #0000ff;">if</span> (typedMessage == <span style="color: #0000ff;">null</span><span style="color: #000000;">)
        {
            </span><span style="color: #0000ff;">throw</span> <span style="color: #0000ff;">new</span> Exception($<span style="color: #800000;">"</span><span style="color: #800000;">Unexpected message {message.ToString()}</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        }
        Action</span>&lt;TMessage&gt;<span style="color: #000000;"> action;
        </span><span style="color: #0000ff;">if</span> (!_weakReference.TryGetTarget(<span style="color: #0000ff;">out</span><span style="color: #000000;"> action))
        {
            </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">false</span><span style="color: #000000;">;
        }
        </span><span style="color: #0000ff;">await</span> Task.Run(() =&gt; action?<span style="color: #000000;">.Invoke(typedMessage));
        </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">true</span><span style="color: #000000;">;
    }
}</span></pre> 


它与强订阅的区别在于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);
        }
    </span><span style="color: #0000ff;">private</span> SubscriptionToken SubscribeInternal&lt;TMessage&gt;(Action&lt;TMessage&gt;<span style="color: #000000;"> action, BaseSubscription subscription)
        </span><span style="color: #0000ff;">where</span><span style="color: #000000;"> TMessage : Message
    {
        </span><span style="color: #0000ff;">if</span> (!_subscriptions.TryGetValue(<span style="color: #0000ff;">typeof</span>(TMessage), <span style="color: #0000ff;">out</span> <span style="color: #0000ff;">var</span><span style="color: #000000;"> messageSubscriptions))
        {
            messageSubscriptions </span>= <span style="color: #0000ff;">new</span> ConcurrentDictionary&lt;Guid, BaseSubscription&gt;<span style="color: #000000;">();
            _subscriptions[</span><span style="color: #0000ff;">typeof</span>(TMessage)] =<span style="color: #000000;"> messageSubscriptions;
        }
        messageSubscriptions[subscription.Id] </span>=<span style="color: #000000;"> subscription;
        </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">new</span> SubscriptionToken(subscription.Id, <span style="color: #0000ff;">async</span> () =&gt; <span style="color: #0000ff;">await</span> UnsubscribeInternal&lt;TMessage&gt;<span style="color: #000000;">(subscription.Id), action);
    }</span></pre> 


当我们订阅消息时,我们创建Subscription的实例并将其添加到字典中。 根据您的选择,它可能是强引用或者弱引用。 然后它将创建一个SubscriptionToken,这是一个实现IDisposable接口来管理订阅的类:

    public sealed class SubscriptionToken : IDisposable
    {
        public Guid Id { get; private set; }
        private readonly Action _disposeMe;
        private readonly object _dependentObject;
    </span><span style="color: #0000ff;">public</span> SubscriptionToken(Guid id, Action disposeMe, <span style="color: #0000ff;">object</span><span style="color: #000000;"> dependentObject)
    {
        Id </span>=<span style="color: #000000;"> id;
        _disposeMe </span>=<span style="color: #000000;"> disposeMe;
        _dependentObject </span>=<span style="color: #000000;"> dependentObject;
    }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> Dispose()
    {
        Dispose(</span><span style="color: #0000ff;">true</span><span style="color: #000000;">);
        GC.SuppressFinalize(</span><span style="color: #0000ff;">this</span><span style="color: #000000;">);
    }

    </span><span style="color: #0000ff;">private</span> <span style="color: #0000ff;">void</span> Dispose(<span style="color: #0000ff;">bool</span><span style="color: #000000;"> isDisposing)
    {
        </span><span style="color: #0000ff;">if</span><span style="color: #000000;"> (isDisposing)
        {
            _disposeMe();
        }
    }
}</span></pre> 


当我们创建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();
        </span><span style="color: #0000ff;">if</span> (_subscriptions.TryGetValue(messageType, <span style="color: #0000ff;">out</span> <span style="color: #0000ff;">var</span><span style="color: #000000;"> messageSubscriptions))
        {
            toPublish </span>= messageSubscriptions.Values.OrderByDescending(x =&gt;<span style="color: #000000;"> x.Priority).ToList();
        }

        </span><span style="color: #0000ff;">if</span> (toPublish == <span style="color: #0000ff;">null</span> || toPublish.Count == <span style="color: #800080;">0</span><span style="color: #000000;">)
        {
            </span><span style="color: #0000ff;">return</span><span style="color: #000000;">;
        }

        List</span>&lt;Guid&gt; deadSubscriptionIds = <span style="color: #0000ff;">new</span> List&lt;Guid&gt;<span style="color: #000000;">();
        </span><span style="color: #0000ff;">foreach</span> (<span style="color: #0000ff;">var</span> subscription <span style="color: #0000ff;">in</span><span style="color: #000000;"> toPublish)
        {
            </span><span style="color: #008000;">//</span><span style="color: #008000;"> Execute the action for this message.</span>
            <span style="color: #0000ff;">var</span> result = <span style="color: #0000ff;">await</span><span style="color: #000000;"> subscription.Invoke(message);
            </span><span style="color: #0000ff;">if</span> (!<span style="color: #000000;">result)
            {
                deadSubscriptionIds.Add(subscription.Id);
            }
        }

        </span><span style="color: #0000ff;">if</span><span style="color: #000000;"> (deadSubscriptionIds.Any())
        {
            </span><span style="color: #0000ff;">await</span><span style="color: #000000;"> PurgeDeadSubscriptions(messageType, deadSubscriptionIds);
        }
    }  </span></pre> 


当我们发布一条消息时,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将使其更加复杂。

本文转载自博客园,原文链接:https://www.cnblogs.com/yanxiaodi/p/11895433.html

全部评论: 0

    我有话说: