Simplify IRunnable: Rename Activating→Starting, remove Toplevel complexity

Co-authored-by: tig <585482+tig@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-20 11:10:32 +00:00
parent 54d0a4ba4a
commit 4e1d62b8f5
6 changed files with 111 additions and 804 deletions

View File

@@ -17,7 +17,7 @@ namespace Terminal.Gui.App;
/// </para>
/// <para>
/// This interface follows the Terminal.Gui Cancellable Work Pattern (CWP) for lifecycle events
/// where cancellation makes sense (Stopping, Activating, Deactivating).
/// where cancellation makes sense (Stopping, Starting).
/// </para>
/// </remarks>
public interface IRunnable
@@ -53,60 +53,30 @@ public interface IRunnable
void RaiseStoppingEvent ();
/// <summary>
/// Raises the <see cref="Activating"/> event.
/// Called by <see cref="IApplication.Begin"/> when this runnable is becoming the active session.
/// Raises the <see cref="Starting"/> event.
/// Called by <see cref="IApplication.Begin"/> or <see cref="IApplication.Run"/> when this runnable session is starting.
/// </summary>
/// <param name="deactivated">The previously active runnable being deactivated, or null if none.</param>
/// <returns><see langword="true"/> if activation was canceled; otherwise <see langword="false"/>.</returns>
/// <returns><see langword="true"/> if starting was canceled; otherwise <see langword="false"/>.</returns>
/// <remarks>
/// <para>
/// This method implements the Cancellable Work Pattern for activation:
/// This method implements the Cancellable Work Pattern for starting:
/// 1. Calls virtual method (can cancel)
/// 2. Raises <see cref="Activating"/> event (can cancel)
/// 2. Raises <see cref="Starting"/> event (can cancel)
/// 3. If canceled, returns true
/// 4. If not canceled, calls <see cref="RaiseActivatedEvent"/> and returns false
/// 4. If not canceled, calls <see cref="RaiseStartedEvent"/> and returns false
/// </para>
/// </remarks>
bool RaiseActivatingEvent (IRunnable? deactivated);
bool RaiseStartingEvent ();
/// <summary>
/// Raises the <see cref="Activated"/> event.
/// Called by <see cref="RaiseActivatingEvent"/> after activation succeeds.
/// Raises the <see cref="Started"/> event.
/// Called by <see cref="RaiseStartingEvent"/> after starting succeeds.
/// </summary>
/// <param name="deactivated">The previously active runnable that was deactivated, or null if none.</param>
/// <remarks>
/// This is the post-notification phase of activation. Implementations should raise the
/// <see cref="Activated"/> event.
/// This is the post-notification phase of starting. Implementations should raise the
/// <see cref="Started"/> event.
/// </remarks>
void RaiseActivatedEvent (IRunnable? deactivated);
/// <summary>
/// Raises the <see cref="Deactivating"/> event.
/// Called by <see cref="IApplication.Begin"/> when switching to a new runnable.
/// </summary>
/// <param name="activated">The newly activated runnable, or null if none.</param>
/// <returns><see langword="true"/> if deactivation was canceled; otherwise <see langword="false"/>.</returns>
/// <remarks>
/// <para>
/// This method implements the Cancellable Work Pattern for deactivation:
/// 1. Calls virtual method (can cancel)
/// 2. Raises <see cref="Deactivating"/> event (can cancel)
/// 3. If canceled, returns true
/// 4. If not canceled, calls <see cref="RaiseDeactivatedEvent"/> and returns false
/// </para>
/// </remarks>
bool RaiseDeactivatingEvent (IRunnable? activated);
/// <summary>
/// Raises the <see cref="Deactivated"/> event.
/// Called by <see cref="RaiseDeactivatingEvent"/> after deactivation succeeds.
/// </summary>
/// <param name="activated">The newly activated runnable, or null if none.</param>
/// <remarks>
/// This is the post-notification phase of deactivation. Implementations should raise the
/// <see cref="Deactivated"/> event.
/// </remarks>
void RaiseDeactivatedEvent (IRunnable? activated);
void RaiseStartedEvent ();
#endregion
@@ -162,40 +132,24 @@ public interface IRunnable
event EventHandler? Stopped;
/// <summary>
/// Raised when the runnable session is about to become active (the current session).
/// Can be canceled by setting <see cref="RunnableActivatingEventArgs.Cancel"/> to <see langword="true"/>.
/// Raised when the runnable session is about to start running.
/// Can be canceled by setting <see cref="System.ComponentModel.CancelEventArgs.Cancel"/> to <see langword="true"/>.
/// </summary>
/// <remarks>
/// Subscribe to this event to prevent activation or to perform pre-activation work.
/// Subscribe to this event to prevent starting or to perform pre-start work.
/// This aligns with <see cref="Running"/> and <see cref="IApplication.Run"/>.
/// </remarks>
event EventHandler<RunnableActivatingEventArgs>? Activating;
event EventHandler<System.ComponentModel.CancelEventArgs>? Starting;
/// <summary>
/// Raised when the runnable session has become active.
/// Raised when the runnable session has started running.
/// </summary>
/// <remarks>
/// This is the post-notification event in the Cancellable Work Pattern pair with <see cref="Activating"/>.
/// Subscribe to this event for post-activation logic (e.g., setting focus).
/// This is the post-notification event in the Cancellable Work Pattern pair with <see cref="Starting"/>.
/// Subscribe to this event for post-start logic (e.g., setting focus).
/// This aligns with <see cref="Running"/> and <see cref="IApplication.Run"/>.
/// </remarks>
event EventHandler<RunnableEventArgs>? Activated;
/// <summary>
/// Raised when the runnable session is about to cease being active (another session is activating).
/// Can be canceled by setting <see cref="RunnableDeactivatingEventArgs.Cancel"/> to <see langword="true"/>.
/// </summary>
/// <remarks>
/// Subscribe to this event to prevent deactivation or to perform pre-deactivation work.
/// </remarks>
event EventHandler<RunnableDeactivatingEventArgs>? Deactivating;
/// <summary>
/// Raised when the runnable session has ceased being active.
/// </summary>
/// <remarks>
/// This is the post-notification event in the Cancellable Work Pattern pair with <see cref="Deactivating"/>.
/// Subscribe to this event for cleanup or state preservation after deactivation.
/// </remarks>
event EventHandler<RunnableEventArgs>? Deactivated;
event EventHandler? Started;
#endregion
}

