Skip to content

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:

MyTabPage.vb — ShowContextMenu (before)
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 for m_CoreTabPage) doesn't set m_popContextMenu.
  • DefinePopContextMenu populates the Pulse tab popup whose items don't apply at the module level.

The new dispatch branches on PageType:

MyTabPage.vb — ShowContextMenu (after)
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:

  1. Lazy singleton. s_CoreModule_ContextMenuForm is created on first right-click, not at app startup. This matches the Pulse tab popup pattern and keeps startup cheap.
  2. Defensive rebuild. If ItemLinks.Count = 0 for 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:

FCoreModule_Popup.vb — DefineContextMenu
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.