Skip to content

Fix Overview crosshair disappearing after tab switches / layout passes#864

Merged
erikdarlingdata merged 1 commit intodevfrom
fix/overview-crosshair-stuck
Apr 19, 2026
Merged

Fix Overview crosshair disappearing after tab switches / layout passes#864
erikdarlingdata merged 1 commit intodevfrom
fix/overview-crosshair-stuck

Conversation

@erikdarlingdata
Copy link
Copy Markdown
Owner

Summary

Crosshair on the Overview tab's correlated timeline lanes was working on first open but disappearing after some combination of auto-refreshes, tab switches, or layout events. Diagnosed via logging: the manager's lane list was going from 5 to 0 unexpectedly.

Root cause: the control had Unloaded += ...Dispose() on the crosshair manager. WPF fires Unloaded for transient reasons (tab virtualization, layout rebuilds) — not just when the control is truly going away. Dispose() clears the lane list, after which ReattachVLines runs over an empty list and the crosshair is permanently gone.

Changes

  • Remove the Unloaded → Dispose() handler in both Lite and Dashboard copies. The manager holds only managed state (a Popup + lane references) — GC will clean it up with the control.
  • Remove the redundant _isRefreshing flag from CorrelatedCrosshairManager. The lane.VLine == null check in OnMouseMove is a sufficient "not ready" guard and is self-healing once VLines are recreated.
  • Wrap ReattachVLines in a try/finally on the control side, with a new idempotent EnsureVLinesAttached() safety net that only creates VLines for lanes where they're still null.
  • Per-lane exception handling in CreateVLine so one failing chart can't prevent the others from recovering.

Test plan

  • Builds clean (Lite + Dashboard)
  • Crosshair survives multiple auto-refresh cycles on Lite (verified with logging before commit)
  • Crosshair survives tab switches (Overview → Queries → back to Overview)
  • Crosshair works on manual time range changes

🤖 Generated with Claude Code

Root cause: the control wired `Unloaded += ...Dispose()` on the crosshair
manager, and WPF fires Unloaded for transient reasons (tab virtualization,
layout rebuilds, etc.), not just when the control is actually going away.
Dispose() clears the manager's lane list, after which ReattachVLines runs
over an empty list and the crosshair is gone permanently.

Changes:
- Remove the Unloaded → Dispose() handler in both Lite and Dashboard copies.
  The manager holds only managed state (a Popup + lane references) — GC
  will clean it up with the control.
- Remove the now-redundant `_isRefreshing` flag from CorrelatedCrosshairManager.
  The `lane.VLine == null` check in OnMouseMove is a sufficient "not ready"
  guard and is self-healing once VLines are recreated.
- Wrap ReattachVLines in a try/finally on the control side, with a new
  idempotent EnsureVLinesAttached() safety net that only creates VLines
  for lanes where they're still null.
- Make CreateVLine catch per-lane exceptions so one failing chart can't
  prevent the others from recovering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@erikdarlingdata erikdarlingdata merged commit d319039 into dev Apr 19, 2026
3 checks passed
Copy link
Copy Markdown
Owner Author

@erikdarlingdata erikdarlingdata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review comments inline. Summary to follow as an issue comment.


Generated by Claude Code

