Browse Source

Fixes #4495 - Adds SuperViewChanging event to View following Cancellable Work Pattern (#4503)

* Initial plan

* Add SuperViewChanging event infrastructure and tests

Co-authored-by: tig <[email protected]>

* Refactor SuperViewChanging to follow Cancellable Work Pattern with cancellation support

Co-authored-by: tig <[email protected]>

* Remove SuperViewChangingEventArgs class and use CancelEventArgs<View?> directly

Co-authored-by: tig <[email protected]>

* Simplified SuperViewChanged

* Refactor SuperView change to use CWP and event args

Refactored the handling of SuperView changes in the View hierarchy to use the Cancellable Work Pattern (CWP) and standardized event argument types. The SuperView property is now settable only via an internal SetSuperView method, which leverages CWP for property changes and cancellation.

- Updated OnSuperViewChanging to accept ValueChangingEventArgs<View?>.
- SuperViewChanging event now uses EventHandler<ValueChangingEventArgs<View?>>; cancellation is via e.Handled = true.
- OnSuperViewChanged and SuperViewChanged now use ValueChangedEventArgs<View?>.
- All SuperView assignments in Add, Remove, and RemoveAll now use SetSuperView, which returns a bool for cancellation.
- CWPPropertyHelper.ChangeProperty now accepts a sender parameter, passed to event handlers.
- All property changes in View now pass this as sender to ChangeProperty.
- Updated event handler signatures and overrides in MenuBar, StatusBar, TextField, and TextView.
- Updated unit tests to use new event args and cancellation pattern.
- Minor code cleanups and improved comments.

These changes modernize and standardize property change handling, improving API consistency, extensibility, and testability.

* Refactor subview removal and event arg types

Removed redundant index check when removing subviews in View.cs, simplifying the cleanup loop. Updated TextField.OnSuperViewChanged to use a non-nullable ValueChangedEventArgs<View> parameter, enforcing stricter type safety.

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: tig <[email protected]>
Co-authored-by: Tig <[email protected]>
Copilot 1 day ago
parent
commit
f4b10511c0

+ 11 - 2
Examples/UICatalog/Scenarios/EditorsAndHelpers/EditorBase.cs

@@ -158,14 +158,23 @@ public abstract class EditorBase : View
     }
     }
 
 
     /// <inheritdoc />
     /// <inheritdoc />
-    protected override void Dispose (bool disposing)
+    protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args)
     {
     {
-        if (disposing && App is {})
+        // Clean up event handlers before SuperView is set to null
+        // This ensures App is still accessible for proper cleanup
+        if (App is {})
         {
         {
             App.Navigation!.FocusedChanged -= NavigationOnFocusedChanged;
             App.Navigation!.FocusedChanged -= NavigationOnFocusedChanged;
             App.Mouse.MouseEvent -= ApplicationOnMouseEvent;
             App.Mouse.MouseEvent -= ApplicationOnMouseEvent;
         }
         }
 
 
+        return base.OnSuperViewChanging (args);
+    }
+
+    /// <inheritdoc />
+    protected override void Dispose (bool disposing)
+    {
+        // Event handlers are now cleaned up in OnSuperViewChanging
         base.Dispose (disposing);
         base.Dispose (disposing);
     }
     }
 }
 }

+ 4 - 1
Terminal.Gui/App/CWP/CWPPropertyHelper.cs

@@ -20,6 +20,8 @@ public static class CWPPropertyHelper
     ///     The type of the property value, which may be a nullable reference type (e.g., <see cref="string"/>
     ///     The type of the property value, which may be a nullable reference type (e.g., <see cref="string"/>
     ///     ?).
     ///     ?).
     /// </typeparam>
     /// </typeparam>
+    /// <param name="sender">The sender of the event. Will be provided as the sender in <paramref name="changingEvent"/>
+    /// and <paramref name="changedEvent"/>.</param>
     /// <param name="currentValue">
     /// <param name="currentValue">
     ///     Reference to the current property value, which may be null for nullable types. If the change is not cancelled, this
     ///     Reference to the current property value, which may be null for nullable types. If the change is not cancelled, this
     ///     will be set to <paramref name="finalValue"/>.
     ///     will be set to <paramref name="finalValue"/>.