View File

@@ -1,87 +1,5 @@
namespace Terminal.Gui.App;
/// <summary>
/// Event args for <see cref="IRunnable"/> lifecycle events that provide information about the runnable.
/// </summary>
/// <remarks>
/// Used for post-notification events that cannot be canceled:
/// <see cref="IRunnable.Activated"/> and <see cref="IRunnable.Deactivated"/>.
/// </remarks>
public class RunnableEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of <see cref="RunnableEventArgs"/>.
/// </summary>
/// <param name="runnable">The runnable involved in the event.</param>
public RunnableEventArgs (IRunnable runnable)
{
Runnable = runnable;
}
/// <summary>
/// Gets the runnable involved in the event.
/// </summary>
public IRunnable Runnable { get; }
}
/// <summary>
/// Event args for <see cref="IRunnable.Activating"/> event. Allows cancellation.
/// </summary>
/// <remarks>
/// This event is raised when a runnable session is about to become active. It can be canceled
/// by setting <see cref="System.ComponentModel.CancelEventArgs.Cancel"/> to <see langword="true"/>.
/// </remarks>
public class RunnableActivatingEventArgs : System.ComponentModel.CancelEventArgs
{
/// <summary>
/// Initializes a new instance of <see cref="RunnableActivatingEventArgs"/>.
/// </summary>
/// <param name="activating">The runnable that is being activated.</param>
/// <param name="deactivated">The runnable that is being deactivated, or null if none.</param>
public RunnableActivatingEventArgs (IRunnable activating, IRunnable? deactivated)
{
Activating = activating;
Deactivated = deactivated;
}
/// <summary>
/// Gets the runnable that is being activated.
/// </summary>
public IRunnable Activating { get; }
/// <summary>
/// Gets the runnable that is being deactivated, or null if none.
/// </summary>
public IRunnable? Deactivated { get; }
}
/// <summary>
/// Event args for <see cref="IRunnable.Deactivating"/> event. Allows cancellation.
/// </summary>
/// <remarks>
/// This event is raised when a runnable session is about to cease being active. It can be canceled
/// by setting <see cref="System.ComponentModel.CancelEventArgs.Cancel"/> to <see langword="true"/>.
/// </remarks>
public class RunnableDeactivatingEventArgs : System.ComponentModel.CancelEventArgs
{
/// <summary>
/// Initializes a new instance of <see cref="RunnableDeactivatingEventArgs"/>.
/// </summary>
/// <param name="deactivating">The runnable that is being deactivated.</param>
/// <param name="activated">The runnable that is being activated, or null if none.</param>
public RunnableDeactivatingEventArgs (IRunnable deactivating, IRunnable? activated)
{
Deactivating = deactivating;
Activated = activated;
}
/// <summary>
/// Gets the runnable that is being deactivated.
/// </summary>
public IRunnable Deactivating { get; }
/// <summary>
/// Gets the runnable that is being activated, or null if none.
/// </summary>
public IRunnable? Activated { get; }
}
// Note: IRunnable lifecycle events (Starting, Started, Stopping, Stopped, Initializing, Initialized)
// use standard EventArgs or System.ComponentModel.CancelEventArgs.
// No custom event args are needed for the simplified IRunnable interface.

