locked
UserPreferenceChangedEventHandler and Object Retention RRS feed

  • General discussion

  • Hi All,

    I've spent the last few weeks tracking down a problem reported by a customer where they were seeing an out of memory exception when performing certain actions with our application.

    Cutting a long story short, we had 2 forms being used in a process that were not being tidied up correctly.  Using a number of tools available online, I tracked this down to the "UserPreferenceChangedEventHandler" that is created by a StatusStrip object and a ToolStrip object but never removed leaving a reference to the child object on the form and hence the form itself long after the form has been closed.

    I've found a few references to similar problems online such as http://www.scitech.se/blog/index.php/2007/10/05/memory-leak-in-toolstriptextboxcontrol

    and I found the following on Connect which concerns me a little. http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=441221

    What i want to know is does anyone have a definitive list of what is affected by this bug and what we can do to work around it.  

    For the record (just in case someone else comes across similar) the actions that I undertook to fix what i was seeing are as follows:

    A new helper class
    public class ToolStripTextBoxFixer
    {
        bool m_visible = false;
        ToolStripTextBoxFixer(System.Windows.Forms.Control ctrl) { ctrl.VisibleChanged += this.onVisibleChanged; }
    
        public static void FixToolbarTextBoxes(System.Windows.Forms.Control view)
        {
            foreach (System.Windows.Forms.Control ctrl in view.Controls)
            {
                if ((ctrl.GetType().Name.IndexOf("StatusStrip") >= 0) || (ctrl.GetType().Name.IndexOf("ToolStrip") >= 0) )
                    new ToolStripTextBoxFixer(ctrl);
                else FixToolbarTextBoxes(ctrl);
            }
        }
    
        void onVisibleChanged(object sender, EventArgs args)
        {
            System.Windows.Forms.Control ctrl = sender as System.Windows.Forms.Control;
            if (m_visible == ctrl.Visible)
                Microsoft.Win32.SystemEvents.UserPreferenceChanged -= (Microsoft.Win32.UserPreferenceChangedEventHandler)
                        Delegate.CreateDelegate(typeof(Microsoft.Win32.UserPreferenceChangedEventHandler), 
                        sender, "OnUserPreferenceChanged");
            m_visible = ctrl.Visible;
        }
    }
    
    Then adding the following to the Form Constuctor (After Initialize Component)
        InitializeComponent();
        ToolStripTextBoxFixer.FixToolbarTextBoxes(this);
    
    Finally the real gotcha, even with the above in place, I still had to hide the controls prior to closing the form!!
    .
    .
    statusStrip1.Visible = false;
    toolStripPrintingOptions.Visible = false;
    .
    .
    
    Regards,

    SiD
    Tuesday, June 23, 2009 8:05 AM

