锐英源软件
第一信赖

精通

英语

开源

擅长

开发

培训

胸怀四海 

第一信赖

当前位置:锐英源 / 开源技术 / C#开源 / MVVMLight / MVVMLight事件深度剖析
服务方向
人工智能数据处理
人工智能培训
kaldi数据准备
小语种语音识别
语音识别标注
语音识别系统
语音识别转文字
kaldi开发技术服务
软件开发
运动控制卡上位机
机械加工软件
软件开发培训
Java 安卓移动开发
VC++
C#软件
汇编和破解
驱动开发
联系方式
固话:0371-63888850
手机:138-0381-0136
Q Q:396806883
微信:ryysoft

锐英源精品原创,禁止全文或局部转载,禁止任何形式的非法使用,侵权必究。点名“简易百科”和闲暇巴盗用锐英源原创内容。

MVVMLight事件深度剖析


锐英源软件在使用MVVMLight开发了企业信息化软件MIS软件后,想深入学习MVVMLight,就翻译了些外文精华,本文英文来源于微软官网,特此注明。这里面对初学者最难理解的可能是事件的多个发送者和多个接收者,多个代表复杂环境,复杂环境里有多个思维点,不太好串联。

 

这个关于 Model-View-ViewModel (MVVM) 模式和 MVVM Light Toolkit 的系列文章自从我一年前开始以来已经涵盖了很多领域,从在 MVVM 应用程序中使用 IOC 容器到处理跨线程访问的方法和 MVVM Light 的 DispatcherHelper 组件。我还谈到了命令(使用 RelayCommand 和 EventToCommand)、查看服务,例如 Navigation 和 Dialog 服务,并简要讨论了 Messenger 组件。

Messenger 组件实际上是 MVVM Light Toolkit 中一个非常强大的元素,由于其易用性,它经常吸引开发人员,但也引发了一些争议,因为如果它被滥用可能会产生风险。这个组件值得有一篇文章来解释它的工作原理、风险是什么以及它最有意义的场景。

在本文中,我将讨论 Messenger 实现背后的一般原则,并看看为什么这种实现比更传统的方法更易于使用。如果不采取某些预防措施,我还将探讨这种方法如何影响内存。最后,我将更详细地讨论 MVVM Light Messenger 本身,特别是一些内置消息及其使用。

事件聚合和 Messenger 简化

像 Messenger 这样的系统有时被称为事件总线或事件聚合器。这些组件连接发送者和接收者(有时分别称为“发布者”和“订阅者”)。在创建 MVVM Light 时,许多消息传递系统需要接收方或发送方实现特定的方法。例如,可能有一个 IReceiver 接口指定了一个 Receive 方法,并且要向消息传递系统注册,一个对象必须实现这个接口。这种约束很烦人,因为它限制了谁可以实际使用消息传递系统。例如,如果您使用第三方程序集,则无法从该库向消息传递系统注册实例,因为您无权访问代码并且无法修改第三方类以实现接收器。

创建 MVVM Light Messenger 的目的是简化此场景,前提是:任何对象都可以是接收器;任何对象都可以是发送者;任何对象都可以是消息。

词汇也被简化了。与其使用诸如“事件聚合”之类难以定义的词,而是讨论消息,这相当容易理解。订阅者成为接收者,发布者成为发送者。有消息而不是事件。语言中的这些简化以及简化的实现使您更容易开始使用 Messenger 并了解它是如何工作的。

例如,考虑图 1中的代码. 如您所见,MVVM Light Messenger 在两个单独的对象中使用。Registration 对象向所有 RegisteredUser 实例发送一条消息。这种场景可以通过多种方式实现,Messenger 可能并不总是最好的解决方案。但是,根据您的架构,它可能是实现此功能的一个非常好的工具,特别是如果发送方和接收方位于应保持解耦的应用程序部分。请注意 Registration 实例如何不显式发送到 RegisteredUser 实例。相反,它通过 Messenger 广播消息。任何实例都可以注册此类消息并在发送时收到通知。在本例中,发送的消息是一个 RegistrationInfo 实例。但是,可以从简单的值(int、bool 等)到专用的消息对象。稍后我将讨论使用消息并查看 MVVM Light 中的一些内置消息类型。

图 1 发送和接收消息

XML

