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 はまだそこにあり、ガベージ コレクションのフラグが立てられていません。

4

3 に答える 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
}
于 2012-10-04T11:22:01.830 に答える
1

ツリービューをOneTimeモードでバインドしたため、コレクションが「スナップショット」されました。述べたように:

更新しました:

EffectiveValueEntryDependencyObjectsの値を保存する方法についてDependencyPropertiesです。このコレクションは、treeView がアイテムを選択している限り、オブジェクトを保持します。他のコレクションを選択するとすぐに更新されます。

于 2012-10-04T08:11:32.627 に答える