All replies

  • I don't see any bug in .NET 3.5 SP1.  The ToolStrip and ToolStripTextBox class indeed listen for the SystemEvents.UserPreferenceChanged event.  But they correctly unregister their event handler in the Dispose() method with a call to HookStaticEvents(false).  The analysis in the blog post is not accurate, it does check if the event is already hooked.  Don't take any stock in the feedback article either, that's the boilerplate answer since the WF team got disbanded.

    Consider the possibility that you are dynamically adding and removing controls derived from either and forgot to call their Dispose() method when you removed them.  That's a very common mistake in Windows Forms programming since controls are normally automatically disposed.



    Hans Passant.
    Tuesday, June 23, 2009 8:50 AM
  • Hi Hans,

    Thanks for the response, I'm always cautious when reading things that are out on the web, but what i can confirm is that 

    a) The controls are being created during the Initialize component call using the designer generated code - AFAIK, this means that i don't need to call the Dispose myself
    b) Unless the controls were hidden before calling "this.Close()" on the form, the objects were not being gc'd
    c) Adding the code as documented fixed the problem.

    A further "oddity" has now crept into the equation, that being when i resize the form to such a size that my toolstrip is larger than the area allocated and it gets the extended popdown menu type items at the end, if i open that menu up when i close the form it again reverts to not disposing correctly, i recall in my searches seeing something similar reported against this as well.

    There is clearly something not quite right in this area, and as far as i can tell i'm doing everything that i should do.

    I'm going to revisit my "solution" and see what happens if i just hide the controls before closing i.e. remove the helper class and its event handler hook up fixing and will get back to you.

    Can i assume from the statement "Don't take any stock in the feedback article either, that's the boilerplate answer since the WF team got disbanded. "that even if this is proven to be a bug, the likelihood of it being fixed is remote??

    I shall also attempt to provide a simple sample that exhibits this behaviour.

    Regards,

    SiD
    Tuesday, June 23, 2009 9:10 AM
  • OK, more information

    I created myself a little test application as follows

    Main Form
    Status Strip
    StatusStripLabel
    Button - On Click Opens Up Child Form

    Child Form
    Status Strip
    StatusStripLabel
    ToolStrip
    Button - On Click Opens Up a Grandchild Form
    Button - On Click Hides Status Strip
    Button - On Click Hides ToolStrip
    TextBoxControl - Has random text on it

    GrandChildForm
    PictureBox
    Status Strip
    StatusStripLabel
    Button - On Click Hides StatusStrip

    Run up the application, click on button to open Child Form, Then button to open Grandchild form.
    Now close the grandchild form, followed by the child form.
    Examining the object retention now reveals that both forms although closed are still being retained due to them being referenced by the UserPreferenceChangedEventHandler of the statusstrip

    Repeating the above but clicking the button on the grandchild form to hide the status strip prior to closing will remove this reference and allow the grandchild form to be garbage collected.

    Just opening the child form and closing it reveals similar behaviour, hiding the status strip and the toolstrip  is not sufficent as the ToolStripTextBoxControl within the toolstrip also appears to retain a reference to the same EventHandler, only when the Toolstriptextboxcontrol is also made not visible does it remove the event handler connections.

    Anyone any idea what is going on in there to break this so badly?

    Regards,

    SiD
    Tuesday, June 23, 2009 10:45 AM
  • I Opened a bug report on Connect http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=469277 I think that they have acknowledged the fault by changing it resolved with a resolution of won't fix it!! Not the best response in the world, they could have at least given some feedback as to which controls are affected by the problem. Hope this helps somebody somewhere. Regards SiD
    Wednesday, July 1, 2009 7:59 AM
  • Here's what I ended up with. It works on the ReportViewer as well as other controls. The problem I had with some other solutions is that the ToolStrip.Controls collection is not populated immediately.

    using System;
    using System.Collections;
    using System.Reflection;
    using System.Windows.Forms;
    using Microsoft.Win32;
    
    namespace MyNameSpace
    { public sealed class ToolStripTextBoxFixer { public static void FixToolbarTextBoxes(Control ts) { if (ts is ToolStrip) // as far as we know, toolstrips aren't nestable { foreach (var item in ((ToolStrip)ts).Items) { if (item is ToolStripTextBox) new ToolStripTextBoxFixer(((ToolStripTextBox)item).Control); } } else foreach (Control ctrl in ts.Controls) { FixToolbarTextBoxes(ctrl); } } Control _ctrl; ToolStripTextBoxFixer(Control ctrl) { ctrl.Disposed += onDisposed; _ctrl = ctrl; } void onDisposed(object sender, EventArgs e) { // I pulled this code from some internet sources combined with the reflector to remove a well-known leak var handlers = typeof(SystemEvents).GetField("_handlers", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); var upcHandler = typeof(SystemEvents).GetField("OnUserPreferenceChangedEvent", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); var lck = typeof(SystemEvents).GetField("eventLockObject", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); lock (lck) { var upcHandlerList = (IList)((IDictionary)handlers)[upcHandler]; for (int i = upcHandlerList.Count - 1; i >= 0; i--) { var target = (Delegate)upcHandlerList[i].GetType().GetField("_delegate", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(upcHandlerList[i]); if (target.Target == _ctrl) { upcHandlerList.RemoveAt(i); } } } _ctrl.Disposed -= onDisposed; _ctrl = null; } } }

    • Edited by Brannon Friday, July 17, 2009 2:44 PM
    Friday, July 17, 2009 2:42 PM
  • This happens on the RichTextBox control on Framework 3.5 SP1. I confirmed it using MemProfiler. I am trying to fix the issue by removing the event handler, but so far I am not having any luck.
    Wednesday, September 2, 2009 3:22 AM