{
/* If attach fails, return null so OnMouseMove skips this lane.
Next refresh will try again. */
return null;
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent catch-all. Everywhere else in CorrelatedTimelineLanesControl.RefreshAsync a Debug.WriteLine(...) accompanies catch (Exception ex) (e.g. line 116, 161, 198). If Plot.Add.VerticalLine ever starts throwing, the only signal will be a silently-missing crosshair on one lane — exactly the class of bug this PR exists to diagnose. At minimum catch (Exception ex) { Debug.WriteLine($"CorrelatedCrosshairManager: VLine attach failed: {ex.Message}"); return null; }.


Generated by Claude Code

{
/* If attach fails, return null so OnMouseMove skips this lane.
Next refresh will try again. */
return null;
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same silent-catch issue as the Dashboard copy. Kept in sync, but fix belongs in both — log the exception instead of swallowing it. A VLine attach failure here is invisible except as a missing crosshair.


Generated by Claude Code

Comment on lines 77 to +240
@@ -225,8 +233,18 @@ public async Task RefreshAsync(int hoursBack, DateTime? fromDate, DateTime? toDa
_crosshairManager?.SetComparisonLabel(ComparisonLabel(comparisonRange.Value, fromDate, hoursBack));
}

/* VLines must be re-attached before SyncXAxes so they're part of
the render set when the chart refreshes. */
_crosshairManager?.ReattachVLines();
SyncXAxes(hoursBack, fromDate, toDate);
}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The body of this try (lines 80-239) is not re-indented and there's a stray blank line at 79. Compiles fine, but the raw file now has ~160 lines sitting at outer-method indent inside a try block, which will confuse anyone reading the file without a diff view. I get that leaving it un-indented minimizes this diff — worth a follow-up tidy-up commit to re-indent the block so future readers don't do a double-take.


Generated by Claude Code

/// block — if chart state is invalid, a failure on one lane won't prevent
/// the others from recovering.
/// </summary>
public void ReattachVLines()
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up: after removing the Unloaded → Dispose() handler, nothing in the codebase calls CorrelatedCrosshairManager.Dispose() anymore (the class still implements IDisposable and keeps a Dispose() at line 371). That's now dead code. Two options for a follow-up: (a) wire Dispose() to a real teardown point such as ServerTab close so _lanes.Clear() actually runs when the tab goes away, or (b) drop IDisposable + Dispose() entirely and let GC handle it (matches the comment rationale on the constructor). Leaving IDisposable declared with no caller sets a false expectation for the next reader. Mirror applies in Lite/Helpers/CorrelatedCrosshairManager.cs.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Review summary

Note: PR was already merged by the time this review ran — comments are for follow-up consideration, not blocking.

What this PR does

Fixes Overview-tab crosshair disappearing after tab switches / layout passes. Root cause: Unloaded += _crosshairManager.Dispose() fires on transient layout events (tab virtualization), clears the lane list, and subsequent ReattachVLines iterates an empty list. Fix removes the Unloaded handler, drops the now-redundant _isRefreshing flag (the lane.VLine == null guard in OnMouseMove is sufficient), wraps refresh in try/finally with an idempotent EnsureVLinesAttached() recovery path, and isolates per-lane VLine creation with try/catch.

What's good

  • Fix applied to both Dashboard/Helpers/CorrelatedCrosshairManager.cs and Lite/Helpers/CorrelatedCrosshairManager.cs — copies stay in sync.
  • Base branch is dev. ✓
  • Diagnosis in the PR body is solid and the EnsureVLinesAttached() idempotency reasoning is correct.
  • No schema / install-script / workflow changes, so upgrade-path and SignPath concerns don't apply.

What needs attention

  1. Silent catch in CreateVLine (Dashboard/Helpers/CorrelatedCrosshairManager.cs:202-207, mirror in Lite). Everywhere else in the control's refresh flow, catch is paired with Debug.WriteLine. Swallowing here hides exactly the class of failure this PR exists to diagnose. Log it.
  2. Dead Dispose() (CorrelatedCrosshairManager.cs:371 in both copies). After removing the Unloaded handler, nothing calls Dispose() anymore, but the class still implements IDisposable. Either wire it to a real teardown point (ServerTab close) or drop IDisposable. Leaving it declared misleads future readers.
  3. Un-reindented try body (Dashboard/Controls/CorrelatedTimelineLanesControl.xaml.cs:77-240). The try wraps ~160 lines but the body stays at the outer indent. Compiles fine — diff-minimizing but ugly in the raw file. Lite's copy was already structured with try/finally so it didn't have this problem. Worth a cosmetic follow-up.
  4. Test plan — the crosshair-survives-tab-switches item (the exact scenario this PR claims to fix) is unchecked. Manual validation still outstanding per the PR description.
  5. Lite-first: this fix landed in Lite + Dashboard simultaneously rather than Lite-first-then-port. Defensible for a parity bugfix where both copies had the same bug, but noting it against the stated ordering.

No tests were added — UI-timing behavior in WPF/Avalonia is hard to cover with the existing harness; calling it out rather than flagging as blocking.


Generated by Claude Code

@erikdarlingdata erikdarlingdata deleted the fix/overview-crosshair-stuck branch April 19, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant