Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 41 additions & 17 deletions src/System.Windows.Forms/System/Windows/Forms/Control.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1335,15 +1335,9 @@ public virtual ContextMenuStrip? ContextMenuStrip
return;
}

if (oldValue is not null)
{
oldValue.Disposed -= DetachContextMenuStrip;
}
oldValue?.Disposed -= DetachContextMenuStrip;

if (value is not null)
{
value.Disposed += DetachContextMenuStrip;
}
value?.Disposed += DetachContextMenuStrip;

OnContextMenuStripChanged(EventArgs.Empty);
}
Expand Down Expand Up @@ -11278,6 +11272,38 @@ internal void WmContextMenu(ref Message m, Control sourceControl)
}
}

/// <summary>
/// Handles the WM_MENUSELECT message.
/// </summary>
private void WmMenuSelect(ref Message m)
{
#pragma warning disable WFDEV006 // Type or member is obsolete
if (Properties.TryGetValue(s_contextMenuProperty, out ContextMenu? contextMenu))
{
contextMenu.ProcessMenuSelect(ref m);
}

DefWndProc(ref m);
#pragma warning restore WFDEV006
}

/// <summary>
/// Handles the WM_EXITMENULOOP message. If this control has a context menu, its
/// Collapse event is raised.
/// </summary>
private void WmExitMenuLoop(ref Message m)
{
#pragma warning disable WFDEV006 // Type or member is obsolete
if (m.WParamInternal != 0u &&
Properties.TryGetValue(s_contextMenuProperty, out ContextMenu? contextMenu))
{
contextMenu.OnCollapse(EventArgs.Empty);
}

DefWndProc(ref m);
#pragma warning restore WFDEV006
}

/// <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
Expand Down Expand Up @@ -12765,8 +12791,12 @@ protected virtual void WndProc(ref Message m)
WmInitMenuPopup(ref m);
break;

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

// If we received a thread execute message, then execute it.
Expand Down Expand Up @@ -13078,15 +13108,9 @@ public virtual ContextMenu ContextMenu
return;
}

if (oldValue is not null)
{
oldValue.Disposed -= DetachContextMenu;
}
oldValue?.Disposed -= DetachContextMenu;

if (value is not null)
{
value.Disposed += DetachContextMenu;
}
value?.Disposed += DetachContextMenu;

OnContextMenuChanged(EventArgs.Empty);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ internal static class LegacyMenuUnsafeNativeMethods
[DllImport(Libraries.User32, ExactSpelling = true)]
public static extern bool SetMenuDefaultItem(HandleRef hMenu, int uItem, bool fByPos);

[DllImport(Libraries.User32, ExactSpelling = true)]
public static extern int GetMenuItemID(IntPtr hMenu, int nPos);

[DllImport(Libraries.User32, ExactSpelling = true)]
public static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos);

public static bool GetMenuItemInfo(HandleRef hMenu, int uItem, bool fByPosition, LegacyMenuNativeMethods.MENUITEMINFO_T lpmii)
{
bool result = GetMenuItemInfo(hMenu.Handle, uItem, fByPosition, lpmii);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,82 @@ internal virtual bool ProcessInitMenuPopup(IntPtr handle)
return false;
}

internal bool ProcessMenuSelect(ref Message m)
{
const int MF_POPUP = 0x0010;
const int MF_SYSMENU = 0x2000;

int item = PARAM.SignedLOWORD((nint)m.WParamInternal);
int flags = PARAM.SignedHIWORD((nint)m.WParamInternal);
MenuItem? menuItem = null;

if ((flags & MF_SYSMENU) == 0)
{
if ((flags & MF_POPUP) == 0)
{
Command? command = Command.GetCommandFromID(item);
if (command?.Target is MenuItem.MenuItemData data)
{
menuItem = data.baseItem;
}
}
else
{
// Use native Win32 APIs to find the correct MenuItem. We cannot use
// managed MenuItems[index] because hidden items (Visible = false) are
// skipped in the native menu, causing the native index to differ from
// the managed collection index.
menuItem = GetMenuItemFromNativeIndex((IntPtr)(nint)m.LParamInternal, item);
}
}

if (menuItem is not null)
{
menuItem.PerformSelect();
return true;
}

return false;
}

