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:
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
- Bails out of the entire handler if the last tab's
PageViewInfoisNothing. - The value pulled from the last tab (
nLine) is never used afterward — the early access exists only to feed the guard. - The inner loop already has its own per-page
pageViewInfo Is Nothingcheck 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:
SuspendDrawing(tcMain) ' (1)
For Each curCoreModule As CCoreModule In m_CoreModules
curCoreModule.Settings.LoadFromVirtualFile() ' (2)
Next
' ...
ResumeDrawing(tcMain) ' (3)
SuspendDrawingcallsWM_SETREDRAWontcMainto silence layout updates.- Each module's
LoadFromVirtualFilefiresDataLoad, andCCoreModule.Settings_DataLoadEnddisposes/recreates the innerMyTabPageinstances (the Pulse reporting tabs). The outer core module pages intcMainare not touched. ResumeDrawingre-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:
- 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. - Body right-click through
MyTabPage_Click(which handlesMe.MouseClickand the split panel'sMouseClick). This path goes straight toShowContextMenuand never touchesHandletabMouseClick.
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.
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.
pageViewInfois 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:
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¶
- Launch app → right-click any core module header → menu appears. ✅
- Click an item to confirm the action runs.
- Use the "Log in as User" dropdown to switch to another user. Wait for the load to complete.
- Right-click any core module header → menu appears. ✅ (this was the regression)
- Switch back to the original user → right-click → menu still appears. ✅
- Right-click a Pulse reporting tab header → original Pulse tab popup appears (not regressed). ✅
Risk assessment¶
- Blast radius:
HandletabMouseClickis the entry point for all tab right-clicks in the application, bothtcMainand every innerMyTabControl. 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
Nothingcheck has been there all along, so any tab withPageViewInfo = Nothingalready gracefully skips. The early-exit was never the only safety net — it was a redundant first one.