Quantcast
Channel: Silverlight 5 forum
Viewing all articles
Browse latest Browse all 1083

Colossal Memory Leak - ContextMenu on DataGridRow

$
0
0

There is a very common scenario where you might want to put to a ContextMenu on an entire row of a DataGrid. For some reason, doing this is not straight forward and can't be done at the Xaml level, so many people use a code based workaround. This is what most people do:

privatevoidGrid1OnLoadingRow(object sender,DataGridRowEventArgs e){var contextMenu =newContextMenu();var deleteMenuItem =newSystem.Windows.Controls.MenuItem{Header="text"};
        contextMenu.Items.Add(deleteMenuItem);ContextMenuService.SetContextMenu(e.Row, contextMenu);}

However, this causes a terrible memory leak. The ContextMenus get instantiated, but they are never finalised (i.e picked up by the garbage collector) and destroyed. So, the app eventually becomes sluggish and memory gets eaten up. I can prove this. Below is a sample app. Just past the code in to a new Silverlight project.

MainPage.xaml.cs:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System;

namespace DataGridRowMemoryLeakTest
{
    public partial class MainPage : Grid
    {
        #region Constructor
        public MainPage()
        {
            InitializeComponent();

            var users = new ObservableCollection<User>();
            users.Add(new User { Name = "Bob Smith", Username = "bobsmith" });
            users.Add(new User { Name = "Sam Black", Username = "samblack" });
            users.Add(new User { Name = "Roger Davis", Username = "rogerdavis" });
            users.Add(new User { Name = "James Clemments", Username = "jamesclemments" });
            users.Add(new User { Name = "Phil James", Username = "philjames" });
            TheDataGrid.ItemsSource = users;
            TheDataGrid.LoadingRow += DataGrid_LoadingRow;
            TheDataGrid.UnloadingRow += DataGrid_UnloadingRow;
        }
        #endregion

        #region Event Handlers
        private void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
        {
            var contextMenu = ContextMenuService.GetContextMenu(e.Row);
            if (contextMenu == null)
            {
                contextMenu = new CountableContextMenu();
                var deleteMenuItem = new MenuItem { Header = "Delete User" };
                contextMenu.Items.Add(deleteMenuItem);
                ContextMenuService.SetContextMenu(e.Row, contextMenu);
            }
        }

        void DataGrid_UnloadingRow(object sender, DataGridRowEventArgs e)
        {
            var contextMenu = ContextMenuService.GetContextMenu(e.Row);
            contextMenu.Items.Clear();
            contextMenu.ClearValue(DataContextProperty);
            ContextMenuService.SetContextMenu(e.Row, null);
            e.Row.ClearValue(ContextMenuService.ContextMenuProperty);
        }

        #endregion
    }

    public class CountableContextMenu : ContextMenu
    {
        #region Fields
        private static int Counter = 0;
        #endregion

        #region Constructor
        public CountableContextMenu()
            : base()
        {
            Counter++;

            if (Counter % 20 == 0)
            {
                GC.Collect();
            }

            System.Diagnostics.Debug.WriteLine(string.Format("Contruct: {0}", Counter));
        }
        #endregion

        #region Finaliser
        ~CountableContextMenu()
        {
            Counter--;
            System.Diagnostics.Debug.WriteLine(string.Format("Finalise: {0}", Counter));
        }
        #endregion
    }

    public class User : INotifyPropertyChanged
    {
        #region Events
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Fields
        private string _Name;
        private string _Username;
        #endregion

        #region Public Properties
        public string Name
        {
            get
            {
                return _Name;
            }
            set
            {
                _Name = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Name"));
                }
            }
        }

        public string Username
        {
            get
            {
                return _Username;
            }
            set
            {
                _Username = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Username"));
                }
            }
        }
        #endregion
    }
}


Here's the Xaml (MainPage.xaml):

<Grid xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"  x:Class="DataGridRowMemoryLeakTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"><sdk:DataGrid  Height="120" Margin="4" Name="TheDataGrid"  ><sdk:DataGrid.Columns><sdk:DataGridTextColumn Header="Name" Binding="{Binding Name}"  /><sdk:DataGridTextColumn Header="Username" Binding="{Binding Username}"  /></sdk:DataGrid.Columns></sdk:DataGrid></Grid>

To recreate the problem, watch the Output window of the debugger and scroll up and down on the DataGrid so that rows are loaded and unloaded.  What you notice is that it is creating a new ContextMenu each time a row goes off screen, but that same ContextMenu is never finalised (removed from the memory heap). After scrolling for a while you will notice a significant performance slowdown.

If you use a normal ContextMenu instead of my CountableContextMenu, you will still see the slowdown after a while but you won't see how many ContextMenus are actually being created.

Has anyone else come across this problem? Is there are better way to deal with this?


Viewing all articles
Browse latest Browse all 1083

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>