Windows Phone 8.1 WinRT memory leak with ObservableCollection

I work with a large number of objects (POIs) that are displayed on MapControl . I help myself with MVVM Light obey the rules of the MVVM approach.

Since I must display every object on the map, I need to use the MapItemsControl collection, not MapElements . This collection is bound to the ObservableCollection<PushpinViewModel> ( Pushpins ) Pushpins in the corresponding ViewModel . Everything works as expected, until the moment when I want to update Pushpins . The problem is a memory leak. But first, the code to visualize the problem:

XAML:

 <maps:MapControl x:Name="Map" x:Uid="MapControl"> <maps:MapItemsControl ItemsSource="{Binding Pushpins}"> <maps:MapItemsControl.ItemTemplate> <DataTemplate> <Image Source="{Binding Image}"/> </DataTemplate> </maps:MapItemsControl.ItemTemplate> </maps:MapItemsControl> 

MainViewModel:

 public class MainViewModel : ViewModelBase { public RelayCommand AddCommand { get; set; } public RelayCommand ClearCommand { get; set; } public RelayCommand CollectCommand { get; set; } public ObservableCollection<PushpinViewModel> Pushpins { get; set; } /* Ctor, initialization of Pushpins and stuff like that */ private void Collect() { GC.Collect(2); GC.WaitForPendingFinalizers(); GC.Collect(2); PrintCurrentMemory(); } private void Clear() { Pushpins.Clear(); PrintCurrentMemory(); } private void Add() { for (int i = 0; i < 1000; i++) { Pushpins.Add(new PushpinViewModel()); } PrintCurrentMemory(); } private void PrintCurrentMemory() { Logger.Log(String.Format("Total Memory: {0}", GC.GetTotalMemory(true) / 1024.0)); } } 

PushpinViewModel:

 public class PushpinViewModel: ViewModelBase { public string Image { get { return "/Assets/SomeImage.png"; } } ~PushpinViewModel() { Logger.Log("This finalizer never gets called!"); } } 

Now consider the following scenario. I am adding to the elements of the collection Pushpins 1000 PushpinViewModel . They are visualized, memory is allocated, everything is fine. Now I want to clear the collection and add another (another in the real scenario) 1000 elements. So, I call the Clear() method. But ... nothing happens! Pushpins cleared, but PushpinViewModel finalizers are not called! Then I add 1000 elements again and the memory usage is doubled. You can guess what will happen next. When I repeat this procedure Clear() - Add() 3-5 times, my application crashes.

So what is the problem? Obviously, the ObservableCollection contains references to PushpinViewModel objects after executing Clear() , so they cannot be garbage collected. Of course, forcing the GC to collect garbage does not help (sometimes it even makes the situation worse).

This bothers me for 2 days, I tried many different scenarios to try to overcome this problem, but, frankly, nothing helped. Nothing was worth it - I don’t remember the exact script, but when I assigned Pushpins = null and then did something else, the VehiceViewModel was destroyed. But this does not work for me, because I also remember that I had a problem visualizing these contacts on the map after Clear() .

Do you have any idea what might cause a memory leak? How can I get OC members to be destroyed? Maybe there is some alternative for OC ? Thanks in advance for your help!

EDIT:

I did some tests using XAML Map Control - https://xamlmapcontrol.codeplex.com/ , and the results are surprising. The overall performance of the map with the addition of 1000 elements to it is worse than the native MapControl , BUT, if I call Add() x1000, then Clear() , then Add() x1000, the PushpinViewModel finalizers will get a call! Memory is freed and the application does not crash. So with Microsoft MapControl ... there is definitely something wrong ...

+6
source share
1 answer

OK, here is the behavior I did that emulates what MapItemsControl does. Please note that this is rather untested - it works in my application, but in fact it has not been tried anywhere. And I never tested the RemoveItems function, because my application just adds elements to the ObservableCollection and clears them; it never removes items step by step.

Also note that these are XAML tags with the hash code of the element to which it is bound; this is how he identifies which stamps are removed from the card if the collection changes. This may not work for your circumstances, but it seems effective.

Using:

Note: NumberedCircle is a user control that is just a red circle that displays a number inside it; replace any XAML control you want to use as a button. Destinations is my ObservableCollection objects that have the Number property (to display inside the button) and the Point property (button location).

 <map:MapControl> <i:Interaction.Behaviors> <behaviors:PushpinCollectionBehavior ItemsSource="{Binding Path=Destinations}"> <behaviors:PushpinCollectionBehavior.ItemTemplate> <DataTemplate> <controls:NumberedCircle Number="{Binding Path=Number}" map:MapControl.Location="{Binding Path=Point}" /> </DataTemplate> </behaviors:PushpinCollectionBehavior.ItemTemplate> </behaviors:PushpinCollectionBehavior> </i:Interaction.Behaviors> </map:MapControl> 

