diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs
index b16525a45d9..5bdf0c058f6 100644
--- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs
+++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs
@@ -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);
}
@@ -11278,6 +11272,38 @@ internal void WmContextMenu(ref Message m, Control sourceControl)
}
}
+ ///
+ /// Handles the WM_MENUSELECT message.
+ ///
+ 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
+ }
+
+ ///
+ /// Handles the WM_EXITMENULOOP message. If this control has a context menu, its
+ /// Collapse event is raised.
+ ///
+ 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
+ }
+
///
/// Handles the WM_INITMENUPOPUP message. Dispatches to the legacy so
/// that events fire for submenus. Without this, controls that own
@@ -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.
@@ -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);
}
diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LegacyMenuInteropCompat.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LegacyMenuInteropCompat.cs
index cc5e9fb3d3e..9903a3f8943 100644
--- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LegacyMenuInteropCompat.cs
+++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LegacyMenuInteropCompat.cs
@@ -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);
diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs
index 32bff340c60..be078620bfa 100644
--- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs
+++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs
@@ -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;
+ }
+
+ ///
+ /// 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
+ /// GetSubMenu, then recursively searches the submenu's children to find
+ /// a managed whose is the
+ /// popup item we need. This approach is immune to index mismatches caused by
+ /// hidden (Visible = false) menu items.
+ ///
+ 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);
diff --git a/src/System.Windows.Forms/System/Windows/Forms/Form.cs b/src/System.Windows.Forms/System/Windows/Forms/Form.cs
index 43074b0f95f..c5c1571a8aa 100644
--- a/src/System.Windows.Forms/System/Windows/Forms/Form.cs
+++ b/src/System.Windows.Forms/System/Windows/Forms/Form.cs
@@ -7004,6 +7004,21 @@ private void WmInitMenuPopup(ref Message m)
base.WndProc(ref m);
}
+ ///
+ /// Handles the WM_MENUSELECT message.
+ ///
+ 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);
+ }
+
///
/// Handles the WM_MENUCHAR message.
///
@@ -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;
diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ContextMenu/ContextMenuSubMenuPopupTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/ContextMenuSubMenuPopupTests.cs
similarity index 90%
rename from src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ContextMenu/ContextMenuSubMenuPopupTests.cs
rename to src/test/unit/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/ContextMenuSubMenuPopupTests.cs
index 23189e4b774..15872c375c9 100644
--- a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ContextMenu/ContextMenuSubMenuPopupTests.cs
+++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/ContextMenuSubMenuPopupTests.cs
@@ -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;
///
/// Regression tests for WM_INITMENUPOPUP dispatch on a plain that
@@ -20,8 +19,8 @@ namespace System.Windows.Forms.Legacy.Tests;
///
///
/// Without the fix in , WM_INITMENUPOPUP falls through to
-/// DefWndProc (grouped with WM_EXITMENULOOP / default), so
-/// never fires and placeholder items are never replaced.
+/// DefWndProc, so is never reached,
+/// never fires, and placeholder items are never replaced.
///
///
public class ContextMenuSubMenuPopupTests
@@ -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
@@ -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);
}
}
diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LazyMenuPopulationRegressionTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LazyMenuPopulationRegressionTests.cs
new file mode 100644
index 00000000000..08fbe19e988
--- /dev/null
+++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LazyMenuPopulationRegressionTests.cs
@@ -0,0 +1,240 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace System.Windows.Forms.Tests;
+
+///
+/// Regression tests proving that the top-level Universal Copy and Grid Colors
+/// context-menu entries are actually populated when the user hovers them — not just that the
+/// internal event wiring exists.
+///
+///
+///
+/// The screenshot that drove this test shows the Universal Copy submenu open with four
+/// entries: "New Copy Template", "Edit Copy Template", "New Copy Template From", and
+/// "Copy Schedules". Without the WM_MENUSELECT fix in the
+/// Grid Colors menu's handler never fires and the submenu
+/// stays on stale placeholder items; without the WM_INITMENUPOPUP fix the Universal Copy
+/// handler never fires and the placeholder is never replaced.
+///
+///
+/// These tests simulate the real message sequence Windows delivers when the user hovers a
+/// top-level context-menu entry and assert the resulting menu item count and text, mirroring
+/// the production patterns in GridColourSchemeManager (Select-based) and
+/// ZFilterGridModule.AddUniversalCopyMenuItems (Popup-based).
+///
+///
+public class LazyMenuPopulationRegressionTests
+{
+ private const uint WM_MENUSELECT = 0x011F;
+ private const uint WM_INITMENUPOPUP = 0x0117;
+ private const int MF_POPUP = 0x0010;
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
+
+ ///
+ /// Grid Colors pattern (GridColourSchemeManager.ToggleAndPopulateGridColourMenuItems):
+ /// the top-level "Grid Colors" menu item subscribes to and
+ /// repopulates its children from the colour store each time the user hovers it.
+ ///
+ ///
+ ///
+ /// Before the WM_MENUSELECT fix the message was dropped in ,
+ /// so never fired and the submenu stayed on the original
+ /// placeholder items ("Select Color Scheme", "Create New Scheme", "Manage Color Schemes").
+ /// This test verifies the submenu is replaced with live entries after the message lands.
+ ///
+ ///
+ [StaFact]
+ public void GridColors_SelectHandler_PopulatesSubMenuItems_WhenWmMenuSelectReceived()
+ {
+ // Arrange: replicate the GridColourSchemeManager setup — parent item with three static
+ // placeholders, replaced on Select by a dynamically sourced set of colour schemes.
+ using Control control = new() { Visible = true };
+
+ MenuItem gridColorsItem = new("Grid Colors");
+ gridColorsItem.MenuItems.Add(new MenuItem("Select Color Scheme"));
+ gridColorsItem.MenuItems.Add(new MenuItem("Create New Scheme"));
+ gridColorsItem.MenuItems.Add(new MenuItem("Manage Color Schemes"));
+
+ gridColorsItem.Select += (_, _) =>
+ {
+ // Mirror ToggleAndPopulateGridColourMenuItems(): clear placeholders, add live entries.
+ gridColorsItem.MenuItems.Clear();
+ gridColorsItem.MenuItems.Add(new MenuItem("Standard*"));
+ gridColorsItem.MenuItems.Add(new MenuItem("Dark Theme"));
+ gridColorsItem.MenuItems.Add(new MenuItem("High Contrast"));
+ };
+
+ ContextMenu contextMenu = new(new[] { gridColorsItem });
+ control.ContextMenu = contextMenu;
+
+ // Force native handles so ProcessMenuSelect can match by HMENU.
+ IntPtr controlHandle = control.Handle;
+ IntPtr contextMenuHandle = contextMenu.Handle;
+
+ Assert.NotEqual(IntPtr.Zero, controlHandle);
+ Assert.NotEqual(IntPtr.Zero, contextMenuHandle);
+
+ // "Grid Colors" is at native index 0 in the context menu; it has children so Windows sets MF_POPUP.
+ IntPtr wParam = unchecked((IntPtr)(0 | (MF_POPUP << 16)));
+
+ // Act: simulate Windows delivering WM_MENUSELECT when the user hovers "Grid Colors".
+ SendMessage(controlHandle, WM_MENUSELECT, wParam, contextMenuHandle);
+
+ // Assert: the submenu must contain the live entries, not the original placeholders.
+ Assert.Equal(3, gridColorsItem.MenuItems.Count);
+ Assert.Equal("Standard*", gridColorsItem.MenuItems[0].Text);
+ Assert.Equal("Dark Theme", gridColorsItem.MenuItems[1].Text);
+ Assert.Equal("High Contrast", gridColorsItem.MenuItems[2].Text);
+ }
+
+ ///
+ /// Regression test for the hidden-items index mismatch: when context menu items have
+ /// Visible = false, the native menu skips them, so the native index differs from
+ /// the managed index. The original ProcessMenuSelect used
+ /// MenuItems[nativeIndex] which returned the wrong item. The fix uses native
+ /// Win32 APIs (GetMenuItemID, GetSubMenu) to navigate the actual menu structure.
+ ///
+ [StaFact]
+ public void GridColors_SelectHandler_WorksWithHiddenItems_WhenNativeIndexDiffersFromManaged()
+ {
+ using Control control = new() { Visible = true };
+
+ // Build a context menu with hidden items before Grid Colors to create index mismatch.
+ MenuItem viewItem = new("View");
+ MenuItem deactivateItem = new("Deactivate") { Visible = false };
+ MenuItem activateItem = new("Activate") { Visible = false };
+ MenuItem gridColorsItem = new("Grid Colors");
+ gridColorsItem.MenuItems.Add(new MenuItem("Placeholder"));
+
+ bool selectFired = false;
+ gridColorsItem.Select += (_, _) =>
+ {
+ selectFired = true;
+ gridColorsItem.MenuItems.Clear();
+ gridColorsItem.MenuItems.Add(new MenuItem("Scheme A"));
+ gridColorsItem.MenuItems.Add(new MenuItem("Scheme B"));
+ };
+
+ // Managed indices: View=0, Deactivate=1(hidden), Activate=2(hidden), GridColors=3
+ // Native indices: View=0, GridColors=1 (hidden items skipped)
+ ContextMenu contextMenu = new(new[] { viewItem, deactivateItem, activateItem, gridColorsItem });
+ control.ContextMenu = contextMenu;
+
+ IntPtr controlHandle = control.Handle;
+ IntPtr contextMenuHandle = contextMenu.Handle;
+
+ // Grid Colors is at NATIVE index 1 (after View), not managed index 3.
+ IntPtr wParam = unchecked((IntPtr)(1 | (MF_POPUP << 16)));
+
+ SendMessage(controlHandle, WM_MENUSELECT, wParam, contextMenuHandle);
+
+ Assert.True(selectFired, "Select event should fire on Grid Colors despite hidden items shifting the native index.");
+ Assert.Equal(2, gridColorsItem.MenuItems.Count);
+ Assert.Equal("Scheme A", gridColorsItem.MenuItems[0].Text);
+ Assert.Equal("Scheme B", gridColorsItem.MenuItems[1].Text);
+ }
+
+ ///
+ /// Universal Copy pattern (ZFilterGridModule.AddUniversalCopyMenuItems via
+ /// ): the "Universal Copy" submenu starts with a single
+ /// placeholder and switches to four real entries the first time it is opened.
+ ///
+ ///
+ ///
+ /// The screenshot captures exactly these four entries:
+ /// "New Copy Template", "Edit Copy Template", "New Copy Template From", "Copy Schedules".
+ /// This test asserts all four are present after WM_INITMENUPOPUP is delivered to the owning
+ /// control — proving the user-visible submenu content, not merely that Popup fired.
+ ///
+ ///
+ [StaFact]
+ public void UniversalCopy_PopupHandler_PopulatesExactSubMenuItems_WhenWmInitMenuPopupReceived()
+ {
+ // Arrange: replicate the Universal Copy entry — starts with a loading placeholder,
+ // real items added on first Popup (AddUniversalCopyMenuItems pattern from PR #53445).
+ using Control control = new() { Visible = true };
+
+ MenuItem universalCopyItem = new("Universal Copy");
+ universalCopyItem.MenuItems.Add(new MenuItem("")); // placeholder
+
+ bool alreadyPopulated = false;
+ universalCopyItem.Popup += (_, _) =>
+ {
+ if (alreadyPopulated) return;
+ alreadyPopulated = true;
+
+ // Mirror AddUniversalCopyMenuItems(): replace placeholder with real entries.
+ universalCopyItem.MenuItems.Clear();
+ universalCopyItem.MenuItems.Add(new MenuItem("New Copy Template"));
+ universalCopyItem.MenuItems.Add(new MenuItem("Edit Copy Template"));
+ universalCopyItem.MenuItems.Add(new MenuItem("New Copy Template From"));
+ universalCopyItem.MenuItems.Add(new MenuItem("Copy Schedules"));
+ };
+
+ ContextMenu contextMenu = new(new[] { universalCopyItem });
+ control.ContextMenu = contextMenu;
+
+ IntPtr controlHandle = control.Handle;
+ IntPtr universalCopyHandle = universalCopyItem.Handle;
+
+ Assert.NotEqual(IntPtr.Zero, controlHandle);
+ Assert.NotEqual(IntPtr.Zero, universalCopyHandle);
+
+ // Act: simulate Windows delivering WM_INITMENUPOPUP when Universal Copy is hovered.
+ // Before the WM_INITMENUPOPUP fix in Control.WndProc, ProcessInitMenuPopup was never
+ // reached, Popup never fired, and the "" placeholder was never replaced.
+ SendMessage(controlHandle, WM_INITMENUPOPUP, universalCopyHandle, IntPtr.Zero);
+
+ // Assert: exact submenu items visible in the screenshot.
+ Assert.Equal(4, universalCopyItem.MenuItems.Count);
+ Assert.Equal("New Copy Template", universalCopyItem.MenuItems[0].Text);
+ Assert.Equal("Edit Copy Template", universalCopyItem.MenuItems[1].Text);
+ Assert.Equal("New Copy Template From", universalCopyItem.MenuItems[2].Text);
+ Assert.Equal("Copy Schedules", universalCopyItem.MenuItems[3].Text);
+ }
+
+ ///
+ /// Regression guard: sending WM_MENUSELECT a second time must not duplicate entries.
+ /// The Grid Colors handler clears and rebuilds on every Select; verifies idempotent
+ /// population when the user repeatedly moves the mouse away and back.
+ ///
+ [StaFact]
+ public void GridColors_SelectHandler_ReplacesItemsOnRepeatHover_NoDuplicates()
+ {
+ using Control control = new() { Visible = true };
+
+ MenuItem gridColorsItem = new("Grid Colors");
+ gridColorsItem.MenuItems.Add(new MenuItem("Select Color Scheme"));
+
+ int populateCount = 0;
+ gridColorsItem.Select += (_, _) =>
+ {
+ populateCount++;
+ gridColorsItem.MenuItems.Clear();
+ gridColorsItem.MenuItems.Add(new MenuItem($"Scheme A (call {populateCount})"));
+ gridColorsItem.MenuItems.Add(new MenuItem($"Scheme B (call {populateCount})"));
+ };
+
+ ContextMenu contextMenu = new(new[] { gridColorsItem });
+ control.ContextMenu = contextMenu;
+
+ IntPtr controlHandle = control.Handle;
+ IntPtr contextMenuHandle = contextMenu.Handle;
+ IntPtr wParam = unchecked((IntPtr)(0 | (MF_POPUP << 16)));
+
+ // First hover
+ SendMessage(controlHandle, WM_MENUSELECT, wParam, contextMenuHandle);
+ Assert.Equal(2, gridColorsItem.MenuItems.Count);
+ Assert.Equal("Scheme A (call 1)", gridColorsItem.MenuItems[0].Text);
+
+ // Second hover — must replace, not append
+ SendMessage(controlHandle, WM_MENUSELECT, wParam, contextMenuHandle);
+ Assert.Equal(2, gridColorsItem.MenuItems.Count);
+ Assert.Equal("Scheme A (call 2)", gridColorsItem.MenuItems[0].Text);
+ }
+}
diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuSelectTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuSelectTests.cs
new file mode 100644
index 00000000000..ce5f6ef7acd
--- /dev/null
+++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuSelectTests.cs
@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+namespace System.Windows.Forms.Tests;
+
+public class MenuSelectTests
+{
+ private const uint WM_MENUSELECT = 0x011F;
+ private const int MF_POPUP = 0x0010;
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
+
+ [StaFact]
+ public void Control_WmMenuSelect_CommandItem_FiresSelectEvent()
+ {
+ using Control control = new() { Visible = true };
+
+ MenuItem menuItem = new("Command Item");
+ bool selectFired = false;
+ menuItem.Select += (_, _) => selectFired = true;
+
+ ContextMenu contextMenu = new(new[] { menuItem });
+ control.ContextMenu = contextMenu;
+
+ IntPtr controlHandle = control.Handle;
+ IntPtr contextMenuHandle = contextMenu.Handle;
+ int commandId = GetCommandId(menuItem);
+
+ SendMessage(controlHandle, WM_MENUSELECT, unchecked((IntPtr)commandId), contextMenuHandle);
+
+ Assert.True(selectFired);
+ }
+
+ [StaFact]
+ public void Form_WmMenuSelect_PopupMenuItem_FiresSelectEvent()
+ {
+ using Form form = new();
+
+ MainMenu mainMenu = new();
+ MenuItem fileMenuItem = new("File");
+ bool selectFired = false;
+ fileMenuItem.Select += (_, _) => selectFired = true;
+
+ mainMenu.MenuItems.Add(fileMenuItem);
+ form.Menu = mainMenu;
+
+ IntPtr formHandle = form.Handle;
+ IntPtr mainMenuHandle = mainMenu.Handle;
+ IntPtr wParam = unchecked((IntPtr)(MF_POPUP << 16));
+
+ SendMessage(formHandle, WM_MENUSELECT, wParam, mainMenuHandle);
+
+ Assert.True(selectFired);
+ }
+
+ private static int GetCommandId(MenuItem menuItem)
+ {
+ _ = menuItem.Handle;
+
+ FieldInfo dataField = typeof(MenuItem).GetField("_data", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ object data = dataField.GetValue(menuItem)!;
+
+ MethodInfo getMenuId = data.GetType().GetMethod("GetMenuID", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ return (int)getMenuId.Invoke(data, null)!;
+ }
+}