View File

@@ -11,8 +11,7 @@ namespace Terminal.Gui.ViewBase;
/// </para>
/// <para>
/// To customize lifecycle behavior, override the protected virtual methods:
/// <see cref="OnStopping"/>, <see cref="OnStopped"/>, <see cref="OnActivating"/>,
/// <see cref="OnActivated"/>, <see cref="OnDeactivating"/>, <see cref="OnDeactivated"/>.
/// <see cref="OnStarting"/>, <see cref="OnStarted"/>, <see cref="OnStopping"/>, <see cref="OnStopped"/>.
/// </para>
/// </remarks>
public class Runnable : View, IRunnable
@@ -22,6 +21,42 @@ public class Runnable : View, IRunnable
#region IRunnable Implementation (RaiseXxxEvent Methods)
/// <inheritdoc/>
public virtual bool RaiseStartingEvent ()
{
// CWP Phase 1: Pre-notification via virtual method (can cancel)
if (OnStarting ())
{
return true; // Starting canceled
}
// CWP Phase 2: Event notification (can cancel)
var args = new System.ComponentModel.CancelEventArgs ();
Starting?.Invoke (this, args);
if (args.Cancel)
{
return true; // Starting canceled
}
// CWP Phase 3: Perform the work (mark as running)
Running = true;
// CWP Phase 4: Post-notification via virtual method
OnStarted ();
// CWP Phase 5: Post-notification event
Started?.Invoke (this, EventArgs.Empty);
return false; // Starting succeeded
}
/// <inheritdoc/>
public virtual void RaiseStartedEvent ()
{
Started?.Invoke (this, EventArgs.Empty);
}
/// <inheritdoc/>
public virtual void RaiseStoppingEvent ()
{
@@ -50,72 +85,47 @@ public class Runnable : View, IRunnable
Stopped?.Invoke (this, EventArgs.Empty);
}
/// <inheritdoc/>
public virtual bool RaiseActivatingEvent (IRunnable? deactivated)
{
// CWP Phase 1: Pre-notification via virtual method (can cancel)
if (OnActivating (deactivated))
{
return true; // Activation canceled
}
// CWP Phase 2: Event notification (can cancel)
var args = new RunnableActivatingEventArgs (this, deactivated);
Activating?.Invoke (this, args);
if (args.Cancel)
{
return true; // Activation canceled
}
// CWP Phase 3: Work is done by Application (setting Current)
// CWP Phase 4 & 5: Call post-notification methods
OnActivated (deactivated);
return false; // Activation succeeded
}
/// <inheritdoc/>
public virtual void RaiseActivatedEvent (IRunnable? deactivated)
{
Activated?.Invoke (this, new RunnableEventArgs (this));
}
/// <inheritdoc/>
public virtual bool RaiseDeactivatingEvent (IRunnable? activated)
{
// CWP Phase 1: Pre-notification via virtual method (can cancel)
if (OnDeactivating (activated))
{
return true; // Deactivation canceled
}
// CWP Phase 2: Event notification (can cancel)
var args = new RunnableDeactivatingEventArgs (this, activated);
Deactivating?.Invoke (this, args);
if (args.Cancel)
{
return true; // Deactivation canceled
}
// CWP Phase 3: Work is done by Application (changing Current)
// CWP Phase 4 & 5: Call post-notification methods
OnDeactivated (activated);
return false; // Deactivation succeeded
}
/// <inheritdoc/>
public virtual void RaiseDeactivatedEvent (IRunnable? activated)
{
Deactivated?.Invoke (this, new RunnableEventArgs (this));
}
#endregion
#region Protected Virtual Methods (Override Pattern)
/// <summary>
/// Called before <see cref="Starting"/> event. Override to cancel starting.
/// </summary>
/// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
/// <remarks>
/// <para>
/// This is the first phase of the Cancellable Work Pattern for starting.
/// Default implementation returns <see langword="false"/> (allow starting).
/// </para>
/// <para>
/// Override this method to provide custom logic for determining whether the runnable
/// should start (e.g., validating preconditions).
/// </para>
/// </remarks>
protected virtual bool OnStarting ()
{
return false; // Default: allow starting
}
/// <summary>
/// Called after session has started. Override for post-start work.
/// </summary>
/// <remarks>
/// <para>
/// This is the fourth phase of the Cancellable Work Pattern for starting.
/// At this point, <see cref="Running"/> is <see langword="true"/>.
/// Default implementation does nothing.
/// </para>
/// <para>
/// Override this method to perform work that should occur after the session starts.
/// </para>
/// </remarks>
protected virtual void OnStarted ()
{
// Default: do nothing
}
/// <summary>
/// Called before <see cref="Stopping"/> event. Override to cancel stopping.
/// </summary>
@@ -153,110 +163,24 @@ public class Runnable : View, IRunnable
// Default: do nothing
}
/// <summary>
/// Called before <see cref="Activating"/> event. Override to cancel activation.
/// </summary>
/// <param name="deactivated">The previously active runnable being deactivated, or null if none.</param>
/// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
/// <remarks>
/// <para>
/// This is the first phase of the Cancellable Work Pattern for activation.
/// Default implementation returns <see langword="false"/> (allow activation).
/// </para>
/// <para>
/// Override this method to provide custom logic for determining whether the runnable
/// should become active.
/// </para>
/// </remarks>
protected virtual bool OnActivating (IRunnable? deactivated)
{
return false; // Default: allow activation
}
/// <summary>
/// Called after activation succeeds. Override for post-activation logic.
/// </summary>
/// <param name="deactivated">The previously active runnable that was deactivated, or null if none.</param>
/// <remarks>
/// <para>
/// This is the fourth phase of the Cancellable Work Pattern for activation.
/// Default implementation calls <see cref="RaiseActivatedEvent"/>.
/// </para>
/// <para>
/// Override this method to perform work that should occur after activation
/// (e.g., setting focus, updating UI). Overrides must call base to ensure the
/// <see cref="Activated"/> event is raised.
/// </para>
/// </remarks>
protected virtual void OnActivated (IRunnable? deactivated)
{
RaiseActivatedEvent (deactivated);
}
/// <summary>
/// Called before <see cref="Deactivating"/> event. Override to cancel deactivation.
/// </summary>
/// <param name="activated">The newly activated runnable, or null if none.</param>
/// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
/// <remarks>
/// <para>
/// This is the first phase of the Cancellable Work Pattern for deactivation.
/// Default implementation returns <see langword="false"/> (allow deactivation).
/// </para>
/// <para>
/// Override this method to provide custom logic for determining whether the runnable
/// should be deactivated (e.g., preventing switching away if unsaved changes exist).
/// </para>
/// </remarks>
protected virtual bool OnDeactivating (IRunnable? activated)
{
return false; // Default: allow deactivation
}
/// <summary>
/// Called after deactivation succeeds. Override for post-deactivation logic.
/// </summary>
/// <param name="activated">The newly activated runnable, or null if none.</param>
/// <remarks>
/// <para>
/// This is the fourth phase of the Cancellable Work Pattern for deactivation.
/// Default implementation calls <see cref="RaiseDeactivatedEvent"/>.
/// </para>
/// <para>
/// Override this method to perform work that should occur after deactivation
/// (e.g., saving state, releasing resources). Overrides must call base to ensure the
/// <see cref="Deactivated"/> event is raised.
/// </para>
/// </remarks>
protected virtual void OnDeactivated (IRunnable? activated)
{
RaiseDeactivatedEvent (activated);
}
#endregion
#region Events
// Note: Initializing and Initialized events are inherited from View (ISupportInitialize pattern)
/// <inheritdoc/>
public event EventHandler<System.ComponentModel.CancelEventArgs>? Starting;
/// <inheritdoc/>
public event EventHandler? Started;
/// <inheritdoc/>
public event EventHandler<System.ComponentModel.CancelEventArgs>? Stopping;
/// <inheritdoc/>
public event EventHandler? Stopped;
/// <inheritdoc/>
public event EventHandler<RunnableActivatingEventArgs>? Activating;
/// <inheritdoc/>
public event EventHandler<RunnableEventArgs>? Activated;
/// <inheritdoc/>
public event EventHandler<RunnableDeactivatingEventArgs>? Deactivating;
/// <inheritdoc/>
public event EventHandler<RunnableEventArgs>? Deactivated;
#endregion
/// <summary>

