Terminal.Gui Events Deep Dive
This document provides practical guidance for implementing events in Terminal.Gui using the Cancellable Work Pattern (CWP).
Tip
New to CWP? Read the Cancellable Work Pattern conceptual overview first.
Quick Start: Which Pattern Do I Need?
Use this decision tree to choose the right pattern:
┌─────────────────────────────────────────────────────────────┐
│ Which Event Pattern Should I Use? │
├─────────────────────────────────────────────────────────────┤
│ │
│ Need to notify about something? │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Can it be │──► NO ──► Simple EventHandler │
│ │ cancelled? │ (no CWP needed) │
│ └────────┬────────┘ │
│ │ YES │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Property or │──► PROPERTY ──► CWPPropertyHelper │
│ │ Action/Workflow?│ (Recipe 1) │
│ └────────┬────────┘ │
│ │ ACTION │
│ ▼ │
│ Manual CWP or CWPWorkflowHelper │
│ (Recipe 2) │
│ │
└─────────────────────────────────────────────────────────────┘
| Scenario | Pattern | Jump to |
|---|---|---|
| Property change (cancellable) | CWPPropertyHelper |
Recipe 1 |
| Action/workflow (cancellable) | Manual CWP or CWPWorkflowHelper |
Recipe 2 |
| Simple notification (no cancel) | EventHandler |
Recipe 3 |
| Property notification (MVVM) | INotifyPropertyChanged |
Recipe 4 |
See Also
- Cancellable Work Pattern - Conceptual overview
- Command Deep Dive - Command system details
- Lexicon & Taxonomy
Lexicon and Taxonomy
| Term | Meaning |
|---|---|
| Action | A delegate type that represents a method that can be called with specific parameters but returns no value. Used for simple callbacks in Terminal.Gui. |
| Cancel/Cancelling/Cancelled | Applies to scenarios where something can be cancelled. Changing the Orientation of a Slider is cancelable. |
| Cancellation | Mechanisms to halt a phase or workflow in the Cancellable Work Pattern, such as setting Cancel/Handled properties in event arguments or returning bool from virtual methods. |
| Command | A pattern that encapsulates a request as an object, allowing for parameterization and queuing of requests. See Command Deep Dive. |
| Context | Data passed to observers for informed decision-making in the Cancellable Work Pattern, such as DrawContext (drawing), Key (keyboard), ICommandContext (commands), or CancelEventArgs<Orientation> (orientation). |
| Default Behavior | A standard implementation for each phase in the Cancellable Work Pattern, such as DrawText (drawing), InvokeCommands (keyboard and application-level), RaiseActivating (commands), or updating a property (OrientationHelper). |
| Event | A notification mechanism that allows objects to communicate when something of interest occurs. Terminal.Gui uses events extensively for UI interactions. |
| Handle/Handling/Handled | Applies to scenarios where an event can either be handled by an event listener (or override) vs not handled. Events that originate from a user action like mouse moves and key presses are examples. |
| Invoke | The act of calling or triggering an event, action, or method. |
| Listen | The act of subscribing to or registering for an event to receive notifications when it occurs. |
| Notifications | Events (e.g., DrawingText, KeyDown, Activating, OrientationChanging) and virtual methods (e.g., OnDrawingText, OnKeyDown, OnActivating, OnOrientationChanging) raised at each phase to notify observers in the Cancellable Work Pattern. |
| Raise | The act of triggering an event, notifying all registered event handlers that the event has occurred. |
| Workflow | A sequence of phases in the Cancellable Work Pattern, which may be multi-phase (e.g., rendering in View.Draw), linear (e.g., key processing in View.Keyboard), per-unit (e.g., command execution in View.Command), or event-driven (e.g., key handling in Application.Keyboard, property changes in OrientationHelper). |
Recipes: Implementing CWP in Terminal.Gui
Recipe 1: Cancellable Property Change
Use when: A property change can be vetoed/cancelled by subclasses or subscribers.
Step 1: Define the Events and Virtual Methods
public class MyDataView : View
{
private object? _dataSource;
// Pre-change event (cancellable)
public event EventHandler<ValueChangingEventArgs<object?>>? DataSourceChanging;
// Post-change event (notification)
public event EventHandler<ValueChangedEventArgs<object?>>? DataSourceChanged;
// Virtual method for subclasses (pre-change) - returns true to cancel
protected virtual bool OnDataSourceChanging(ValueChangingEventArgs<object?> args) => false;
// Virtual method for subclasses (post-change) - void, cannot cancel
protected virtual void OnDataSourceChanged(ValueChangedEventArgs<object?> args) { }
}
Step 2: Implement the Property with CWPPropertyHelper
public object? DataSource
{
get => _dataSource;
set
{
if (CWPPropertyHelper.ChangeProperty (
sender: this,
currentValue: ref _dataSource,
newValue: value,
onChanging: OnDataSourceChanging,
changingEvent: DataSourceChanging,
doWork: newDataSource =>
{
// Additional work AFTER value changes but BEFORE Changed events
// e.g., refresh display, update selection
SetNeedsDraw ();
},
onChanged: OnDataSourceChanged,
changedEvent: DataSourceChanged,
out _))
{
// Property was changed (not cancelled)
}
}
}
Step 3: Consuming the Events
// External subscriber (event)
myDataView.DataSourceChanging += (sender, args) =>
{
if (args.NewValue is null)
{
args.Handled = true; // Prevent null assignment
}
};
myDataView.DataSourceChanged += (sender, args) =>
{
Console.WriteLine($"DataSource changed from {args.OldValue} to {args.NewValue}");
};
// Subclass (virtual method override)
public class MyCustomDataView : MyDataView
{
protected override bool OnDataSourceChanging(ValueChangingEventArgs<object?> args)
{
// Validate new data source
if (args.NewValue is ICollection { Count: 0 })
{
return true; // Cancel - don't allow empty collections
}
return false;
}
}
Recipe 2: Cancellable Action/Workflow
Use when: An action or operation can be cancelled by subclasses or subscribers.
Example: Custom view with an Executing event.
Option A: Manual CWP Implementation
public class MyProcessor : View
{
// Event for external subscribers
public event EventHandler<CancelEventArgs>? Processing;
// Virtual method for subclasses
protected virtual bool OnProcessing(CancelEventArgs args)
{
return false; // Return true to cancel
}
// Internal method that implements CWP
public bool Process()
{
CancelEventArgs args = new ();
// Step 1: Call virtual method (subclass gets first chance)
if (OnProcessing(args) || args.Cancel)
{
return false; // Cancelled
}
// Step 2: Raise event (external subscribers get a chance)
Processing?.Invoke(this, args);
if (args.Cancel)
{
return false; // Cancelled
}
// Step 3: Execute default behavior
DoProcessing();
return true;
}
private void DoProcessing()
{
// Default processing logic
}
}
Option B: Using CWPWorkflowHelper
public class MyProcessor : View
{
public event EventHandler<ResultEventArgs<bool>>? Processing;
protected virtual bool OnProcessing(ResultEventArgs<bool> args)
{
return false; // Return true to cancel
}
public bool? Process()
{
ResultEventArgs<bool> args = new ();
return CWPWorkflowHelper.Execute(
onMethod: OnProcessing,
eventHandler: Processing,
args: args,
defaultAction: () =>
{
// Default processing logic
DoProcessing();
args.Result = true;
});
}
private void DoProcessing()
{
// Processing logic
}
}
Recipe 3: Simple Notification
Use when: You just need to notify that something happened (no cancellation needed).
Important
The virtual method must be a no-op by default. It exists solely for subclasses to override.
The event invocation happens in a separate Raise* method, NOT in the virtual method.
public class MyView : View
{
// Simple event - no cancellation
public event EventHandler? SelectionMade;
// Virtual method for subclasses - NO-OP by default
protected virtual void OnSelectionMade()
{
// Does nothing by default.
// Subclasses override this to react to the selection.
}
// Internal method that raises the notification
private void RaiseSelectionMade()
{
// 1. Call virtual method first (subclasses get priority)
OnSelectionMade();
// 2. Raise event (external subscribers)
SelectionMade?.Invoke(this, EventArgs.Empty);
}
private void HandleSelection()
{
// ... selection logic ...
RaiseSelectionMade();
}
}
// Subclass example
public class MyCustomView : MyView
{
protected override void OnSelectionMade()
{
// React to selection in subclass
UpdateStatusBar();
}
}
Recipe 4: MVVM Property Notification
Use when: You need data binding support via INotifyPropertyChanged.
public class ViewModel : INotifyPropertyChanged
{
private string _name = string.Empty;
public event PropertyChangedEventHandler? PropertyChanged;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
}
Event Categories in Terminal.Gui
Terminal.Gui uses several types of events:
| Category | Examples | Pattern |
|---|---|---|
| UI Interaction | KeyDown, MouseClick |
CWP with Handled |
| View Lifecycle | Initialized, Disposed |
Simple notification |
| Property Change | TextChanging, TextChanged |
CWP with Handled |
| Drawing | DrawingContent, DrawComplete |
CWP with Handled |
| Command | Accepting, Activating |
CWP with Handled |
Event Context and Arguments
Standard Event Arguments
Terminal.Gui provides these event argument types:
| Type | Use Case | Key Properties |
|---|---|---|
ValueChangingEventArgs<T> |
Pre-property-change | CurrentValue, NewValue, Handled |
ValueChangedEventArgs<T> |
Post-property-change | OldValue, NewValue |
CommandEventArgs |
Command execution | Context, Handled |
CancelEventArgs |
Cancellable operations | Cancel |
MouseEventArgs |
Mouse input | Position, Flags, Handled |
Command Context
When handling command events, rich context is available through ICommandContext:
public interface ICommandContext
{
Command Command { get; set; } // The command being invoked
View? Source { get; set; } // The view that first invoked the command
IInputBinding? Binding { get; } // The binding that triggered the command
}
Binding Types and Pattern Matching
Terminal.Gui provides three binding types. Use pattern matching to access binding-specific details:
public override bool OnAccepting(object? sender, CommandEventArgs e)
{
// Determine what triggered the command
switch (e.Context?.Binding)
{
case KeyBinding kb:
// Keyboard-triggered
Key key = kb.Key;
break;
case MouseBinding mb:
// Mouse-triggered
Point position = mb.MouseEvent.Position;
MouseFlags flags = mb.MouseEvent.Flags;
break;
case InputBinding ib:
// Programmatic invocation
object? data = ib.Data;
break;
}
return false;
}
// Or use property patterns for concise access:
if (e.Context?.Binding is MouseBinding { MouseEvent: { } mouse })
{
Point position = mouse.Position;
}
Source Tracking During Propagation
Understanding the difference between sources is important during event propagation:
| Property | Description | Changes During Propagation? |
|---|---|---|
ICommandContext.Source |
View that first invoked the command | No (constant) |
IInputBinding.Source |
View where binding was defined | No (constant) |
sender (event parameter) |
View currently raising the event | Yes |
public override bool OnAccepting(object? sender, CommandEventArgs e)
{
// sender = current view raising the event (changes as it bubbles)
// e.Context?.Source = original view that started the command (constant)
View? currentView = sender as View;
View? originalView = e.Context?.Source;
...
}
Best Practices
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Pre-change event | <Property>Changing |
TextChanging, SourceChanging |
| Post-change event | <Property>Changed |
TextChanged, SourceChanged |
| Pre-change virtual | On<Property>Changing |
OnTextChanging |
| Post-change virtual | On<Property>Changed |
OnTextChanged |
| Handled property | Handled |
args.Handled = true |
Implementation Guidelines
- Virtual methods return
boolfor cancellable operations (returntrueto cancel) - Virtual methods return
voidfor post-change notifications (cannot cancel) - Always call virtual method BEFORE raising the event (subclasses get priority)
- Execute default behavior AFTER both checks pass
- Unsubscribe in Dispose to prevent memory leaks
// ✅ CORRECT order: Virtual → Event → Default behavior
protected void DoSomething()
{
SomeEventArgs args = new ();
// 1. Virtual method first
if (OnDoingSomething(args))
{
return; // Cancelled by subclass
}
// 2. Event second
DoingSomething?.Invoke(this, args);
if (args.Handled)
{
return; // Cancelled by subscriber
}
// 3. Default behavior
ExecuteDefaultBehavior();
// 4. Post-change notification (if applicable)
OnDidSomething(new DidSomethingEventArgs(...));
DidSomething?.Invoke(this, new DidSomethingEventArgs(...));
}
Common Pitfalls
1. Memory Leaks from Unsubscribed Events
// ❌ BAD: Potential memory leak
view.Accepting += OnAccepting;
// ✅ GOOD: Unsubscribe in Dispose
protected override void Dispose(bool disposing)
{
if (disposing)
{
view.Accepting -= OnAccepting;
}
base.Dispose(disposing);
}
2. Using Wrong Cancellation Property
// ❌ WRONG: Using non-existent Cancel property
args.Cancel = true; // ValueChangingEventArgs doesn't have Cancel!
// ✅ CORRECT: Use Handled for all CWP events
args.Handled = true;
3. Wrong Order of Virtual Method and Event
// ❌ WRONG: Event raised before virtual method
DoingSomething?.Invoke(this, args);
if (OnDoingSomething(args)) { return; } // Too late!
// ✅ CORRECT: Virtual method first, then event
if (OnDoingSomething(args)) { return; }
DoingSomething?.Invoke(this, args);
4. Forgetting to Check Both Cancellation Points
// ❌ WRONG: Only checking virtual method
if (OnDoingSomething(args)) { return; }
DoingSomething?.Invoke(this, args);
ExecuteDefault(); // Bug: Event subscribers can't cancel!
// ✅ CORRECT: Check both virtual method AND event args
if (OnDoingSomething(args) || args.Handled) { return; }
DoingSomething?.Invoke(this, args);
if (args.Handled) { return; }
ExecuteDefault();