NET Framework 上 DataTemplate (XAML) 中的 WPF 事件取消订阅泄漏
WPF event unsubscribe leak in DataTemplate (XAML) on NET Framework
想象一下 UserControl
和 ListBox
在 DataTemplate
中有 CheckBox
。 ListBox
的 ItemsSource
是一些全局列表。 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 模式中,视图绑定到视图模型,当垃圾收集完成它的工作时,视图不会继续存在。
在您提供的示例中,视图模型是一个静态对象,根据定义不能被垃圾回收,因此视图也不能被垃圾回收。
没有自动解除绑定,因为您可以重复使用用户控件的实例(它可以是 Loaded
和 UnLoaded
多次)。
修复此内存泄漏的最简单方法¹ 是在 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: 正确的方法是将静态列表包装在专用的列表视图模型中,使视图模型成为一次性的(在处理时从静态列表中解除包装),在删除时处理视图模型。
想象一下 UserControl
和 ListBox
在 DataTemplate
中有 CheckBox
。 ListBox
的 ItemsSource
是一些全局列表。 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 模式中,视图绑定到视图模型,当垃圾收集完成它的工作时,视图不会继续存在。
在您提供的示例中,视图模型是一个静态对象,根据定义不能被垃圾回收,因此视图也不能被垃圾回收。
没有自动解除绑定,因为您可以重复使用用户控件的实例(它可以是 Loaded
和 UnLoaded
多次)。
修复此内存泄漏的最简单方法¹ 是在 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: 正确的方法是将静态列表包装在专用的列表视图模型中,使视图模型成为一次性的(在处理时从静态列表中解除包装),在删除时处理视图模型。