复制
public class Registration
{
public void SendUpdate()
{
var info = new RegistrationInfo
{
// ... Some properties
};
Messenger.Default.Send(info);
}
}
public class RegisteredUser
{
public RegisteredUser()
{
Messenger.Default.Register<RegistrationInfo>(
this,
HandleRegistrationInfo);
}
private void HandleRegistrationInfo(RegistrationInfo info)
{
// Update registered user info
}
}
public class RegistrationInfo
{
// ... Some properties
}
图 1中的代码显示注册消息类型 (RegistrationInfo) 是通过委托 (HandleRegistrationInfo) 完成的。这是 Microsoft .NET Framework 中的一种常见机制。例如,在 C# 中注册事件处理程序也可以通过将委托传递给事件来完成,可以是命名方法或匿名 lambda 表达式。同样,您可以使用命名方法或匿名 lambda 向 Messenger 注册接收者,如图 2所示。

图 2 使用命名方法或 Lambda 注册

XML

复制
public UserControl()
{
InitializeComponent();
// Registering with named methods ----
Loaded += Figure2ControlLoaded;
Messenger.Default.Register<AnyMessage>(
this,
HandleAnyMessage);
// Registering with anonymous lambdas ----
Loaded += (s, e) =>
{
// Do something
};
Messenger.Default.Register<AnyMessage>(
this,
message =>
{
// Do something
});
}
private void HandleAnyMessage(AnyMessage message)
{
// Do something
}
private void Figure2ControlLoaded (object sender, RoutedEventArgs e)
{
// Do something
}

跨线程访问

Messenger 不做的一件事是查看消息发送到哪个线程。如果您阅读了我之前的文章“MVVM 应用程序中的多线程和调度”( bit.ly/1mgZ0Cb ),您就会知道,当线程上运行的对象尝试访问属于另一个线程的对象时,需要采取一些预防措施。这个问题经常出现在后台线程和 UI 线程拥有的控件之间。在上一篇文章中,您看到了如何使用 MVVM Light DispatcherHelper 在 UI 线程上“调度”操作并避免跨线程访问异常。

一些事件聚合器允许您自动分派发送到 UI 线程的消息。然而,MVVM Light Messenger 从不这样做,因为希望简化 Messenger API。添加自动将消息分派到 UI 线程的选项将为注册方法添加更多参数。此外,它会使调度变得不那么明确,对于经验不足的开发人员来说,可能更难以理解幕后发生的事情。

相反,如果需要,您应该显式地将消息分派到 UI 线程。最好的方法是使用 MVVM Light DispatcherHelper。如上一篇文章所示,CheckBeginInvokeOnUI 方法只会在必要时调度操作。如果 Messenger 已经在 UI 线程上运行,则消息可以立即分发而无需调度:

XML

复制
public void RunOnBackgroundThread()
{
// Do some background operation
DispatcherHelper.CheckBeginInvokeOnUI(
() =>
{
Messenger.Default.Send(new ConfirmationMessage());
});
}

注:这类似InvokeRequired处理。

内存处理

每个允许对象在不知道对方的情况下进行通信的系统,都面临着必须保存对消息接收者的引用的艰巨任务。例如,考虑 .NET 事件处理系统可以在引发事件的对象和订阅事件的对象之间创建强引用。图 3中的代码在 _first 和 _second 之间建立了牢固的联系。这意味着如果调用了 CleanUp 方法,并且 _second 设置为 null,垃圾收集器无法将其从内存中删除,因为 _first 仍然有对它的引用。垃圾收集器依靠对对象的引用计数来了解它是否可以从内存中删除,而第二个实例不会发生这种情况,因此会产生内存泄漏。随着时间的推移,这可能会导致很多问题;应用程序可能会显着减慢,最终甚至会崩溃。

图 3 实例之间的强引用

XML

复制
public class Setup
{
private First _first = new First();
private Second _second = new Second();
public void InitializeObjects()
{
_first.AddRelationTo(_second);
}
public void Cleanup()
{
_second = null;
// Even though this is set to null, the Second instance is
// still kept in memory because the reference count isn't
// zero (there's still a reference in _first).
}
}
public class First
{
private object _another;
public void AddRelationTo(object another)
{
_another = another;
}
}
public class Second
{
}
为了缓解这种情况,.NET 开发人员提出了 WeakReference 对象。此类允许以“弱”方式存储对对象的引用。如果对该对象的所有其他引用都设置为 null,则垃圾收集器仍然可以收集该对象,即使有一个 WeakReference 使用它。这非常方便,如果使用得当,它可以缓解内存泄漏问题,尽管它并不总能解决所有问题。为了说明这一点,图 4显示了一个简单的通信系统,其中 SimpleMessenger 对象将 Receiver 的引用存储在 WeakReference 中。请注意在处理消息之前对 IsAlive 属性的检查。如果 Receiver 之前已被删除和垃圾回收,则 IsAlive 属性将为 false。这表明 WeakReference 不再有效,应该被删除。

