13 KiB
CWP Pattern Analysis - Executive Summary & Recommendations
Quick Facts
- Total CWP Implementations: 33+ event pairs
- Virtual Method Overrides: 100+ across codebase
- Current Order: Virtual method → Event (inheritance priority)
- Proposed Order: Event → Virtual method (composition priority)
- Breaking Change Magnitude: VERY HIGH
- Estimated Effort: 4-6 weeks for complete refactor
- Risk Level: HIGH
The Core Issue (#3714)
External code cannot prevent views like Slider from handling mouse events because:
// What users want but CANNOT do today:
slider.MouseEvent += (s, e) =>
{
if (someCondition)
e.Handled = true; // TOO LATE! Slider.OnMouseEvent already handled it
};
The virtual method (OnMouseEvent) runs first and can mark the event as handled, preventing the event from firing.
Four Solution Options
Option 1: Reverse Order Globally ⚠️ HIGH RISK
Change: Event first → Virtual method second (for all 33+ CWP patterns)
Pros:
- Solves #3714 completely
- Consistent pattern everywhere
- Enables external control (composition over inheritance)
- Single comprehensive solution
Cons:
- MAJOR breaking change
- 100+ view overrides need review/update
- Changes fundamental framework philosophy
- Breaks existing user code expectations
- Requires extensive testing
- Updates to helper classes, documentation, tests
Effort: 4-6 weeks Risk: VERY HIGH Recommendation: ❌ NOT RECOMMENDED unless major version change
Option 2: Add "Before" Events 🎯 RECOMMENDED
Change: Add new pre-phase events (e.g., BeforeMouseEvent, BeforeKeyDown)
// New pattern for critical events
protected bool RaiseMouseEvent(MouseEventArgs args)
{
// New: BeforeMouseEvent (external code gets first chance)
BeforeMouseEvent?.Invoke(this, args);
if (args.Handled)
return true;
// Existing: OnMouseEvent (virtual method)
if (OnMouseEvent(args) || args.Handled)
return true;
// Existing: MouseEvent (external code gets second chance)
MouseEvent?.Invoke(this, args);
return args.Handled;
}
Pros:
- Non-breaking (additive change only)
- Solves #3714 for mouse events
- Can apply selectively to problematic events
- Gradual migration path
- Clear intent (Before/During/After phases)
Cons:
- More API surface
- Two patterns coexist (inconsistency)
- More complex for new developers
- Partial solution only
Implementation Plan:
- Phase 1 (1 week): Add
BeforeMouseEvent,BeforeMouseClick,BeforeMouseWheel - Phase 2 (1 week): Add
BeforeKeyDownif needed - Phase 3 (ongoing): Add others as needed, deprecate old pattern gradually
Effort: 2-3 weeks initial, ongoing refinement Risk: LOW Recommendation: ✅ RECOMMENDED - Best balance of solving issue vs. risk
Option 3: Add MouseInputEnabled Property 🔧 QUICK FIX
Change: Add property to View to disable mouse handling
public class View
{
public bool MouseInputEnabled { get; set; } = true;
public bool? NewMouseEvent(MouseEventArgs mouseEvent)
{
if (!MouseInputEnabled)
return false; // Don't process
// ... existing code
}
}
// Usage
slider.MouseInputEnabled = false; // Disable slider mouse handling
Pros:
- Minimal change
- Solves immediate #3714 issue
- No breaking changes
- Easy to implement
- Easy to understand
Cons:
- Band-aid solution (doesn't address root cause)
- Mouse-specific (doesn't help keyboard, etc.)
- All-or-nothing (can't selectively disable)
- Adds more properties to View
Effort: 1 week Risk: VERY LOW Recommendation: ⚠️ Acceptable as short-term fix, not long-term solution
Option 4: Reverse Order for Mouse Only 🎯 TARGETED FIX
Change: Event first → Virtual method second, but ONLY for mouse events
Pros:
- Solves #3714 directly
- Limited scope reduces risk
- Mouse is the primary concern
- Clearer than "Before" events
Cons:
- Inconsistent pattern (mouse different from keyboard)
- Still breaking for mouse event overrides
- Confusing to have different orders
- Doesn't help future similar issues
Effort: 2 weeks Risk: MEDIUM Recommendation: ⚠️ Better than Option 1, but Option 2 is cleaner
Detailed Recommendation: Option 2 (Before Events)
Implementation Specification
For Mouse Events (High Priority)
public partial class View // Mouse APIs
{
/// <summary>
/// Event raised BEFORE OnMouseEvent is called, allowing external code
/// to cancel mouse event processing before the view handles it.
/// </summary>
public event EventHandler<MouseEventArgs>? BeforeMouseEvent;
/// <summary>
/// Event raised BEFORE OnMouseClick is called, allowing external code
/// to cancel mouse click processing before the view handles it.
/// </summary>
public event EventHandler<MouseEventArgs>? BeforeMouseClick;
public bool RaiseMouseEvent (MouseEventArgs mouseEvent)
{
// Phase 1: Before event (external pre-processing)
BeforeMouseEvent?.Invoke(this, mouseEvent);
if (mouseEvent.Handled)
{
return true;
}
// Phase 2: Virtual method (view processing)
if (OnMouseEvent (mouseEvent) || mouseEvent.Handled)
{
return true;
}
// Phase 3: After event (external post-processing)
MouseEvent?.Invoke (this, mouseEvent);
return mouseEvent.Handled;
}
protected bool RaiseMouseClickEvent (MouseEventArgs args)
{
if (!Enabled)
{
return args.Handled = false;
}
// Phase 1: Before event
BeforeMouseClick?.Invoke(this, args);
if (args.Handled)
{
return args.Handled;
}
// Phase 2: Virtual method
if (OnMouseClick (args) || args.Handled)
{
return args.Handled;
}
// Phase 3: After event
MouseClick?.Invoke (this, args);
if (args.Handled)
{
return true;
}
// Post-conditions
args.Handled = InvokeCommandsBoundToMouse (args) == true;
return args.Handled;
}
}
Usage Example (Solves #3714)
var slider = new Slider();
// NOW THIS WORKS - external code can prevent slider from handling mouse
slider.BeforeMouseEvent += (sender, args) =>
{
if (someCondition)
{
args.Handled = true; // Prevents Slider.OnMouseEvent from being called
}
};
// Or use lambda with specific logic
slider.BeforeMouseEvent += (sender, args) =>
{
if (args.Flags.HasFlag(MouseFlags.Button1Clicked) && IsModalDialogOpen())
{
args.Handled = true; // Block slider interaction when dialog is open
}
};
For Keyboard Events (Medium Priority)
public partial class View // Keyboard APIs
{
/// <summary>
/// Event raised BEFORE OnKeyDown is called, allowing external code
/// to cancel key event processing before the view handles it.
/// </summary>
public event EventHandler<Key>? BeforeKeyDown;
public bool NewKeyDownEvent (Key k)
{
// Phase 1: Before event
BeforeKeyDown?.Invoke(this, k);
if (k.Handled)
{
return true;
}
// Phase 2: Virtual method
if (OnKeyDown (k) || k.Handled)
{
return true;
}
// Phase 3: After event (existing KeyDown)
KeyDown?.Invoke (this, k);
return k.Handled;
}
}
For Other Events (Low Priority - As Needed)
Add Before* events for other patterns only if/when users request them.
Migration Path
v2.x (Current)
- Keep existing pattern
- Add
Before*events - Document both patterns
- Mark as "preferred" for external control
v3.0 (Future)
- Consider deprecating old pattern
- Maybe reverse order globally
- Or standardize on Before/After pattern
Documentation Updates
Add to docfx/docs/events.md:
#### Three-Phase CWP Pattern
For critical events like mouse and keyboard input, a three-phase pattern is available:
```csharp
public class MyView : View
{
public bool ProcessMouseEvent(MouseEventArgs args)
{
// Phase 1: BeforeMouseEvent - External pre-processing
BeforeMouseEvent?.Invoke(this, args);
if (args.Handled) return true;
// Phase 2: OnMouseEvent - View processing (virtual method)
if (OnMouseEvent(args) || args.Handled) return true;
// Phase 3: MouseEvent - External post-processing
MouseEvent?.Invoke(this, args);
return args.Handled;
}
}
```
**When to use each phase**:
- **BeforeXxx**: Use when you need to prevent the view from processing the event
- Example: Disabling a slider when a modal dialog is open
- Example: Implementing global keyboard shortcuts
- **OnXxx**: Override in derived classes to customize view behavior
- Example: Custom mouse handling in a custom view
- Example: Custom key handling in a specialized control
- **Xxx**: Use for external observation and additional processing
- Example: Logging mouse activity
- Example: Implementing additional behavior without inheritance
Testing Strategy
- Unit Tests: Add tests for new Before* events
- Integration Tests: Test interaction between phases
- Scenario Tests: Test #3714 scenario specifically
- Regression Tests: Ensure existing code still works
- Documentation Tests: Verify examples work
Benefits of This Approach
- Solves #3714: Users can now prevent Slider from handling mouse
- Non-Breaking: All existing code continues to work
- Clear Intent: "Before" explicitly means "external pre-processing"
- Selective Application: Add to problematic events, not all 33
- Future-Proof: Creates migration path to v3.0
- Minimal Risk: Additive only, no changes to existing behavior
- Gradual Adoption: Users can adopt at their own pace
Risks & Mitigations
Risk: Three phases more complex than two Mitigation: Good documentation, clear examples, gradual rollout
Risk: People still use wrong phase Mitigation: Intellisense XML docs explain when to use each
Risk: Two patterns coexist Mitigation: Document clearly, provide migration examples
Alternative Considerations
Could We Just Fix Slider?
No - this is a systemic issue. Any view that overrides OnMouseEvent has the same problem. Fixing just Slider doesn't help:
- ListView
- TextView
- TableView
- TreeView
- ScrollBar
- And 20+ others
Could We Change Slider To Not Override OnMouseEvent?
Yes, but:
- Still doesn't solve systemic issue
- Other views have same problem
- Not a general solution
- Slider's implementation is reasonable
Could We Add Multiple Overload Virtual Methods?
E.g., OnMouseEventBefore and OnMouseEventAfter?
No - virtual methods can't solve this because:
- External code can't override virtual methods
- Virtual methods are for inheritance, not composition
- Issue is specifically about external code priority
Implementation Checklist (Option 2)
-
Week 1: Mouse Events
- Add
BeforeMouseEventto View - Add
BeforeMouseClickto View - Add
BeforeMouseWheelto View - Update
RaiseMouseEventto invoke BeforeMouseEvent - Update
RaiseMouseClickEventto invoke BeforeMouseClick - Update
RaiseMouseWheelEventto invoke BeforeMouseWheel - Add unit tests for new events
- Test Slider scenario specifically
- Add
-
Week 2: Documentation & Examples
- Update
events.mdwith three-phase pattern - Update
cancellable-work-pattern.mdwith new pattern - Add examples showing how to use BeforeMouseEvent
- Add example solving #3714 specifically
- Update API docs (XML comments)
- Update
-
Week 3: Keyboard Events (If Needed)
- Assess if keyboard has similar issues
- Add
BeforeKeyDownif needed - Add unit tests
- Update documentation
-
Week 4: Other Events (As Needed)
- Assess other event patterns
- Add Before* events as needed
- Update documentation
-
Week 5-6: Testing & Refinement
- Integration testing
- Scenario testing
- User testing
- Bug fixes
- Documentation refinement
Conclusion
Recommended Solution: Option 2 - Add Before Events
This solution: ✅ Solves issue #3714 ✅ Non-breaking change ✅ Clear and understandable ✅ Minimal risk ✅ Provides migration path ✅ Can be applied selectively
Estimated effort: 2-3 weeks Risk level: LOW
The key insight is that we don't need to change the existing pattern - we can ADD to it. This preserves all existing behavior while enabling the new use cases that #3714 requires.
References
- Issue #3714: Mouse event interception problem
- Issue #3417: Related mouse event handling issue
docfx/docs/events.md: Current CWP documentationdocfx/docs/cancellable-work-pattern.md: CWP philosophyTerminal.Gui/ViewBase/View.Mouse.cs: Mouse event implementationTerminal.Gui/ViewBase/View.Keyboard.cs: Keyboard event implementationTerminal.Gui/App/CWP/CWPWorkflowHelper.cs: CWP helper class