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
...
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With