Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF TreeView leaking the selected item

I currently have a strange memory leak with WPF TreeView. When I select an item in the TreeView, the corresponding bound ViewModel is strongely hold in the TreeView EffectiveValueEntry[] collection. The issue is that it is not released when the ViewModel is removed from it's parent collection.

Here is a simple code to reproduce the issue :

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>

To reproduce the issue

Select the item, then click the button to clear the ObservableCollection. Now check the EffectiveValueEntry[] on the TreeView control : the ViewModel is still there and is not flagged for garbage collection.

like image 406
Sisyphe Avatar asked Oct 04 '12 08:10

Sisyphe


2 Answers

Well I finally came up with a rather violent solution. I remove the reference from the EffectiveValues collection myself when deleting the last object in the TreeView. It may be overkill but at least, it works.

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
}
like image 130
Sisyphe Avatar answered Sep 30 '22 16:09

Sisyphe


It is because you binded your treeview with OneTime mode, so your collection was 'snapshotted'. As stated:

UPDATED:

EffectiveValueEntry is about how DependencyObjects store the values of their DependencyProperties. This collection will hold object as long as treeView has selected item. As soon as you select something else collection will be updated.

like image 33
JleruOHeP Avatar answered Sep 30 '22 18:09

JleruOHeP