docfx/docs/events.md
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.
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 |
[!INCLUDE Events Lexicon]
Use when: A property change can be vetoed/cancelled by subclasses or subscribers.
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) { }
}
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)
}
}
}
// 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;
}
}
Use when: An action or operation can be cancelled by subclasses or subscribers.
Example: Custom view with an Executing event.
[!NOTE] This recipe uses
CancelEventArgs.Cancelfor standalone workflows that are not part of the command system. For command-related events (e.g., xref:Terminal.Gui.ViewBase.View.Accepting, xref:Terminal.Gui.ViewBase.View.Activating), useCommandEventArgs.Handledinstead (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
}
}
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
}
}
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 ();
}
}
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)));
}
}
}
}
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 | xref:Terminal.Gui.ViewBase.View.Accepting, xref:Terminal.Gui.ViewBase.View.Activating | CWP with Handled |
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 |
| xref:Terminal.Gui.Input.CommandEventArgs | Command execution | Context, Handled |
CancelEventArgs | Cancellable operations | Cancel |
MouseEventArgs | Mouse input | Position, Flags, Handled |
When handling command events, rich context is available through xref:Terminal.Gui.Input.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])
}
xref:Terminal.Gui.Input.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 xref:Terminal.Gui.ViewBase.IValue-implementing view appends its value. Ordered from innermost (originator) to outermost.Value — Convenience accessor for Values[^1] (the most recently appended value), or null if Values is empty.See the Value Propagation section in the Command Deep Dive for details.
[!NOTE]
Sourceis aWeakReference<View>to prevent memory leaks during command propagation. Usectx.TryGetSource (out View? view)orctx.Source?.TryGetTarget (out View? view)to safely access the source view.
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;
}
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 | xref:Terminal.Gui.Input.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;
}
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.
/// <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;
}
| View | Value Type | Meaning |
|---|---|---|
| xref:Terminal.Gui.Views.CheckBox | CheckState | Current checked state (Unchecked, Checked, CheckedMark) |
| xref:Terminal.Gui.Views.TextField | string | Text content |
| xref:Terminal.Gui.Views.TextView | string | Full text content |
DateEditor | DateTime? | Selected date |
TimeEditor | TimeSpan | Selected time |
ScrollBar | int | Current scroll position |
Slider | int | Current slider value |
| xref:Terminal.Gui.Views.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 |
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
};
}
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.
| 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 |
bool for cancellable operations (return true to cancel)void for post-change notifications (cannot cancel)// 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 ());
}
// 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);
}
// ❌ WRONG: Using non-existent Cancel property
args.Cancel = true; // ValueChangingEventArgs doesn't have Cancel!
// ✅ CORRECT: Use Handled for all CWP events
args.Handled = true;
// 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);
// 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 ();