code:

 using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Xaml.Interactivity; using Windows.Devices.Geolocation; using Windows.Foundation; using Windows.Storage.Streams; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls.Maps; namespace Foo.Behaviors { /// <summary> /// Behavior to draw pushpins on a map. This effectively replaces MapItemsControl, which is flaky as hell. /// </summary> public class PushpinCollectionBehavior : DependencyObject, IBehavior { #region IBehavior public DependencyObject AssociatedObject { get; private set; } public void Attach(Windows.UI.Xaml.DependencyObject associatedObject) { var mapControl = associatedObject as MapControl; if (mapControl == null) throw new ArgumentException("PushpinCollectionBehavior can be attached only to MapControl"); AssociatedObject = associatedObject; mapControl.Unloaded += MapControlUnloaded; } public void Detach() { var mapControl = AssociatedObject as MapControl; if (mapControl != null) mapControl.Unloaded -= MapControlUnloaded; } #endregion #region Dependency Properties /// <summary> /// The dependency property of the item that contains the pushpin locations. /// </summary> public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(object), typeof(PushpinCollectionBehavior), new PropertyMetadata(null, OnItemsSourcePropertyChanged)); /// <summary> /// The item that contains the pushpin locations. /// </summary> public object ItemsSource { get { return GetValue(ItemsSourceProperty); } set { SetValue(ItemsSourceProperty, value); } } /// <summary> /// Adds, moves, or removes the pushpin when the item source changes. /// </summary> private static void OnItemsSourcePropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { var behavior = dependencyObject as PushpinCollectionBehavior; var mapControl = behavior.AssociatedObject as MapControl; // add the items if (behavior.ItemsSource is IList) behavior.AddItems(behavior.ItemsSource as IList); else throw new Exception("PushpinCollectionBehavior needs an IList as the items source."); // subscribe to changes in the collection if (behavior.ItemsSource is INotifyCollectionChanged) { var items = behavior.ItemsSource as INotifyCollectionChanged; items.CollectionChanged += behavior.CollectionChanged; } } // <summary> /// The dependency property of the pushpin template. /// </summary> public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(PushpinCollectionBehavior), new PropertyMetadata(null)); /// <summary> /// The pushpin template. /// </summary> public DataTemplate ItemTemplate { get { return (DataTemplate)GetValue(ItemTemplateProperty); } set { SetValue(ItemTemplateProperty, value); } } #endregion #region Events /// <summary> /// Adds or removes the items on the map. /// </summary> private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: AddItems(e.NewItems); break; case NotifyCollectionChangedAction.Remove: RemoveItems(e.OldItems); break; case NotifyCollectionChangedAction.Reset: ClearItems(); break; } } /// <summary> /// Removes the CollectionChanged event handler from the ItemsSource when the map is unloaded. /// </summary> void MapControlUnloaded(object sender, RoutedEventArgs e) { var items = ItemsSource as INotifyCollectionChanged; if (items != null) items.CollectionChanged -= CollectionChanged; } #endregion #region Private Functions /// <summary> /// Adds items to the map. /// </summary> private void AddItems(IList items) { var mapControl = AssociatedObject as MapControl; foreach (var item in items) { var templateInstance = ItemTemplate.LoadContent() as FrameworkElement; var hashCode = item.GetHashCode(); templateInstance.Tag = hashCode; templateInstance.DataContext = item; mapControl.Children.Add(templateInstance); Tags.Add(hashCode); } } /// <summary> /// Removes items from the map. /// </summary> private void RemoveItems(IList items) { var mapControl = AssociatedObject as MapControl; foreach (var item in items) { var hashCode = item.GetHashCode(); foreach (var child in mapControl.Children.Where(c => c is FrameworkElement)) { var frameworkElement = child as FrameworkElement; if (hashCode.Equals(frameworkElement.Tag)) { mapControl.Children.Remove(frameworkElement); continue; } } Tags.Remove(hashCode); } } /// <summary> /// Clears items from the map. /// </summary> private void ClearItems() { var mapControl = AssociatedObject as MapControl; foreach (var tag in Tags) { foreach (var child in mapControl.Children.Where(c => c is FrameworkElement)) { var frameworkElement = child as FrameworkElement; if (tag.Equals(frameworkElement.Tag)) { mapControl.Children.Remove(frameworkElement); continue; } } } Tags.Clear(); } #endregion #region Private Properties /// <summary> /// The object tags of the items this behavior has placed on the map. /// </summary> private List<int> Tags { get { if (_tags == null) _tags = new List<int>(); return _tags; } } private List<int> _tags; #endregion } } 
+8
source

Source: https://habr.com/ru/post/976249/


All Articles