Search code examples
c#winformsgarbage-collectionnon-modal

How to ensure garbage collection when user closes a non-modal window?


In my C# Winforms App, I have the following (minimal code shown)

Form1 is the main app that the user uses to do stuff. Form2 shows a help file that explains how to use the features on Form1 to do stuff. I want the user to be able to display (modeless) and close the help file at will so long as Form1 is visible.

I also worry about the memory leak that may occur as the user opens and closes Form2. So, when the user closes Form2, it raises an event that Form1 subscribes to. When the Form1 event method is invoked, is calls Dispose() on Form2, sets the Form2 object to null and calls the garbage collector.

Will this remove the chance for a memory leak caused by the user opening and closing Form2? Is it overkill? Is there a better way of ensuring garbage collection at the point in time that Form2 is closed? I don't want to rely on Windows doing it later when it decides to

UPDATE

Jimi pointed out that I don't need a custom event handler for the Form2 Closed event. He's right. In my Form1 class, I'm now using the standard FormClosedEventHandler for my Form2. The code itself, however remains pretty much the same. However, when I remove the call to GC.Collect(), I'm seeing evidence of a memory leak using Task Manager.

Here's my data:

Run #1. Form2_FormClosed method has:
------------------------------------
f_HelpForm.Dispose();
f_HelpForm = null;
GC.Collect();

Start App
Task Manager Memory for app: 6.7 MB
Open and Close Form2 20 times
Task Manager Memory for app: 8.2 MB

Run #2. Form2_FormClosed method has:
------------------------------------
f_HelpForm.Dispose();
f_HelpForm = null;
//GC.Collect();

Start App
Task Manager Memory for app: 6.9 MB
Open and Close Form2 20 times
Task Manager Memory for app: 18.9 MB

Run #3. Form2_FormClosed method has:
------------------------------------
//f_HelpForm.Dispose();
f_HelpForm = null;
//GC.Collect();

Start App
Task Manager Memory for app: 6.9 MB
Open and Close Form2 20 times
Task Manager Memory for app: 18.1 M

Without the call to GC.Collect(), and with or without the call to Dispose(), the footprint grows 100% bigger as compared to the code that includes the call to GC.collect().

I hear what you guys are saying, but .......... I think I'll leave my code in its "Run #1" configuration

The code

Note: I acknowledge that setting form2 = null has a direct influence on behind-the-scenes garbage collection. However, my purpose in setting form2 = null is to provide a signal to the Form2Button_Click method that it can use to decide whether or not to instantiate a Form2 object

public partial class Form1 : Form
{
    Form2 form2;
    
    public Form1()
    {
        form2 = null;
    }  

    private void Form2Button_Click(object sender, EventArgs e)
    {
        if (form2 == null)
        {
            form2 = new ClsHelpForm(this);
            form2.Form2Closed += Form2_FormClosed;
        }

        form2.Select();
        form2.Show();
    }

    //When this user clicks the Help button on Form1, this method is invoked
    private void Form2_FormClosed(object sender, EventArgs e)
    {
        form2.Form2Closed -= Form2_FormClosed;
        form2.Dispose();
        form2 = null;
        GC.Collect();
    }   
{

public partial class Form2 : Form
{
    public event EventHandler Form2Closed;

    public Form2()
    {
    }

    //When the user clicks the "X" button on Form2, this method is invoked
    private void Form2_FormClosed(object sender, Form2EventArgs e)
    {
        Form2Closed?.Invoke(this, EventArgs.Empty);
    }
}

Solution

  • A member instance of Form2 doesn't take a lot of room on the heap and there seems to be little cause to create and destroy its Handle every time the user wants to show it.

    managed memory

    Why not just prevent the destruction of the Form2 handle until app closes?

    public partial class Form2 : Form
    {
        public Form2(Form owner)
        {
            InitializeComponent();
            Owner = owner;
            StartPosition = FormStartPosition.Manual;
        }
        protected override void OnVisibleChanged(EventArgs e)
        {
            base.OnVisibleChanged(e);
            if(Visible)
            {
                Location = new Point(
                    Owner.Location.X + Owner.Width + 10,
                    Owner.Location.Y);
            }
        }
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
            if(e.CloseReason.Equals(CloseReason.UserClosing))
            {
                e.Cancel = true;
                Hide();
            }
        }
    }
    

    When the form2 is cycled (by automation) 100 times, monitoring the process memory shows zero GCs and no direct correlation to the number of times shown.

    process memory

    Where:

    public partial class Form1 : Form
    {
        Form2 _form2;
        public Form1()
        {
            InitializeComponent();
            _form2 = new Form2(this);
            Disposed += (sender, e) => _form2.Dispose();
            buttonShowHelp.Click += async (sender, e) =>
            {
                for (int i = 0; i < numericUpDown.Value; i++)
                {
                    _form2.Visible = true;
                    await Task.Delay(500);
                    _form2.Visible = false;
                    await Task.Delay(500);
                }
                // But leave it open after cycling.
                _form2.Visible = true;
            };
        }
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
        }
    }