Skip to content

HandletabMouseClick Early-Exit

Symptom

After switching users via the "Log in as User" dropdown on the main form, right-clicking a core module tab header produced no popup at all. Right-clicking a Pulse reporting tab still worked. Restarting the application restored the right-click on core module tabs — until the next user switch.

Root cause

MyTabControl.HandletabMouseClick had a defensive guard at the top:

MyTabControl.vb — original guard (removed)
Public Sub HandletabMouseClick(ByVal e As System.Windows.Forms.MouseEventArgs)
    If TabPages.Count = 0 Then Exit Sub

    Dim pr As PropertyInfo = GetType(MyTabPage).GetProperty(
        "PageViewInfo",
        BindingFlags.NonPublic Or BindingFlags.Instance)
    Dim pageViewInfo As BaseTabPageViewInfo = TryCast(
        pr.GetValue(TabPages(TabPages.Count - 1), Nothing),
        BaseTabPageViewInfo)
    If pageViewInfo Is Nothing Then
        Exit Sub                       ' <-- (1)
    End If
    Dim nLine As Integer = pageViewInfo.Bounds.Location.Y   ' <-- (2)

    For i As Integer = 0 To TabPages.Count - 1
        pageViewInfo = TryCast(pr.GetValue(TabPages(i), Nothing), BaseTabPageViewInfo)
        If pageViewInfo Is Nothing Then
            Continue For               ' <-- (3)
        End If
        ' ... per-page hit-test and ShowContextMenu call ...
    Next
End Sub
  1. Bails out of the entire handler if the last tab's PageViewInfo is Nothing.
  2. The value pulled from the last tab (nLine) is never used afterward — the early access exists only to feed the guard.
  3. The inner loop already has its own per-page pageViewInfo Is Nothing check that safely skips problematic pages.

So the early guard was both:

  • Misfiring — bailing on every tab when only one tab (the last) had a layout issue.
  • Unnecessary — the inner per-page check covers the same case more precisely.

Why a user switch triggered it

The user-switch path runs Main.LoadFromFiles() (via CPulseConnection.Init). Inside LoadFromFiles:

Main.vb — LoadFromFiles excerpt
SuspendDrawing(tcMain)                          ' (1)

For Each curCoreModule As CCoreModule In m_CoreModules
    curCoreModule.Settings.LoadFromVirtualFile()  ' (2)
Next
' ...
ResumeDrawing(tcMain)                           ' (3)
  1. SuspendDrawing calls WM_SETREDRAW on tcMain to silence layout updates.
  2. Each module's LoadFromVirtualFile fires DataLoad, and CCoreModule.Settings_DataLoadEnd disposes/recreates the inner MyTabPage instances (the Pulse reporting tabs). The outer core module pages in tcMain are not touched.
  3. ResumeDrawing re-enables redraw.

Even though the outer pages weren't touched, the redraw cycle didn't always rebuild DevExpress's internal PageViewInfo for the non-currently-visible outer pages. Whichever core module happened to be last in tcMain.TabPages would have PageViewInfo = Nothing after ResumeDrawing until something forced a layout pass — and the early guard then suppressed every right-click on any tab.

Why Pulse tabs kept working

Pulse reporting tabs receive right-clicks two ways:

  1. Header right-click through their inner MyTabControl.HandletabMouseClick — same broken guard, but the inner tab control's last page typically was laid out because the user was actively viewing the inner tabs.
  2. Body right-click through MyTabPage_Click (which handles Me.MouseClick and the split panel's MouseClick). This path goes straight to ShowContextMenu and never touches HandletabMouseClick.

Core module pages have no body to right-click — the inner m_TabControl covers everything with Dock = DockStyle.Fill. The header is the only surface, so the guard always blocked them.

The fix

Removed the early-exit. The inner per-page Nothing check is sufficient and more precise.

MyTabControl.vb — patched
Public Sub HandletabMouseClick(ByVal e As System.Windows.Forms.MouseEventArgs)
    ' JLN 5/15/2024

    If TabPages.Count = 0 Then Exit Sub

    Dim pr As PropertyInfo = GetType(MyTabPage).GetProperty(
        "PageViewInfo",
        BindingFlags.NonPublic Or BindingFlags.Instance)
    ' Note: previously bailed out here if the LAST tab's PageViewInfo
    ' was Nothing, but that incorrectly suppressed right-click handling
    ' on every tab whenever a single tab hadn't been laid out yet
    ' (e.g. immediately after a user switch and a redraw cycle).
    ' The per-page Nothing check inside the loop below is sufficient.
    Dim pageViewInfo As BaseTabPageViewInfo

    For i As Integer = 0 To TabPages.Count - 1
        pageViewInfo = TryCast(pr.GetValue(TabPages(i), Nothing), BaseTabPageViewInfo)
        If pageViewInfo Is Nothing Then
            Continue For
        End If
        ' ... rest unchanged ...
    Next
End Sub

Two things changed:

  • The early-exit block is gone.
  • pageViewInfo is now declared without an initial value. Its first assignment is inside the loop.

The per-page guard (Continue For) handles the same edge case — if a particular tab hasn't been laid out, we skip that tab's hit-test rather than bailing out of every tab's hit-test.

Defensive companion change

Independently, MyTabPage.ShowContextMenu was made defensive in case the singleton popup's links get cleared by some unforeseen path:

MyTabPage.vb — ShowContextMenu (CORE_MODULE branch)
If s_CoreModule_ContextMenuForm.PopupMenu1.ItemLinks Is Nothing _
   OrElse s_CoreModule_ContextMenuForm.PopupMenu1.ItemLinks.Count = 0 Then
    s_CoreModule_ContextMenuForm.DefineContextMenu()
End If

Under normal flow this only runs once (on first creation). The Or clause is belt-and-suspenders: if anything in the future ever clears ItemLinks, the next right-click rebuilds them automatically instead of showing an empty popup.

Test plan

  1. Launch app → right-click any core module header → menu appears. ✅
  2. Click an item to confirm the action runs.
  3. Use the "Log in as User" dropdown to switch to another user. Wait for the load to complete.
  4. Right-click any core module header → menu appears. ✅ (this was the regression)
  5. Switch back to the original user → right-click → menu still appears. ✅
  6. Right-click a Pulse reporting tab header → original Pulse tab popup appears (not regressed). ✅

Risk assessment

  • Blast radius: HandletabMouseClick is the entry point for all tab right-clicks in the application, both tcMain and every inner MyTabControl. Any logic change here affects every tab popup.
  • Reversibility: Trivial — restore the original four lines.
  • Likelihood of new edge cases: Low. The inner loop's per-page Nothing check has been there all along, so any tab with PageViewInfo = Nothing already gracefully skips. The early-exit was never the only safety net — it was a redundant first one.