Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Windows Phone 8.1 WinRT memory leak with ObservableCollection

I'm working with large amount of objects (POI's) that are getting displayed on a MapControl. I'm helping myself with a MVVM Light to obey the rules of the MVVM approach.

Since I'm obligated to display every object on the map, I have to use MapItemsControl collection, and not the MapElements one. This collection binds to the the ObservableCollection<PushpinViewModel> object (Pushpins) in corresponding ViewModel. Everything works as expected, up to the point, when I want to refresh Pushpins. The problem is memory leak. But first, some 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 add to the Pushpins collection 1000 PushpinViewModel elements. They are rendered, memory is allocated, everything's fine. Now I want to clear the collection, and add another (different in real scenario) 1000 elements. So, I call Clear() method. But.. nothing happens! Pushpins gets cleared, but PushpinViewModel's finalizers are not called! Then I add 1000 elements again, and my memory usage doubles. You can guess what happens next. When I repeat this Clear() - Add() procedure 3-5 times my app crashes.

So, what is the problem? Clearly ObservableCollection is holding references to the PushpinViewModel objects after Clear() has been performed on it, so they cannot be garbage collected. Of course forcing GC to perform garbage collection does not help (it sometimes even makes the situation worse).

It's bothering me for 2 days now, I have tried many different scenarios to try and overcome this problem, but to be honest, nothing helped. There was only one thing worth nothing - I don't remember the exact scenario, but when I had assigned Pushpins = null, and then did something more, VehiceViewModel's were destroyed. But that does not work for me, because I also remember that I had problem with visualizing these pins on the map after the Clear().

Do you have any ideas what can cause this memory leak? How can I force OC's members to destroy? Maybe there is some kind of alternative for OC? Thanks in advance for any help!

EDIT:

I did some tests with XAML Map Control - https://xamlmapcontrol.codeplex.com/, and results are surprising. Overall map performance with >1000 elements added to it, is poorer than a native MapControl, BUT, if I call Add() x1000, then Clear(), then Add() x1000, the PushpinViewModel's finalizers are geting called! Memory gets freed, and app does not crash. So there is definitely something wrong with Microsoft's MapControl...

like image 405
Malutek Avatar asked Oct 04 '14 12:10

Malutek


1 Answers

OK, here's the behavior I made that emulates what MapItemsControl does. Note that this is pretty untested -- it works in my app, but has really not been tried anywhere else. And I have never tested the RemoveItems function because my app just adds items to an ObservableCollection and clears them; it never removes items incrementally.

Also note that it tags the XAML pushpins with the hash code of the item it is bound to; this is how it identifies which pushpins to remove from the map if the collection changes. This may not work for your circumstances, but it seems to be effective.

Usage:

Note: NumberedCircle is a user control that is simply a red circle that displays a number inside of it; replace with whatever XAML control you want to use as a pushpin. Destinations is my ObservableCollection of objects that have a Number property (to display inside the pushpin) and a Point property (the pushpin 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
    }
}
like image 57
Paul Abbott Avatar answered Nov 06 '22 13:11

Paul Abbott