图 4 使用弱引用实例

XML

复制
public class SuperSimpleMessenger
{
private readonly List<WeakReference> _receivers
= new List<WeakReference>();
public void Register(IReceiver receiver)
{
_receivers.Add(new WeakReference(receiver));
}
public void Send(object message)
{
// Locking the receivers to avoid multithreaded issues.
lock (_receivers)
{
var toRemove = new List<WeakReference>();
foreach (var reference in _receivers.ToList())
{
if (reference.IsAlive)
{
((IReceiver)reference.Target).Receive(message);
}
else
{
toRemove.Add(reference);
}
}
// Prune dead references.
// Do this in another loop to avoid an exception
// when modifying a collection currently iterated.
foreach (var dead in toRemove)
{
_receivers.Remove(dead);
}
}
}
}
MVVM Light Messenger 的构建原理大致相同,尽管它当然要复杂得多!值得注意的是,由于 Messenger 不需要 Receiver 实现任何给定的接口,它需要存储对将用于传输消息的方法(回调)的引用。在 Windows Presentation Foundation (WPF) 和 Windows 运行时中,这不是问题。然而,在 Silverlight 和 Windows Phone 中,框架更加安全,API 可以防止某些操作发生。在某些情况下,这些限制之一会影响 Messenger 系统。

要理解这一点,您需要知道可以注册什么样的方法来处理消息。总而言之,接收方法可以是静态的,这绝不是问题;或者它可以是实例方法,在这种情况下,您可以区分公共、内部和私有。在很多情况下,接收方法是一个匿名的 lambda 表达式,这与私有方法相同。

当一个方法是静态的或公共的时,就不会有造成内存泄漏的风险。当处理方法是内部或私有(或匿名 lambda)时,在 Silverlight 和 Windows Phone 中可能存在风险。不幸的是,在这些情况下,Messenger 无法使用 WeakReference。同样,这在 WPF 或 Windows 运行时中不是问题。图 5总结了这些信息。

图 5 没有取消注册的内存泄漏风险

能见度 WPF Silverlight Windows Phone 8 Windows 运行时

静态 没有风险 没有风险 没有风险 没有风险

公有 没有风险 没有风险 没有风险 没有风险

内部 没有风险 风险 风险 没有风险

私有 没有风险 风险 风险 没有风险

匿名 Lambda 没有风险 风险 风险 没有风险
重要的是要注意,即使存在如图 5 所示的风险,取消注册失败并不总是会造成内存泄漏。也就是说,为了确保不会导致内存泄漏,最好在不再需要接收器时从 Messenger 显式取消注册接收器。这可以使用 Unregister 方法完成。请注意,Unregister 有多个重载。接收器可以从 Messenger 中完全注销,或者您可以选择仅注销一种给定方法,但保持其他方法处于活动状态。

使用 Messenger 时的其他风险

正如我所指出的,尽管 MVVM Light Messenger 是一个非常强大且用途广泛的组件,但重要的是要记住使用它存在一些风险。我已经提到了 Silverlight 和 Windows Phone 中潜在的内存泄漏。另一个风险是技术性较低的:使用 Messenger 将对象分离得如此之多,以至于很难准确理解发送和接收消息时发生的情况。对于以前从未使用过事件总线的经验不足的开发人员来说,可能很难遵循操作流程。例如,如果您正在单步执行某个方法的调用,并且该方法调用 Messenger.Send 方法,则调试流程将丢失,除非您知道要搜索相应的 Messenger.Receive 方法并在其中放置断点。也就是说,Messenger 操作是同步的,

我倾向于将 Messenger 用作“最后的手段”,因为更传统的编程技术要么是不可能的,要么会导致我希望尽可能解耦的应用程序部分之间存在过多的依赖关系。然而,有时最好使用其他工具,例如 IOC 容器和服务,以更明确的方式实现类似的结果。我在本系列的第一篇文章 ( bit.ly/1m9HTBX )中谈到了 IOC 和查看服务。

