WPF TreeView泄露选定项

11

我目前在使用 WPF TreeView 时遇到了一个奇怪的内存泄漏问题。当我选择 TreeView 中的一个项目时,相应的 ViewModel 被强制保存在 TreeView 的 EffectiveValueEntry[] 集合中。问题在于,即使 ViewModel 从其父集合中删除,它也不会被释放。

以下是一个简单的代码来重现此问题:

MainWindow.xaml

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls.Primitives;

namespace TreeViewMemoryLeak
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        public ObservableCollection<Entry> Entries
        {
            get
            {
                if (entries == null)
                {
                    entries = new ObservableCollection<Entry>() { new Entry() { DisplayName = "First Entry" } };
                }
                return entries;
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e) { entries.Clear(); }

        private ObservableCollection<Entry> entries;

    }

    public class Entry : DependencyObject
    {
        public string DisplayName { get; set; }
    }
}

MainWindow.xaml.cs

<Window x:Class="TreeViewMemoryLeak.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TreeViewMemoryLeak"
    Title="MainWindow" Height="350" Width="250">

    <Window.Resources>
        <DataTemplate DataType="{x:Type local:Entry}">
            <TextBlock Text="{Binding DisplayName}" />
        </DataTemplate>
    </Window.Resources>

    <StackPanel>
        <Button Content="delete item" Click="Button_Click" Grid.Row="0" Margin="10"/>
        <TreeView x:Name="treeView" ItemsSource="{Binding Entries}" Grid.Row="1" Margin="10" BorderBrush="Black" BorderThickness="1" />
    </StackPanel>

</Window>

重现问题的方法:

选择该项,然后单击按钮清除ObservableCollection。 然后检查TreeView控件上的EffectiveValueEntry []:ViewModel仍然存在,没有被标记为垃圾回收。


你正在使用哪个 .Net 版本? - JleruOHeP
我遇到了.NET 3.5和4.0的问题。我完全忘记提到了,很抱歉。我现在会测试4.5。 - Sisyphe
1
仍然存在使用.NET 4.5的问题。 - Sisyphe
1
我在使用TreeView时遇到了同样的问题。我已经在Microsoft Connect上发布了一个错误报告:http://connect.microsoft.com/VisualStudio/feedback/details/778557/wpf-treeview-memory-leak-net-4-5 - jbe
3个回答

3

我最终想出了一个相当暴力的解决方案。在删除TreeView中的最后一个对象时,我自己从EffectiveValues集合中移除引用。这可能有些过度,但至少它能够工作。

public class MyTreeView : TreeView
{
    protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
    {
        base.OnSelectedItemChanged(e);

        if (Items.Count == 0)
        {
            var lastObjectDeleted = e.OldValue;
            if (lastObjectDeleted != null)
            {
                var effectiveValues = EffectiveValuesGetMethod.Invoke(this, null) as Array;
                if (effectiveValues == null)
                    throw new InvalidOperationException();

                bool foundEntry = false;
                int index = 0;
                foreach (var effectiveValueEntry in effectiveValues)
                {
                    var value = EffectiveValueEntryValueGetMethod.Invoke(effectiveValueEntry, null);
                    if (value == lastObjectDeleted)
                    {
                        foundEntry = true;
                        break;
                    }
                    index++;
                }

                if (foundEntry)
                {
                    effectiveValues.SetValue(null, index);
                }
            }
        }
    }

    protected MethodInfo EffectiveValueEntryValueGetMethod
    {
        get
        {
            if (effectiveValueEntryValueGetMethod == null)
            {
                var effectiveValueEntryType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Where(t => t.Name == "EffectiveValueEntry").FirstOrDefault();
                if (effectiveValueEntryType == null)
                    throw new InvalidOperationException();

                var effectiveValueEntryValuePropertyInfo = effectiveValueEntryType.GetProperty("Value", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Instance);
                if (effectiveValueEntryValuePropertyInfo == null)
                    throw new InvalidOperationException();

                effectiveValueEntryValueGetMethod = effectiveValueEntryValuePropertyInfo.GetGetMethod(nonPublic: true);
                if (effectiveValueEntryValueGetMethod == null)
                    throw new InvalidOperationException();

            }
            return effectiveValueEntryValueGetMethod;
        }
    }

    protected MethodInfo EffectiveValuesGetMethod
    {
        get
        {
            if (effectiveValuesGetMethod == null)
            {
                var dependencyObjectType = typeof(DependencyObject);
                var effectiveValuesPropertyInfo = dependencyObjectType.GetProperty("EffectiveValues", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Instance);
                if (effectiveValuesPropertyInfo == null)
                    throw new InvalidOperationException();

                effectiveValuesGetMethod = effectiveValuesPropertyInfo.GetGetMethod(nonPublic: true);
                if (effectiveValuesGetMethod == null)
                    throw new InvalidOperationException();
            }
            return effectiveValuesGetMethod;
        }
    }

    #region Private fields
    private MethodInfo effectiveValueEntryValueGetMethod;
    private MethodInfo effectiveValuesGetMethod;
    #endregion
}

我们发现在 effectiveValues[index + 1] 中有一个 BindingExpression,它也引用了 lastObjectDeleted(即 ((BindingExpression)value).ParentBinding.Source == lastObjectDeleted),所以我们也将其删除了。 - Johannes Egger

1

这是因为您将您的树形视图绑定到了OneTime模式,所以您的集合被“快照”了。如下所述:

更新:

EffectiveValueEntry 是关于 DependencyObjects 如何存储它们的 DependencyProperties 值的。只要 treeView 有选中项,该集合就会保存对象。一旦您选择其他内容,集合将被更新。


实际上,“OneTime”绑定是解决我在另一个线程中发现的问题的失败尝试。删除它不会改变任何东西,也不是问题的原因。编辑:我将尝试使用OneWay绑定来查看是否解决了该问题。编辑2:不起作用,输入仍然存在。 - Sisyphe
是的,这与依赖属性存储有关。令人烦恼的是,如果最后一个对象被选中时,内存不会在其被移除后释放。在这种情况下,当我在treeView中没有更多对象时,该对象将一直保留,直到应用程序关闭。无论如何,感谢您的回答,虽然我无法解决我的问题,但至少您确认了我的想法。 - Sisyphe

1

我曾经遇到同样的问题,通过链接(由Tom Goff发布的)中的一个解决方案来解决它。按照以下步骤操作:

ClearSelection(this.treeView);
this.treeView.SelectedValuePath = ".";
this.treeView.ClearValue(TreeView.SelectedValuePathProperty);
this.treeView.ItemsSource = null;

...

public static void ClearSelection(TreeView treeView)
{
    if (treeView != null)
        ClearSelection(treeView.Items, treeView.ItemContainerGenerator);
}

private static void ClearSelection(ItemCollection collection, ItemContainerGenerator generator)
{
    if ((collection != null) && (generator != null))
    {
        for (int i = 0; i < collection.Count; i++)
        {
            TreeViewItem treeViewItem = generator.ContainerFromIndex(i) as TreeViewItem;
            if (treeViewItem != null)
            {
                ClearSelection(treeViewItem.Items, treeViewItem.ItemContainerGenerator);
                treeViewItem.IsSelected = false;
            }
        }
    }
}

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