View File

@@ -7,19 +7,13 @@ namespace Terminal.Gui.Views;
/// scheme.
/// </summary>
/// <remarks>
/// <para>
/// To run the <see cref="Dialog"/> modally, create the <see cref="Dialog"/>, and pass it to
/// <see cref="IApplication.Run(Toplevel, Func{Exception, bool})"/>. This will execute the dialog until
/// it terminates via the <see cref="Application.QuitKey"/> (`Esc` by default),
/// or when one of the views or buttons added to the dialog calls
/// <see cref="Application.RequestStop"/>.
/// </para>
/// <para>
/// Dialog implements <see cref="IModalRunnable{TResult}"/> with <c>int?</c> as the result type.
/// The <see cref="Result"/> property contains the index of the button that was clicked, or null if canceled.
/// </para>
/// </remarks>
public class Dialog : Window, IModalRunnable<int?>
public class Dialog : Window
{
/// <summary>
/// Initializes a new instance of the <see cref="Dialog"/> class with no <see cref="Button"/>s.
@@ -66,24 +60,6 @@ public class Dialog : Window, IModalRunnable<int?>
_buttons.Add (button);
Add (button);
// Subscribe to the button's Accept command to set Result
button.Accepting += Button_Accepting;
}
private void Button_Accepting (object? sender, CommandEventArgs e)
{
// Set Result to the index of the button that was clicked
if (sender is Button button)
{
int index = _buttons.IndexOf (button);
if (index >= 0)
{
Result = index;
// For backward compatibility, set Canceled = false
Canceled = false;
}
}
}
// TODO: Update button.X = Pos.Justify when alignment changes
@@ -109,14 +85,7 @@ public class Dialog : Window, IModalRunnable<int?>
}
/// <summary>Gets a value indicating whether the <see cref="Dialog"/> was canceled.</summary>
/// <remarks>
/// <para>The default value is <see langword="true"/>.</para>
/// <para>
/// <b>Obsolete:</b> Use <see cref="Result"/> instead. When <see cref="Result"/> is null, the dialog was canceled.
/// This property is maintained for backward compatibility.
/// </para>
/// </remarks>
[Obsolete ("Use Result property instead. Result == null indicates the dialog was canceled.")]
/// <remarks>The default value is <see langword="true"/>.</remarks>
public bool Canceled
{
get { return _canceled; }
@@ -132,29 +101,6 @@ public class Dialog : Window, IModalRunnable<int?>
}
}
/// <summary>
/// Gets or sets the result of the modal dialog operation.
/// </summary>
/// <remarks>
/// <para>
/// Contains the zero-based index of the button that was clicked to close the dialog,
/// or null if the dialog was canceled (e.g., ESC key pressed).
/// </para>
/// <para>
/// The button index corresponds to the order buttons were added via <see cref="AddButton"/> or
/// the <see cref="Buttons"/> initializer.
/// </para>
/// <para>
/// For backward compatibility with the <see cref="Canceled"/> property:
/// - <see cref="Result"/> == null means the dialog was canceled (<see cref="Canceled"/> == true)
/// - <see cref="Result"/> != null means a button was clicked (<see cref="Canceled"/> == false)
/// </para>
/// <para>
/// This property implements <see cref="IModalRunnable{TResult}.Result"/> where TResult is <c>int?</c>.
/// </para>
/// </remarks>
public int? Result { get; set; }
/// <summary>
/// Defines the default border styling for <see cref="Dialog"/>. Can be configured via
/// <see cref="ConfigurationManager"/>.