一个或多个信使

诸如 MVVM Light Messenger 之类的消息传递系统的优点之一是它们甚至可以跨程序集使用——例如,在插件场景中。这是构建大型应用程序的常用架构,尤其是在 WPF 中。但是插件系统对于较小的应用程序也很有用,例如,无需重新编译主要部分即可轻松添加新功能。一旦 DLL 被加载到应用程序的 AppDomain 中,它包含的类就可以使用 MVVM Light Messenger 与同一应用程序中的任何其他组件进行通信。这是非常强大的,尤其是当主应用程序不知道加载了多少子组件时,这通常是基于插件的应用程序中的情况。

通常,应用程序只需要一个 Messenger 实例即可涵盖所有通信。存储在 Messenger.Default 属性中的静态实例可能就是您所需要的。但是,如果需要,您可以创建新的 Messenger 实例。在这种情况下,每个 Messenger 都充当单独的通信渠道。如果您想确保给定对象永远不会收到不适合它的消息,这将很有用。例如,在图 6中的代码中,两个类注册了相同的消息类型。收到消息后,两个实例都需要执行一些检查以查看消息的作用。

图 6 使用默认 Messenger 并检查发件人

XML

复制
public class FirstViewModel
{
public FirstViewModel()
{
Messenger.Default.Register<NotificationMessage>(
this,
message =>
{
if (message.Sender is MainViewModel)
{
// This message is for me.
}
});
}
}
public class SecondViewModel
{
public SecondViewModel()
{
Messenger.Default.Register<NotificationMessage>(
this,
message =>
{
if (message.Sender is SettingsViewModel)
{
// This message is for me
}
});
}
}
图 7显示了具有私有 Messenger 实例的实现。在这种情况下,SecondViewModel 将永远不会收到消息,因为它订阅了不同的 Messenger 实例并监听不同的频道。

图 7 使用私人信使

XML

复制
public class MainViewModel
{
private Messenger _privateMessenger;
public MainViewModel()
{
_privateMessenger = new Messenger();
SimpleIoc.Default.Register(() => _privateMessenger,
"PrivateMessenger");
}
public void Update()
{
_privateMessenger.Send(new NotificationMessage("DoSomething"));
}
}
public class FirstViewModel
{
public FirstViewModel()
{
var messenger
= SimpleIoc.Default.GetInstance<Messenger>("PrivateMessenger");
messenger.Register<NotificationMessage>(
this,
message =>
{
// This message is for me.
});
}
}

避免向特定接收者发送给定消息的另一种方法是使用令牌,如图 8所示。这是发送者和接收者之间的一种契约。通常,令牌是唯一标识符,例如 GUID,但它可以是任何对象。如果发送者和接收者都使用相同的令牌,则在两个对象之间打开一个私有通信通道。在这种情况下,未使用令牌的 SecondViewModel 将永远不会收到正在发送消息的通知。主要优点是接收者不需要编写逻辑来确保消息确实是针对它的。相反,Messenger 根据令牌过滤掉消息。

图 8 带有令牌的不同通信渠道

XML

复制
public class MainViewModel
{
public static readonly Guid Token = Guid.NewGuid();
public void Update()
{
Messenger.Default.Send(new NotificationMessage("DoSomething"),
Token);
}
}
public class FirstViewModel
{
public FirstViewModel()
{
Messenger.Default.Register<NotificationMessage>(
this,
MainViewModel.Token,
message =>
{
// This message is for me.
});
}
}

使用消息

标记是过滤消息的好方法,但这并不会改变消息应该带有一些上下文以便被理解的事实。例如,您可以使用带有布尔内容的 Send 和 Receive 方法,如图 9所示。但是,如果多个发送者发送布尔消息,接收者应该如何知道该消息是针对谁以及如何处理它呢?这就是为什么最好使用专用消息类型以使上下文清晰的原因。

图 9 使用消息类型定义上下文

XML

