WPF TreeView 泄漏所选项目

Posted

技术标签:

【中文标题】WPF TreeView 泄漏所选项目【英文标题】:WPF TreeView leaking the selected item 【发布时间】:2012-09-25 05:25:54 【问题描述】:

我目前有一个奇怪的 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 版本? 我遇到了 .NET 3.5 和 4.0 的问题。我完全忘了提,对不起。我现在用 4.5 进行测试。 .NET 4.5 仍然存在问题 我对 TreeView 有同样的问题。我在 Microsoft Connect 上发布了一个错误条目:connect.microsoft.com/VisualStudio/feedback/details/778557/… 【参考方案1】:

我遇到了同样的问题,并使用link(由 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;
            
        
    

【讨论】:

【参考方案2】:

好吧,我终于想出了一个相当暴力的解决方案。在删除 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),所以我们也删除了它。【参考方案3】:

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

更新:

EffectiveValueEntry 是关于DependencyObjects 如何存储DependencyProperties 的值。只要 treeView 选择了项目,这个集合就会保存对象。只要您选择其他内容,集合就会更新。

【讨论】:

实际上,“OneTime”绑定是解决我在另一个线程上发现的问题的失败尝试。删除它不会改变任何东西,也不是问题的原因。编辑:我将尝试使用 OneWay 绑定来查看问题是否已解决。 EDIT2:不起作用,条目仍然存在。 是的,它是关于依赖属性存储的。如果选择了最后一个对象,则删除最后一个对象时不会释放内存,这很烦人。在这种情况下,treeView 中没有更多对象,并且该对象将一直保留到应用程序关闭为止。无论如何,谢谢你的回答,我无法解决我的问题,但至少,你证实了我的想法。

以上是关于WPF TreeView 泄漏所选项目的主要内容,如果未能解决你的问题,请参考以下文章

WPF C# TreeView 获取所选项目的文本

带有 ExtendedSelection 的 Qml TreeView 中的 selectedRows() 落后了一步

bootstrap treevie只展开一个节点,关闭其他节点

使用WPF在虚拟化TreeView中选择节点

WPF中的TreeView,如何改变子节点的顺序, 给个例子,多谢

Wpf Xaml - TreeView 分层数据模板 - 多个项目源