Table of Contents

Terminal.Gui Event Deep Dive

This document provides a comprehensive overview of how events work in Terminal.Gui. For the conceptual overview of the Cancellable Work Pattern, see Cancellable Work Pattern.

See Also

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).

Event Categories

Terminal.Gui uses several types of events:

  1. UI Interaction Events: Events triggered by user input (keyboard, mouse)
  2. View Lifecycle Events: Events related to view creation, activation, and disposal
  3. Property Change Events: Events for property value changes
  4. Drawing Events: Events related to view rendering
  5. Command Events: Events for command execution and workflow control

Event Patterns

1. Cancellable Work Pattern (CWP)

The Cancellable Work Pattern (CWP) is a core pattern in Terminal.Gui that provides a consistent way to handle cancellable operations. An "event" has two components:

  1. Virtual Method: protected virtual OnMethod() that can be overridden in a subclass so the subclass can participate
  2. Event: public event EventHandler<> that allows external subscribers to participate

The virtual method is called first, letting subclasses have priority. Then the event is invoked.

Optional CWP Helper Classes are provided to provide consistency.

Manual CWP Implementation

The basic CWP pattern combines a protected virtual method with a public event:

public class MyView : View
{
    // Public event
    public event EventHandler<MouseEventArgs>? MouseEvent;

    // Protected virtual method
    protected virtual bool OnMouseEvent(MouseEventArgs args)
    {
        // Return true to handle the event and stop propagation
        return false;
    }

    // Internal method to raise the event
    internal bool RaiseMouseEvent(MouseEventArgs args)
    {
        // Call virtual method first
        if (OnMouseEvent(args) || args.Handled)
        {
            return true;
        }

        // Then raise the event
        MouseEvent?.Invoke(this, args);
        return args.Handled;
    }
}

CWP with Helper Classes

Terminal.Gui provides static helper classes to implement CWP:

Property Changes

For property changes, use CWPPropertyHelper.ChangeProperty:

public class MyView : View
{
    private string _text = string.Empty;
    public event EventHandler<ValueChangingEventArgs<string>>? TextChanging;
    public event EventHandler<ValueChangedEventArgs<string>>? TextChanged;

    public string Text
    {
        get => _text;
        set
        {
            if (CWPPropertyHelper.ChangeProperty(
                currentValue: _text,
                newValue: value,
                onChanging: args => OnTextChanging(args),
                changingEvent: TextChanging,
                onChanged: args => OnTextChanged(args),
                changedEvent: TextChanged,
                out string finalValue))
            {
                _text = finalValue;
            }
        }
    }

    // Virtual method called before the change
    protected virtual bool OnTextChanging(ValueChangingEventArgs<string> args)
    {
        // Return true to cancel the change
        return false;
    }

    // Virtual method called after the change
    protected virtual void OnTextChanged(ValueChangedEventArgs<string> args)
    {
        // React to the change
    }
}

Workflows

For general workflows, use CWPWorkflowHelper:

public class MyView : View
{
    public bool? ExecuteWorkflow()
    {
        ResultEventArgs<bool> args = new();
        return CWPWorkflowHelper.Execute(
            onMethod: args => OnExecuting(args),
            eventHandler: Executing,
            args: args,
            defaultAction: () =>
            {
                // Main execution logic
                DoWork();
                args.Result = true;
            });
    }

    // Virtual method called before execution
    protected virtual bool OnExecuting(ResultEventArgs<bool> args)
    {
        // Return true to cancel execution
        return false;
    }

    public event EventHandler<ResultEventArgs<bool>>? Executing;
}

2. Action Callbacks

For simple callbacks without cancellation, use Action. For example, in Shortcut:

public class Shortcut : View
{
    /// <summary>
    ///     Gets or sets the action to be invoked when the shortcut key is pressed or the shortcut is clicked on with the
    ///     mouse.
    /// </summary>
    /// <remarks>
    ///     Note, the <see cref="View.Accepting"/> event is fired first, and if cancelled, the event will not be invoked.
    /// </remarks>
    public Action? Action { get; set; }

    internal virtual bool? DispatchCommand(ICommandContext? commandContext)
    {
        bool cancel = base.DispatchCommand(commandContext) == true;

        if (cancel)
        {
            return true;
        }

        if (Action is { })
        {
            Logging.Debug($"{Title} ({commandContext?.Source?.Title}) - Invoke Action...");
            Action.Invoke();

            // Assume if there's a subscriber to Action, it's handled.
            cancel = true;
        }

        return cancel;
    }
}

3. Property Change Notifications

For property change notifications, implement INotifyPropertyChanged. For example, in Aligner:

public class Aligner : INotifyPropertyChanged
{
    private Alignment _alignment;
    public event PropertyChangedEventHandler? PropertyChanged;

    public Alignment Alignment
    {
        get => _alignment;
        set
        {
            _alignment = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Alignment)));
        }
    }
}

4. Event Propagation

Events in Terminal.Gui often propagate through the view hierarchy. For example, in Button, the Selecting and Accepting events are raised as part of the command handling process:

