* Initial plan
* Rename Command.Select to Command.Activate and Selecting to Activating
Co-authored-by: tig <585482+tig@users.noreply.github.com>
* Add Activating event propagation to SuperView
Co-authored-by: tig <585482+tig@users.noreply.github.com>
* Update all comments and docs referencing Select to Activate
Co-authored-by: tig <585482+tig@users.noreply.github.com>
* Fix event log messages in examples to use Activating/Activate
Co-authored-by: tig <585482+tig@users.noreply.github.com>
* Revert automatic Activating event propagation that broke tests
Co-authored-by: tig <585482+tig@users.noreply.github.com>
* Update docfx documentation to use Activate/Activating terminology
Co-authored-by: tig <585482+tig@users.noreply.github.com>
* renames
* Revert "Add Activating event propagation to SuperView"
This reverts commit 6d82bee9ad.
* added command diagrams
* mermaid
* updated level 3
* again
* Select->Activate in MouseTests.cs
* Update Terminal.Gui/Views/Selectors/FlagSelector.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Refactor: Rename Selecting to Activating in View APIs
Renamed the `Selecting` event and `OnSelecting` method to
`Activating` and `OnActivating` to better reflect their purpose.
Updated all related comments, test method names, variables,
and assertions in `View` and `ViewCommandTests` to align with
the new terminology.
Improved code clarity by using `_` for unused parameters in
lambda expressions. Renamed properties like `HandleSelecting`
to `HandleActivating` and adjusted naming conventions for
consistency (e.g., `OnactivatingCount` to `OnActivatingCount`).
These changes enhance readability, maintainability, and
terminology consistency across the codebase.
* Update Terminal.Gui/Views/Selectors/OptionSelector.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Typos
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
12 KiB
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
[!INCLUDE Events Lexicon]
Event Categories
Terminal.Gui uses several types of events:
- UI Interaction Events: Events triggered by user input (keyboard, mouse)
- View Lifecycle Events: Events related to view creation, activation, and disposal
- Property Change Events: Events for property value changes
- Drawing Events: Events related to view rendering
- 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:
- Virtual Method:
protected virtual OnMethod()that can be overridden in a subclass so the subclass can participate - 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 Activating 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 (RaiseActivating (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 Activating 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
-
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
- Use past tense for completed events (e.g.,
-
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
-
Event Context:
- Provide rich context in event args
- Include source view and binding details
- Add view-specific state when needed
-
Event Propagation:
- Use appropriate propagation mechanisms
- Avoid unnecessary event bubbling
- Consider using
PropagatedCommandsfor hierarchical views
Common Pitfalls
-
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); } -
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 -
Missing Context:
// BAD: Missing context Activating?.Invoke(this, new CommandEventArgs()); // GOOD: Including context Activating?.Invoke(this, new CommandEventArgs { Context = ctx });
Useful External Documentation
- .NET Naming Guidelines - Names of Events
- .NET Design for Extensibility - Events and Callbacks
- C# Event Implementation Fundamentals, Best Practices and Conventions
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., MenuBar popovers, Dialog closing), a PropagatedCommands property is proposed (Issue #4050):
-
Change: Add
IReadOnlyList<Command> PropagatedCommandstoView, defaulting to[Command.Accept].Raise*methods propagate if the command is inSuperView?.PropagatedCommandsandargs.Handledisfalse. -
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.Activatepropagation forMenuBarwhile preservingCommand.Acceptpropagation, maintaining decoupling and avoiding noise from irrelevant commands.
Conflation in FlagSelector:
- Issue:
CheckBox.ActivatingtriggersAccepting, conflating state change and confirmation. - Recommendation: Refactor to separate
ActivatingandAccepting:checkbox.Activating += (sender, args) => { if (RaiseAccepting(args.Context) is true) { args.Handled = true; } };
Propagation Limitations:
- Issue: Local
Command.ActivaterestrictsMenuBarcoordination;Command.Acceptuses hacks (#3925). - Recommendation: Adopt
PropagatedCommandsto 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.