NET Framework 上 DataTemplate (XAML) 中的 WPF 事件取消订阅泄漏

WPF event unsubscribe leak in DataTemplate (XAML) on NET Framework

想象一下 UserControlListBoxDataTemplate 中有 CheckBoxListBoxItemsSource 是一些全局列表。 CheckBox 附加了 Checked/Unchecked 个事件。

<ListBox ItemsSource="{Binding Source={x:Static a:MainWindow.Source}}">
    <ListBox.ItemTemplate>
        <DataTemplate DataType="{x:Type a:Data}">
            <CheckBox Content="{Binding Path=Name}"
                      Checked="ToggleButton_OnChecked"
                      Unchecked="ToggleButton_OnUnchecked"
                      IsChecked="{Binding Path=IsEnabled}"
                      Padding="10"
                      VerticalContentAlignment="Center"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

我正在 window 主日志中记录 loaded/unloaded/checked/unchecked 事件。

    private void ToggleButton_OnChecked(object sender, RoutedEventArgs e)
    {
        Log("Checked");
    }

    private void ToggleButton_OnUnchecked(object sender, RoutedEventArgs e)
    {
        Log("Unchecked");
    }

    private void UserControl1_OnLoaded(object sender, RoutedEventArgs e)
    {
        Log("Loaded");
    }

    private void UserControl1_OnUnloaded(object sender, RoutedEventArgs e)
    {
        Log("Unloaded");
    }

主要 window 具有 UserControl1 个实例的动态列表(从一个开始)。有 add/remove 个按钮可以让我添加更多实例。

<UniformGrid Rows="2">
    <DockPanel>
        <Button DockPanel.Dock="Top" Click="Add">Add</Button>
        <Button DockPanel.Dock="Top" Click="Remove">Remove</Button>
        <ListBox x:Name="ListBox">
            <local:UserControl1 />
        </ListBox>
    </DockPanel>

    <ListBox ItemsSource="{Binding ElementName=This,Path=Log}" FontFamily="Courier New"/>
</UniformGrid>

window 的代码隐藏:

    private void Add(object sender, RoutedEventArgs e)
    {
        ListBox.Items.Add(new UserControl1());
    }

    private void Remove(object sender, RoutedEventArgs e)
    {
        if (ListBox.Items.Count == 0) return;
        ListBox.Items.RemoveAt(0);
    }

当我 运行 应用程序时,只有一个 UserControl1 实例。如果我再添加一个,然后立即删除其中一个,然后单击屏幕上的唯一复选框,我会看到记录了两个“已检查”事件。如果我现在取消选中它,则会有两个“未选中”事件(即使之前清楚地记录了“卸载”事件。左侧的十六进制数字显示 GetHashCode() 的输出,这清楚地表明事件是由不同的处理的UserControl1 个实例。

因此,即使 UserControl1 之一被卸载,事件似乎也不会自动取消订阅。我曾尝试升级到 NET Framework 4.8 但无济于事。我看到了同样的行为。如果我添加 10 个新控件并立即删除它们,我将观察到 10 个“选中”或“未选中”事件。

我曾尝试搜索类似的问题,但没有找到。是不是我遗漏了什么,或者我只是遇到了一个错误?寻找解决方法。

GitHub 上提供了完整的源代码。 https://github.com/wpfwannabe/datacontext-event-leak

MVVM 模式中,视图绑定到视图模型,当垃圾收集完成它的工作时,视图不会继续存在。

在您提供的示例中,视图模型是一个静态对象,根据定义不能被垃圾回收,因此视图也不能被垃圾回收。

没有自动解除绑定,因为您可以重复使用用户控件的实例(它可以是 LoadedUnLoaded 多次)。

修复此内存泄漏的最简单方法¹ 是在 unload 上执行 unbinding :

Wpf

// First give the ListBox a name
<ListBox x:Name="ListBox" ItemsSource="{Binding Source={x:Static a:MainWindow.Source}}">

代码隐藏

private void UserControl1_OnUnloaded(object sender, RoutedEventArgs e)
{
    Log("Unloaded");
    ListBox.ItemsSource = null;
}

1: 正确的方法是将静态列表包装在专用的列表视图模型中,使视图模型成为一次性的(在处理时从静态列表中解除包装),在删除时处理视图模型。