/// <summary>
/// Resolves a popup menu item from its native menu position using Win32 APIs.
/// For a popup item (one with a submenu), this retrieves the submenu handle via
/// <c>GetSubMenu</c>, then recursively searches the submenu's children to find
/// a managed <see cref="MenuItem"/> whose <see cref="MenuItem.Parent"/> is the
/// popup item we need. This approach is immune to index mismatches caused by
/// hidden (<c>Visible = false</c>) menu items.
/// </summary>
private static MenuItem? GetMenuItemFromNativeIndex(IntPtr hmenu, int index)
{
int id = UnsafeNativeMethods.GetMenuItemID(hmenu, index);
if (id == unchecked((int)0xFFFFFFFF))
{
// The item is a popup — get its submenu handle and search children.
IntPtr childMenu = UnsafeNativeMethods.GetSubMenu(hmenu, index);
int count = UnsafeNativeMethods.GetMenuItemCount(new HandleRef(null, childMenu));
for (int i = 0; i < count; i++)
{
MenuItem? child = GetMenuItemFromNativeIndex(childMenu, i);
if (child?.Parent is MenuItem parentItem)
{
return parentItem;
}
}
}
else
{
// Non-popup item — look up by command ID.
Command? command = Command.GetCommandFromID(id);
if (command?.Target is MenuItem.MenuItemData data)
{
return data.baseItem;
}
}

return null;
}

protected internal virtual bool ProcessCmdKey(ref Message msg, Keys keyData)
{
MenuItem item = FindMenuItem(FindShortcut, (IntPtr)(int)keyData);
Expand Down
18 changes: 18 additions & 0 deletions src/System.Windows.Forms/System/Windows/Forms/Form.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7004,6 +7004,21 @@ private void WmInitMenuPopup(ref Message m)
base.WndProc(ref m);
}

/// <summary>
/// Handles the WM_MENUSELECT message.
/// </summary>
private void WmMenuSelect(ref Message m)
{
#pragma warning disable WFDEV006 // Type or member is obsolete
if (Properties.TryGetValue(s_propCurMenu, out MainMenu? curMenu))
{
curMenu.ProcessMenuSelect(ref m);
}
#pragma warning restore WFDEV006

base.WndProc(ref m);
}

/// <summary>
/// Handles the WM_MENUCHAR message.
/// </summary>
Expand Down Expand Up @@ -7406,6 +7421,9 @@ protected override void WndProc(ref Message m)
case PInvokeCore.WM_INITMENUPOPUP:
WmInitMenuPopup(ref m);
break;
case PInvokeCore.WM_MENUSELECT:
WmMenuSelect(ref m);
break;
case PInvokeCore.WM_UNINITMENUPOPUP:
WmUnInitMenuPopup(ref m);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace System.Windows.Forms.Legacy.Tests;
namespace System.Windows.Forms.Tests;

/// <summary>
/// Regression tests for WM_INITMENUPOPUP dispatch on a plain <see cref="Control"/> that
Expand All @@ -20,8 +19,8 @@ namespace System.Windows.Forms.Legacy.Tests;
/// </para>
/// <para>
/// Without the fix in <see cref="Control.WndProc"/>, WM_INITMENUPOPUP falls through to
/// DefWndProc (grouped with WM_EXITMENULOOP / default), so
/// <see cref="MenuItem.Popup"/> never fires and placeholder items are never replaced.
/// DefWndProc, so <see cref="Menu.ProcessInitMenuPopup"/> is never reached,
/// <see cref="MenuItem.Popup"/> never fires, and placeholder items are never replaced.
/// </para>
/// </remarks>
public class ContextMenuSubMenuPopupTests
Expand Down Expand Up @@ -69,9 +68,9 @@ public void Control_WmInitMenuPopup_DirectSubMenu_FiresPopupEvent()

// Act: simulate Windows delivering WM_INITMENUPOPUP to the control's HWND for the submenu.
//
// Before the fix, Control.WndProc groups WM_INITMENUPOPUP with WM_EXITMENULOOP and calls
// DefWndProc — ContextMenu.ProcessInitMenuPopup is never reached, Popup never fires, and
// the placeholder is never replaced.
// Before the fix, Control.WndProc fell through to DefWndProc —
// ContextMenu.ProcessInitMenuPopup is never reached, Popup never fires, and the
// placeholder is never replaced.
SendMessage(controlHandle, WM_INITMENUPOPUP, subMenuHandle, IntPtr.Zero);

// Assert
Expand Down Expand Up @@ -126,7 +125,7 @@ public void Control_WmInitMenuPopup_NestedSubMenu_FiresPopupEvent()
nestedPopupFired,
"MenuItem.Popup must fire for a nested submenu when WM_INITMENUPOPUP targets its HMENU.");

Assert.Equal(1, nestedItem.MenuItems.Count);
Assert.Single(nestedItem.MenuItems);
Assert.Equal("NestedDynamicItem", nestedItem.MenuItems[0].Text);
}
}
Loading
Loading