mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-02-10 04:03:41 +01:00
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
This commit is contained in:
@@ -1,21 +1,19 @@
|
||||
# Cancellable Work Pattern in Terminal.Gui
|
||||
# Cancellable Work Pattern (CWP)
|
||||
|
||||
The *Cancellable Work Pattern* is a core design pattern in Terminal.Gui, used to structure workflows that can be executed in a default manner, modified by external code or subclasses, or cancelled entirely. This pattern is prevalent across various components of Terminal.Gui, including the `View` class for rendering, keyboard input, and command execution, as well as application-level input handling and property changes. Unlike traditional inheritance-based approaches that rely on overriding virtual methods (which often require subclasses to understand base class implementation details), the *Cancellable Work Pattern* prioritizes events for loose coupling, supplemented by optional virtual methods for flexibility.
|
||||
The *Cancellable Work Pattern* (CWP) is a design pattern for structuring workflows that can be executed in a default manner, modified by external code or subclasses, or cancelled entirely. Unlike traditional inheritance-based approaches that rely on overriding virtual methods (which often require subclasses to understand base class implementation details), CWP prioritizes **events for loose coupling**, supplemented by optional **virtual methods** for flexibility.
|
||||
|
||||
This document is a conceptual definition of *Cancellable Work Pattern* and outlines its components and goals, and illustrates its implementation through examples in `View.Draw`, `View.Keyboard`, `View.Command`, `Application.Keyboard`, and `OrientationHelper`.
|
||||
This document provides a conceptual definition of the pattern, its components, and goals, illustrated through generic examples applicable to any .NET framework or application.
|
||||
|
||||
See the [Events Deep Dive](events.md) for a concrete deep dive and tutorial.
|
||||
|
||||
> [!NOTE]
|
||||
> Some terms in this document are based on a yet-to-be addressed Issue: https://github.com/gui-cs/Terminal.Gui/issues/4050
|
||||
> [!TIP]
|
||||
> For Terminal.Gui-specific implementation details and practical guidance, see the [Events Deep Dive](events.md).
|
||||
|
||||
## Definition
|
||||
|
||||
The *Cancellable Work Pattern* is a design pattern for executing a structured workflow with one or more phases, where each phase can:
|
||||
|
||||
- Proceed in a default manner.
|
||||
- Be modified by external code or subclasses.
|
||||
- Be cancelled to halt further processing.
|
||||
- **Proceed** in a default manner
|
||||
- **Be modified** by external code or subclasses
|
||||
- **Be cancelled** to halt further processing
|
||||
|
||||
The pattern uses events as the primary mechanism for notification and customization, supplemented by virtual methods for subclassing when needed. It is a specialization of the **Observer Pattern**, extended with structured workflows, explicit cancellation mechanisms, and context-aware notifications. It also incorporates elements of the **Template Method Pattern** (via virtual methods) and **Pipeline Pattern** (via sequential phases).
|
||||
|
||||
@@ -25,42 +23,81 @@ The pattern uses events as the primary mechanism for notification and customizat
|
||||
|
||||
## Core Concept
|
||||
|
||||
At its core, CWP defines a workflow as a sequence of one or more distinct phases, each representing a unit of work within a larger operation. For each phase, the pattern provides mechanisms to:
|
||||
At its core, CWP defines a workflow as a sequence of one or more distinct **phases**, each representing a unit of work within a larger operation. For each phase, the pattern provides mechanisms to:
|
||||
|
||||
- **Execute Default Behavior**: A predefined implementation that executes if no external intervention occurs, ensuring the system remains functional out of the box.
|
||||
- **Allow Customization**: Through event subscriptions or method overrides, external code or subclasses can inject custom logic to alter the phase's behavior without needing to replace the entire workflow.
|
||||
- **Support Cancellation**: A explicit mechanism to halt the execution of a phase or the entire workflow, preventing further processing when certain conditions are met (e.g., user intervention, error states, or logical constraints).
|
||||
- **Support Cancellation**: An explicit mechanism to halt the execution of a phase or the entire workflow, preventing further processing when certain conditions are met (e.g., user intervention, error states, or logical constraints).
|
||||
|
||||
This triadic structure—default execution, customization, and cancellation—distinguishes CWP from simpler event-driven or inheritance-based approaches. It ensures that workflows are both robust (via defaults) and flexible (via customization and cancellation), making it ideal for complex systems like terminal user interfaces where multiple stakeholders (e.g., framework developers, application developers, and end-users) interact with the same processes.
|
||||
This triadic structure—default execution, customization, and cancellation—distinguishes CWP from simpler event-driven or inheritance-based approaches. It ensures that workflows are both robust (via defaults) and flexible (via customization and cancellation).
|
||||
|
||||
### Structural Components
|
||||
|
||||
The Cancellable Work Pattern typically comprises the following components:
|
||||
The Cancellable Work Pattern comprises the following components:
|
||||
|
||||
1. **Workflow Container**: The entity (often a class or object) that encapsulates the overall workflow, defining the sequence of phases and orchestrating their execution. In Terminal.Gui, this might be a `View` object managing rendering or input handling.
|
||||
1. **Workflow Container**: The class that encapsulates the overall workflow, defining the sequence of phases and orchestrating their execution (e.g., a `DocumentProcessor` managing parsing, validation, and saving phases).
|
||||
|
||||
2. **Phases**: Individual steps within the workflow, each representing a discrete unit of work. Each phase has a default implementation and points for intervention. For example, rendering text in a view could be a single phase within a broader drawing workflow.
|
||||
2. **Phases**: Individual steps within the workflow, each representing a discrete unit of work. Each phase has a default implementation and points for intervention (e.g., a "Validation" phase within a document processing workflow).
|
||||
|
||||
3. **Notification Mechanisms**: Events or callbacks that notify external observers of a phase's impending execution, allowing them to intervene. These are typically implemented as delegate-based events (e.g., `DrawingText` event in Terminal.Gui) or virtual methods (e.g., `OnDrawingText`).
|
||||
3. **Notification Mechanisms**: Events or callbacks that notify external observers of a phase's impending execution, allowing them to intervene. These are typically implemented as:
|
||||
- **Events**: `public event EventHandler<TEventArgs>?` for external subscribers
|
||||
- **Virtual Methods**: `protected virtual bool OnPhaseExecuting()` for subclasses
|
||||
|
||||
4. **Cancellation Flags**: Boolean indicators or properties within event arguments that signal whether a phase or workflow should be halted. In Terminal.Gui, this is often seen as `Handled` or `Cancel` properties in event args objects.
|
||||
4. **Cancellation Flags**: Boolean indicators within event arguments that signal whether a phase or workflow should be halted (commonly named `Cancel` or `Handled`).
|
||||
|
||||
5. **Context Objects**: Data structures passed to notification handlers, providing relevant state or parameters about the phase (e.g., `DrawContext` or `Key` objects in Terminal.Gui), enabling informed decision-making by custom logic.
|
||||
5. **Context Objects**: Data structures passed to notification handlers, providing relevant state or parameters about the phase, enabling informed decision-making by custom logic.
|
||||
|
||||
### Operational Flow
|
||||
|
||||
The operational flow of CWP follows a consistent pattern for each phase within a workflow:
|
||||
|
||||
1. **Pre-Phase Notification**: Before executing a phase's default behavior, the workflow container raises an event or calls a virtual method to notify potential observers or subclasses. This step allows for preemptive customization or cancellation.
|
||||
1. **Pre-Phase Notification**: Before executing a phase's default behavior, the workflow container calls a virtual method and/or raises an event to notify potential observers or subclasses. This step allows for preemptive customization or cancellation.
|
||||
|
||||
2. **Cancellation Check**: If the notification mechanism indicates cancellation (e.g., a return value of `true` from a virtual method or a `Cancel` flag set in event args), the phase is aborted, and control may return or move to the next phase, depending on the workflow design.
|
||||
|
||||
3. **Default Execution**: If no cancellation occurs, the phase's default behavior is executed. This ensures the workflow progresses as intended in the absence of external intervention.
|
||||
|
||||
4. **Post-Phase Notification (Optional)**: In some implementations, a secondary notification occurs after the phase completes, informing observers of the outcome or updated state (e.g., `OrientationChanged` event after a property update in Terminal.Gui).
|
||||
4. **Post-Phase Notification (Optional)**: In some implementations, a secondary notification occurs after the phase completes, informing observers of the outcome or updated state.
|
||||
|
||||
This flow repeats for each phase, allowing granular control over complex operations. Importantly, CWP decouples the workflow's structure from its customization, as external code can subscribe to events without needing to subclass or understand the container's internal logic.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CWP Operational Flow │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ 1. Call Virtual │──► returns true? ──► CANCELLED │
|
||||
│ │ OnXxxExecuting() │ │
|
||||
│ └──────────┬───────────┘ │
|
||||
│ │ returns false │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ 2. Raise Event │──► args.Cancel? ──► CANCELLED │
|
||||
│ │ XxxExecuting │ │
|
||||
│ └──────────┬───────────┘ │
|
||||
│ │ not cancelled │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ 3. Execute Default │ │
|
||||
│ │ Behavior │ │
|
||||
│ └──────────┬───────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ 4. Call Virtual │ │
|
||||
│ │ OnXxxExecuted() │ │
|
||||
│ └──────────┬───────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ 5. Raise Event │ │
|
||||
│ │ XxxExecuted │ │
|
||||
│ └──────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Advantages
|
||||
|
||||
- **Flexibility**: Developers can modify specific phases without altering the entire workflow, supporting a wide range of use cases from minor tweaks to complete overrides.
|
||||
@@ -75,178 +112,295 @@ This flow repeats for each phase, allowing granular control over complex operati
|
||||
|
||||
- **Complexity**: Multi-phase workflows can become intricate, especially when numerous events and cancellation points are involved, potentially leading to debugging challenges.
|
||||
|
||||
- **Performance Overhead**: Raising events and checking cancellation flags for each phase introduces minor performance costs, which may accumulate in high-frequency operations like rendering.
|
||||
- **Performance Overhead**: Raising events and checking cancellation flags for each phase introduces minor performance costs, which may accumulate in high-frequency operations.
|
||||
|
||||
- **Learning Curve**: Understanding the pattern's structure and knowing when to use events versus overrides requires familiarity, which may pose a barrier to novice developers.
|
||||
|
||||
## Applicability
|
||||
|
||||
CWP is particularly suited to domains where workflows must balance standardization with adaptability, such as user interface frameworks (e.g., Terminal.Gui), game engines, or workflow automation systems. It excels in scenarios where operations are inherently interruptible—such as responding to user input, rendering dynamic content, or managing state transitions—and where multiple components or developers need to collaborate on the same process without tight dependencies.
|
||||
CWP is particularly suited to domains where workflows must balance standardization with adaptability, such as:
|
||||
|
||||
In the context of Terminal.Gui, CWP underpins critical functionalities like view rendering, keyboard input processing, command execution, and property change handling, ensuring that these operations are both predictable by default and customizable as needed by application developers.
|
||||
- **User interface frameworks** (handling input, rendering, commands)
|
||||
- **Document processing systems** (parsing, validation, transformation)
|
||||
- **Game engines** (update loops, collision handling, rendering)
|
||||
- **Workflow automation systems** (approval processes, state machines)
|
||||
|
||||
## Implementation in Terminal.Gui
|
||||
It excels in scenarios where operations are inherently interruptible—such as responding to user input, processing documents, or managing state transitions—and where multiple components or developers need to collaborate on the same process without tight dependencies.
|
||||
|
||||
The *Cancellable Work Pattern* is implemented consistently across several key areas of Terminal.Gui's `v2_develop` branch. Below are five primary examples, each illustrating the pattern in a different domain: rendering, keyboard input at the view level, command execution, application-level keyboard input, and property changes.
|
||||
## Implementation Examples
|
||||
|
||||
### 1. View.Draw: Rendering Workflow
|
||||
The following examples demonstrate CWP in different contexts using generic, fictional classes.
|
||||
|
||||
The `View.Draw` method orchestrates the rendering of a view, including its adornments (margin, border, padding), viewport, text, content, subviews, and line canvas. It is a multi-phase workflow where each phase can be customized or cancelled.
|
||||
### Example 1: Workflow Phase (Document Processing)
|
||||
|
||||
#### Example: `DoDrawText`
|
||||
A `DocumentProcessor` class processes documents through multiple phases. Each phase can be customized or cancelled.
|
||||
|
||||
The `DoDrawText` method, responsible for drawing a view's text, exemplifies the pattern:
|
||||
```csharp
|
||||
private void DoDrawText(DrawContext? context = null)
|
||||
public class DocumentProcessor
|
||||
{
|
||||
if (OnDrawingText(context)) // Virtual method for subclasses
|
||||
// Event for external subscribers
|
||||
public event EventHandler<ProcessingEventArgs>? Validating;
|
||||
|
||||
// Virtual method for subclasses
|
||||
protected virtual bool OnValidating(ProcessingEventArgs args)
|
||||
{
|
||||
return; // Cancel if true
|
||||
// Return true to cancel validation
|
||||
return false;
|
||||
}
|
||||
if (OnDrawingText()) // Legacy virtual method
|
||||
|
||||
private void DoValidation(Document document)
|
||||
{
|
||||
return; // Cancel if true
|
||||
ProcessingEventArgs args = new (document);
|
||||
|
||||
// Step 1: Call virtual method (subclass gets first chance)
|
||||
if (OnValidating(args))
|
||||
{
|
||||
return; // Cancelled by subclass
|
||||
}
|
||||
|
||||
// Step 2: Raise event (external subscribers get a chance)
|
||||
Validating?.Invoke(this, args);
|
||||
if (args.Cancel)
|
||||
{
|
||||
return; // Cancelled by event subscriber
|
||||
}
|
||||
|
||||
// Step 3: Execute default behavior
|
||||
ValidateDocument(document);
|
||||
}
|
||||
var dev = new DrawEventArgs(Viewport, Rectangle.Empty, context);
|
||||
DrawingText?.Invoke(this, dev); // Notify observers
|
||||
if (dev.Cancel) // Check for cancellation
|
||||
|
||||
private void ValidateDocument(Document document)
|
||||
{
|
||||
return;
|
||||
// Default validation logic
|
||||
}
|
||||
DrawText(context); // Default behavior
|
||||
}
|
||||
```
|
||||
|
||||
- **Workflow**: Single phase for text drawing within the broader `Draw` workflow.
|
||||
- **Notifications**: `OnDrawingText` (virtual), `DrawingText` (event).
|
||||
- **Cancellation**: `OnDrawingText` returning `true` or `dev.Cancel = true`.
|
||||
- **Context**: `DrawContext` and `DrawEventArgs` provide rendering details.
|
||||
- **Default Behavior**: `DrawText` renders the view's text.
|
||||
- **Use Case**: Allows customization of text rendering (e.g., custom formatting) or cancellation (e.g., skipping text for performance).
|
||||
**Key Points:**
|
||||
- Virtual method `OnValidating` is called first, giving subclasses priority
|
||||
- Event `Validating` is raised second, allowing external customization
|
||||
- Either can cancel by returning `true` or setting `args.Cancel = true`
|
||||
- Default behavior `ValidateDocument` only runs if not cancelled
|
||||
|
||||
### 2. View.Keyboard: View-Level Keyboard Input
|
||||
### Example 2: Input Handling
|
||||
|
||||
The `View.NewKeyDownEvent` method processes keyboard input for a view, mapping keys to commands or handling them directly. It is a linear workflow with a single phase per key event.
|
||||
|
||||
#### Example: `NewKeyDownEvent`
|
||||
An `InputHandler` class processes input events, allowing customization of how input is handled.
|
||||
|
||||
```csharp
|
||||
public bool NewKeyDownEvent(Key key)
|
||||
public class InputHandler
|
||||
{
|
||||
if (OnKeyDown(key)) // Virtual method
|
||||
{
|
||||
return true; // Cancel if true
|
||||
}
|
||||
KeyDown?.Invoke(this, key); // Notify observers
|
||||
if (key.Handled) // Check for cancellation
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool handled = InvokeCommands(key, KeyBindingScope.HotKey | KeyBindingScope.Focused); // Default behavior
|
||||
return handled;
|
||||
}
|
||||
```
|
||||
- **Workflow**: Linear, processing one key event.
|
||||
- **Notifications**: `OnKeyDown` (virtual), `KeyDown` (event).
|
||||
- **Cancellation**: `OnKeyDown` returning `true` or `key.Handled = true`.
|
||||
- **Context**: `Key` provides key details and bindings.
|
||||
- **Default Behavior**: `InvokeCommands` maps keys to commands (e.g., `Command.Accept`).
|
||||
- **Use Case**: Allows views to customize key handling (e.g., `TextField` capturing input) or cancel default command execution.
|
||||
public event EventHandler<InputEventArgs>? InputReceived;
|
||||
|
||||
### 3. View.Command: Command Execution
|
||||
|
||||
The `View.Command` APIs execute commands like `Command.Activate` and `Command.Accept`, used for state changes (e.g., `CheckBox` toggling) and action confirmation (e.g., dialog submission). It is a per-unit workflow, with one phase per command.
|
||||
|
||||
#### Example: `RaiseAccepting`
|
||||
|
||||
```csharp
|
||||
protected bool? RaiseAccepting(ICommandContext? ctx)
|
||||
{
|
||||
CommandEventArgs args = new() { Context = ctx };
|
||||
if (OnAccepting(args) || args.Handled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Accepting?.Invoke(this, args);
|
||||
return Accepting is null ? null : args.Handled;
|
||||
}
|
||||
```
|
||||
|
||||
- **Workflow**: Single phase for `Command.Accept`.
|
||||
- **Notifications**: `OnAccepting` (virtual), `Accepting` (event).
|
||||
- **Cancellation**: `OnAccepting` returning `true` or `args.Handled = true`.
|
||||
- **Context**: `ICommandContext` provides `Command`, `Source`, and `Binding`.
|
||||
- **Default Behavior**: Propagates to `SuperView` or default button if not handled.
|
||||
- **Use Case**: Allows customization of state changes (e.g., `CheckBox` toggling) or cancellation (e.g., preventing focus in `MenuItem`).
|
||||
|
||||
#### Propagation Challenge
|
||||
|
||||
- `Command.Activate` is local, limiting hierarchical coordination (e.g., `MenuBar` popovers). A proposed `PropagatedCommands` property addresses this, as detailed in the appendix.
|
||||
|
||||
### 4. Application.Keyboard: Application-Level Keyboard Input
|
||||
|
||||
The `Application.OnKeyDown` method processes application-wide keyboard input, raising events for global key handling. It is an event-driven workflow, with a single phase per key event.
|
||||
|
||||
#### Example: `OnKeyDown`
|
||||
|
||||
```csharp
|
||||
public static bool OnKeyDown(Key key)
|
||||
{
|
||||
if (KeyDown is null)
|
||||
protected virtual bool OnInputReceived(InputEventArgs args)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
KeyDown?.Invoke(null, key); // Notify observers
|
||||
return key.Handled; // Check for cancellation
|
||||
}
|
||||
```
|
||||
|
||||
- **Workflow**: Event-driven, processing one key event.
|
||||
- **Notifications**: `KeyDown` (event, no virtual method).
|
||||
- **Cancellation**: `key.Handled = true`.
|
||||
- **Context**: `Key` provides key details.
|
||||
- **Default Behavior**: None; relies on subscribers (e.g., `Top` view processing).
|
||||
- **Use Case**: Allows global key bindings (e.g., `Ctrl+Q` to quit) or cancellation of default view handling.
|
||||
|
||||
### 5. OrientationHelper: Property Changes
|
||||
|
||||
The `OrientationHelper` class manages orientation changes (e.g., in `StackPanel`), raising events for property updates. It is an event-driven workflow, with a single phase per change.
|
||||
|
||||
#### Example: `Orientation` Setter
|
||||
|
||||
```csharp
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => _orientation;
|
||||
set
|
||||
public bool ProcessInput(InputEventArgs args)
|
||||
{
|
||||
if (_orientation == value)
|
||||
// Virtual method for subclasses
|
||||
if (OnInputReceived(args))
|
||||
{
|
||||
return;
|
||||
return true; // Handled by subclass
|
||||
}
|
||||
var oldOrientation = _orientation;
|
||||
var args = new CancelEventArgs<Orientation>(_orientation, ref value);
|
||||
if (OnOrientationChanging(args))
|
||||
|
||||
// Event for external subscribers
|
||||
InputReceived?.Invoke(this, args);
|
||||
if (args.Handled)
|
||||
{
|
||||
return; // Cancel if true
|
||||
return true; // Handled by subscriber
|
||||
}
|
||||
OrientationChanging?.Invoke(this, args);
|
||||
if (args.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_orientation = value;
|
||||
var changedArgs = new EventArgs<Orientation>(oldOrientation, _orientation);
|
||||
OnOrientationChanged(changedArgs);
|
||||
OrientationChanged?.Invoke(this, changedArgs);
|
||||
|
||||
// Default behavior: dispatch to appropriate handler
|
||||
return DispatchToDefaultHandler(args);
|
||||
}
|
||||
|
||||
private bool DispatchToDefaultHandler(InputEventArgs args)
|
||||
{
|
||||
// Default input handling logic
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Workflow**: Event-driven, processing one property change.
|
||||
- **Notifications**: `OnOrientationChanging` (virtual), `OrientationChanging` (event), `OnOrientationChanged`, `OrientationChanged` (post-event).
|
||||
- **Cancellation**: `OnOrientationChanging` returning `true` or `args.Cancel = true`.
|
||||
- **Context**: `CancelEventArgs<Orientation>` provides old and new values.
|
||||
- **Default Behavior**: Updates `_orientation` and notifies via `OrientationChanged`.
|
||||
- **Use Case**: Allows customization of orientation changes (e.g., adjusting layout) or cancellation (e.g., rejecting invalid orientations).
|
||||
**Key Points:**
|
||||
- Uses `Handled` property (common for input events) instead of `Cancel`
|
||||
- Returns `bool` to indicate whether the input was processed
|
||||
- Subclasses can override `OnInputReceived` for custom behavior
|
||||
|
||||
### Example 3: Command Execution
|
||||
|
||||
A `CommandExecutor` class executes commands with cancellable pre-execution hooks.
|
||||
|
||||
```csharp
|
||||
public class CommandExecutor
|
||||
{
|
||||
public event EventHandler<CommandEventArgs>? Executing;
|
||||
|
||||
protected virtual bool OnExecuting(CommandEventArgs args)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool? Execute(Command command)
|
||||
{
|
||||
CommandEventArgs args = new () { Command = command };
|
||||
|
||||
// Pre-execution notification
|
||||
if (OnExecuting(args) || args.Handled)
|
||||
{
|
||||
return true; // Cancelled or handled by subclass
|
||||
}
|
||||
|
||||
Executing?.Invoke(this, args);
|
||||
if (args.Handled)
|
||||
{
|
||||
return true; // Handled by subscriber
|
||||
}
|
||||
|
||||
// Default execution
|
||||
return command.Execute();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Combines virtual method return value with `args.Handled` check
|
||||
- Returns `bool?` to distinguish between handled (`true`), not handled (`false`), and no subscribers (`null`)
|
||||
|
||||
### Example 4: Property Changes (Cancellable)
|
||||
|
||||
A `ConfigurationManager` class manages configuration properties with cancellable change notifications.
|
||||
|
||||
```csharp
|
||||
public class ConfigurationManager
|
||||
{
|
||||
private string _theme = "Default";
|
||||
|
||||
// Pre-change event (cancellable)
|
||||
public event EventHandler<ValueChangingEventArgs<string>>? ThemeChanging;
|
||||
// Post-change event (notification only)
|
||||
public event EventHandler<ValueChangedEventArgs<string>>? ThemeChanged;
|
||||
|
||||
protected virtual bool OnThemeChanging(ValueChangingEventArgs<string> args)
|
||||
{
|
||||
return false; // Return true to cancel
|
||||
}
|
||||
|
||||
protected virtual void OnThemeChanged(ValueChangedEventArgs<string> args)
|
||||
{
|
||||
// React to the completed change
|
||||
}
|
||||
|
||||
public string Theme
|
||||
{
|
||||
get => _theme;
|
||||
set
|
||||
{
|
||||
if (_theme == value)
|
||||
{
|
||||
return; // No change
|
||||
}
|
||||
|
||||
string currentValue = _theme;
|
||||
ValueChangingEventArgs<string> changingArgs = new (currentValue, value);
|
||||
|
||||
// Pre-change: virtual method
|
||||
if (OnThemeChanging(changingArgs) || changingArgs.Handled)
|
||||
{
|
||||
return; // Cancelled by subclass
|
||||
}
|
||||
|
||||
// Pre-change: event
|
||||
ThemeChanging?.Invoke(this, changingArgs);
|
||||
if (changingArgs.Handled)
|
||||
{
|
||||
return; // Cancelled by subscriber
|
||||
}
|
||||
|
||||
// Apply the change (may use modified value from args)
|
||||
_theme = changingArgs.NewValue;
|
||||
|
||||
// Post-change notifications
|
||||
ValueChangedEventArgs<string> changedArgs = new (currentValue, _theme);
|
||||
OnThemeChanged(changedArgs);
|
||||
ThemeChanged?.Invoke(this, changedArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Two events: `ThemeChanging` (cancellable, pre-change) and `ThemeChanged` (notification, post-change)
|
||||
- `ValueChangingEventArgs` allows subscribers to modify the new value via `args.NewValue`
|
||||
- `ValueChangedEventArgs` provides old and new values for notification
|
||||
- Post-change virtual method `OnThemeChanged` returns `void` (not cancellable)
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
Consistent naming is critical for CWP implementations:
|
||||
|
||||
| Element | Convention | Example |
|
||||
|---------|------------|---------|
|
||||
| Pre-change/execution event | `<Action>ing` | `Validating`, `ThemeChanging` |
|
||||
| Post-change/execution event | `<Action>ed` | `Validated`, `ThemeChanged` |
|
||||
| Pre-change virtual method | `On<Action>ing` | `OnValidating`, `OnThemeChanging` |
|
||||
| Post-change virtual method | `On<Action>ed` | `OnValidated`, `OnThemeChanged` |
|
||||
| Handled property | `Handled` | `args.Handled = true` |
|
||||
|
||||
## Event Arguments
|
||||
|
||||
### For Cancellable Operations
|
||||
|
||||
```csharp
|
||||
public class ValueChangingEventArgs<T> : EventArgs
|
||||
{
|
||||
public T CurrentValue { get; }
|
||||
public T NewValue { get; set; } // Can be modified
|
||||
public bool Handled { get; set; }
|
||||
|
||||
public ValueChangingEventArgs(T currentValue, T newValue)
|
||||
{
|
||||
CurrentValue = currentValue;
|
||||
NewValue = newValue;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### For Post-Change Notifications
|
||||
|
||||
```csharp
|
||||
public class ValueChangedEventArgs<T> : EventArgs
|
||||
{
|
||||
public T OldValue { get; }
|
||||
public T NewValue { get; }
|
||||
|
||||
public ValueChangedEventArgs(T oldValue, T newValue)
|
||||
{
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### For Input/Command Handling
|
||||
|
||||
```csharp
|
||||
public class InputEventArgs : EventArgs
|
||||
{
|
||||
public bool Handled { get; set; }
|
||||
// Additional context properties...
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation in Terminal.Gui
|
||||
|
||||
Terminal.Gui implements CWP consistently across the framework. For detailed, Terminal.Gui-specific guidance including:
|
||||
|
||||
- Property changes with `CWPPropertyHelper`
|
||||
- Workflow execution with `CWPWorkflowHelper`
|
||||
- View rendering, keyboard input, and command handling
|
||||
- Practical code templates and recipes
|
||||
|
||||
See the [Events Deep Dive](events.md).
|
||||
|
||||
|
||||
@@ -1,467 +1,524 @@
|
||||
# Terminal.Gui Event Deep Dive
|
||||
# Terminal.Gui Events 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](cancellable-work-pattern.md).
|
||||
This document provides practical guidance for implementing events in Terminal.Gui using the [Cancellable Work Pattern (CWP)](cancellable-work-pattern.md).
|
||||
|
||||
> [!TIP]
|
||||
> **New to CWP?** Read the [Cancellable Work Pattern](cancellable-work-pattern.md) 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](#recipe-1-cancellable-property-change) |
|
||||
| Action/workflow (cancellable) | Manual CWP or `CWPWorkflowHelper` | [Recipe 2](#recipe-2-cancellable-action-workflow) |
|
||||
| Simple notification (no cancel) | `EventHandler` | [Recipe 3](#recipe-3-simple-notification) |
|
||||
| Property notification (MVVM) | `INotifyPropertyChanged` | [Recipe 4](#recipe-4-mvvm-property-notification) |
|
||||
|
||||
## See Also
|
||||
|
||||
* [Cancellable Work Pattern](cancellable-work-pattern.md)
|
||||
* [Command Deep Dive](command.md)
|
||||
* [Cancellable Work Pattern](cancellable-work-pattern.md) - Conceptual overview
|
||||
* [Command Deep Dive](command.md) - Command system details
|
||||
* [Lexicon & Taxonomy](lexicon.md)
|
||||
|
||||
## Lexicon and Taxonomy
|
||||
|
||||
[!INCLUDE [Events Lexicon](~/includes/events-lexicon.md)]
|
||||
|
||||
## Event Categories
|
||||
---
|
||||
|
||||
Terminal.Gui uses several types of events:
|
||||
## Recipes: Implementing CWP in Terminal.Gui
|
||||
|
||||
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
|
||||
### Recipe 1: Cancellable Property Change
|
||||
|
||||
## Event Patterns
|
||||
**Use when:** A property change can be vetoed/cancelled by subclasses or subscribers.
|
||||
|
||||
### 1. Cancellable Work Pattern (CWP)
|
||||
|
||||
The [Cancellable Work Pattern (CWP)](cancellable-work-pattern.md) 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:
|
||||
#### Step 1: Define the Events and Virtual Methods
|
||||
|
||||
```csharp
|
||||
public class MyView : View
|
||||
public class MyDataView : View
|
||||
{
|
||||
// Public event
|
||||
public event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
private object? _dataSource;
|
||||
|
||||
// Protected virtual method
|
||||
protected virtual bool OnMouseEvent(MouseEventArgs args)
|
||||
{
|
||||
// Return true to handle the event and stop propagation
|
||||
return false;
|
||||
}
|
||||
// Pre-change event (cancellable)
|
||||
public event EventHandler<ValueChangingEventArgs<object?>>? DataSourceChanging;
|
||||
|
||||
// Internal method to raise the event
|
||||
internal bool RaiseMouseEvent(MouseEventArgs args)
|
||||
{
|
||||
// Call virtual method first
|
||||
if (OnMouseEvent(args) || args.Handled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Post-change event (notification)
|
||||
public event EventHandler<ValueChangedEventArgs<object?>>? DataSourceChanged;
|
||||
|
||||
// Then raise the event
|
||||
MouseEvent?.Invoke(this, args);
|
||||
return args.Handled;
|
||||
}
|
||||
// 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) { }
|
||||
}
|
||||
```
|
||||
|
||||
#### CWP with Helper Classes
|
||||
|
||||
Terminal.Gui provides static helper classes to implement CWP:
|
||||
|
||||
#### Property Changes
|
||||
|
||||
For property changes, use `CWPPropertyHelper.ChangeProperty`:
|
||||
#### Step 2: Implement the Property with CWPPropertyHelper
|
||||
|
||||
```csharp
|
||||
public class MyView : View
|
||||
public object? DataSource
|
||||
{
|
||||
private string _text = string.Empty;
|
||||
public event EventHandler<ValueChangingEventArgs<string>>? TextChanging;
|
||||
public event EventHandler<ValueChangedEventArgs<string>>? TextChanged;
|
||||
|
||||
public string Text
|
||||
get => _dataSource;
|
||||
set
|
||||
{
|
||||
get => _text;
|
||||
set
|
||||
{
|
||||
if (CWPPropertyHelper.ChangeProperty(
|
||||
currentValue: _text,
|
||||
if (CWPPropertyHelper.ChangeProperty (
|
||||
sender: this,
|
||||
currentValue: ref _dataSource,
|
||||
newValue: value,
|
||||
onChanging: args => OnTextChanging(args),
|
||||
changingEvent: TextChanging,
|
||||
onChanged: args => OnTextChanged(args),
|
||||
changedEvent: TextChanged,
|
||||
out string finalValue))
|
||||
{
|
||||
_text = finalValue;
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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`:
|
||||
#### Step 3: Consuming the Events
|
||||
|
||||
```csharp
|
||||
public class MyView : View
|
||||
// External subscriber (event)
|
||||
myDataView.DataSourceChanging += (sender, args) =>
|
||||
{
|
||||
public bool? ExecuteWorkflow()
|
||||
if (args.NewValue is null)
|
||||
{
|
||||
ResultEventArgs<bool> args = new();
|
||||
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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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: args => OnExecuting(args),
|
||||
eventHandler: Executing,
|
||||
onMethod: OnProcessing,
|
||||
eventHandler: Processing,
|
||||
args: args,
|
||||
defaultAction: () =>
|
||||
{
|
||||
// Main execution logic
|
||||
DoWork();
|
||||
// Default processing logic
|
||||
DoProcessing();
|
||||
args.Result = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Virtual method called before execution
|
||||
protected virtual bool OnExecuting(ResultEventArgs<bool> args)
|
||||
private void DoProcessing()
|
||||
{
|
||||
// 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`:
|
||||
|
||||
```csharp
|
||||
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;
|
||||
// Processing logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Property Change Notifications
|
||||
---
|
||||
|
||||
For property change notifications, implement `INotifyPropertyChanged`. For example, in `Aligner`:
|
||||
### 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.
|
||||
|
||||
```csharp
|
||||
public class Aligner : INotifyPropertyChanged
|
||||
public class MyView : View
|
||||
{
|
||||
private Alignment _alignment;
|
||||
// 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`.
|
||||
|
||||
```csharp
|
||||
public class ViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private string _name = string.Empty;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public Alignment Alignment
|
||||
public string Name
|
||||
{
|
||||
get => _alignment;
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
_alignment = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Alignment)));
|
||||
if (_name != value)
|
||||
{
|
||||
_name = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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:
|
||||
## Event Categories in Terminal.Gui
|
||||
|
||||
```csharp
|
||||
private bool? HandleHotKeyCommand (ICommandContext commandContext)
|
||||
{
|
||||
bool cachedIsDefault = IsDefault; // Supports "Swap Default" in Buttons scenario where IsDefault changes
|
||||
Terminal.Gui uses several types of events:
|
||||
|
||||
if (RaiseActivating (commandContext) is true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
| 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` |
|
||||
|
||||
bool? handled = RaiseAccepting (commandContext);
|
||||
## Event Context and Arguments
|
||||
|
||||
if (handled == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
### Standard Event Arguments
|
||||
|
||||
SetFocus ();
|
||||
Terminal.Gui provides these event argument types:
|
||||
|
||||
// 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`:
|
||||
|
||||
```csharp
|
||||
public class CommandEventArgs : EventArgs
|
||||
{
|
||||
public ICommandContext? Context { get; set; }
|
||||
public bool Handled { get; set; }
|
||||
public bool Cancel { get; set; }
|
||||
}
|
||||
```
|
||||
| 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
|
||||
|
||||
Command execution includes context through `ICommandContext` and input bindings:
|
||||
When handling command events, rich context is available through `ICommandContext`:
|
||||
|
||||
```csharp
|
||||
public interface ICommandContext
|
||||
{
|
||||
/// <summary>The command being invoked.</summary>
|
||||
Command Command { get; set; }
|
||||
|
||||
/// <summary>The View that first invoked the command (the source).</summary>
|
||||
View? Source { get; set; }
|
||||
|
||||
/// <summary>The binding that triggered the command. Use pattern matching to access specific binding types.</summary>
|
||||
IInputBinding? Binding { get; }
|
||||
}
|
||||
|
||||
public interface IInputBinding
|
||||
{
|
||||
/// <summary>The commands this binding will invoke.</summary>
|
||||
Command[] Commands { get; set; }
|
||||
|
||||
/// <summary>Arbitrary context data.</summary>
|
||||
object? Data { get; set; }
|
||||
|
||||
/// <summary>The View that is the origin of this binding.</summary>
|
||||
View? Source { get; set; }
|
||||
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
|
||||
### Binding Types and Pattern Matching
|
||||
|
||||
Terminal.Gui provides three binding types:
|
||||
|
||||
- **`KeyBinding`**: For keyboard-triggered commands. Has `Key` and `Target` properties.
|
||||
- **`MouseBinding`**: For mouse-triggered commands. Has `MouseEvent` property.
|
||||
- **`InputBinding`**: For programmatic/generic command invocations.
|
||||
|
||||
#### Accessing Binding Details
|
||||
|
||||
When handling command events, you can access the binding that triggered the command through pattern matching.
|
||||
|
||||
**Using `ICommandContext.Binding` (polymorphic access):**
|
||||
Terminal.Gui provides three binding types. Use pattern matching to access binding-specific details:
|
||||
|
||||
```csharp
|
||||
// Pattern match on ctx.Binding for polymorphic access
|
||||
if (ctx.Binding is KeyBinding kb)
|
||||
public override bool OnAccepting(object? sender, CommandEventArgs e)
|
||||
{
|
||||
Key? key = kb.Key;
|
||||
}
|
||||
else if (ctx.Binding is MouseBinding mb)
|
||||
{
|
||||
Mouse? mouse = mb.MouseEvent;
|
||||
}
|
||||
else if (ctx.Binding is InputBinding ib)
|
||||
{
|
||||
// Programmatic invocation
|
||||
}
|
||||
```
|
||||
|
||||
**Examples of pattern matching on `Binding`:**
|
||||
|
||||
```csharp
|
||||
// For mouse-triggered commands:
|
||||
if (e.Context?.Binding is MouseBinding { MouseEvent: { } mouse })
|
||||
// Determine what triggered the command
|
||||
switch (e.Context?.Binding)
|
||||
{
|
||||
Point position = mouse.Position!.Value;
|
||||
MouseFlags flags = mouse.Flags;
|
||||
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;
|
||||
}
|
||||
|
||||
// For key-triggered commands:
|
||||
if (e.Context?.Binding is KeyBinding { Key: { } key })
|
||||
{
|
||||
// Handle key-specific logic
|
||||
}
|
||||
```
|
||||
// Or use property patterns for concise access:
|
||||
if (e.Context?.Binding is MouseBinding { MouseEvent: { } mouse })
|
||||
{
|
||||
Point position = mouse.Position;
|
||||
}
|
||||
```
|
||||
|
||||
#### Source Tracking
|
||||
### Source Tracking During Propagation
|
||||
|
||||
- **`ICommandContext.Source`**: The View that first invoked the command. This remains constant during command propagation.
|
||||
- **`IInputBinding.Source`**: The View where the binding was defined/added.
|
||||
- **`sender` (event parameter)**: The View currently raising the event (changes 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** |
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
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
|
||||
### Naming Conventions
|
||||
|
||||
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
|
||||
| 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` |
|
||||
|
||||
3. **Event Context**:
|
||||
- Provide rich context in event args
|
||||
- Include source view and binding details
|
||||
- Add view-specific state when needed
|
||||
### Implementation Guidelines
|
||||
|
||||
4. **Event Propagation**:
|
||||
- Use appropriate propagation mechanisms
|
||||
- Avoid unnecessary event bubbling
|
||||
- Consider using `PropagatedCommands` for hierarchical views
|
||||
1. **Virtual methods return `bool`** for cancellable operations (return `true` to cancel)
|
||||
2. **Virtual methods return `void`** for post-change notifications (cannot cancel)
|
||||
3. **Always call virtual method BEFORE raising the event** (subclasses get priority)
|
||||
4. **Execute default behavior AFTER both checks pass**
|
||||
5. **Unsubscribe in Dispose** to prevent memory leaks
|
||||
|
||||
```csharp
|
||||
// ✅ 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**:
|
||||
```csharp
|
||||
// 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);
|
||||
}
|
||||
```
|
||||
### 1. Memory Leaks from Unsubscribed Events
|
||||
|
||||
2. **Incorrect Event Cancellation**:
|
||||
```csharp
|
||||
// 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
|
||||
```
|
||||
```csharp
|
||||
// ❌ BAD: Potential memory leak
|
||||
view.Accepting += OnAccepting;
|
||||
|
||||
3. **Missing Context**:
|
||||
```csharp
|
||||
// BAD: Missing context
|
||||
Activating?.Invoke(this, new CommandEventArgs());
|
||||
|
||||
// GOOD: Including context
|
||||
Activating?.Invoke(this, new CommandEventArgs { Context = ctx });
|
||||
```
|
||||
// ✅ GOOD: Unsubscribe in Dispose
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
view.Accepting -= OnAccepting;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
```
|
||||
|
||||
## Useful External Documentation
|
||||
### 2. Using Wrong Cancellation Property
|
||||
|
||||
```csharp
|
||||
// ❌ 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
|
||||
|
||||
```csharp
|
||||
// ❌ 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
|
||||
|
||||
```csharp
|
||||
// ❌ 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();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## External Resources
|
||||
|
||||
* [.NET Naming Guidelines - Names of Events](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-type-members?redirectedfrom=MSDN#names-of-events)
|
||||
* [.NET Design for Extensibility - Events and Callbacks](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/events-and-callbacks)
|
||||
* [C# Event Implementation Fundamentals, Best Practices and Conventions](https://www.codeproject.com/Articles/20550/C-Event-Implementation-Fundamentals-Best-Practices)
|
||||
|
||||
## Naming
|
||||
|
||||
TG follows the *naming* advice provided in [.NET Naming Guidelines - Names of Events](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-type-members?redirectedfrom=MSDN#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> PropagatedCommands` to `View`, defaulting to `[Command.Accept]`. `Raise*` methods propagate if the command is in `SuperView?.PropagatedCommands` and `args.Handled` is `false`.
|
||||
- **Example**:
|
||||
|
||||
```csharp
|
||||
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 `MenuBar` 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`:
|
||||
```csharp
|
||||
checkbox.Activating += (sender, args) =>
|
||||
{
|
||||
if (RaiseAccepting(args.Context) is true)
|
||||
{
|
||||
args.Handled = true;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### **Propagation Limitations**:
|
||||
- **Issue**: Local `Command.Activate` restricts `MenuBar` 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.
|
||||
|
||||
Reference in New Issue
Block a user