Sunday, August 22, 2010

Unhooking a leaking C# object from Microsoft.Win32.SystemEvents.UserPreferenceChanged

Before I get started I'd like to clarify what the tem "leak" means for the purposes of this discussion. Any object that your application is no longer using but cannot be garbage collected is "leaking" for the purposes of this discussion. I know that technically the memory is still accessible and it is possible to release it but the fact of the matter is that your application is not using this memory and it will not ever be released. Given enough time this will eventually lead to an out of memory exception.

For anyone that has used a memory profiler it's possible that you've come across leaks related to the System.Windows.Form.ToolStripTextBox control. If you haven't, here's a link to a blog post that can shed some light on this issue. It's a very good explanation of the problem but in summary, the ToolStripTextBox control is hooking the Microsoft.Win32.SystemEvents.UserPreferenceChanged repeatedly and unhooking only once. This is causing leaks because the UserPreferenceChanged event is static and does not go out of scope for the life of your application. This creates a root reference for any listeners of that event. Unfortunately, the workaround posted on the linked page did not seem to work very well for me. After modifying the workaround until it sufficiently suited my needs I found that the ToolStripTextBox control is not the only class that fails to unhook from this event causing leaks. For the project I'm working on this was causing a chain reaction of leaking objects that was fairly significant.

There are three ways to solve this problem that I can see. The first option is that you can try to predict the nature of the bug in the class that is leaking. Using this information you can code a very specific solution to this problem. The problem with this is that it requires that you are correct in your analysis of the problem which is buried in a class that you don't have the source code for and your targeted solution also works correctly. The second option I see here is the brute force method. Just unhook the method a bunch of times and hopefully the problem goes away. If I need to explain what's wrong with that don't waste your time reading further. The third option I see for solving this problem is to create a generic method that will look at the listeners of this event, find all references to the leaking object, and unhook them. This is the approach I have used to solve this problem.

