Architecture¶
How the new context menu fits into the existing tab and popup machinery.
Tab hierarchy¶
Main form
└── tcMain (PulseDevTools.TabControl.MyTabControl)
├── m_CoreTabPage (MyTabPage, default ctor — PageType = CORE_MODULE)
│ └── m_TabControl (inner MyTabControl, owned by CCoreModule)
│ ├── MyTabPage (Pulse reporting tab — PageType = PULSE_TAB_PAGE)
│ ├── MyTabPage
│ └── ...
├── m_CoreTabPage (next core module)
└── ...
A MyTabPage can be either a core module page (default constructor, no split container, no m_popContextMenu set) or a Pulse reporting tab (New(True) constructor, has split container, m_popContextMenu = s_PulseTab_ContextMenuForm.PopupMenu1). The PageType property distinguishes them at runtime by checking whether m_PrivateCoreModule is non-Nothing.
Right-click routing¶
When the user right-clicks anywhere on tcMain or its child MyTabControls, the path is:
MouseClick event on the MyTabControl
└── MyTabControl_MouseClick (Handles Me.MouseClick)
└── HandletabMouseClick(e)
└── for each tab whose AdjustedPageBounds contains e.Location:
└── MyTabPage.ShowContextMenu(PointToScreen(e.Location))
When the user right-clicks inside a Pulse tab body (not the header), the path is shorter:
MouseClick event on the MyTabPage or its split panels
└── MyTabPage_Click (Handles Me.MouseClick, m_SplitContainer.Panel1.MouseClick, ...)
└── ShowContextMenu(MousePosition)
This second path doesn't exist for core module pages because the inner m_TabControl is Dock = DockStyle.Fill and covers the entire body. Right-clicking anywhere on a core module page hits the inner MyTabControl, not the outer m_CoreTabPage — so the only way to reach m_CoreTabPage.ShowContextMenu is through the header via tcMain's HandletabMouseClick.
This is why the HandletabMouseClick early-exit bug was so disruptive for core module tabs but invisible for Pulse tabs.
ShowContextMenu dispatch¶
Before this change, ShowContextMenu always showed s_PulseTab_ContextMenuForm.PopupMenu1:
Public Sub ShowContextMenu(Position As System.Drawing.Point)
If m_popContextMenu Is Nothing Then DefinePopContextMenu()
If m_popContextMenu IsNot Nothing Then
s_PulseTab_ContextMenuForm.Activator = Me
m_popContextMenu.ShowPopup(Position)
End If
End Sub
For core module pages this was a problem because:
- The default
MyTabPage.New()constructor (used form_CoreTabPage) doesn't setm_popContextMenu. DefinePopContextMenupopulates the Pulse tab popup whose items don't apply at the module level.
The new dispatch branches on PageType:
Public Sub ShowContextMenu(Position As System.Drawing.Point)
If PageType = ePageType.CORE_MODULE Then
If s_CoreModule_ContextMenuForm Is Nothing Then
s_CoreModule_ContextMenuForm = New FCoreModule_Popup
End If
' Defensive: rebuild links if they've been cleared.
If s_CoreModule_ContextMenuForm.PopupMenu1.ItemLinks Is Nothing _
OrElse s_CoreModule_ContextMenuForm.PopupMenu1.ItemLinks.Count = 0 Then
s_CoreModule_ContextMenuForm.DefineContextMenu()
End If
s_CoreModule_ContextMenuForm.Activator = Me
s_CoreModule_ContextMenuForm.PopupMenu1.ShowPopup(Position)
Exit Sub
End If
' Pulse tab path — unchanged.
If m_popContextMenu Is Nothing Then DefinePopContextMenu()
If m_popContextMenu IsNot Nothing Then
s_PulseTab_ContextMenuForm.Activator = Me
m_popContextMenu.ShowPopup(Position)
End If
End Sub
Two design choices worth calling out:
- Lazy singleton.
s_CoreModule_ContextMenuFormis created on first right-click, not at app startup. This matches the Pulse tab popup pattern and keeps startup cheap. - Defensive rebuild. If
ItemLinks.Count = 0for any reason (state corruption, unforeseen side-effect of a settings reload),DefineContextMenu()is called to repopulate. This is belt-and-suspenders — under normal flow, the rebuild happens once on first creation.
Why DefineContextMenu and not LinksPersistInfo¶
The designer file's LinksPersistInfo.AddRange(...) is meant to wire the popup's items at construction time. In practice, DevExpress's LinksPersistInfo mechanism is sensitive to ordering — the items it references must already be associated with the BarManager when the popup is first asked to render. The auto-generated InitializeComponent in this project sets LinksPersistInfo before BarManager1.Items.AddRange(...), so the popup can render empty on first show.
The proven-to-work pattern is the one used by MyTabPage.DefinePopContextMenu for the Pulse popup: explicitly call ClearLinks() then AddItem(...) for each item, after the form is fully constructed. FCoreModule_Popup.DefineContextMenu() does the same:
Public Sub DefineContextMenu()
PopupMenu1.Manager = BarManager1
PopupMenu1.ShowCaption = True
PopupMenu1.ClearLinks()
PopupMenu1.AddItem(btnRefresh)
PopupMenu1.AddItem(btnResetToDefault).BeginGroup = True
PopupMenu1.AddItem(btnSaveAndShare)
PopupMenu1.AddItem(btnPublish)
PopupMenu1.AddItem(btnLoadFromFile)
End Sub
The BeginGroup = True on btnResetToDefault produces the visual separator between Refresh Module and the four state-mutating items.
Singleton lifecycle¶
sequenceDiagram
participant User
participant tcMain as tcMain (MyTabControl)
participant Page as m_CoreTabPage (MyTabPage)
participant Form as s_CoreModule_ContextMenuForm
participant Popup as PopupMenu1
User->>tcMain: Right-click header
tcMain->>tcMain: HandletabMouseClick(e)
tcMain->>Page: ShowContextMenu(screenPoint)
Page->>Page: PageType? CORE_MODULE
alt First time
Page->>Form: New FCoreModule_Popup
Page->>Form: DefineContextMenu()
Form->>Popup: Manager = BarManager1
Form->>Popup: ClearLinks() + AddItem ×5
else Already exists
Page->>Popup: ItemLinks.Count = 0?
opt Yes (defensive)
Page->>Form: DefineContextMenu()
end
end
Page->>Form: Activator = Me
Page->>Popup: ShowPopup(position)
Popup->>User: Menu rendered
User->>Popup: Click an item
Popup->>Form: ItemClick handler runs
The form lives for the lifetime of the application and is reused across all core module pages.