private bool? HandleHotKeyCommand (ICommandContext commandContext)
{
    bool cachedIsDefault = IsDefault; // Supports "Swap Default" in Buttons scenario where IsDefault changes

    if (RaiseSelecting (commandContext) is true)
    {
        return true;
    }

    bool? handled = RaiseAccepting (commandContext);

    if (handled == true)
    {
        return true;
    }

    SetFocus ();

    // If Accept was not handled...
    if (cachedIsDefault && SuperView is { })
    {
        return SuperView.InvokeCommand (Command.Accept);
    }

    return false;
}

This example shows how Button first raises the Selecting event, and if not canceled, proceeds to raise the Accepting event. If Accepting is not handled and the button is the default, it invokes the Accept command on the SuperView, demonstrating event propagation up the view hierarchy.

Event Context

Event Arguments

Terminal.Gui provides rich context through event arguments. For example, CommandEventArgs:

public class CommandEventArgs : EventArgs
{
    public ICommandContext? Context { get; set; }
    public bool Handled { get; set; }
    public bool Cancel { get; set; }
}

Command Context

Command execution includes context through ICommandContext:

public interface ICommandContext
{
    View Source { get; }
    object? Parameter { get; }
    IDictionary<string, object> State { get; }
}

Best Practices

  1. Event Naming:

    • Use past tense for completed events (e.g., Clicked, Changed)
    • Use present tense for ongoing events (e.g., Clicking, Changing)
    • Use "ing" suffix for cancellable events
  2. Event Handler Implementation:

    • Keep handlers short and focused
    • Use async/await for long-running tasks
    • Unsubscribe from events in Dispose
    • Use weak event patterns for long-lived subscriptions
  3. Event Context:

    • Provide rich context in event args
    • Include source view and binding details
    • Add view-specific state when needed
  4. Event Propagation:

    • Use appropriate propagation mechanisms
    • Avoid unnecessary event bubbling
    • Consider using PropagatedCommands for hierarchical views

Common Pitfalls

  1. Memory Leaks:

    // BAD: Potential memory leak
    view.Activating += OnActivating;
    
    // GOOD: Unsubscribe in Dispose
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            view.Activating -= OnActivating;
        }
        base.Dispose(disposing);
    }
    
  2. Incorrect Event Cancellation:

    // BAD: Using Cancel for event handling
    args.Cancel = true;  // Wrong for MouseEventArgs
    
    // GOOD: Using Handled for event handling
    args.Handled = true;  // Correct for MouseEventArgs
    
    // GOOD: Using Cancel for operation cancellation
    args.Cancel = true;  // Correct for CancelEventArgs
    
  3. Missing Context:

    // BAD: Missing context
    Activating?.Invoke(this, new CommandEventArgs());
    
    // GOOD: Including context
    Activating?.Invoke(this, new CommandEventArgs { Context = ctx });
    

Useful External Documentation

Naming

TG follows the naming advice provided in .NET Naming Guidelines - Names of Events.

Known Issues

Proposed Enhancement: Command Propagation

The Cancellable Work Pattern in View.Command currently supports local Command.Activate and propagating Command.Accept. To address hierarchical coordination needs (e.g., MenuBarv2 popovers, Dialog closing), a PropagatedCommands property is proposed (Issue #4050):

  • Change: Add IReadOnlyList<Command> PropagatedCommands to View, defaulting to [Command.Accept]. Raise* methods propagate if the command is in SuperView?.PropagatedCommands and args.Handled is false.

  • Example:

    public IReadOnlyList<Command> PropagatedCommands { get; set; } = new List<Command> { Command.Accept };
    protected bool? RaiseAccepting(ICommandContext? ctx)
    {
        CommandEventArgs args = new() { Context = ctx };
        if (OnAccepting(args) || args.Handled)
        {
            return true;
        }
        Accepting?.Invoke(this, args);
        if (!args.Handled && SuperView?.PropagatedCommands.Contains(Command.Accept) == true)
        {
            return SuperView.InvokeCommand(Command.Accept, ctx);
        }
        return Accepting is null ? null : args.Handled;
    }
    
  • Impact: Enables Command.Activate propagation for MenuBarv2 while preserving Command.Accept propagation, maintaining decoupling and avoiding noise from irrelevant commands.

Conflation in FlagSelector:

  • Issue: CheckBox.Activating triggers Accepting, conflating state change and confirmation.
  • Recommendation: Refactor to separate Activating and Accepting:
    checkbox.Activating += (sender, args) =>
    {
        if (RaiseAccepting(args.Context) is true)
        {
            args.Handled = true;
        }
    };
    

Propagation Limitations:

  • Issue: Local Command.Activate restricts MenuBarv2 coordination; Command.Accept uses hacks (#3925).
  • Recommendation: Adopt PropagatedCommands to enable targeted propagation, as proposed.

Complexity in Multi-Phase Workflows:

  • Issue: View.Draw's multi-phase workflow can be complex for developers to customize.
  • Recommendation: Provide clearer phase-specific documentation and examples.