【问题标题】:Does a ViewModel have to unsubscribe from events of an object instantiated in the ViewModel?ViewModel 是否必须取消订阅在 ViewModel 中实例化的对象的事件?
【发布时间】:2020-06-26 18:41:43
【问题描述】:

我将 WPF 与 MVVM 一起使用。我有一个 ViewModel 将对象 MyService 实例化为属性。 ViewModel 订阅了MyService 的事件。 MyService 属性绑定到 View 中的某些元素。

ViewModel 不再使用时,MyService 是否会因为事件订阅而使我的ViewModel 保持活动状态并阻止垃圾收集 (GC)? 如果是,是否存在解决这个问题的简单方法?我应该在哪里取消订阅MyService? (虽然我无法控制调用我的 View/Viewmodel 的那个)

public class ViewModel 
{
    public MyService MyService { get; set; } = new MyService();
    
    public ViewModel()
    {
        MyService.MyEvent += OnMyEvent;
    } 

    private void OnMyEvent(object sender, EventArgs e)
    {
        // do something
    }
}

【问题讨论】:

  • 绑定将使用弱引用来订阅事件,所以如果你正确地处理你的虚拟机,那么就不会有问题。话虽如此,VM可能在GC的Gen2,然后它会在那里徘徊一段时间,通常是10分钟。如果您有内存泄漏,那么我建议使用适当的工具(我使用的是 ANTS 免费版)并找到对您的服务的引用。可能是其他地方的另一个实例?
  • @XAMlMAX Bindng 没有使用弱引用。它使用了对Binding.Source 的强引用。事实上,如果你绑定到一个没有实现INotifyPropertyChanged 的源,绑定引擎会创建一个静态字段来引用源。由于静态内存永远不会被垃圾收集,因此您会引入内存泄漏。这就是为什么绑定源“必须”实现INotyfyPropertyChanged 甚至更好的原因(就性能而言,实现DependencyProperty
  • @XAMlMAX 但是Binding 使用弱事件模式以便使用PropertyChangedEventManager 监听PropertyChanged 事件。

标签: c# wpf events mvvm


【解决方案1】:

通常,您应该始终取消订阅事件,最好是在事件处理程序中。

public void DownloadFile()
{
  this.ServiceClient.DownloadCompleted += OnDownloadCompleted;
}

public void OnDownloadCompleted(object sender, EventArgs e)
{
  this.ServiceClient.DownloadCompleted -= OnDownloadCompleted;

  // Do something
}

如果您不知道事件源的生命周期,请使用弱事件模式或IDisposable 模式(但弱事件模式应该是首选)。

要实现弱事件模式,您可以尝试使用现有的 WeakEventManager 实现(例如,PropertyChangedEventManager)。或者如果不存在,您可以使用通用的WeakEventManager<T>。由于该类使用反射来解析和订阅事件委托,建议扩展抽象类WeakEventManager创建自定义类型。
Microsoft Docs: Weak Event Patterns

public MyService MyService { get; set; } = new MyService();

public ViewModel()
{
  // MyService.MyEvent += OnMyEvent;

  WeakEventManager<MyService, EventArgs>.AddHandler(
    this.MyService,
    nameof(MyService.MyEvent), 
    OnMyEvent);
} 

是否可以避免取消订阅事件源或忽略弱事件模式,取决于事件源的生命周期。

为了执行事件处理程序,事件源必须“知道”侦听器才能访问回调(或更严格地说,是为侦听器实例分配的内存空间)。因此,委托保持对实例的强引用,该实例存储在 Delegate.Target 属性中。

如果事件源 MyService 的寿命比监听器 ViewModel 长,那么监听器不能被垃圾回收,直到事件源本身被垃圾回收或强引用被移除(例如通过取消订阅或设置事件委托给null)。

这种情况是可能的,例如,当事件源是一个聚合实例时,它被允许在类的范围之外存在或被引用,例如,通过公共属性或作为方法的返回值或事件源被定义static.

在您的代码中MyService(事件源)定义为public。这意味着ViewModel(事件侦听器)无法控制此实例的生命周期。
如果在ViewModel 范围之外且生命周期比ViewModel 更长的某个实例获得对此public 属性值的引用,MyService(因此事件侦听器ViewModel)将保持活动状态,甚至如果ViewModel 将属性MyService 设置为null

如果属性MyServiceprivate,并且您永远不会将此属性的引用返回给public 方法的调用者,那么您应该是安全的,因为MyService 的生命周期现在是耦合的到ViewModel 的生命周期。破坏ViewModel 也会破坏MyService
换句话说,您必须保证事件源的生命周期与事件侦听器的生命周期耦合(或更短),或者它们之间没有“无”耦合(弱事件模式,取消订阅)。

您最好始终遵循订阅/取消订阅或WeakEventManager 的模式。这样您就不必担心对象的生命周期来防止内存泄漏。

How to Implement the Weak Event Pattern

【讨论】:

  • 感谢您的详细回答。当 ViewModel 拥有 MyService 时,我不确定垃圾收集是否有效。但这是有道理的,因为我猜 ViewModel 和 MyService 都没有引用它们的根对象,因此可以被垃圾收集。剩下的问题是绑定。我需要检查在这种情况下是否实现了 INPC,以确保绑定是通过弱引用发生的。我考虑过使用弱事件模式,但以前从未这样做过。也许我试一试,只是为了省钱。
  • 如果MyService 是数据绑定的来源,您必须实现INotifyPropertyChanged。如果没有它,绑定当然可以工作,但是绑定引擎会创建一个对ViewModelstatic 引用来处理INotifyPropertyChanged 的缺失。 static 对象永远不会被垃圾回收,因此其生命周期与应用程序生命周期绑定。这是一个潜在的内存泄漏,尤其是当您创建多个 ViewModel 实例时,因为您希望旧实例被垃圾收集器销毁。
  • 如果ViewModel 无论如何都会在整个应用程序生命周期中存在,那么唯一的成本是绑定到一个对象的(显着)性能损失,该对象要么没有将源属性实现为DependencyProperty或者不实现INotifyPropertyChanged。这就是为什么视图模型应该总是实现这个接口。您可以将实现移动到共享基类,每个视图模型都可以扩展。
猜你喜欢
  • 2011-08-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-05
  • 1970-01-01
  • 2011-12-11
  • 2020-08-11
相关资源
最近更新 更多