使用 MVVM 中的 dispose 取消订阅事件

3

实际上,我正在尝试通过从我的ViewModel触发事件来关闭窗口。一切都很好,但我知道我必须取消订阅事件以避免内存泄漏。因此,我实现了IDisposable接口,并在Dispose方法中取消订阅事件。

以下是我的代码:

public partial class MainWindow : Window, IDisposable
{
    private MainViewModel viewModel;
    public MainWindow()
    {
        InitializeComponent();
        DataContext = viewModel =  new MainViewModel();
        this.viewModel.RequestClose += CloseWindow;
    }

    void CloseWindow(object sender, EventArgs e)
    {
        this.Close();
    }

    public void Dispose()
    {
        ////here we need to unsubscribe the event
        this.viewModel.RequestClose -= this.CloseWindow;
    }
}

我需要知道:

  1. 这段代码是否正确
  2. 垃圾回收器(GC)何时调用并执行dispose方法
  3. 是否有更好的方法来完成这件事

4
在这种设置中,您根本不需要取消订阅事件 - 事件将持有指向窗口的指针,但是当没有路径到达窗口时,也就没有路径到达视图模型,因此只要窗口不存在,则视图模型也会被收集。 - Random Dev
提醒一下,你可能需要了解一些正确的 IDispose 实现。(https://dev59.com/2WMl5IYBdhLWcg3wgnSl,http://www.codeproject.com/Articles/15360/Implementing-IDisposable-and-the-Dispose-Pattern-P) - Stefan
@Carsten,你能否更详细地解释一下,在哪些情况下我们必须实现dispose方法? - Moez Rebai
2
尝试使用附加行为,实际上我不太喜欢事件,尤其在MVVM中。 - Bruno Joaquim
4个回答

6

但我知道我必须取消订阅我的事件以避免内存泄漏

当短暂存在的对象订阅长期存在的对象(或静态事件)的事件并且未在之后取消订阅时,会发生内存泄漏(例如,请参见this答案)。我想这不是你的情况。

当GC被调用并执行dispose方法时

GC不会调用IDisposable.Dispose(例如,请参见this答案)。根本不会。 如果您没有任何代码显式地调用MainWindow.Dispose,它将永远不会被调用。

有更好的方法来做这样的事吗?

我会避免使用IDisposable和事件。在这里,附加行为更方便,我认为(至少,这是可重用的):

public static class WindowClosingBehavior
{
        public static bool GetIsClosingInitiated(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsClosingInitiatedProperty);
        }

        public static void SetIsClosingInitiated(DependencyObject obj, bool value)
        {
            obj.SetValue(IsClosingInitiatedProperty, value);
        }

        public static readonly DependencyProperty IsClosingInitiatedProperty = DependencyProperty.RegisterAttached(
            "IsClosingInitiated", 
            typeof(bool), 
            typeof(WindowClosingBehavior),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, IsClosingInitiatedChanged));

        private static void IsClosingInitiatedChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
        {
            var window = target as Window;
            if (window != null && (bool)e.NewValue)
            {
                window.Close();
            }
        }
}

在 Windows 的 XAML 中的某个地方:

behaviors:WindowClosingBehavior.IsClosingInitiated="{Binding IsClosingInitiated}"

其中IsClosingInitiated是来自视图模型的属性:

public class SomeViewModel
{
     // ...

     private void Foo()
     {
         // ...
         IsClosingInitiated = true;
     }
}

实际上我不想使用附加属性。 - Moez Rebai
Dennis,你能给我提供一个使用事件而导致内存泄漏的例子吗? - Moez Rebai
@MoezRebai:只需从任何短期对象订阅任何静态事件(例如 CommandManager.RequerySuggested),您就会出现内存泄漏(没有取消订阅)。 - Dennis

4

仅在源和处理程序的生命周期不同时才需要取消订阅事件,否则它们会同时超出范围并一起进行垃圾回收。

因此,在这种情况下,不需要使用IDisposable。无论如何,如果您实现了IDisposable,您需要明确调用它,否则您将无法控制何时调用它。


那么你认为在CloseWindow方法中调用dispose方法怎么样? - Moez Rebai
我更喜欢在视图之外处理视图的关闭方法,并在那里调用dispose以保持单一职责原则。 - Ignacio Soler Garcia

1
实际上,当 Window.CloseWindow 订阅事件时,它会使视图模型指向窗口。
反之亦然,因为 Window 中有一个 ViewModel 字段。
窗口和视图模型相互引用。
如果它们没有其他引用,垃圾回收将完成工作。
如果某些代码调用它,Dispose 将被调用。
据我所知,这不会发生,除非您使用 using 包围窗口的创建或显式调用 Dispose
这里最好的方法是不实现 IDisposable/Dispose:保持简单。
问候。

0

我认为使用事件是实现这一目的的一种完全可接受的方法。对于更完整的释放模式,请使用以下代码片段:

#region IDisposable

//Dispose() calls Dispose(true)
public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

// NOTE: Delete the finalizer if this class doesn't 
// own unmanaged resources itself.
~ClassName() 
{
    //Finalizer calls Dispose(false)
    Dispose(false);
}

//The bulk of the clean-up code is implemented in Dispose(bool)
protected virtual void Dispose(bool disposing)
{
    if (disposing) 
    {
        //free managed resources (Example below)
        if (managedResource != null)
        {
            managedResource.Dispose();
            managedResource = null;
        }
    }

    //Free native resources if there are any. (Example below)
    if (nativeResource != IntPtr.Zero) 
    {
        Marshal.FreeHGlobal(nativeResource);
        nativeResource = IntPtr.Zero;
    }
}

#endregion

在你的情况下,你的 dispose 方法将是这样的:
public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

~MainWindow()
{
    Dispose();
}

protected virtual void Dispose(bool disposing)
{
    if (disposing) 
    {
        if (viewModel != null)
        {
            viewModel.RequestClose -= CloseWindow;
            viewModel.Dispose();
            viewModel = null;
        }
    }
}

正如Dennis所指出的那样,您需要保留终结器以确保在关闭MainWindow时调用Dispose,例如在应用程序退出事件中。

有一个小问题。在 OP 的示例中,会调用 Dispose - Dennis
@Dennis 非常好的观点。如果 OP 没有管理Dispose(我认为他没有),他可以在他的 MainWindow 中添加一个析构函数 - Mike Eason
你知道吗,finalizer(析构函数)只应该释放非托管资源(而事件是纯托管资源)?此外,你知道吗,无法预测何时会调用finalizer? - Dennis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接