@@ -53,6 +55,7 @@ public static class CWPPropertyHelper
     ///     </code>
     ///     </code>
     /// </example>
     /// </example>
     public static bool ChangeProperty<T> (
     public static bool ChangeProperty<T> (
+        object? sender,
         ref T currentValue,
         ref T currentValue,
         T newValue,
         T newValue,
         Func<ValueChangingEventArgs<T>, bool>? onChanging,
         Func<ValueChangingEventArgs<T>, bool>? onChanging,
@@ -85,7 +88,7 @@ public static class CWPPropertyHelper
         }
         }
 
 
         // BUGBUG: This should pass this not null; need to test
         // BUGBUG: This should pass this not null; need to test
-        changingEvent?.Invoke (null, args);
+        changingEvent?.Invoke (sender, args);
 
 
         if (args.Handled)
         if (args.Handled)
         {
         {

+ 2 - 0
Terminal.Gui/ViewBase/View.Drawing.Scheme.cs

@@ -25,6 +25,7 @@ public partial class View
         set
         set
         {
         {
             CWPPropertyHelper.ChangeProperty (
             CWPPropertyHelper.ChangeProperty (
+                                              this,
                                               ref _schemeName,
                                               ref _schemeName,
                                               value,
                                               value,
                                               OnSchemeNameChanging,
                                               OnSchemeNameChanging,
@@ -208,6 +209,7 @@ public partial class View
     public bool SetScheme (Scheme? scheme)
     public bool SetScheme (Scheme? scheme)
     {
     {
         return CWPPropertyHelper.ChangeProperty (
         return CWPPropertyHelper.ChangeProperty (
+                                                 this,
                                                  ref _scheme,
                                                  ref _scheme,
                                                  scheme,
                                                  scheme,
                                                  OnSettingScheme,
                                                  OnSettingScheme,

+ 101 - 36
Terminal.Gui/ViewBase/View.Hierarchy.cs

@@ -1,4 +1,3 @@
-using System.Collections.ObjectModel;
 using System.Diagnostics;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
 
 
@@ -6,7 +5,6 @@ namespace Terminal.Gui.ViewBase;
 
 
 public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.)
 public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.)
 {
 {
-    [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
     private static readonly IReadOnlyCollection<View> _empty = [];
     private static readonly IReadOnlyCollection<View> _empty = [];
 
 
     private readonly List<View>? _subviews = [];
     private readonly List<View>? _subviews = [];
@@ -27,48 +25,73 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     ///     Gets this Views SuperView (the View's container), or <see langword="null"/> if this view has not been added as a
     ///     Gets this Views SuperView (the View's container), or <see langword="null"/> if this view has not been added as a
     ///     SubView.
     ///     SubView.
     /// </summary>
     /// </summary>
+    /// <seealso cref="OnSuperViewChanging"/>
+    /// <seealso cref="SuperViewChanging"/>
     /// <seealso cref="OnSuperViewChanged"/>
     /// <seealso cref="OnSuperViewChanged"/>
     /// <seealso cref="SuperViewChanged"/>
     /// <seealso cref="SuperViewChanged"/>
-    public View? SuperView
-    {
-        get => _superView!;
-        private set => SetSuperView (value);
-    }
+    public View? SuperView => _superView!;
 
 
-    private void SetSuperView (View? value)
+    /// <summary>
+    ///     INTERNAL: Sets the SuperView of this View.
+    /// </summary>
+    /// <param name="value"></param>
+    /// <returns><see langword="true"/> if the SuperView was changed; otherwise, <see langword="false"/>.</returns>
+    private bool SetSuperView (View? value)
     {
     {
         if (_superView == value)
         if (_superView == value)
         {
         {
-            return;
+           return true;
         }
         }
 
 
-        _superView = value;
-        RaiseSuperViewChanged ();
+        return CWPPropertyHelper.ChangeProperty (
+                                                 this,
+                                                 ref _superView,
+                                                 value,
+                                                 OnSuperViewChanging,
+                                                 SuperViewChanging,
+                                                 newValue => _superView = newValue,
+                                                 OnSuperViewChanged,
+                                                 SuperViewChanged,
+                                                 out View? _);
     }
     }
 
 
-    private void RaiseSuperViewChanged ()
-    {
-        SuperViewChangedEventArgs args = new (SuperView, this);
-        OnSuperViewChanged (args);
+    /// <summary>
+    ///     Called when the SuperView of this View is about to be changed. This is called before the SuperView property
+    ///     is updated, allowing access to the current SuperView and its resources (such as <see cref="App"/>) for
+    ///     cleanup purposes.
+    /// </summary>
+    /// <param name="args">Hold the new SuperView that will be set, or <see langword="null"/> if being removed.</param>
+    /// <returns><see langword="true"/> to cancel the change; <see langword="false"/> to allow it.</returns>
+    protected virtual bool OnSuperViewChanging (ValueChangingEventArgs<View?> args) => false;
 
 
-        SuperViewChanged?.Invoke (this, args);
-    }
+    /// <summary>
+    ///     Raised when the SuperView of this View is about to be changed. This is raised before the SuperView property
+    ///     is updated, allowing access to the current SuperView and its resources (such as <see cref="App"/>) for
+    ///     cleanup purposes.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         This event follows the Cancellable Work Pattern (CWP). Set <see cref="ValueChangingEventArgs{T}.Handled"/>
+    ///         to <see langword="true"/> in the event args to cancel the change.
+    ///     </para>
+    /// </remarks>
+    public event EventHandler<ValueChangingEventArgs<View?>>? SuperViewChanging;
 
 
     /// <summary>
     /// <summary>
     ///     Called when the SuperView of this View has changed.
     ///     Called when the SuperView of this View has changed.
     /// </summary>
     /// </summary>
-    /// <param name="e"></param>
-    protected virtual void OnSuperViewChanged (SuperViewChangedEventArgs e) { }
+    protected virtual void OnSuperViewChanged (ValueChangedEventArgs<View?> args) { }
 
 
     /// <summary>Raised when the SuperView of this View has changed.</summary>
     /// <summary>Raised when the SuperView of this View has changed.</summary>
-    public event EventHandler<SuperViewChangedEventArgs>? SuperViewChanged;
+    public event EventHandler<ValueChangedEventArgs<View?>>? SuperViewChanged;
 
 
     #region AddRemove
     #region AddRemove
 
 
+    // TODO: Make this non-virtual once WizardStep is refactored to use events
     /// <summary>Adds a SubView (child) to this view.</summary>
     /// <summary>Adds a SubView (child) to this view.</summary>
     /// <remarks>
     /// <remarks>
     ///     <para>
     ///     <para>
-    ///         The Views that have been added to this view can be retrieved via the <see cref="SubViews"/> property. 
+    ///         The Views that have been added to this view can be retrieved via the <see cref="SubViews"/> property.
     ///     </para>
     ///     </para>
     ///     <para>
     ///     <para>
     ///         To check if a View has been added to this View, compare it's <see cref="SuperView"/> property to this View.
     ///         To check if a View has been added to this View, compare it's <see cref="SuperView"/> property to this View.
@@ -90,7 +113,10 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     /// <seealso cref="RemoveAll"/>
     /// <seealso cref="RemoveAll"/>
     /// <seealso cref="OnSubViewAdded"/>
     /// <seealso cref="OnSubViewAdded"/>
     /// <seealso cref="SubViewAdded"/>
     /// <seealso cref="SubViewAdded"/>
-
+    /// <seealso cref="OnSuperViewChanging"/>
+    /// <seealso cref="SuperViewChanging"/>
+    /// <seealso cref="OnSuperViewChanged"/>
+    /// <seealso cref="SuperViewChanged"/>
     public virtual View? Add (View? view)
     public virtual View? Add (View? view)
     {
     {
         if (view is null)
         if (view is null)
@@ -99,7 +125,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
         }
         }
 
 
         //Debug.Assert (view.SuperView is null, $"{view} already has a SuperView: {view.SuperView}.");
         //Debug.Assert (view.SuperView is null, $"{view} already has a SuperView: {view.SuperView}.");
-        if (view.SuperView is {})
+        if (view.SuperView is { })
         {
         {
             Logging.Warning ($"{view} already has a SuperView: {view.SuperView}.");
             Logging.Warning ($"{view} already has a SuperView: {view.SuperView}.");
         }
         }
@@ -110,12 +136,19 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
             Logging.Warning ($"{view} has already been Added to {this}.");
             Logging.Warning ($"{view} has already been Added to {this}.");
         }
         }
 
 
-        // Ensure views don't have focus when being added
-        view.HasFocus = false;
-
         // TODO: Make this thread safe
         // TODO: Make this thread safe
         InternalSubViews.Add (view);
         InternalSubViews.Add (view);
-        view.SuperView = this;
+
+        // Try to set the SuperView - this may be cancelled
+        if (!view.SetSuperView (this))
+        {
+            InternalSubViews.Remove (view);
+            // The change was cancelled
+            return null;
+        }
+
+        // Ensure views don't have focus when being added
+        view.HasFocus = false;
 
 
         if (view is { Enabled: true, Visible: true, CanFocus: true })
         if (view is { Enabled: true, Visible: true, CanFocus: true })
         {
         {
@@ -193,6 +226,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     /// </remarks>
     /// </remarks>
     public event EventHandler<SuperViewChangedEventArgs>? SubViewAdded;
     public event EventHandler<SuperViewChangedEventArgs>? SubViewAdded;
 
 
+    // TODO: Make this non-virtual once WizardStep is refactored to use events
     /// <summary>Removes a SubView added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary>
     /// <summary>Removes a SubView added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary>
     /// <remarks>
     /// <remarks>
     ///     <para>
     ///     <para>
@@ -210,8 +244,15 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     /// <returns>
     /// <returns>
     ///     The removed View. <see langword="null"/> if the View could not be removed.
     ///     The removed View. <see langword="null"/> if the View could not be removed.
     /// </returns>
     /// </returns>
+    /// <seealso cref="Add(View)"/>
+    /// <seealso cref="RemoveAll"/>
+    /// <seealso cref="OnSubViewAdded"/>
     /// <seealso cref="OnSubViewRemoved"/>
     /// <seealso cref="OnSubViewRemoved"/>
-    /// <seealso cref="SubViewRemoved"/>"/>
+    /// <seealso cref="SubViewRemoved"/>
+    /// <seealso cref="OnSuperViewChanging"/>
+    /// <seealso cref="SuperViewChanging"/>
+    /// <seealso cref="OnSuperViewChanged"/>
+    /// <seealso cref="SuperViewChanged"/>
     public virtual View? Remove (View? view)
     public virtual View? Remove (View? view)
     {
     {
         if (view is null)
         if (view is null)
@@ -221,7 +262,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
 
 
         if (InternalSubViews.Count == 0)
         if (InternalSubViews.Count == 0)
         {
         {
-           return view;
+            return view;
         }
         }
 
 
         if (view.SuperView is null)
         if (view.SuperView is null)
@@ -256,18 +297,28 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
 
 
         Debug.Assert (!view.HasFocus);
         Debug.Assert (!view.HasFocus);
 
 
+        View? previousSuperView = view.SuperView;
+
+        // Try to clear the SuperView - this may be cancelled
+        if (!view.SetSuperView (null))
+        {
+            // The change was cancelled, restore state and return null
+            view.CanFocus = couldFocus;
+
+            return null;
+        }
+
+        Debug.Assert(view.SuperView is null);
         InternalSubViews.Remove (view);
         InternalSubViews.Remove (view);
 
 
         // Clean up focus stuff
         // Clean up focus stuff
         _previouslyFocused = null;
         _previouslyFocused = null;
 
 
-        if (view.SuperView is { } && view.SuperView._previouslyFocused == this)
+        if (previousSuperView is { } && previousSuperView._previouslyFocused == this)
         {
         {
-            view.SuperView._previouslyFocused = null;
+            previousSuperView._previouslyFocused = null;
         }
         }
 
 
-        view.SuperView = null;
-
         SetNeedsLayout ();
         SetNeedsLayout ();
         SetNeedsDraw ();
         SetNeedsDraw ();
 
 
@@ -306,6 +357,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     /// <summary>Raised when a SubView has been added to this View.</summary>
     /// <summary>Raised when a SubView has been added to this View.</summary>
     public event EventHandler<SuperViewChangedEventArgs>? SubViewRemoved;
     public event EventHandler<SuperViewChangedEventArgs>? SubViewRemoved;
 
 
+    // TODO: Make this non-virtual once WizardStep is refactored to use events
     /// <summary>
     /// <summary>
     ///     Removes all SubViews added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
     ///     Removes all SubViews added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
     /// </summary>
     /// </summary>
@@ -320,12 +372,23 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     /// <returns>
     /// <returns>
     ///     A list of removed Views.
     ///     A list of removed Views.
     /// </returns>
     /// </returns>
+    /// <seealso cref="Add(View)"/>
+    /// <seealso cref="Remove(View)"/>
+    /// <seealso cref="OnSubViewAdded"/>
+    /// <seealso cref="OnSubViewRemoved"/>
+    /// <seealso cref="SubViewRemoved"/>
+    /// <seealso cref="OnSuperViewChanging"/>
+    /// <seealso cref="SuperViewChanging"/>
+    /// <seealso cref="OnSuperViewChanged"/>
+    /// <seealso cref="SuperViewChanged"/>
     public virtual IReadOnlyCollection<View> RemoveAll ()
     public virtual IReadOnlyCollection<View> RemoveAll ()
     {
     {
-        List<View> removedList = new List<View> ();
+        List<View> removedList = new ();
+
         while (InternalSubViews.Count > 0)
         while (InternalSubViews.Count > 0)
         {
         {
             View? removed = Remove (InternalSubViews [0]);
             View? removed = Remove (InternalSubViews [0]);
+
             if (removed is { })
             if (removed is { })
             {
             {
                 removedList.Add (removed);
                 removedList.Add (removed);
@@ -349,14 +412,16 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     /// <returns>
     /// <returns>
     ///     A list of removed Views.
     ///     A list of removed Views.
     /// </returns>
     /// </returns>
-    public virtual IReadOnlyCollection<TView> RemoveAll<TView> () where TView : View
+    public IReadOnlyCollection<TView> RemoveAll<TView> () where TView : View
     {
     {
-        List<TView> removedList = new List<TView> ();
+        List<TView> removedList = new ();
+
         foreach (TView view in InternalSubViews.OfType<TView> ().ToList ())
         foreach (TView view in InternalSubViews.OfType<TView> ().ToList ())
         {
         {
             Remove (view);
             Remove (view);
             removedList.Add (view);
             removedList.Add (view);
         }
         }
+
         return removedList.AsReadOnly ();
         return removedList.AsReadOnly ();
     }
     }
 
 

+ 2 - 0
Terminal.Gui/ViewBase/View.Layout.cs

@@ -327,6 +327,7 @@ public partial class View // Layout APIs
         set
         set
         {
         {
             CWPPropertyHelper.ChangeProperty (
             CWPPropertyHelper.ChangeProperty (
+                                              this,
                                               ref _height,
                                               ref _height,
                                               value,
                                               value,
                                               OnHeightChanging,
                                               OnHeightChanging,
@@ -415,6 +416,7 @@ public partial class View // Layout APIs
         set
         set
         {
         {
             CWPPropertyHelper.ChangeProperty (
             CWPPropertyHelper.ChangeProperty (
+                                              this,
                                               ref _width,
                                               ref _width,
                                               value,
                                               value,
                                               OnWidthChanging,
                                               OnWidthChanging,

+ 2 - 3
Terminal.Gui/Views/Menu/MenuBar.cs

@@ -93,7 +93,6 @@ public class MenuBar : Menu, IDesignable
         BorderStyle = DefaultBorderStyle;
         BorderStyle = DefaultBorderStyle;
 
 
         ConfigurationManager.Applied += OnConfigurationManagerApplied;
         ConfigurationManager.Applied += OnConfigurationManagerApplied;
-        SuperViewChanged += OnSuperViewChanged;
 
 
         return;
         return;
 
 
@@ -102,7 +101,8 @@ public class MenuBar : Menu, IDesignable
         bool? MoveRight (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); }
         bool? MoveRight (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); }
     }
     }
 
 
-    private void OnSuperViewChanged (object? sender, SuperViewChangedEventArgs e)
+    /// <inheritdoc />
+    protected override void OnSuperViewChanged (ValueChangedEventArgs<View?> e)
     {
     {
         if (SuperView is null)
         if (SuperView is null)
         {
         {
@@ -758,7 +758,6 @@ public class MenuBar : Menu, IDesignable
 
 
         if (disposing)
         if (disposing)
         {
         {
-            SuperViewChanged += OnSuperViewChanged;
             ConfigurationManager.Applied -= OnConfigurationManagerApplied;
             ConfigurationManager.Applied -= OnConfigurationManagerApplied;
         }
         }
     }
     }

+ 2 - 3
Terminal.Gui/Views/StatusBar.cs

@@ -31,10 +31,10 @@ public class StatusBar : Bar, IDesignable
         SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Menu);
         SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Menu);
 
 
         ConfigurationManager.Applied += OnConfigurationManagerApplied;
         ConfigurationManager.Applied += OnConfigurationManagerApplied;
-        SuperViewChanged += OnSuperViewChanged;
     }
     }
 
 
-    private void OnSuperViewChanged (object? sender, SuperViewChangedEventArgs e)
+    /// <inheritdoc />
+    protected override void OnSuperViewChanged (ValueChangedEventArgs<View?> e)
     {
     {
         if (SuperView is null)
         if (SuperView is null)
         {
         {
@@ -174,7 +174,6 @@ public class StatusBar : Bar, IDesignable
     {
     {
         base.Dispose (disposing);
         base.Dispose (disposing);
 
 
-        SuperViewChanged -= OnSuperViewChanged;
         ConfigurationManager.Applied -= OnConfigurationManagerApplied;
         ConfigurationManager.Applied -= OnConfigurationManagerApplied;
     }
     }
 }
 }

+ 5 - 4
Terminal.Gui/Views/TextInput/TextField.cs

@@ -42,8 +42,6 @@ public class TextField : View, IDesignable
 
 
         Initialized += TextField_Initialized;
         Initialized += TextField_Initialized;
 
 
-        SuperViewChanged += TextField_SuperViewChanged;
-
         // Things this view knows how to do
         // Things this view knows how to do
         AddCommand (
         AddCommand (
                     Command.DeleteCharRight,
                     Command.DeleteCharRight,
@@ -1776,9 +1774,11 @@ public class TextField : View, IDesignable
         }
         }
     }
     }
 
 
-    private void TextField_SuperViewChanged (object sender, SuperViewChangedEventArgs e)
+    /// <inheritdoc />
+    protected override void OnSuperViewChanged (ValueChangedEventArgs<View> args)
     {
     {
-        if (e.SuperView is { })
+        base.OnSuperViewChanged (args);
+        if (SuperView is { })
         {
         {
             if (Autocomplete.HostControl is null)
             if (Autocomplete.HostControl is null)
             {
             {
@@ -1792,6 +1792,7 @@ public class TextField : View, IDesignable
         }
         }
     }
     }
 
 
+
     private void TextField_Initialized (object sender, EventArgs e)
     private void TextField_Initialized (object sender, EventArgs e)
     {
     {
         _cursorPosition = Text.GetRuneCount ();
         _cursorPosition = Text.GetRuneCount ();

+ 5 - 4
Terminal.Gui/Views/TextInput/TextView.cs

@@ -120,8 +120,6 @@ public class TextView : View, IDesignable
 
 
         Initialized += TextView_Initialized!;
         Initialized += TextView_Initialized!;
 
 
-        SuperViewChanged += TextView_SuperViewChanged!;
-
         SubViewsLaidOut += TextView_LayoutComplete;
         SubViewsLaidOut += TextView_LayoutComplete;
 
 
         // Things this view knows how to do
         // Things this view knows how to do
@@ -4626,9 +4624,12 @@ public class TextView : View, IDesignable
         return Encoding.Unicode.GetString (encoded, 0, offset);
         return Encoding.Unicode.GetString (encoded, 0, offset);
     }
     }
 
 
-    private void TextView_SuperViewChanged (object sender, SuperViewChangedEventArgs e)
+
+    /// <inheritdoc />
+    protected override void OnSuperViewChanged (ValueChangedEventArgs<View?> args)
     {
     {
-        if (e.SuperView is { })
+        base.OnSuperViewChanged (args);
+        if (SuperView is { })
         {
         {
             if (Autocomplete.HostControl is null)
             if (Autocomplete.HostControl is null)
             {
             {

+ 299 - 8
Tests/UnitTestsParallelizable/ViewBase/SubviewTests.cs

@@ -18,7 +18,7 @@ public class SubViewTests
                               };
                               };
         sub.SuperViewChanged += (s, e) =>
         sub.SuperViewChanged += (s, e) =>
                                 {
                                 {
-                                    if (e.SuperView is { })
+                                    if (sub.SuperView is { })
                                     {
                                     {
                                         subRaisedCount++;
                                         subRaisedCount++;
                                     }
                                     }
@@ -46,7 +46,7 @@ public class SubViewTests
                                 };
                                 };
         sub.SuperViewChanged += (s, e) =>
         sub.SuperViewChanged += (s, e) =>
                               {
                               {
-                                  if (e.SuperView is null)
+                                  if (sub.SuperView is null)
                                   {
                                   {
                                       subRaisedCount++;
                                       subRaisedCount++;
                                   }
                                   }
@@ -393,23 +393,23 @@ public class SubViewTests
         var superView = new View ();
         var superView = new View ();
 
 
         int superViewChangedCount = 0;
         int superViewChangedCount = 0;
-        //int superViewChangingCount = 0;
+        int superViewChangingCount = 0;
 
 
         view.SuperViewChanged += (s, e) =>
         view.SuperViewChanged += (s, e) =>
         {
         {
             superViewChangedCount++;
             superViewChangedCount++;
         };
         };
 
 
-        //view.SuperViewChanging += (s, e) =>
-        //{
-        //    superViewChangingCount++;
-        //};
+        view.SuperViewChanging += (s, e) =>
+        {
+            superViewChangingCount++;
+        };
 
 
         // Act
         // Act
         superView.Add (view);
         superView.Add (view);
 
 
         // Assert
         // Assert
-        //Assert.Equal (1, superViewChangingCount);
+        Assert.Equal (1, superViewChangingCount);
         Assert.Equal (1, superViewChangedCount);
         Assert.Equal (1, superViewChangedCount);
 
 
     }
     }
@@ -692,4 +692,295 @@ public class SubViewTests
         Assert.Single (removedViews);
         Assert.Single (removedViews);
         Assert.Contains (subView2, removedViews);
         Assert.Contains (subView2, removedViews);
     }
     }
+
+    [Fact]
+    public void SuperViewChanging_Raised_Before_SuperViewChanged ()
+    {
+        // Arrange
+        var superView = new View ();
+        var subView = new View ();
+
+        var events = new List<string> ();
+
+        subView.SuperViewChanging += (s, e) => { events.Add ("SuperViewChanging"); };
+
+        subView.SuperViewChanged += (s, e) => { events.Add ("SuperViewChanged"); };
+
+        // Act
+        superView.Add (subView);
+
+        // Assert
+        Assert.Equal (2, events.Count);
+        Assert.Equal ("SuperViewChanging", events [0]);
+        Assert.Equal ("SuperViewChanged", events [1]);
+    }
+
+    [Fact]
+    public void SuperViewChanging_Provides_OldSuperView_On_Add ()
+    {
+        // Arrange
+        var superView = new View ();
+        var subView = new View ();
+
+        View? currentValueInEvent = new View (); // Set to non-null to ensure it gets updated
+        View? newValueInEvent = null;
+
+        subView.SuperViewChanging += (s, e) =>
+                                     {
+                                         currentValueInEvent = e.CurrentValue;
+                                         newValueInEvent = e.NewValue;
+                                     };
+
+        // Act
+        superView.Add (subView);
+
+        // Assert
+        Assert.Null (currentValueInEvent); // Was null before add
+        Assert.Equal (superView, newValueInEvent); // Will be superView after add
+    }
+
+    [Fact]
+    public void SuperViewChanging_Provides_OldSuperView_On_Remove ()
+    {
+        // Arrange
+        var superView = new View ();
+        var subView = new View ();
+
+        superView.Add (subView);
+
+        View? currentValueInEvent = null;
+        View? newValueInEvent = new View (); // Set to non-null to ensure it gets updated
+
+        subView.SuperViewChanging += (s, e) =>
+                                     {
+                                         currentValueInEvent = e.CurrentValue;
+                                         newValueInEvent = e.NewValue;
+                                     };
+
+        // Act
+        superView.Remove (subView);
+
+        // Assert
+        Assert.Equal (superView, currentValueInEvent); // Was superView before remove
+        Assert.Null (newValueInEvent); // Will be null after remove
+    }
+
+    [Fact]
+    public void SuperViewChanging_Allows_Access_To_App_Before_Remove ()
+    {
+        // Arrange
+        using IApplication app = Application.Create ();
+        var runnable = new Runnable<bool> ();
+        var subView = new View ();
+
+        runnable.Add (subView);
+        SessionToken? token = app.Begin (runnable);
+
+        IApplication? appInEvent = null;
+
+        subView.SuperViewChanging += (s, e) =>
+                                     {
+                                         Assert.NotNull (s);
+                                         // At this point, SuperView is still set, so App should be accessible
+                                         appInEvent = (s as View)?.App;
+                                     };
+
+
+        Assert.NotNull (runnable.App);
+
+        // Act
+        runnable.Remove (subView);
+
+        // Assert
+        Assert.NotNull (appInEvent);
+        Assert.Equal (app, appInEvent);
+
+        app.End (token!);
+        runnable.Dispose ();
+    }
+
+    [Fact]
+    public void OnSuperViewChanging_Called_Before_OnSuperViewChanged ()
+    {
+        // Arrange
+        var superView = new View ();
+        var events = new List<string> ();
+
+        var subView = new TestViewWithSuperViewEvents (events);
+
+        // Act
+        superView.Add (subView);
+
+        // Assert
+        Assert.Equal (2, events.Count);
+        Assert.Equal ("OnSuperViewChanging", events [0]);
+        Assert.Equal ("OnSuperViewChanged", events [1]);
+    }
+
+    [Fact]
+    public void SuperViewChanging_Raised_When_Changing_Between_SuperViews ()
+    {
+        // Arrange
+        var superView1 = new View ();
+        var superView2 = new View ();
+        var subView = new View ();
+
+        superView1.Add (subView);
+
+        View? currentValueInEvent = null;
+        View? newValueInEvent = null;
+
+        subView.SuperViewChanging += (s, e) =>
+                                     {
+                                         currentValueInEvent = e.CurrentValue;
+                                         newValueInEvent = e.NewValue;
+                                     };
+
+        // Act
+        superView2.Add (subView);
+
+        // Assert
+        Assert.Equal (superView1, currentValueInEvent);
+        Assert.Equal (superView2, newValueInEvent);
+    }
+
+    // Helper class for testing virtual method calls
+    private class TestViewWithSuperViewEvents : View
+    {
+        private readonly List<string> _events;
+
+        public TestViewWithSuperViewEvents (List<string> events) { _events = events; }
+
+        protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args)
+        {
+            _events.Add ("OnSuperViewChanging");
+            return base.OnSuperViewChanging (args);
+        }
+
+        protected override void OnSuperViewChanged (ValueChangedEventArgs<View?> args)
+        {
+            _events.Add ("OnSuperViewChanged");
+            base.OnSuperViewChanged (args);
+        }
+    }
+
+    [Fact]
+    public void SuperViewChanging_Can_Be_Cancelled_Via_Event ()
+    {
+        // Arrange
+        var superView = new View ();
+        var subView = new View ();
+
+        subView.SuperViewChanging += (s, e) =>
+                                     {
+                                         e.Handled = true; // Cancel the change
+                                     };
+
+        // Act
+        superView.Add (subView);
+
+        // Assert - SuperView should not be set because the change was cancelled
+        Assert.Null (subView.SuperView);
+        Assert.Empty (superView.SubViews);
+    }
+
+    [Fact]
+    public void SuperViewChanging_Can_Be_Cancelled_Via_Virtual_Method ()
+    {
+        // Arrange
+        var superView = new View ();
+        var subView = new TestViewThatCancelsChange ();
+
+        // Act
+        superView.Add (subView);
+
+        // Assert - SuperView should not be set because the change was cancelled
+        Assert.Null (subView.SuperView);
+        Assert.Empty (superView.SubViews);
+    }
+
+    [Fact]
+    public void SuperViewChanging_Virtual_Method_Cancellation_Prevents_Event ()
+    {
+        // Arrange
+        var superView = new View ();
+        var subView = new TestViewThatCancelsChange ();
+
+        var eventRaised = false;
+        subView.SuperViewChanging += (s, e) =>
+                                     {
+                                         eventRaised = true;
+                                     };
+
+        // Act
+        superView.Add (subView);
+
+        // Assert - Event should not be raised because virtual method cancelled first
+        Assert.False (eventRaised);
+        Assert.Null (subView.SuperView);
+    }
+
+    [Fact]
+    public void SuperViewChanging_Cancellation_On_Remove ()
+    {
+        // Arrange
+        var superView = new View ();
+        var subView = new View ();
+
+        superView.Add (subView);
+        Assert.Equal (superView, subView.SuperView);
+
+        subView.SuperViewChanging += (s, e) =>
+                                     {
+                                         // Cancel removal if trying to set to null
+                                         if (e.NewValue is null)
+                                         {
+                                             e.Handled = true;
+                                         }
+                                     };
+
+        // Act
+        superView.Remove (subView);
+
+        // Assert - SuperView should still be set because removal was cancelled
+        Assert.Equal (superView, subView.SuperView);
+        Assert.Single (superView.SubViews);
+    }
+
+    [Fact]
+    public void SuperViewChanging_Cancellation_When_Changing_Between_SuperViews ()
+    {
+        // Arrange
+        var superView1 = new View ();
+        var superView2 = new View ();
+        var subView = new View ();
+
+        superView1.Add (subView);
+
+        subView.SuperViewChanging += (s, e) =>
+                                     {
+                                         // Cancel if trying to move to superView2
+                                         if (e.NewValue == superView2)
+                                         {
+                                             e.Handled = true;
+                                         }
+                                     };
+
+        // Act
+        superView2.Add (subView);
+
+        // Assert - Should still be in superView1 because change was cancelled
+        Assert.Equal (superView1, subView.SuperView);
+        Assert.Single (superView1.SubViews);
+        Assert.Empty (superView2.SubViews);
+    }
+
+    // Helper class for testing cancellation
+    private class TestViewThatCancelsChange : View
+    {
+        protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args)
+        {
+            return true; // Always cancel the change
+        }
+    }
 }
 }