To implement this solution I created a class UnhookSystemEventUserPreferenceChangedEvent. In this class there is a static public method UnhookObject(object pObjectToUnhook). This will take any object that is directly hooked (ToolStripTextBox and ToolStripItemOverflow are the two I've had problems with) to the Microsoft.Win32.SystemEvents.UserPreferenceChanged event and will unhook them. It's preferable to call this method in response to the disposing event of the leaking object. Rather than an abstract explanation of the code I've tried to comment the code itself as thoroughly as possible so that I could just post the code and let you walk through it. If there's another solution out there I didn't see it so for anyone who's been agonizing over this I hope this helps.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Win32;

namespace HelperClasses
{
     public class UnhookSystemEventUserPreferenceChangedEvent
     {
          //we'll use a static List to cache a reference to the internal list of UserPreferenceChangedEvent listeners so that
          // we do not need to search for it every time.
          static System.Collections.IList _UserPreferenceChangedList = null;

          static public void UnhookObject(object pObjectToUnhook)
          {
               //First check for null and get a ref to the UserPreferenceChangedEvent's internal list of listeners if needed.
               if (_UserPreferenceChangedList == null) GetUserPreferenceChangedList();
               //then, scan that list for any delegates that point to pObjectToUnhook.
               SearchListAndRemoveEventHandlers(pObjectToUnhook);
          }

          static private void GetUserPreferenceChangedList()
          {
               Type oSystemEventsType = typeof(SystemEvents);

               //Using reflection, get the FieldInfo object for the internal collection of handlers
               // we will use this collection to find the handler we want to unhook and remove it.
               // as you can guess by the naming convention it is a private member.
               System.Reflection.FieldInfo oFieldInfo = oSystemEventsType.GetField("_handlers",
                                   System.Reflection.BindingFlags.Static |
                                   System.Reflection.BindingFlags.GetField |
                                   System.Reflection.BindingFlags.FlattenHierarchy |
                                   System.Reflection.BindingFlags.NonPublic);

               //now, get a reference to the value of this field so that you can manipulate it.
               //pass null to GetValue() because we are working with a static member.
               object oFieldInfoValue = oFieldInfo.GetValue(null);

               //the returned object is of type Dictionary<object, List<SystemEventInvokeInfo>>
               //each of the Lists<> in the Dictionary<> is used to maintain a different event implementation.
               //It may be more efficient to figure out how the UserPreferenceChanged event is keyed here but a quick-and-dirty
               // method is to just scan them all the first time and then cache the List<> object once it's found.

               System.Collections.IDictionary dictFieldInfoValue = oFieldInfoValue as System.Collections.IDictionary;
               foreach (object oEvent in dictFieldInfoValue)
               {
                    System.Collections.DictionaryEntry deEvent = (System.Collections.DictionaryEntry)oEvent;
                    System.Collections.IList listEventListeners = deEvent.Value as System.Collections.IList;

                    //unfortunately, SystemEventInvokeInfo is a private class so we can't declare a reference of that type.
                    //we will use object and then use reflection to get what we need...
                    List<Delegate> listDelegatesToRemove = new List<Delegate>();

                    //we need to take the first item in the list, get it's delegate and check the type...
                    if (listEventListeners.Count > 0 && listEventListeners[0] != null)
                    {
                         Delegate oDelegate = GetDelegateFromSystemEventInvokeInfo(listEventListeners[0]);
                         if (oDelegate is UserPreferenceChangedEventHandler)
                         { _UserPreferenceChangedList = listEventListeners; }
                    }
                    //if we've found the list, no need to continue searching
                    if (_UserPreferenceChangedList != null) break;
               }
          }

          static private void SearchListAndRemoveEventHandlers(object pObjectToUnhook)
          {
               if (_UserPreferenceChangedList == null) return; //Do not run if we somehow haven't found the list.

               //unfortunately, SystemEventInvokeInfo is a private class so we can't declare a reference of that type.
               //we will use object and then use reflection to get what we need...
               List<UserPreferenceChangedEventHandler> listDelegatesToRemove = new List<UserPreferenceChangedEventHandler>();

               //this is NOT threadsafe. Unfortunately, if the collection is modified an exception will be thrown during iteration.
               // This will happen any time another thread hooks or unhooks the UserPreferenceChanged event while we iterate.
               // Modify this to be threadsafe somehow if that is required.
               foreach (object oSystemEventInvokeInfo in _UserPreferenceChangedList)
               {
                    UserPreferenceChangedEventHandler oDelegate =
                         GetDelegateFromSystemEventInvokeInfo(oSystemEventInvokeInfo) as UserPreferenceChangedEventHandler;

                    if (oDelegate != null && oDelegate.Target == pObjectToUnhook)
                    {
                         //at this point we have found an event handler that must be unhooked.
                         listDelegatesToRemove.Add(oDelegate);
                    }
               }

               //We should unhook using the public method because the internal implementation of this event is unknown.
               // iterating the private internal list is already shady enough without manipulating it directly...
               foreach (UserPreferenceChangedEventHandler itemToRemove in listDelegatesToRemove)
               { SystemEvents.UserPreferenceChanged -= itemToRemove; }
          }

          static private Delegate GetDelegateFromSystemEventInvokeInfo(object pSystemEventInvokeInfo)
          {
               Type typeSystemEventInvokeInfo = pSystemEventInvokeInfo.GetType();
               System.Reflection.FieldInfo oTmpFieldInfo = typeSystemEventInvokeInfo.GetField("_delegate",
                                   System.Reflection.BindingFlags.Instance |
                                   System.Reflection.BindingFlags.GetField |
                                   System.Reflection.BindingFlags.FlattenHierarchy |
                                   System.Reflection.BindingFlags.NonPublic);

               //Here we are NOT working with a static field so we will supply the SystemEventInvokeInfo
               // object that we found in the List<> object to the GetValue() function.
               Delegate oReturn = oTmpFieldInfo.GetValue(pSystemEventInvokeInfo) as Delegate;

               return oReturn;
          }
     }
}