2012-10-04 40 views
10

Actualmente tengo una extraña pérdida de memoria con WPF TreeView. Cuando selecciono un elemento en TreeView, el ViewModel encuadernado correspondiente se mantiene fuertemente en la colección TreeView EffectiveValueEntry []. El problema es que no se libera cuando ViewModel se elimina de su colección principal.WPF TreeView goteando el elemento seleccionado

Aquí es un simple código para reproducir el 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> 

Para reproducir el problema

Seleccione el elemento, luego haga clic en el botón para borrar la ObservableCollection. Ahora compruebe EffectiveValueEntry [] en el control TreeView: el ViewModel todavía está allí y no está marcado para la recolección de elementos no utilizados.

+0

Lo .NET versión está usando? – JleruOHeP

+0

Tengo el problema con .NET 3.5 y 4.0. Me olvidé de mencionarlo por completo, lo siento. Voy a probar con 4.5 en este momento. – Sisyphe

+1

Problema todavía presente con .NET 4.5 – Sisyphe

Respuesta

3

Bueno, finalmente se me ocurrió una solución bastante violenta. Elimino la referencia de la colección EffectiveValues ​​al eliminar el último objeto en TreeView. Puede ser exagerado, pero al menos, funciona.

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

Encontramos que en 'effectiveValues ​​[index + 1]' había un 'BindingExpression' que también hacía referencia a' lastObjectDeleted' (es decir '((BindingExpression) value) .ParentBinding.Source == lastObjectDeleted'), por lo que también lo eliminamos. –

1

Es porque enlazó su vista de árbol con el modo OneTime, por lo que su colección fue 'capturada'. Como se dijo:

Actualizado:

EffectiveValueEntry es acerca de cómo DependencyObjects tienda de los valores de su DependencyProperties. Esta colección contendrá objetos siempre que TreeView haya seleccionado el elemento. Tan pronto como seleccione algo más, la colección se actualizará.

+0

En realidad, el " El enlace "OneTime" fue un intento fallido de resolver el problema que encontré en otro hilo. Eliminarlo no cambia nada y no es la causa del problema. EDITAR: Voy a probar el enlace OneWay para ver si el problema está resuelto. EDIT2: no funciona, la entrada sigue viva. – Sisyphe

+0

Actualicé la publicación original para eliminar el enlace de OneTime. – Sisyphe

+0

Sí, se trata de almacenamiento de propiedades de dependencia. Es molesto que la memoria no se libere cuando se elimina el último objeto si se seleccionó. En este caso, no tengo más objetos en treeView, y el objeto se mantendrá hasta que se cierre la aplicación. De todos modos, gracias por tu respuesta, no puedo resolver mi problema, pero al menos, confirmaste lo que estaba pensando. – Sisyphe

1

que tenían el mismo problema y lo resolvió mediante una de las soluciones en este link (publicado por Tom Goff). Haga lo siguiente:

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