2012-10-04 14 views
10

Attualmente ho una strana perdita di memoria con WPF TreeView. Quando seleziono un oggetto in TreeView, il corrispondente ViewModel associato è fortemente conservato nella collezione TreeView EffectiveValueEntry []. Il problema è che non viene rilasciato quando ViewModel viene rimosso dalla sua raccolta principale.WPF TreeView che perde l'elemento selezionato

Ecco un semplice codice di riprodurre il problema:

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> 

Per riprodurre il problema

Selezionare l'elemento, quindi fare clic sul pulsante per cancellare ObservableCollection. Ora controlla EffectiveValueEntry [] sul controllo TreeView: ViewModel è ancora lì e non è contrassegnato per la garbage collection.

+0

Cosa Net versione stai usando? – JleruOHeP

+0

Ho il problema con .NET 3.5 e 4.0. Ho completamente dimenticato di dirlo, mi dispiace. Verificherò con 4.5 in questo momento. – Sisyphe

+1

Problema ancora presente con .NET 4.5 – Sisyphe

risposta

3

Bene, finalmente ho trovato una soluzione piuttosto violenta. Rimuovo il riferimento dalla raccolta EffectiveValues ​​durante l'eliminazione dell'ultimo oggetto in TreeView. Potrebbe essere eccessivo ma, almeno, funziona.

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 
} 
+0

Abbiamo trovato che a 'effectiveValues ​​[index + 1]' c'era un 'BindingExpression' che faceva anche riferimento a' lastObjectDeleted' (valore i.e '((BindingExpression)) .ParentBinding.Source == lastObjectDeleted'), quindi abbiamo eliminato anche quello. –

1

E 'perché hai legato la vista ad albero con la modalità OneTime, quindi la tua collezione è stata "fotografata". Come affermato:

aggiornamento:

EffectiveValueEntry è su come DependencyObjects memorizzare i valori della loro DependencyProperties. Questa raccolta manterrà l'oggetto finché treeView ha selezionato l'elemento. Non appena selezioni qualcos'altro, la raccolta verrà aggiornata.

+0

In realtà, il " Il binding di OneTime è stato un tentativo fallito di risolvere il problema riscontrato su un altro thread. Rimozione non cambia nulla e non è la causa del problema. EDIT: proverò il binding OneWay per vedere se il problema è risolto. EDIT2: non funziona, voce ancora in vita. – Sisyphe

+0

Ho aggiornato il post originale per rimuovere il binding OneTime. – Sisyphe

+0

Sì, si tratta dello storage di proprietà di dipendenza. È solo fastidioso che la memoria non venga rilasciata quando l'ultimo oggetto viene rimosso se è stato selezionato. In questo caso, non ho più oggetti nella struttura ad albero e l'oggetto resterà in attesa fino alla chiusura dell'applicazione. Comunque grazie per la tua risposta, non posso risolvere il mio problema, ma almeno hai confermato quello che stavo pensando. – Sisyphe

1

Ho avuto lo stesso problema e risolto utilizzando una delle soluzioni su questo link (pubblicato da Tom Goff). Effettuare le seguenti operazioni:

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; 
      } 
     } 
    } 
}