Skip to content

Fix: ContextMenu submenus fire Popup on non-Form controls#17

Merged
zuizuihao merged 2 commits into
masterfrom
SPG/WI01075819/WM_INITMENUPOPUP_Missing
May 15, 2026
Merged

Fix: ContextMenu submenus fire Popup on non-Form controls#17
zuizuihao merged 2 commits into
masterfrom
SPG/WI01075819/WM_INITMENUPOPUP_Missing

Conversation

@zuizuihao
Copy link
Copy Markdown

ContextMenu Submenu Popup Events Not Firing — Root Cause & Fix

Problem

On .NET 10, the MenuItem.Popup event does not fire for submenus when a ContextMenu is shown
via TrackPopupMenuEx. This is a regression from .NET Framework / WinForms 3.0, where the event
fired correctly.

Any pattern that relies on MenuItem.Popup to populate a submenu dynamically (lazy-loading,
placeholder replacement, etc.) is silently broken.


Root Cause

How WM_INITMENUPOPUP reaches a ContextMenu

ContextMenu.Show() calls TrackPopupMenuEx, passing the owner control's HWND:

// Controls\Unsupported\ContextMenu\ContextMenu.cs
private void Show(Control control, Point pos, int flags)
{
    sourceControl = control;
    OnPopup(EventArgs.Empty);   // fires top-level Popup only
    pos = control.PointToScreen(pos);
    SafeNativeMethods.TrackPopupMenuEx(
        new HandleRef(this, Handle),
        flags, pos.X, pos.Y,
        new HandleRef(control, control.Handle),   // <── owner HWND
        null);
}

OnPopup fires immediately for the top-level menu. For each submenu that is about to open,
Windows sends WM_INITMENUPOPUP to the owner HWND. The correct handler calls
ContextMenu.ProcessInitMenuPopup, which recursively finds the matching MenuItem by its native
HMENU handle and fires MenuItem.OnPopupPopup event.

The bug: Control.WndProc silently drops WM_INITMENUPOPUP

File: src/System.Windows.Forms/System/Windows/Forms/Control.cs

// BEFORE (buggy) — WM_INITMENUPOPUP grouped into the default fall-through:
case PInvokeCore.WM_EXITMENULOOP:
case PInvokeCore.WM_INITMENUPOPUP:    // <── falls straight through to DefWndProc
case PInvokeCore.WM_MENUSELECT:
default:
    DefWndProc(ref m);     // ContextMenu.ProcessInitMenuPopup is never called
    break;

WM_INITMENUPOPUP is sent to the owner control's HWND, not the form's HWND. Because all
controls inherit WndProc from Control, and Control.WndProc lumps this message into the
default fall-through, ContextMenu.ProcessInitMenuPopup is never reached and no submenu
Popup event fires.

Why Form.WmInitMenuPopup does not help

Form.cs has its own WmInitMenuPopup, but it only handles the MainMenu (menu bar):

// Form.cs — only MainMenu is checked
private void WmInitMenuPopup(ref Message m)
{
    if (Properties.TryGetValue(s_propCurMenu, out MainMenu? curMenu)
        && curMenu.ProcessInitMenuPopup((nint)m.WParamInternal))
        return;

    base.WndProc(ref m);   // falls back to Control.WndProc → DefWndProc
}

Additionally, WM_INITMENUPOPUP is sent to the owner control's HWND (which is typically not
the form), so Form.WmInitMenuPopup is not even reached for ContextMenu submenus.


Reference: WinForms 3.0 Has the Correct Pattern

The upstream dotnet/winforms repository at release/3.0 handled this correctly in Control.cs.
The WTG WinForms fork migrated the legacy ContextMenu/MenuItem/Menu types but did not port
this message handling.

Source (online):

WinForms 3.0 approach:

private void WmInitMenuPopup(ref Message m)
{
    ContextMenu contextMenu = (ContextMenu)Properties.GetObject(s_contextMenuProperty);
    if (contextMenu != null)
    {
        if (contextMenu.ProcessInitMenuPopup(m.WParam))
            return;
    }
    DefWndProc(ref m);
}

case WindowMessages.WM_INITMENUPOPUP:
    WmInitMenuPopup(ref m);
    break;

Fix Applied

File: src/System.Windows.Forms/System/Windows/Forms/Control.cs

1. New WmInitMenuPopup method (placed near WmContextMenu)

/// <summary>
///  Handles the WM_INITMENUPOPUP message. Dispatches to the legacy <see cref="ContextMenu"/> so
///  that <see cref="MenuItem.Popup"/> events fire for submenus. Without this, controls that own
///  a ContextMenu (e.g. via TrackPopupMenuEx) would never receive submenu init notifications.
/// </summary>
private void WmInitMenuPopup(ref Message m)
{
#pragma warning disable WFDEV006 // Type or member is obsolete
    if (Properties.TryGetValue(s_contextMenuProperty, out ContextMenu? contextMenu)
        && contextMenu.ProcessInitMenuPopup((nint)m.WParamInternal))
    {
        return;
    }
#pragma warning restore WFDEV006

    DefWndProc(ref m);
}

2. WM_INITMENUPOPUP broken out of the fall-through group in WndProc

case PInvokeCore.WM_INITMENUPOPUP:
    WmInitMenuPopup(ref m);
    break;

case PInvokeCore.WM_EXITMENULOOP:
case PInvokeCore.WM_MENUSELECT:
default:
    DefWndProc(ref m);
    break;

Regression Tests

File: src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ContextMenu/ContextMenuSubMenuPopupTests.cs

Test Scenario
Control_WmInitMenuPopup_DirectSubMenu_FiresPopupEvent Control with a ContextMenu whose MenuItem has a placeholder child. Sends WM_INITMENUPOPUP for that submenu's HMENU. Asserts Popup fired and the placeholder was replaced.
Control_WmInitMenuPopup_NestedSubMenu_FiresPopupEvent Same, but two levels deep — confirms ProcessInitMenuPopup recurses correctly via FindMenuItem.

Both tests fail before the fix and pass after.


Files Changed

File Change
src/System.Windows.Forms/System/Windows/Forms/Control.cs Added WmInitMenuPopup; routed WM_INITMENUPOPUP case to it in WndProc
src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ContextMenu/ContextMenuSubMenuPopupTests.cs New regression tests (2 [StaFact] tests)

WM_INITMENUPOPUP is now handled in Control to dispatch to ContextMenu.ProcessInitMenuPopup, ensuring MenuItem.Popup events fire for submenus shown from non-Form controls (e.g., DataGrid). This enables dynamic submenu population and fixes the issue where placeholder items were never replaced. The MenuStackForm demo is updated to use a DataGrid with dynamic submenus to visually repro the bug. Regression tests are added to verify correct Popup event firing for both direct and nested submenus.
@zuizuihao zuizuihao merged commit 7a15ef7 into master May 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants