From f4b10511c0720481743064ebcf6e7b56ddc009eb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 06:32:01 -0700 Subject: [PATCH] Fixes #4495 - Adds SuperViewChanging event to View following Cancellable Work Pattern (#4503) * Initial plan * Add SuperViewChanging event infrastructure and tests Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor SuperViewChanging to follow Cancellable Work Pattern with cancellation support Co-authored-by: tig <585482+tig@users.noreply.github.com> * Remove SuperViewChangingEventArgs class and use CancelEventArgs directly Co-authored-by: tig <585482+tig@users.noreply.github.com> * 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. - SuperViewChanging event now uses EventHandler>; cancellation is via e.Handled = true. - OnSuperViewChanged and SuperViewChanged now use ValueChangedEventArgs. - 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 parameter, enforcing stricter type safety. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: Tig --- .../Scenarios/EditorsAndHelpers/EditorBase.cs | 13 +- Terminal.Gui/App/CWP/CWPPropertyHelper.cs | 5 +- Terminal.Gui/ViewBase/View.Drawing.Scheme.cs | 2 + Terminal.Gui/ViewBase/View.Hierarchy.cs | 137 ++++++-- Terminal.Gui/ViewBase/View.Layout.cs | 2 + Terminal.Gui/Views/Menu/MenuBar.cs | 5 +- Terminal.Gui/Views/StatusBar.cs | 5 +- Terminal.Gui/Views/TextInput/TextField.cs | 9 +- Terminal.Gui/Views/TextInput/TextView.cs | 9 +- .../ViewBase/SubviewTests.cs | 307 +++++++++++++++++- 10 files changed, 433 insertions(+), 61 deletions(-) diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/EditorBase.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/EditorBase.cs index 9556be4a2..b37b9f02b 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/EditorBase.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/EditorBase.cs @@ -158,14 +158,23 @@ public abstract class EditorBase : View } /// - protected override void Dispose (bool disposing) + protected override bool OnSuperViewChanging (ValueChangingEventArgs 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.Mouse.MouseEvent -= ApplicationOnMouseEvent; } + return base.OnSuperViewChanging (args); + } + + /// + protected override void Dispose (bool disposing) + { + // Event handlers are now cleaned up in OnSuperViewChanging base.Dispose (disposing); } } diff --git a/Terminal.Gui/App/CWP/CWPPropertyHelper.cs b/Terminal.Gui/App/CWP/CWPPropertyHelper.cs index d7095fd7e..739502115 100644 --- a/Terminal.Gui/App/CWP/CWPPropertyHelper.cs +++ b/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., /// ?). /// + /// The sender of the event. Will be provided as the sender in + /// and . /// /// Reference to the current property value, which may be null for nullable types. If the change is not cancelled, this /// will be set to . @@ -53,6 +55,7 @@ public static class CWPPropertyHelper /// /// public static bool ChangeProperty ( + object? sender, ref T currentValue, T newValue, Func, bool>? onChanging, @@ -85,7 +88,7 @@ public static class CWPPropertyHelper } // BUGBUG: This should pass this not null; need to test - changingEvent?.Invoke (null, args); + changingEvent?.Invoke (sender, args); if (args.Handled) { diff --git a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs index 5ad8c0101..4ecf644b3 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs @@ -25,6 +25,7 @@ public partial class View set { CWPPropertyHelper.ChangeProperty ( + this, ref _schemeName, value, OnSchemeNameChanging, @@ -208,6 +209,7 @@ public partial class View public bool SetScheme (Scheme? scheme) { return CWPPropertyHelper.ChangeProperty ( + this, ref _scheme, scheme, OnSettingScheme, diff --git a/Terminal.Gui/ViewBase/View.Hierarchy.cs b/Terminal.Gui/ViewBase/View.Hierarchy.cs index 53848fbfd..9c27dfdc6 100644 --- a/Terminal.Gui/ViewBase/View.Hierarchy.cs +++ b/Terminal.Gui/ViewBase/View.Hierarchy.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using System.Diagnostics; 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.) { - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] private static readonly IReadOnlyCollection _empty = []; private readonly List? _subviews = []; @@ -27,48 +25,73 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, /// Gets this Views SuperView (the View's container), or if this view has not been added as a /// SubView. /// + /// + /// /// /// - public View? SuperView - { - get => _superView!; - private set => SetSuperView (value); - } + public View? SuperView => _superView!; - private void SetSuperView (View? value) + /// + /// INTERNAL: Sets the SuperView of this View. + /// + /// + /// if the SuperView was changed; otherwise, . + private bool SetSuperView (View? 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); + /// + /// 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 ) for + /// cleanup purposes. + /// + /// Hold the new SuperView that will be set, or if being removed. + /// to cancel the change; to allow it. + protected virtual bool OnSuperViewChanging (ValueChangingEventArgs args) => false; - SuperViewChanged?.Invoke (this, args); - } + /// + /// 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 ) for + /// cleanup purposes. + /// + /// + /// + /// This event follows the Cancellable Work Pattern (CWP). Set + /// to in the event args to cancel the change. + /// + /// + public event EventHandler>? SuperViewChanging; /// /// Called when the SuperView of this View has changed. /// - /// - protected virtual void OnSuperViewChanged (SuperViewChangedEventArgs e) { } + protected virtual void OnSuperViewChanged (ValueChangedEventArgs args) { } /// Raised when the SuperView of this View has changed. - public event EventHandler? SuperViewChanged; + public event EventHandler>? SuperViewChanged; #region AddRemove + // TODO: Make this non-virtual once WizardStep is refactored to use events /// Adds a SubView (child) to this view. /// /// - /// The Views that have been added to this view can be retrieved via the property. + /// The Views that have been added to this view can be retrieved via the property. /// /// /// To check if a View has been added to this View, compare it's property to this View. @@ -90,7 +113,10 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, /// /// /// - + /// + /// + /// + /// public virtual View? Add (View? view) { 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}."); - if (view.SuperView is {}) + if (view.SuperView is { }) { 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}."); } - // Ensure views don't have focus when being added - view.HasFocus = false; - // TODO: Make this thread safe 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 }) { @@ -193,6 +226,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, /// public event EventHandler? SubViewAdded; + // TODO: Make this non-virtual once WizardStep is refactored to use events /// Removes a SubView added via or from this View. /// /// @@ -210,8 +244,15 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, /// /// The removed View. if the View could not be removed. /// + /// + /// + /// /// - /// "/> + /// + /// + /// + /// + /// public virtual View? Remove (View? view) { if (view is null) @@ -221,7 +262,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, if (InternalSubViews.Count == 0) { - return view; + return view; } if (view.SuperView is null) @@ -256,18 +297,28 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, 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); // Clean up focus stuff _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 (); SetNeedsDraw (); @@ -306,6 +357,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, /// Raised when a SubView has been added to this View. public event EventHandler? SubViewRemoved; + // TODO: Make this non-virtual once WizardStep is refactored to use events /// /// Removes all SubViews added via or from this View. /// @@ -320,12 +372,23 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, /// /// A list of removed Views. /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public virtual IReadOnlyCollection RemoveAll () { - List removedList = new List (); + List removedList = new (); + while (InternalSubViews.Count > 0) { View? removed = Remove (InternalSubViews [0]); + if (removed is { }) { removedList.Add (removed); @@ -349,14 +412,16 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, /// /// A list of removed Views. /// - public virtual IReadOnlyCollection RemoveAll () where TView : View + public IReadOnlyCollection RemoveAll () where TView : View { - List removedList = new List (); + List removedList = new (); + foreach (TView view in InternalSubViews.OfType ().ToList ()) { Remove (view); removedList.Add (view); } + return removedList.AsReadOnly (); } diff --git a/Terminal.Gui/ViewBase/View.Layout.cs b/Terminal.Gui/ViewBase/View.Layout.cs index 618acb6e3..e5915bc5e 100644 --- a/Terminal.Gui/ViewBase/View.Layout.cs +++ b/Terminal.Gui/ViewBase/View.Layout.cs @@ -327,6 +327,7 @@ public partial class View // Layout APIs set { CWPPropertyHelper.ChangeProperty ( + this, ref _height, value, OnHeightChanging, @@ -415,6 +416,7 @@ public partial class View // Layout APIs set { CWPPropertyHelper.ChangeProperty ( + this, ref _width, value, OnWidthChanging, diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 82882b979..1c02d6735 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -93,7 +93,6 @@ public class MenuBar : Menu, IDesignable BorderStyle = DefaultBorderStyle; ConfigurationManager.Applied += OnConfigurationManagerApplied; - SuperViewChanged += OnSuperViewChanged; return; @@ -102,7 +101,8 @@ public class MenuBar : Menu, IDesignable bool? MoveRight (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); } } - private void OnSuperViewChanged (object? sender, SuperViewChangedEventArgs e) + /// + protected override void OnSuperViewChanged (ValueChangedEventArgs e) { if (SuperView is null) { @@ -758,7 +758,6 @@ public class MenuBar : Menu, IDesignable if (disposing) { - SuperViewChanged += OnSuperViewChanged; ConfigurationManager.Applied -= OnConfigurationManagerApplied; } } diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index f2bc31af6..022ba0033 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -31,10 +31,10 @@ public class StatusBar : Bar, IDesignable SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Menu); ConfigurationManager.Applied += OnConfigurationManagerApplied; - SuperViewChanged += OnSuperViewChanged; } - private void OnSuperViewChanged (object? sender, SuperViewChangedEventArgs e) + /// + protected override void OnSuperViewChanged (ValueChangedEventArgs e) { if (SuperView is null) { @@ -174,7 +174,6 @@ public class StatusBar : Bar, IDesignable { base.Dispose (disposing); - SuperViewChanged -= OnSuperViewChanged; ConfigurationManager.Applied -= OnConfigurationManagerApplied; } } diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index ca9e6f519..72d2dc8ec 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -42,8 +42,6 @@ public class TextField : View, IDesignable Initialized += TextField_Initialized; - SuperViewChanged += TextField_SuperViewChanged; - // Things this view knows how to do AddCommand ( Command.DeleteCharRight, @@ -1776,9 +1774,11 @@ public class TextField : View, IDesignable } } - private void TextField_SuperViewChanged (object sender, SuperViewChangedEventArgs e) + /// + protected override void OnSuperViewChanged (ValueChangedEventArgs args) { - if (e.SuperView is { }) + base.OnSuperViewChanged (args); + if (SuperView is { }) { if (Autocomplete.HostControl is null) { @@ -1792,6 +1792,7 @@ public class TextField : View, IDesignable } } + private void TextField_Initialized (object sender, EventArgs e) { _cursorPosition = Text.GetRuneCount (); diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index 8e438abb7..30c486518 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -120,8 +120,6 @@ public class TextView : View, IDesignable Initialized += TextView_Initialized!; - SuperViewChanged += TextView_SuperViewChanged!; - SubViewsLaidOut += TextView_LayoutComplete; // Things this view knows how to do @@ -4626,9 +4624,12 @@ public class TextView : View, IDesignable return Encoding.Unicode.GetString (encoded, 0, offset); } - private void TextView_SuperViewChanged (object sender, SuperViewChangedEventArgs e) + + /// + protected override void OnSuperViewChanged (ValueChangedEventArgs args) { - if (e.SuperView is { }) + base.OnSuperViewChanged (args); + if (SuperView is { }) { if (Autocomplete.HostControl is null) { diff --git a/Tests/UnitTestsParallelizable/ViewBase/SubviewTests.cs b/Tests/UnitTestsParallelizable/ViewBase/SubviewTests.cs index 3835dfa88..cbbcc9081 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/SubviewTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/SubviewTests.cs @@ -18,7 +18,7 @@ public class SubViewTests }; sub.SuperViewChanged += (s, e) => { - if (e.SuperView is { }) + if (sub.SuperView is { }) { subRaisedCount++; } @@ -46,7 +46,7 @@ public class SubViewTests }; sub.SuperViewChanged += (s, e) => { - if (e.SuperView is null) + if (sub.SuperView is null) { subRaisedCount++; } @@ -393,23 +393,23 @@ public class SubViewTests var superView = new View (); int superViewChangedCount = 0; - //int superViewChangingCount = 0; + int superViewChangingCount = 0; view.SuperViewChanged += (s, e) => { superViewChangedCount++; }; - //view.SuperViewChanging += (s, e) => - //{ - // superViewChangingCount++; - //}; + view.SuperViewChanging += (s, e) => + { + superViewChangingCount++; + }; // Act superView.Add (view); // Assert - //Assert.Equal (1, superViewChangingCount); + Assert.Equal (1, superViewChangingCount); Assert.Equal (1, superViewChangedCount); } @@ -692,4 +692,295 @@ public class SubViewTests Assert.Single (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 (); + + 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 (); + 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 (); + + 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 _events; + + public TestViewWithSuperViewEvents (List events) { _events = events; } + + protected override bool OnSuperViewChanging (ValueChangingEventArgs args) + { + _events.Add ("OnSuperViewChanging"); + return base.OnSuperViewChanging (args); + } + + protected override void OnSuperViewChanged (ValueChangedEventArgs 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 args) + { + return true; // Always cancel the change + } + } }