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. |
| Bridge/Bridging | Routing a command across a non-containment boundary (e.g., MenuBarItem ↔ PopoverMenu). CommandBridge subscribes to a remote view's completion events and re-raises them on the owner with CommandRouting.Bridged. See Command Deep Dive. |
| Bubble/Bubbling | Propagating a command upward from a SubView to its SuperView. Opt-in via CommandsToBubbleUp. The upward direction is always "bubble" — never "dispatch." See Command Deep Dive. |
| 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). |
| Dispatch/Dispatching | Sending a command downward from a SuperView to a specific SubView. The downward direction is always "dispatch" — never "bubble." DispatchDown sends with bubbling suppressed; TryDispatchToTarget uses GetDispatchTarget and ConsumeDispatch to automate dispatch for composite views. See Command Deep Dive. |
| 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. |
| Routing | The CommandRouting enum describes how a command is being routed: Direct (local invocation), BubblingUp (upward to SuperView), DispatchingDown (downward to SubView), or Bridged (across non-containment boundary). Carried on ICommandContext.Routing. See Command Deep Dive. |
| 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
Note
This recipe uses CancelEventArgs.Cancel for standalone workflows that are not part of the command system.
For command-related events (e.g., Accepting, Activating), use CommandEventArgs.Handled instead (see Command Deep Dive).
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; } // The command being invoked
WeakReference<View>? Source { get; } // Weak ref to the originating view
ICommandBinding? Binding { get; } // The binding that triggered the command
CommandRouting Routing { get; } // Direct, BubblingUp, DispatchingDown, or Bridged
IReadOnlyList<object?> Values { get; } // Values accumulated as the command propagated
object? Value { get; } // Most recently appended value (Values[^1])
}
CommandContext is an immutable record struct. Use WithCommand(), WithRouting(), or WithValue() to create modified copies.
Values— An append-only chain of values accumulated as the command propagates up the view hierarchy. Each IValue-implementing view appends its value. Ordered from innermost (originator) to outermost.Value— Convenience accessor forValues[^1](the most recently appended value), ornullifValuesis empty.
See the Value Propagation section in the Command Deep Dive for details.
Note
Source is a WeakReference<View> to prevent memory leaks during command propagation.
Use ctx.TryGetSource (out View? view) or ctx.Source?.TryGetTarget (out View? view) to safely access the source view.
Binding Types and Pattern Matching
Terminal.Gui provides three binding types. Use pattern matching to access binding-specific details:
protected override bool OnAccepting (CommandEventArgs args)
{
// Determine what triggered the command
switch (args.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 CommandBinding ib:
// Programmatic invocation
object? data = ib.Data;
break;
}
return false;
}
// Or use property patterns for concise access:
if (args.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 |
WeakReference<View> to the view that first invoked the command |
No (constant) |
ICommandContext.Routing |
CommandRouting enum: Direct, BubblingUp, DispatchingDown, Bridged |
Yes (changes at each hop) |
ICommandBinding.Source |
View where binding was defined | No (constant) |
sender (event parameter) |
View currently raising the event | Yes |
protected override bool OnAccepting (CommandEventArgs args)
{
// In the Accepting event handler, `this` is the view raising the event (changes as it bubbles).
// args.Context?.Source is a WeakReference<View> to the original view that started the command (constant).
View? originalView = null;
args.Context?.Source?.TryGetTarget (out originalView);
return false;
}
Value Access: IValue and IValue<T>
Views that represent user-selectable values implement the IValue<T> interface, providing standardized access to their primary value. This enables generic programming, value propagation during command handling, and consistent event patterns.
IValue Interfaces
/// <summary>
/// Non-generic interface for accessing a View's value as a boxed object.
/// Used by command propagation to carry values without knowing the generic type.
/// </summary>
public interface IValue
{
/// <summary>Gets the value as a boxed object.</summary>
object? GetValue ();
}
/// <summary>
/// Interface for Views that provide a strongly-typed value.
/// </summary>
public interface IValue<TValue> : IValue
{
/// <summary>Gets or sets the value.</summary>
TValue? Value { get; set; }
/// <summary>
/// Raised when <see cref="Value"/> is about to change.
/// Set <see cref="ValueChangingEventArgs{T}.Handled"/> to cancel.
/// </summary>
event EventHandler<ValueChangingEventArgs<TValue?>>? ValueChanging;
/// <summary>
/// Raised when <see cref="Value"/> has changed.
/// </summary>
event EventHandler<ValueChangedEventArgs<TValue?>>? ValueChanged;
/// <inheritdoc/>
object? IValue.GetValue () => Value;
}
Views Implementing IValue<T>
| View | Value Type | Meaning |
|---|---|---|
| CheckBox | CheckState |
Current checked state (Unchecked, Checked, CheckedMark) |
| TextField | string |
Text content |
| TextView | string |
Full text content |
DateEditor |
DateTime? |
Selected date |
TimeEditor |
TimeSpan |
Selected time |
ScrollBar |
int |
Current scroll position |
Slider |
int |
Current slider value |
| ListView | int |
Selected item index |
OptionSelector |
int |
Selected option index |
RadioGroup |
int |
Selected radio button index |
LineCanvas |
List<Line> |
Collection of lines |
CharMap |
Rune |
Selected character |
Using IValue in Handlers
Example: Accessing value during command propagation:
menuBar.Accepting += (_, args) =>
{
// Access the value from the originating view via WeakReference
View? sourceView = null;
args.Context?.Source?.TryGetTarget (out sourceView);
if (sourceView is IValue valueView)
{
object? value = valueView.GetValue ();
// Pattern match on value type
if (value is CheckState checkState)
{
_autoSave = checkState == CheckState.Checked;
}
else if (value is int optionIndex)
{
_selectedOption = optionIndex;
}
}
};
Example: Generic value handling:
void ProcessValueView<T> (IValue<T> valueView)
{
T? currentValue = valueView.Value;
valueView.ValueChanged += (sender, args) =>
{
T? newValue = args.NewValue;
T? oldValue = args.OldValue;
// Handle value change
};
}
Value vs Legacy Properties
Many Views have legacy properties (e.g., TextField.Text, CheckBox.CheckedState) that predate the IValue<T> interface. The Value property typically maps to these legacy properties:
// CheckBox example
public class CheckBox : View, IValue<CheckState>
{
public CheckState CheckedState { get; set; } // Legacy property
public CheckState? Value // IValue<T> property
{
get => CheckedState;
set => CheckedState = value ?? CheckState.None;
}
}
Best practice: Use the Value property for new code, as it provides consistent access patterns across all value-bearing Views.
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 ();