user3737161
user3737161

Reputation:

Manually calling System.GC.Collect() changes action of Windows Forms application

I believed that manually calling System.GC.Collect() only effect performance or memory usage of application. But in this example, calling System.GC.Collect() changes application's action.

using System;
using System.Windows.Forms;

public class Form1 : Form
{
    public Form1()
    {
        this.Click += Form1_Click;
    }

    private void Form1_Click(object sender, EventArgs e)
    {
        CreateMyEventHandler()(sender, e);
        //System.GC.Collect();
    }

    private EventHandler CreateMyEventHandler()
    {
        return (sender, e) =>
        {
            var cm = new ContextMenu(new[] 
                {
                    new MenuItem("Menu item", (sender2, e2) =>
                    {
                        Console.WriteLine("Menu item is clicked!");
                    })
                });
            cm.Show(this, ((MouseEventArgs)e).Location);
        };
    }
}

static class Program
{
    static void Main()
    {
        Application.Run(new Form1());
    }
}

Save above code into a file "Program.cs" and run it as following.

csc Program.cs
.\Program.exe

It opens window as figure below.

sample code

Clicking the window opens context menu, and clicking the menu item prints text massage "Menu item is clicked!" to the console, as expected.

However, with uncommenting the line commented out in above example:

System.GC.Collect();

The action of application changes. Clicking menu item prints nothing.

This was unexpected for me. So why it changes? And how do I prevent unexpected change of this kind in real application?

This example is confirmed with Visual Studio 2013.

Upvotes: 3

Views: 550

Answers (2)

Hans Passant
Hans Passant

Reputation: 941455

It is a premature collection problem. You can see that for yourself by adding this class:

class MyContextMenu : ContextMenu {
    public MyContextMenu(MenuItem[] items) : base(items) { }
    ~MyContextMenu() {
        Console.WriteLine("Oops");
    }
}

And using MyContextMenu instead of ContextMenu. Note how you see "Oops" when you call GC.Collect().

There is a technical reason for this, ContextMenu is a .NET 1.x class that wraps the native menus built into the OS. It does not derive from the Control class so does not participate in the normal way controls are kept alive. Which is through an internal table inside the Winforms plumbing that ensures that Control objects stay referenced as long as their native Handle exists. This same kind of table is missing for Menu class. It gets extra confusing because the .NET wrapper class is gone but the native Windows menu still exists, so seems to operate correctly.

Back in the .NET 1.x days programmers were used to using the designer to create context menus. Which works fine, they stay referenced through the components collection. Which is how you could fix this problem:

    var cm = new ContextMenu(...);
    this.components.Add(cm);
    cm.Show(this, ((MouseEventArgs)e).Location);

Albeit that you now keep adding context menus, that isn't pretty either. Assigning the form's ContextMenu property is another workaround, probably the one you prefer. But, really rather best to retire this ancient 1.x component and use ContextMenuStrip instead, it doesn't have this problem:

    var cm = new ContextMenuStrip();
    cm.Items.AddRange(new[]
    {
        new ToolStripMenuItem("Menu item", null, (sender2, e2) =>
        {
            Console.WriteLine("Menu item is clicked!");
        })
    });
    cm.Show(this, ((MouseEventArgs)e).Location);

Upvotes: 4

Philip Stuyck
Philip Stuyck

Reputation: 7467

Nobody is keeping a reference of your context menu and its menu item. If you are forcing the garbage collector to run, it will detect that and remove them. If you don't force the garbage collector, these items will be garbage collected too, but later, which is why you see the printing.

Upvotes: 2

Related Questions