复制
public class Sender
{
public void SendBoolean()
{
Messenger.Default.Send(true);
}
public void SendNotification()
{
Messenger.Default.Send(
new NotificationMessage<bool>(true, Notifications.PlayPause));
}
}
public class Receiver
{
public Receiver()
{
Messenger.Default.Register<bool>(
this,
b =>
{
// Not quite sure what to do with this boolean.
});
Messenger.Default.Register<NotificationMessage<bool>>(
this,
message =>
{
if (message.Notification == Notifications.PlayPause)
{
// Do something with message.Content.
Debug.WriteLine(message.Notification + ":" +
message.Content);
}
});
}
}
图 9还显示了正在使用的特定消息类型。NotificationMessage<T> 是 MVVM Light Toolkit 中最常用的消息类型之一,它允许任何内容(在本例中为布尔值)与通知字符串一起发送。通常,通知是在名为 Notifications 的静态类中定义的唯一字符串。这允许与消息一起发送指令。

当然,也可以从 NotificationMessage<T>; 派生。使用不同的内置消息类型;或实现您自己的消息类型。MVVM Light Toolkit 包含一个可以为此目的派生的 MessageBase 类,尽管在您的代码中使用它绝对不是强制性的。

另一种内置消息类型是 PropertyChangedMessage<T>。这对于 ObservableObject 和 ViewModelBase 类特别有用,该类通常用作绑定操作中涉及的对象的基类。这些类是 INotifyPropertyChanged 接口的实现,这在具有数据绑定的 MVVM 应用程序中至关重要。例如,在图 10的代码中,BankAccountViewModel 定义了一个名为 Balance 的可观察属性。当此属性更改时,RaisePropertyChanged 方法采用一个布尔参数,该参数导致 ViewModelBase 类“广播”一个 PropertyChangedMessage,其中包含有关此属性的信息,例如其名称、旧值和新值。另一个对象可以订阅此消息类型,并做出相应的反应。

图 10 发送 PropertyChangedMessage

XML

复制
public class BankViewModel : ViewModelBase
{
public const string BalancePropertyName = "Balance";
private double _balance;
public double Balance
{
get
{
return _balance;
}
set
{
if (Math.Abs(_balance - value) < 0.001)
{
return;
}
var oldValue = _balance;
_balance = value;
RaisePropertyChanged(BalancePropertyName, oldValue, value, true);
}
}
}
public class Receiver
{
public Receiver()
{
Messenger.Default.Register<PropertyChangedMessage<double>>(
this,
message =>
{
if (message.PropertyName == BankViewModel.BalancePropertyName)
{
Debug.WriteLine(
message.OldValue + " --> " + message.NewValue);
}
});
}
}
MVVM Light 中还有其他内置消息在各种场景中都很有用。此外,还可以使用构建您自己的自定义消息的基础设施。本质上,这个想法是通过提供足够的上下文让接收者知道如何处理消息内容,从而使接收者的生活更轻松。

总结

事实证明,Messenger 在许多场景中非常有用,如果没有完全解耦的消息传递解决方案,这些场景将很难实现。但是,它是一种高级工具,应谨慎使用,以避免创建可能难以在以后调试和维护的混乱代码。

本文完善了 MVVM Light Toolkit 组件的介绍。对于 .NET 开发人员来说,这是一个激动人心的时刻,他们能够在多个基于 XAML 的平台上使用相同的工具和技术。借助 MVVM Light,您可以在 WPF、Windows 运行时、Windows Phone、Silverlight 甚至适用于 Android 和 iOS 的 Xamarin 平台之间共享代码。我希望您发现本系列文章有助于了解 MVVM Light 如何帮助您高效地开发应用程序,同时使设计、测试和维护这些应用程序变得容易。

Laurent Bugnion 是 IdentityMine Inc. 的高级总监,IdentityMine Inc. 是 Microsoft 的合作伙伴,使用 Windows Presentation Foundation、Silverlight、Pixelsense、Kinect、Windows 8、Windows Phone 和 UX 等技术。他常驻瑞士苏黎世。他还是 Microsoft MVP 和 Microsoft 区域总监。

感谢以下 Microsoft 技术专家对本文的审阅: Jeffrey Ferman
Jeffrey Ferman 目前担任 Visual Studio 的项目经理。四年多来,Jeff 一直专注于 Visual Studio 和 Blend 中的 XAML 工具。他喜欢构建业务线应用程序并尝试不同的设计模式和实践。他还对可扩展性充满热情,并喜欢与客户合作为控件构建设计时体验。

友情链接
版权所有 Copyright(c)2004-2021 锐英源软件
公司注册号:410105000449586 豫ICP备08007559号 最佳分辨率 1024*768
地址:郑州大学北校区院(文化路97号院)内