View File

@@ -17,12 +17,8 @@ namespace Terminal.Gui.Views;
/// and run (e.g. <see cref="Dialog"/>s). To run a Toplevel, create the <see cref="Toplevel"/> and call
/// <see cref="IApplication.Run(Toplevel, Func{Exception, bool})"/>.
/// </para>
/// <para>
/// Toplevel implements <see cref="IRunnable"/> to support the runnable session lifecycle with proper event handling
/// following the Cancellable Work Pattern (CWP).
/// </para>
/// </remarks>
public partial class Toplevel : View, IRunnable
public partial class Toplevel : View
{
/// <summary>
/// Initializes a new instance of the <see cref="Toplevel"/> class,
@@ -96,60 +92,27 @@ public partial class Toplevel : View, IRunnable
/// </summary>
public bool IsLoaded { get; private set; }
// TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Activating/Activate
/// <summary>Invoked when the Toplevel <see cref="SessionToken"/> active.</summary>
/// <remarks>
/// <para>
/// <b>Obsolete:</b> Use <see cref="IRunnable.Activated"/> instead. This event is maintained for backward
/// compatibility and will be raised alongside the new <see cref="IRunnable.Activated"/> event.
/// </para>
/// </remarks>
[Obsolete ("Use IRunnable.Activated instead. This event is maintained for backward compatibility.")]
public event EventHandler<ToplevelEventArgs>? Activate;
// TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Deactivating/Deactivate?
/// <summary>Invoked when the Toplevel<see cref="SessionToken"/> ceases to be active.</summary>
/// <remarks>
/// <para>
/// <b>Obsolete:</b> Use <see cref="IRunnable.Deactivated"/> instead. This event is maintained for backward
/// compatibility and will be raised alongside the new <see cref="IRunnable.Deactivated"/> event.
/// </para>
/// </remarks>
[Obsolete ("Use IRunnable.Deactivated instead. This event is maintained for backward compatibility.")]
public event EventHandler<ToplevelEventArgs>? Deactivate;
/// <summary>Invoked when the Toplevel's <see cref="SessionToken"/> is closed by <see cref="IApplication.End(SessionToken)"/>.</summary>
/// <remarks>
/// <para>
/// <b>Obsolete:</b> This event is maintained for backward compatibility. The IRunnable architecture
/// combines this functionality into the <see cref="IRunnable.Stopped"/> event.
/// </para>
/// </remarks>
[Obsolete ("Use IRunnable.Stopped instead. This event is maintained for backward compatibility.")]
public event EventHandler<ToplevelEventArgs>? Closed;
/// <summary>
/// Invoked when the Toplevel's <see cref="SessionToken"/> is being closed by
/// <see cref="IApplication.RequestStop(Toplevel)"/>.
/// </summary>
/// <remarks>
/// <para>
/// <b>Obsolete:</b> Use <see cref="IRunnable.Stopping"/> instead. This event is maintained for backward
/// compatibility and will be raised alongside the new <see cref="IRunnable.Stopping"/> event.
/// </para>
/// </remarks>
[Obsolete ("Use IRunnable.Stopping instead. This event is maintained for backward compatibility.")]
public event EventHandler<ToplevelClosingEventArgs>? Closing;
/// <summary>
/// Invoked when the <see cref="Toplevel"/> <see cref="SessionToken"/> has begun to be loaded. A Loaded event handler
/// is a good place to finalize initialization before calling Run.
/// </summary>
/// <remarks>
/// <para>
/// <b>Obsolete:</b> Use <see cref="View.Initialized"/> instead. The Loaded event conceptually maps to
/// the Initialized event from the ISupportInitialize pattern, which is now part of IRunnable.
/// </para>
/// </remarks>
[Obsolete ("Use View.Initialized instead. The Loaded event maps to the Initialized event from ISupportInitialize.")]
public event EventHandler? Loaded;
/// <summary>
@@ -181,13 +144,6 @@ public partial class Toplevel : View, IRunnable
/// <see cref="IApplication.Run(Toplevel, Func{Exception, bool})"/> on this <see cref="Toplevel"/>.
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// <b>Obsolete:</b> Use <see cref="View.Initialized"/> instead. The Ready event is similar to Initialized
/// but was fired later in the lifecycle. The IRunnable architecture consolidates these into Initialized.
/// </para>
/// </remarks>
[Obsolete ("Use View.Initialized instead. The Ready event is consolidated into the Initialized event.")]
public event EventHandler? Ready;
/// <summary>
@@ -203,13 +159,6 @@ public partial class Toplevel : View, IRunnable
/// Invoked when the Toplevel <see cref="SessionToken"/> has been unloaded. A Unloaded event handler is a good place
/// to dispose objects after calling <see cref="IApplication.End(SessionToken)"/>.
/// </summary>
/// <remarks>
/// <para>
/// <b>Obsolete:</b> Use <see cref="IRunnable.Stopped"/> instead. The Unloaded event is consolidated into
/// the Stopped event in the IRunnable architecture.
/// </para>
/// </remarks>
[Obsolete ("Use IRunnable.Stopped instead. The Unloaded event is consolidated into the Stopped event.")]
public event EventHandler? Unloaded;
internal virtual void OnActivate (Toplevel deactivated) { Activate?.Invoke (this, new (deactivated)); }
@@ -254,226 +203,6 @@ public partial class Toplevel : View, IRunnable
#endregion
#region IRunnable Implementation
// Note: Running property is already defined above in the Life Cycle region (line 86)
// Note: Initializing and Initialized events are inherited from View (ISupportInitialize pattern)
/// <inheritdoc/>
public event EventHandler<System.ComponentModel.CancelEventArgs>? Stopping;
/// <inheritdoc/>
public event EventHandler? Stopped;
/// <inheritdoc/>
public event EventHandler<RunnableActivatingEventArgs>? Activating;
/// <inheritdoc/>
public event EventHandler<RunnableEventArgs>? Activated;
/// <inheritdoc/>
public event EventHandler<RunnableDeactivatingEventArgs>? Deactivating;
/// <inheritdoc/>
public event EventHandler<RunnableEventArgs>? Deactivated;
/// <inheritdoc/>
public virtual void RaiseStoppingEvent ()
{
// CWP Phase 1: Pre-notification via virtual method (can cancel)
if (OnStopping ())
{
return; // Stopping canceled
}
// CWP Phase 2: Event notification (can cancel)
var args = new System.ComponentModel.CancelEventArgs ();
Stopping?.Invoke (this, args);
if (args.Cancel)
{
return; // Stopping canceled
}
// CWP Phase 3: Perform the work (stop the session)
Running = false;
// CWP Phase 4: Post-notification via virtual method
OnStopped ();
// CWP Phase 5: Post-notification event
Stopped?.Invoke (this, EventArgs.Empty);
}
/// <inheritdoc/>
public virtual bool RaiseActivatingEvent (IRunnable? deactivated)
{
// CWP Phase 1: Pre-notification via virtual method (can cancel)
if (OnActivating (deactivated))
{
return true; // Activation canceled
}
// CWP Phase 2: Event notification (can cancel)
var args = new RunnableActivatingEventArgs (this, deactivated);
Activating?.Invoke (this, args);
if (args.Cancel)
{
return true; // Activation canceled
}
// CWP Phase 3: Work is done by Application (setting Current)
// CWP Phase 4 & 5: Call post-notification methods
OnActivated (deactivated);
return false; // Activation succeeded
}
/// <inheritdoc/>
public virtual void RaiseActivatedEvent (IRunnable? deactivated)
{
Activated?.Invoke (this, new RunnableEventArgs (this));
}
/// <inheritdoc/>
public virtual bool RaiseDeactivatingEvent (IRunnable? activated)
{
// CWP Phase 1: Pre-notification via virtual method (can cancel)
if (OnDeactivating (activated))
{
return true; // Deactivation canceled
}
// CWP Phase 2: Event notification (can cancel)
var args = new RunnableDeactivatingEventArgs (this, activated);
Deactivating?.Invoke (this, args);
if (args.Cancel)
{
return true; // Deactivation canceled
}
// CWP Phase 3: Work is done by Application (changing Current)
// CWP Phase 4 & 5: Call post-notification methods
OnDeactivated (activated);
return false; // Deactivation succeeded
}
/// <inheritdoc/>
public virtual void RaiseDeactivatedEvent (IRunnable? activated)
{
Deactivated?.Invoke (this, new RunnableEventArgs (this));
}
/// <summary>
/// Called before <see cref="Stopping"/> event. Override to cancel stopping.
/// </summary>
/// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
/// <remarks>
/// <para>
/// This is the first phase of the Cancellable Work Pattern for stopping.
/// Default implementation calls the legacy <see cref="OnClosing"/> method for backward compatibility.
/// </para>
/// </remarks>
protected virtual bool OnStopping ()
{
// For backward compatibility, delegate to legacy OnClosing method
var ev = new ToplevelClosingEventArgs (this);
return OnClosing (ev);
}
/// <summary>
/// Called after session has stopped. Override for post-stop cleanup.
/// </summary>
/// <remarks>
/// Default implementation does nothing. For backward compatibility, the legacy <see cref="Closed"/>
/// event is raised by Application.End().
/// </remarks>
protected virtual void OnStopped ()
{
// Default: do nothing
// Note: Legacy Closed event is raised by Application.End()
}
/// <summary>
/// Called before <see cref="Activating"/> event. Override to cancel activation.
/// </summary>
/// <param name="deactivated">The previously active runnable being deactivated, or null if none.</param>
/// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
/// <remarks>
/// Default implementation returns false (allow activation). For backward compatibility,
/// the legacy <see cref="OnActivate"/> method is called after activation succeeds.
/// </remarks>
protected virtual bool OnActivating (IRunnable? deactivated)
{
return false; // Default: allow activation
}
/// <summary>
/// Called after activation succeeds. Override for post-activation logic.
/// </summary>
/// <param name="deactivated">The previously active runnable that was deactivated, or null if none.</param>
/// <remarks>
/// Default implementation raises the <see cref="Activated"/> event and calls the legacy
/// <see cref="OnActivate"/> method for backward compatibility.
/// </remarks>
protected virtual void OnActivated (IRunnable? deactivated)
{
RaiseActivatedEvent (deactivated);
// For backward compatibility, call legacy OnActivate if deactivated is a Toplevel
if (deactivated is Toplevel tl)
{
OnActivate (tl);
}
else
{
// If not a Toplevel, still raise the legacy Activate event with null
Activate?.Invoke (this, new ToplevelEventArgs (null));
}
}
/// <summary>
/// Called before <see cref="Deactivating"/> event. Override to cancel deactivation.
/// </summary>
/// <param name="activated">The newly activated runnable, or null if none.</param>
/// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
/// <remarks>
/// Default implementation returns false (allow deactivation).
/// </remarks>
protected virtual bool OnDeactivating (IRunnable? activated)
{
return false; // Default: allow deactivation
}
/// <summary>
/// Called after deactivation succeeds. Override for post-deactivation logic.
/// </summary>
/// <param name="activated">The newly activated runnable, or null if none.</param>
/// <remarks>
/// Default implementation raises the <see cref="Deactivated"/> event and calls the legacy
/// <see cref="OnDeactivate"/> method for backward compatibility.
/// </remarks>
protected virtual void OnDeactivated (IRunnable? activated)
{
RaiseDeactivatedEvent (activated);
// For backward compatibility, call legacy OnDeactivate if activated is a Toplevel
if (activated is Toplevel tl)
{
OnDeactivate (tl);
}
else
{
// If not a Toplevel, still raise the legacy Deactivate event with null
Deactivate?.Invoke (this, new ToplevelEventArgs (null));
}
}
#endregion
#region Size / Position Management
// TODO: Make cancelable?

View File

@@ -1,164 +0,0 @@
using Xunit;
using Xunit.Abstractions;
namespace UnitTests.ViewsTests;
public class IRunnableTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
[Fact]
public void Toplevel_Implements_IRunnable ()
{
var top = new Toplevel ();
Assert.IsAssignableFrom<IRunnable> (top);
}
[Fact]
public void Dialog_Implements_IModalRunnable ()
{
var dialog = new Dialog ();
Assert.IsAssignableFrom<IRunnable> (dialog);
Assert.IsAssignableFrom<IModalRunnable<int?>> (dialog);
}
[Fact]
public void Runnable_Base_Class_Works ()
{
var runnable = new Runnable ();
Assert.IsAssignableFrom<IRunnable> (runnable);
Assert.False (runnable.Running);
}
[Fact]
[AutoInitShutdown]
public void Dialog_Result_Property_Works ()
{
var dialog = new Dialog ();
var btn1 = new Button { Text = "OK" };
var btn2 = new Button { Text = "Cancel" };
dialog.AddButton (btn1);
dialog.AddButton (btn2);
// Result should be null by default
Assert.Null (dialog.Result);
// Simulate clicking the first button by invoking Accept
btn1.InvokeCommand (Command.Accept);
// Result should now be 0 (first button)
Assert.Equal (0, dialog.Result);
// Reset
dialog.Result = null;
// Simulate clicking the second button
btn2.InvokeCommand (Command.Accept);
// Result should now be 1 (second button)
Assert.Equal (1, dialog.Result);
}
[Fact]
[AutoInitShutdown]
public void Toplevel_Legacy_Events_Still_Work ()
{
// Verify that legacy Toplevel events still fire for backward compatibility
var eventsRaised = new List<string> ();
var top = new Toplevel ();
#pragma warning disable CS0618 // Type or member is obsolete
top.Activate += (s, e) => eventsRaised.Add ("Activate");
#pragma warning restore CS0618
var token = Application.Begin (top);
// Legacy event should still fire
Assert.Contains ("Activate", eventsRaised);
Application.End (token);
}
[Fact]
public void IRunnable_Stopping_Event_Works ()
{
// Test that the Stopping event can be subscribed to and fires
var top = new Toplevel ();
var stoppingFired = false;
var stoppedFired = false;
top.Stopping += (s, e) => { stoppingFired = true; };
top.Stopped += (s, e) => { stoppedFired = true; };
top.Running = true;
top.RaiseStoppingEvent ();
Assert.True (stoppingFired);
Assert.True (stoppedFired);
Assert.False (top.Running);
}
[Fact]
public void IRunnable_Stopping_Can_Be_Canceled ()
{
// Test that the Stopping event can cancel the stop operation
var top = new Toplevel ();
var stoppingFired = false;
var stoppedFired = false;
top.Stopping += (s, e) =>
{
stoppingFired = true;
e.Cancel = true; // Cancel the stop
};
top.Stopped += (s, e) => { stoppedFired = true; };
top.Running = true;
top.RaiseStoppingEvent ();
Assert.True (stoppingFired);
Assert.False (stoppedFired); // Should not fire because it was canceled
Assert.True (top.Running); // Should still be running
}
[Fact]
[AutoInitShutdown]
public void IRunnable_Initialization_Events_Fire ()
{
// Test that Initializing and Initialized events fire
var initializingFired = false;
var initializedFired = false;
var top = new Toplevel ();
top.Initializing += (s, e) => { initializingFired = true; };
top.Initialized += (s, e) => { initializedFired = true; };
var token = Application.Begin (top);
Assert.True (initializingFired);
Assert.True (initializedFired);
Application.End (token);
}
[Fact]
public void IRunnable_Activating_And_Activated_Events_Can_Fire ()
{
// Test that manually calling the activation methods works
var activatingFired = false;
var activatedFired = false;
var top = new Toplevel ();
top.Activating += (s, e) => { activatingFired = true; };
top.Activated += (s, e) => { activatedFired = true; };
// Manually trigger activation
bool canceled = top.RaiseActivatingEvent (null);
Assert.False (canceled);
Assert.True (activatingFired);
Assert.True (activatedFired);
}
}